Beispiel #1
0
    def __init__(self, url, access_key, secret_key):
        self.index_url = url
        self.headers = {
            u'Accept': u'application/json, text/plain',
            u'Content-Type': u'application/json'
        }

        self.signer = BotoRequestSigner(self.index_url, access_key, secret_key)

        self.session = self.requests_retry_session()
        self.session_not_found_ok = self.requests_retry_not_found_ok_session()
Beispiel #2
0
class VinylDNSClient(object):
    def __init__(self, url, access_key, secret_key):
        self.index_url = url
        self.headers = {
            u'Accept': u'application/json, text/plain',
            u'Content-Type': u'application/json'
        }

        self.signer = BotoRequestSigner(self.index_url, access_key, secret_key)

        self.session = self.requests_retry_session()
        self.session_not_found_ok = self.requests_retry_not_found_ok_session()

    def requests_retry_not_found_ok_session(
            self,
            retries=5,
            backoff_factor=0.4,
            status_forcelist=(500, 502, 504),
            session=None,
    ):
        session = session or requests.Session()
        retry = Retry(
            total=retries,
            read=retries,
            connect=retries,
            backoff_factor=backoff_factor,
            status_forcelist=status_forcelist,
        )
        adapter = HTTPAdapter(max_retries=retry)
        session.mount(u'http://', adapter)
        session.mount(u'https://', adapter)
        return session

    def requests_retry_session(
            self,
            retries=5,
            backoff_factor=0.4,
            status_forcelist=(500, 502, 504),
            session=None,
    ):
        session = session or requests.Session()
        retry = Retry(
            total=retries,
            read=retries,
            connect=retries,
            backoff_factor=backoff_factor,
            status_forcelist=status_forcelist,
        )
        adapter = HTTPAdapter(max_retries=retry)
        session.mount(u'http://', adapter)
        session.mount(u'https://', adapter)
        return session

    def make_request(self,
                     url,
                     method=u'GET',
                     headers=None,
                     body_string=None,
                     sign_request=True,
                     not_found_ok=False,
                     **kwargs):

        # pull out status or None
        status_code = kwargs.pop(u'status', None)

        # remove retries arg if provided
        kwargs.pop(u'retries', None)

        path = urlparse(url).path

        # we must parse the query string so we can provide it if it exists so that we can pass it to the
        # build_vinyldns_request so that it can be properly included in the AWS signing...
        query = parse_qs(urlsplit(url).query)

        if query:
            # the problem with parse_qs is that it will return a list for ALL params, even if they are a single value
            # we need to essentially flatten the params if a param has only one value
            query = dict(
                (k, v if len(v) > 1 else v[0]) for k, v in iteritems(query))

        if sign_request:
            signed_headers, signed_body = self.build_vinyldns_request(
                method,
                path,
                body_string,
                query,
                with_headers=headers or {},
                **kwargs)
        else:
            signed_headers = headers or {}
            signed_body = body_string

        if not_found_ok:
            response = self.session_not_found_ok.request(
                method,
                url,
                data=signed_body,
                headers=signed_headers,
                **kwargs)
        else:
            response = self.session.request(method,
                                            url,
                                            data=signed_body,
                                            headers=signed_headers,
                                            **kwargs)

        if status_code is not None:
            if isinstance(status_code, collections.Iterable):
                assert_that(response.status_code, is_in(status_code))
            else:
                assert_that(response.status_code, is_(status_code))

        try:
            return response.status_code, response.json()
        except:
            return response.status_code, response.text

    def ping(self):
        """
        Simple ping request
        :return: the content of the response, which should be PONG
        """
        url = urljoin(self.index_url, '/ping')

        response, data = self.make_request(url)
        return data

    def get_status(self):
        """
        Gets processing status
        :return: the content of the response
        """
        url = urljoin(self.index_url, '/status')

        response, data = self.make_request(url)

        return data

    def post_status(self, status):
        """
        Update processing status
        :return: the content of the response
        """
        url = urljoin(self.index_url,
                      '/status?processingDisabled={}'.format(status))
        response, data = self.make_request(url, 'POST', self.headers)

        return data

    def color(self):
        """
        Gets the current color for the application
        :return: the content of the response, which should be "blue" or "green"
        """
        url = urljoin(self.index_url, '/color')
        response, data = self.make_request(url)
        return data

    def health(self):
        """
        Checks the health of the app, asserts that a 200 should be returned, otherwise
        this will fail
        """
        url = urljoin(self.index_url, '/health')
        self.make_request(url, sign_request=False)

    def create_group(self, group, **kwargs):
        """
        Creates a new group
        :param group: A group dictionary that can be serialized to json
        :return: the content of the response, which should be a group json
        """

        url = urljoin(self.index_url, u'/groups')
        response, data = self.make_request(url, u'POST', self.headers,
                                           json.dumps(group), **kwargs)

        return data

    def get_group(self, group_id, **kwargs):
        """
        Gets a group
        :param group_id: Id of the group to get
        :return: the group json
        """

        url = urljoin(self.index_url, u'/groups/' + group_id)
        response, data = self.make_request(url, u'GET', self.headers, **kwargs)

        return data

    def delete_group(self, group_id, **kwargs):
        """
        Deletes a group
        :param group_id: Id of the group to delete
        :return: the group json
        """

        url = urljoin(self.index_url, u'/groups/' + group_id)
        response, data = self.make_request(url,
                                           u'DELETE',
                                           self.headers,
                                           not_found_ok=True,
                                           **kwargs)

        return data

    def update_group(self, group_id, group, **kwargs):
        """
        Update an existing group
        :param group_id: The id of the group being updated
        :param group: A group dictionary that can be serialized to json
        :return: the content of the response, which should be a group json
        """

        url = urljoin(self.index_url, u'/groups/{0}'.format(group_id))
        response, data = self.make_request(url,
                                           u'PUT',
                                           self.headers,
                                           json.dumps(group),
                                           not_found_ok=True,
                                           **kwargs)

        return data

    def list_my_groups(self,
                       group_name_filter=None,
                       start_from=None,
                       max_items=None,
                       **kwargs):
        """
        Retrieves my groups
        :param start_from: the start key of the page
        :param max_items: the page limit
        :param group_name_filter: only returns groups whose names contain filter string
        :return: the content of the response
        """

        args = []
        if group_name_filter:
            args.append(u'groupNameFilter={0}'.format(group_name_filter))
        if start_from:
            args.append(u'startFrom={0}'.format(start_from))
        if max_items is not None:
            args.append(u'maxItems={0}'.format(max_items))

        url = urljoin(self.index_url, u'/groups') + u'?' + u'&'.join(args)
        response, data = self.make_request(url, u'GET', self.headers, **kwargs)

        return data

    def list_all_my_groups(self, group_name_filter=None, **kwargs):
        """
        Retrieves all my groups
        :param group_name_filter: only returns groups whose names contain filter string
        :return: the content of the response
        """

        groups = []
        args = []
        if group_name_filter:
            args.append(u'groupNameFilter={0}'.format(group_name_filter))

        url = urljoin(self.index_url, u'/groups') + u'?' + u'&'.join(args)
        response, data = self.make_request(url, u'GET', self.headers, **kwargs)

        groups.extend(data[u'groups'])

        while u'nextId' in data:
            args = []

            if group_name_filter:
                args.append(u'groupNameFilter={0}'.format(group_name_filter))
            if u'nextId' in data:
                args.append(u'startFrom={0}'.format(data[u'nextId']))

            response, data = self.make_request(url, u'GET', self.headers,
                                               **kwargs)
            groups.extend(data[u'groups'])

        return groups

    def list_members_group(self,
                           group_id,
                           start_from=None,
                           max_items=None,
                           **kwargs):
        """
        List the members of an existing group
        :param group_id: the Id of an existing group
        :param start_from: the Id a member of the group
        :param max_items: the max number of items to be returned
        :return: the json of the members
        """
        if start_from is None and max_items is None:
            url = urljoin(self.index_url,
                          u'/groups/{0}/members'.format(group_id))
        elif start_from is None and max_items is not None:
            url = urljoin(
                self.index_url, u'/groups/{0}/members?maxItems={1}'.format(
                    group_id, max_items))
        elif start_from is not None and max_items is None:
            url = urljoin(
                self.index_url, u'/groups/{0}/members?startFrom={1}'.format(
                    group_id, start_from))
        elif start_from is not None and max_items is not None:
            url = urljoin(
                self.index_url,
                u'/groups/{0}/members?startFrom={1}&maxItems={2}'.format(
                    group_id, start_from, max_items))

        response, data = self.make_request(url,
                                           u'GET',
                                           self.headers,
                                           not_found_ok=True,
                                           **kwargs)

        return data

    def list_group_admins(self, group_id, **kwargs):
        """
        returns the group admins
        :param group_id: the Id of the group
        :return: the user info of the admins
        """
        url = urljoin(self.index_url, u'/groups/{0}/admins'.format(group_id))
        response, data = self.make_request(url,
                                           u'GET',
                                           self.headers,
                                           not_found_ok=True,
                                           **kwargs)

        return data

    def get_group_changes(self,
                          group_id,
                          start_from=None,
                          max_items=None,
                          **kwargs):
        """
        List the changes of an existing group
        :param group_id: the Id of an existing group
        :param start_from: the Id a group change
        :param max_items: the max number of items to be returned
        :return: the json of the members
        """
        if start_from is None and max_items is None:
            url = urljoin(self.index_url,
                          u'/groups/{0}/activity'.format(group_id))
        elif start_from is None and max_items is not None:
            url = urljoin(
                self.index_url, u'/groups/{0}/activity?maxItems={1}'.format(
                    group_id, max_items))
        elif start_from is not None and max_items is None:
            url = urljoin(
                self.index_url, u'/groups/{0}/activity?startFrom={1}'.format(
                    group_id, start_from))
        elif start_from is not None and max_items is not None:
            url = urljoin(
                self.index_url,
                u'/groups/{0}/activity?startFrom={1}&maxItems={2}'.format(
                    group_id, start_from, max_items))

        response, data = self.make_request(url, u'GET', self.headers, **kwargs)

        return data

    def create_zone(self, zone, **kwargs):
        """
        Creates a new zone with the given name and email
        :param zone: the zone to be created
        :return: the content of the response
        """
        url = urljoin(self.index_url, u'/zones')
        response, data = self.make_request(url, u'POST', self.headers,
                                           json.dumps(zone), **kwargs)
        return data

    def update_zone(self, zone, **kwargs):
        """
        Updates a zone
        :param zone: the zone to be created
        :return: the content of the response
        """
        url = urljoin(self.index_url, u'/zones/{0}'.format(zone[u'id']))
        response, data = self.make_request(url,
                                           u'PUT',
                                           self.headers,
                                           json.dumps(zone),
                                           not_found_ok=True,
                                           **kwargs)
        return data

    def sync_zone(self, zone_id, **kwargs):
        """
        Syncs a zone
        :param zone: the zone to be updated
        :return: the content of the response
        """
        url = urljoin(self.index_url, u'/zones/{0}/sync'.format(zone_id))
        response, data = self.make_request(url,
                                           u'POST',
                                           self.headers,
                                           not_found_ok=True,
                                           **kwargs)

        return data

    def delete_zone(self, zone_id, **kwargs):
        """
        Deletes the zone for the given id
        :param zone_id: the id of the zone to be deleted
        :return: nothing, will fail if the status code was not expected
        """
        url = urljoin(self.index_url, u'/zones/{0}'.format(zone_id))
        response, data = self.make_request(url,
                                           u'DELETE',
                                           self.headers,
                                           not_found_ok=True,
                                           **kwargs)

        return data

    def get_zone(self, zone_id, **kwargs):
        """
        Gets a zone for the given zone id
        :param zone_id: the id of the zone to retrieve
        :return: the zone, or will 404 if not found
        """
        url = urljoin(self.index_url, u'/zones/{0}'.format(zone_id))
        response, data = self.make_request(url,
                                           u'GET',
                                           self.headers,
                                           not_found_ok=True,
                                           **kwargs)

        return data

    def get_zone_history(self, zone_id, **kwargs):
        """
        Gets the zone history for the given zone id
        :param zone_id: the id of the zone to retrieve
        :return: the zone, or will 404 if not found
        """
        url = urljoin(self.index_url, u'/zones/{0}/history'.format(zone_id))

        response, data = self.make_request(url,
                                           u'GET',
                                           self.headers,
                                           not_found_ok=True,
                                           **kwargs)
        return data

    def get_zone_change(self, zone_change, **kwargs):
        """
        Gets a zone change with the provided id

        Unfortunately, there is no endpoint, so we have to get all zone history and parse
        """
        zone_change_id = zone_change[u'id']
        change = None

        def change_id_match(possible_match):
            return possible_match[u'id'] == zone_change_id

        history = self.get_zone_history(zone_change[u'zone'][u'id'])
        if u'zoneChanges' in history:
            zone_changes = history[u'zoneChanges']
            matching_changes = filter(change_id_match, zone_changes)

            if len(matching_changes) > 0:
                change = matching_changes[0]

        return change

    def list_zone_changes(self,
                          zone_id,
                          start_from=None,
                          max_items=None,
                          **kwargs):
        """
        Gets the zone changes for the given zone id
        :param zone_id: the id of the zone to retrieve
        :param start_from: the start key of the page
        :param max_items: the page limit
        :return: the zone, or will 404 if not found
        """
        args = []
        if start_from:
            args.append(u'startFrom={0}'.format(start_from))
        if max_items is not None:
            args.append(u'maxItems={0}'.format(max_items))
        url = urljoin(
            self.index_url,
            u'/zones/{0}/changes'.format(zone_id)) + u'?' + u'&'.join(args)

        response, data = self.make_request(url,
                                           u'GET',
                                           self.headers,
                                           not_found_ok=True,
                                           **kwargs)
        return data

    def list_recordset_changes(self,
                               zone_id,
                               start_from=None,
                               max_items=None,
                               **kwargs):
        """
        Gets the recordset changes for the given zone id
        :param zone_id: the id of the zone to retrieve
        :param start_from: the start key of the page
        :param max_items: the page limit
        :return: the zone, or will 404 if not found
        """
        args = []
        if start_from:
            args.append(u'startFrom={0}'.format(start_from))
        if max_items is not None:
            args.append(u'maxItems={0}'.format(max_items))
        url = urljoin(self.index_url, u'/zones/{0}/recordsetchanges'.format(
            zone_id)) + u'?' + u'&'.join(args)

        response, data = self.make_request(url,
                                           u'GET',
                                           self.headers,
                                           not_found_ok=True,
                                           **kwargs)
        return data

    def list_zones(self,
                   name_filter=None,
                   start_from=None,
                   max_items=None,
                   **kwargs):
        """
        Gets a list of zones that currently exist
        :return: a list of zones
        """
        url = urljoin(self.index_url, u'/zones')

        query = []
        if name_filter:
            query.append(u'nameFilter=' + name_filter)

        if start_from:
            query.append(u'startFrom=' + str(start_from))

        if max_items:
            query.append(u'maxItems=' + str(max_items))

        if query:
            url = url + u'?' + u'&'.join(query)

        response, data = self.make_request(url, u'GET', self.headers, **kwargs)
        return data

    def create_recordset(self, recordset, **kwargs):
        """
        Creates a new recordset
        :param recordset: the recordset to be created
        :return: the content of the response
        """
        if recordset and u'name' in recordset:
            recordset[u'name'] = recordset[u'name'].replace(u'_', u'-')

        url = urljoin(self.index_url,
                      u'/zones/{0}/recordsets'.format(recordset[u'zoneId']))
        response, data = self.make_request(url, u'POST', self.headers,
                                           json.dumps(recordset), **kwargs)
        return data

    def delete_recordset(self, zone_id, rs_id, **kwargs):
        """
        Deletes an existing recordset
        :param zone_id: the zone id the recordset belongs to
        :param rs_id: the id of the recordset to be deleted
        :return: the content of the response
        """
        url = urljoin(self.index_url,
                      u'/zones/{0}/recordsets/{1}'.format(zone_id, rs_id))

        response, data = self.make_request(url,
                                           u'DELETE',
                                           self.headers,
                                           not_found_ok=True,
                                           **kwargs)
        return data

    def update_recordset(self, recordset, **kwargs):
        """
        Deletes an existing recordset
        :param recordset: the recordset to be updated
        :return: the content of the response
        """
        url = urljoin(
            self.index_url,
            u'/zones/{0}/recordsets/{1}'.format(recordset[u'zoneId'],
                                                recordset[u'id']))

        response, data = self.make_request(url,
                                           u'PUT',
                                           self.headers,
                                           json.dumps(recordset),
                                           not_found_ok=True,
                                           **kwargs)
        return data

    def get_recordset(self, zone_id, rs_id, **kwargs):
        """
        Gets an existing recordset
        :param zone_id: the zone id the recordset belongs to
        :param rs_id: the id of the recordset to be retrieved
        :return: the content of the response
        """
        url = urljoin(self.index_url,
                      u'/zones/{0}/recordsets/{1}'.format(zone_id, rs_id))

        response, data = self.make_request(url,
                                           u'GET',
                                           self.headers,
                                           None,
                                           not_found_ok=True,
                                           **kwargs)
        return data

    def get_recordset_change(self, zone_id, rs_id, change_id, **kwargs):
        """
        Gets an existing recordset change
        :param zone_id: the zone id the recordset belongs to
        :param rs_id: the id of the recordset to be retrieved
        :param change_id: the id of the change to be retrieved
        :return: the content of the response
        """
        url = urljoin(
            self.index_url, u'/zones/{0}/recordsets/{1}/changes/{2}'.format(
                zone_id, rs_id, change_id))

        response, data = self.make_request(url,
                                           u'GET',
                                           self.headers,
                                           None,
                                           not_found_ok=True,
                                           **kwargs)
        return data

    def list_recordsets(self,
                        zone_id,
                        start_from=None,
                        max_items=None,
                        record_name_filter=None,
                        **kwargs):
        """
        Retrieves all recordsets in a zone
        :param zone_id: the zone to retrieve
        :param start_from: the start key of the page
        :param max_items: the page limit
        :param record_name_filter: only returns recordsets whose names contain filter string
        :return: the content of the response
        """
        args = []
        if start_from:
            args.append(u'startFrom={0}'.format(start_from))
        if max_items is not None:
            args.append(u'maxItems={0}'.format(max_items))
        if record_name_filter:
            args.append(u'recordNameFilter={0}'.format(record_name_filter))

        url = urljoin(
            self.index_url,
            u'/zones/{0}/recordsets'.format(zone_id)) + u'?' + u'&'.join(args)

        response, data = self.make_request(url, u'GET', self.headers, **kwargs)
        return data

    def create_batch_change(self, batch_change_input, **kwargs):
        """
        Creates a new batch change
        :param batch_change_input: the batchchange to be created
        :return: the content of the response
        """
        url = urljoin(self.index_url, u'/zones/batchrecordchanges')
        response, data = self.make_request(url, u'POST', self.headers,
                                           json.dumps(batch_change_input),
                                           **kwargs)
        return data

    def get_batch_change(self, batch_change_id, **kwargs):
        """
        Gets an existing batch change
        :param batch_change_id: the unique identifier of the batchchange
        :return: the content of the response
        """
        url = urljoin(self.index_url,
                      u'/zones/batchrecordchanges/{0}'.format(batch_change_id))
        response, data = self.make_request(url,
                                           u'GET',
                                           self.headers,
                                           None,
                                           not_found_ok=True,
                                           **kwargs)
        return data

    def list_batch_change_summaries(self,
                                    start_from=None,
                                    max_items=None,
                                    **kwargs):
        """
        Gets list of user's batch change summaries
        :return: the content of the response
        """
        args = []
        if start_from:
            args.append(u'startFrom={0}'.format(start_from))
        if max_items is not None:
            args.append(u'maxItems={0}'.format(max_items))

        url = urljoin(self.index_url,
                      u'/zones/batchrecordchanges') + u'?' + u'&'.join(args)

        response, data = self.make_request(url, u'GET', self.headers, **kwargs)
        return data

    def build_vinyldns_request(self,
                               method,
                               path,
                               body_data,
                               params=None,
                               **kwargs):

        if isinstance(body_data, basestring):
            body_string = body_data
        else:
            body_string = json.dumps(body_data)

        new_headers = {u'X-Amz-Target': u'VinylDNS'}
        new_headers.update(kwargs.get(u'with_headers', dict()))

        suppress_headers = kwargs.get(u'suppress_headers', list())

        headers = self.build_headers(new_headers, suppress_headers)

        auth_header = self.signer.build_auth_header(method, path, headers,
                                                    body_string, params)
        headers[u'Authorization'] = auth_header

        return headers, body_string

    @staticmethod
    def build_headers(new_headers, suppressed_keys):
        """Construct HTTP headers for a request."""
        def canonical_header_name(field_name):
            return u'-'.join(word.capitalize()
                             for word in field_name.split(u'-'))

        import datetime
        now = datetime.datetime.utcnow()
        headers = {
            u'Content-Type': u'application/x-amz-json-1.0',
            u'Date': now.strftime(u'%a, %d %b %Y %H:%M:%S GMT'),
            u'X-Amz-Date': now.strftime(u'%Y%m%dT%H%M%SZ')
        }

        for k, v in iteritems(new_headers):
            headers[canonical_header_name(k)] = v

        for k in map(canonical_header_name, suppressed_keys):
            if k in headers:
                del headers[k]

        return headers

    def add_zone_acl_rule_with_wait(self,
                                    zone_id,
                                    acl_rule,
                                    sign_request=True,
                                    **kwargs):
        """
        Puts an acl rule on the zone and waits for success
        :param zone_id: The id of the zone to attach the acl rule to
        :param acl_rule: The acl rule contents
        :param sign_request: An indicator if we should sign the request; useful for testing auth
        :return: the content of the response
        """
        rule = self.add_zone_acl_rule(zone_id, acl_rule, sign_request,
                                      **kwargs)
        self.wait_until_zone_change_status(rule, 'Synced')

        return rule

    def add_zone_acl_rule(self,
                          zone_id,
                          acl_rule,
                          sign_request=True,
                          **kwargs):
        """
        Puts an acl rule on the zone
        :param zone_id: The id of the zone to attach the acl rule to
        :param acl_rule: The acl rule contents
        :param sign_request: An indicator if we should sign the request; useful for testing auth
        :return: the content of the response
        """
        url = urljoin(self.index_url, '/zones/{0}/acl/rules'.format(zone_id))
        response, data = self.make_request(url,
                                           'PUT',
                                           self.headers,
                                           json.dumps(acl_rule),
                                           sign_request=sign_request,
                                           **kwargs)

        return data

    def delete_zone_acl_rule_with_wait(self,
                                       zone_id,
                                       acl_rule,
                                       sign_request=True,
                                       **kwargs):
        """
        Deletes an acl rule from the zone and waits for success
        :param zone_id: The id of the zone to remove the acl from
        :param acl_rule: The acl rule to remove
        :param sign_request:  An indicator if we should sign the request; useful for testing auth
        :return: the content of the response
        """
        rule = self.delete_zone_acl_rule(zone_id, acl_rule, sign_request,
                                         **kwargs)
        self.wait_until_zone_change_status(rule, 'Synced')

        return rule

    def delete_zone_acl_rule(self,
                             zone_id,
                             acl_rule,
                             sign_request=True,
                             **kwargs):
        """
        Deletes an acl rule from the zone
        :param zone_id: The id of the zone to remove the acl from
        :param acl_rule: The acl rule to remove
        :param sign_request:  An indicator if we should sign the request; useful for testing auth
        :return: the content of the response
        """
        url = urljoin(self.index_url, '/zones/{0}/acl/rules'.format(zone_id))
        response, data = self.make_request(url,
                                           'DELETE',
                                           self.headers,
                                           json.dumps(acl_rule),
                                           sign_request=sign_request,
                                           **kwargs)

        return data

    def wait_until_recordset_deleted(self, zone_id, record_set_id, **kwargs):
        retries = MAX_RETRIES
        url = urljoin(
            self.index_url,
            u'/zones/{0}/recordsets/{1}'.format(zone_id, record_set_id))
        response, data = self.make_request(url,
                                           u'GET',
                                           self.headers,
                                           not_found_ok=True,
                                           status=(200, 404),
                                           **kwargs)
        while response != 404 and retries > 0:
            url = urljoin(
                self.index_url,
                u'/zones/{0}/recordsets/{1}'.format(zone_id, record_set_id))
            response, data = self.make_request(url,
                                               u'GET',
                                               self.headers,
                                               not_found_ok=True,
                                               status=(200, 404),
                                               **kwargs)
            retries -= 1
            time.sleep(RETRY_WAIT)

        return response == 404

    def wait_until_zone_change_status(self, zone_change, expected_status):
        """
        Waits until the zone change status matches the expected status
        """
        zone_change_id = zone_change[u'id']

        def change_id_match(change):
            return change[u'id'] == zone_change_id

        change = zone_change
        retries = MAX_RETRIES
        while change[u'status'] != expected_status and retries > 0:
            history = self.get_zone_history(zone_change[u'zone'][u'id'])

            if u'zoneChanges' in history:
                zone_changes = history[u'zoneChanges']
                matching_changes = filter(change_id_match, zone_changes)

                if len(matching_changes) > 0:
                    change = matching_changes[0]
            time.sleep(RETRY_WAIT)
            retries -= 1

        return change[u'status'] == expected_status

    def wait_until_zone_deleted(self, zone_id, **kwargs):
        """
        Waits a period of time for the zone deletion to complete.

        :param zone_id: the id of the zone that has been deleted.
        :param kw: Additional parameters for the http request
        :return: True when the zone deletion is complete False if the timeout expires
        """
        retries = MAX_RETRIES
        url = urljoin(self.index_url, u'/zones/{0}'.format(zone_id))
        response, data = self.make_request(url,
                                           u'GET',
                                           self.headers,
                                           not_found_ok=True,
                                           status=(200, 404),
                                           **kwargs)
        while response != 404 and retries > 0:
            url = urljoin(self.index_url, u'/zones/{0}'.format(zone_id))
            response, data = self.make_request(url,
                                               u'GET',
                                               self.headers,
                                               not_found_ok=True,
                                               status=(200, 404),
                                               **kwargs)
            retries -= 1
            time.sleep(RETRY_WAIT)

        return response == 404

    def wait_until_zone_exists(self, zone_change, **kwargs):
        """
        Waits a period of time for the zone creation to complete.

        :param zone_change: the create zone change for the zone that has been created.
        :param kw: Additional parameters for the http request
        :return: True when the zone creation is complete False if the timeout expires
        """
        zone_id = zone_change[u'zone'][u'id']
        retries = MAX_RETRIES
        url = urljoin(self.index_url, u'/zones/{0}'.format(zone_id))
        response, data = self.make_request(url,
                                           u'GET',
                                           self.headers,
                                           not_found_ok=True,
                                           status=(200, 404),
                                           **kwargs)
        while response != 200 and retries > 0:
            url = urljoin(self.index_url, u'/zones/{0}'.format(zone_id))
            response, data = self.make_request(url,
                                               u'GET',
                                               self.headers,
                                               not_found_ok=True,
                                               status=(200, 404),
                                               **kwargs)
            retries -= 1
            time.sleep(RETRY_WAIT)

        return response == 200

    def wait_until_recordset_exists(self, zone_id, record_set_id, **kwargs):
        """
        Waits a period of time for the record set creation to complete.

        :param zone_id: the id of the zone the record set lives in
        :param record_set_id: the id of the recprdset that has been created.
        :param kw: Additional parameters for the http request
        :return: True when the recordset creation is complete False if the timeout expires
        """
        retries = MAX_RETRIES
        url = urljoin(
            self.index_url,
            u'/zones/{0}/recordsets/{1}'.format(zone_id, record_set_id))
        response, data = self.make_request(url,
                                           u'GET',
                                           self.headers,
                                           not_found_ok=True,
                                           status=(200, 404),
                                           **kwargs)
        while response != 200 and retries > 0:
            response, data = self.make_request(url,
                                               u'GET',
                                               self.headers,
                                               not_found_ok=True,
                                               status=(200, 404),
                                               **kwargs)
            retries -= 1
            time.sleep(RETRY_WAIT)

        if response == 200:
            return data

        return response == 200

    def abandon_zones(self, zone_ids, **kwargs):
        #delete each zone
        for zone_id in zone_ids:
            self.delete_zone(zone_id, status=(202, 404))

        # Wait until each zone is gone
        for zone_id in zone_ids:
            success = self.wait_until_zone_deleted(zone_id)
            assert_that(success, is_(True))

    def wait_until_recordset_change_status(self, rs_change, expected_status):
        """
        Waits a period of time for a recordset to be active by repeatedly fetching the recordset and testing
        the recordset status
        :param rs_change: The recordset change being evaluated, must include the id and the zone id
        :return: The recordset change that is active, or it could still be pending if the number of retries was exhausted
        """
        change = rs_change
        retries = MAX_RETRIES
        while change['status'] != expected_status and retries > 0:
            latest_change = self.get_recordset_change(
                change['recordSet']['zoneId'],
                change['recordSet']['id'],
                change['id'],
                status=(200, 404))
            print "\r\n --- latest change is " + str(latest_change)
            if "Unable to find record set change" in latest_change:
                change = change
            else:
                change = latest_change

            time.sleep(RETRY_WAIT)
            retries -= 1

        if change['status'] != expected_status:
            print 'Failed waiting for record change status'
            if 'systemMessage' in change:
                print 'systemMessage is ' + change['systemMessage']

        assert_that(change['status'], is_(expected_status))
        return change

    def batch_is_completed(self, batch_change):
        return batch_change['status'] in [
            'Complete', 'Failed', 'PartialFailure'
        ]

    def wait_until_batch_change_completed(self, batch_change):
        """
        Waits a period of time for a batch change to be complete (or failed) by repeatedly fetching the change and testing
        the status
        :param batch_change: The batch change being evaluated
        :return: The batch change that is active, or it could still be pending if the number of retries was exhausted
        """
        change = batch_change
        retries = MAX_RETRIES

        while not self.batch_is_completed(change) and retries > 0:
            latest_change = self.get_batch_change(change['id'],
                                                  status=(200, 404))
            print "\r\n --- latest change is " + str(latest_change)
            if "cannot be found" in latest_change:
                change = change
            else:
                change = latest_change

            time.sleep(RETRY_WAIT)
            retries -= 1

        if not self.batch_is_completed(change):
            print 'Failed waiting for record change status'
            print change

        assert_that(self.batch_is_completed(change), is_(True))
        return change
Beispiel #3
0
class VinylDNSClient(object):
    """TODO: Add class docstring."""
    def __init__(self, url, access_key, secret_key):
        """TODO: Add method docstring."""
        self.index_url = url
        self.headers = {
            u'Accept': u'application/json, text/plain',
            u'Content-Type': u'application/json'
        }

        self.signer = BotoRequestSigner(self.index_url, access_key, secret_key)

        self.session = self.requests_retry_session()
        self.session_not_found_ok = self.requests_retry_not_found_ok_session()

    def requests_retry_not_found_ok_session(
            self,
            retries=5,
            backoff_factor=0.4,
            status_forcelist=(500, 502, 504),
            session=None,
    ):
        """TODO: Add method docstring."""
        session = session or requests.Session()
        retry = Retry(
            total=retries,
            read=retries,
            connect=retries,
            backoff_factor=backoff_factor,
            status_forcelist=status_forcelist,
        )
        adapter = HTTPAdapter(max_retries=retry)
        session.mount(u'http://', adapter)
        session.mount(u'https://', adapter)
        return session

    def requests_retry_session(
            self,
            retries=5,
            backoff_factor=0.4,
            status_forcelist=(500, 502, 504),
            session=None,
    ):
        """TODO: Add method docstring."""
        session = session or requests.Session()
        retry = Retry(
            total=retries,
            read=retries,
            connect=retries,
            backoff_factor=backoff_factor,
            status_forcelist=status_forcelist,
        )
        adapter = HTTPAdapter(max_retries=retry)
        session.mount(u'http://', adapter)
        session.mount(u'https://', adapter)
        return session

    def make_request(self,
                     url,
                     method=u'GET',
                     headers=None,
                     body_string=None,
                     sign_request=True,
                     not_found_ok=False,
                     **kwargs):
        """TODO: Add method docstring."""
        # remove retries arg if provided
        kwargs.pop(u'retries', None)

        path = urlparse(url).path

        # we must parse the query string so we can provide it if it exists
        # so that we can pass it to the build_vinyldns_request so that
        # it can be properly included in the AWS signing...
        query = parse_qs(urlsplit(url).query)

        if query:
            # the problem with parse_qs is that it will return a list
            # for ALL params, even if they are a single value we need
            # to essentially flatten the params if a param has only one value
            query = dict(
                (k, v if len(v) > 1 else v[0]) for k, v in iteritems(query))

        if sign_request:
            signed_headers, signed_body = self.build_vinyldns_request(
                method,
                path,
                body_string,
                query,
                with_headers=headers or {},
                **kwargs)
        else:
            signed_headers = headers or {}
            signed_body = body_string

        if not_found_ok:
            response = self.session_not_found_ok.request(
                method,
                url,
                data=signed_body,
                headers=signed_headers,
                **kwargs)
        else:
            response = self.session.request(method,
                                            url,
                                            data=signed_body,
                                            headers=signed_headers,
                                            **kwargs)

        try:
            return response.status_code, response.json()
        except:
            return response.status_code, response.text

    def ping(self):
        """
        Perform a simple ping request.

        :return: the content of the response, which should be PONG
        """
        url = urljoin(self.index_url, '/ping')

        _, data = self.make_request(url)
        return data

    def get_status(self):
        """
        Get processing status.

        :return: the content of the response
        """
        url = urljoin(self.index_url, '/status')

        _, data = self.make_request(url)

        return data

    def post_status(self, status):
        """
        Update processing status.

        :return: the content of the response
        """
        url = urljoin(self.index_url,
                      '/status?processingDisabled={}'.format(status))
        _, data = self.make_request(url, 'POST', self.headers)

        return data

    def color(self):
        """
        Get the current color for the application.

        :return: the content of the response, which should be
        "blue" or "green"
        """
        url = urljoin(self.index_url, '/color')
        _, data = self.make_request(url)
        return data

    def health(self):
        """
        Check the health of the app.

        Asserts that a 200 should be returned, otherwise this will fail.
        """
        url = urljoin(self.index_url, '/health')
        self.make_request(url, sign_request=False)

    def create_group(self, group, **kwargs):
        """
        Create a new group.

        :param group: A group dictionary that can be serialized to json
        :return: the content of the response, which should be a group json
        """
        url = urljoin(self.index_url, u'/groups')
        _, data = self.make_request(url, u'POST', self.headers,
                                    json.dumps(group), **kwargs)

        return data

    def get_group(self, group_id, **kwargs):
        """
        Get a group.

        :param group_id: Id of the group to get
        :return: the group json
        """
        url = urljoin(self.index_url, u'/groups/' + group_id)

        _, data = self.make_request(url, u'GET', self.headers, **kwargs)

        return data

    def delete_group(self, group_id, **kwargs):
        """
        Delete a group.

        :param group_id: Id of the group to delete
        :return: the group json
        """
        url = urljoin(self.index_url, u'/groups/' + group_id)

        _, data = self.make_request(url,
                                    u'DELETE',
                                    self.headers,
                                    not_found_ok=True,
                                    **kwargs)

        return data

    def update_group(self, group_id, group, **kwargs):
        """
        Update an existing group.

        :param group_id: The id of the group being updated
        :param group: A group dictionary that can be serialized to json
        :return: the content of the response, which should be a group json
        """
        url = urljoin(self.index_url, u'/groups/{0}'.format(group_id))
        _, data = self.make_request(url,
                                    u'PUT',
                                    self.headers,
                                    json.dumps(group),
                                    not_found_ok=True,
                                    **kwargs)

        return data

    def list_my_groups(self,
                       group_name_filter=None,
                       start_from=None,
                       max_items=None,
                       **kwargs):
        """
        Retrieve my groups.

        :param start_from: the start key of the page
        :param max_items: the page limit
        :param group_name_filter: only returns groups whose names contain
        filter string
        :return: the content of the response
        """
        args = []
        if group_name_filter:
            args.append(u'groupNameFilter={0}'.format(group_name_filter))
        if start_from:
            args.append(u'startFrom={0}'.format(start_from))
        if max_items is not None:
            args.append(u'maxItems={0}'.format(max_items))

        url = urljoin(self.index_url, u'/groups') + u'?' + u'&'.join(args)
        _, data = self.make_request(url, u'GET', self.headers, **kwargs)

        return data

    def list_all_my_groups(self, group_name_filter=None, **kwargs):
        """
        Retrieve all my groups.

        :param group_name_filter: only returns groups whose names contain
        filter string
        :return: the content of the response
        """
        groups = []
        args = []
        if group_name_filter:
            args.append(u'groupNameFilter={0}'.format(group_name_filter))

        url = urljoin(self.index_url, u'/groups') + u'?' + u'&'.join(args)
        _, data = self.make_request(url, u'GET', self.headers, **kwargs)

        groups.extend(data[u'groups'])

        while u'nextId' in data:
            args = []

            if group_name_filter:
                args.append(u'groupNameFilter={0}'.format(group_name_filter))
            if u'nextId' in data:
                args.append(u'startFrom={0}'.format(data[u'nextId']))

            _, data = self.make_request(url, u'GET', self.headers, **kwargs)
            groups.extend(data[u'groups'])

        return groups

    def list_members_group(self,
                           group_id,
                           start_from=None,
                           max_items=None,
                           **kwargs):
        """
        List the members of an existing group.

        :param group_id: the Id of an existing group
        :param start_from: the Id a member of the group
        :param max_items: the max number of items to be returned
        :return: the json of the members
        """
        if start_from is None and max_items is None:
            url = urljoin(self.index_url,
                          u'/groups/{0}/members'.format(group_id))
        elif start_from is None and max_items is not None:
            url = urljoin(
                self.index_url, u'/groups/{0}/members?maxItems={1}'.format(
                    group_id, max_items))
        elif start_from is not None and max_items is None:
            url = urljoin(
                self.index_url, u'/groups/{0}/members?startFrom={1}'.format(
                    group_id, start_from))
        elif start_from is not None and max_items is not None:
            url = urljoin(
                self.index_url,
                u'/groups/{0}/members?startFrom={1}&maxItems={2}'.format(
                    group_id, start_from, max_items))

        _, data = self.make_request(url,
                                    u'GET',
                                    self.headers,
                                    not_found_ok=True,
                                    **kwargs)

        return data

    def list_group_admins(self, group_id, **kwargs):
        """
        Return the group admins.

        :param group_id: the Id of the group
        :return: the user info of the admins
        """
        url = urljoin(self.index_url, u'/groups/{0}/admins'.format(group_id))
        _, data = self.make_request(url,
                                    u'GET',
                                    self.headers,
                                    not_found_ok=True,
                                    **kwargs)

        return data

    def get_group_changes(self,
                          group_id,
                          start_from=None,
                          max_items=None,
                          **kwargs):
        """
        List the changes of an existing group.

        :param group_id: the Id of an existing group
        :param start_from: the Id a group change
        :param max_items: the max number of items to be returned
        :return: the json of the members
        """
        if start_from is None and max_items is None:
            url = urljoin(self.index_url,
                          u'/groups/{0}/activity'.format(group_id))
        elif start_from is None and max_items is not None:
            url = urljoin(
                self.index_url, u'/groups/{0}/activity?maxItems={1}'.format(
                    group_id, max_items))
        elif start_from is not None and max_items is None:
            url = urljoin(
                self.index_url, u'/groups/{0}/activity?startFrom={1}'.format(
                    group_id, start_from))
        elif start_from is not None and max_items is not None:
            url = urljoin(
                self.index_url,
                u'/groups/{0}/activity?startFrom={1}&maxItems={2}'.format(
                    group_id, start_from, max_items))

        _, data = self.make_request(url, u'GET', self.headers, **kwargs)

        return data

    def create_zone(self, zone, **kwargs):
        """
        Create a new zone with the given name and email.

        :param zone: the zone to be created
        :return: the content of the response
        """
        url = urljoin(self.index_url, u'/zones')
        _, data = self.make_request(url, u'POST', self.headers,
                                    json.dumps(zone), **kwargs)
        return data

    def update_zone(self, zone, **kwargs):
        """
        Update a zone.

        :param zone: the zone to be created
        :return: the content of the response
        """
        url = urljoin(self.index_url, u'/zones/{0}'.format(zone[u'id']))
        _, data = self.make_request(url,
                                    u'PUT',
                                    self.headers,
                                    json.dumps(zone),
                                    not_found_ok=True,
                                    **kwargs)
        return data

    def sync_zone(self, zone_id, **kwargs):
        """
        Sync a zone.

        :param zone: the zone to be updated
        :return: the content of the response
        """
        url = urljoin(self.index_url, u'/zones/{0}/sync'.format(zone_id))
        _, data = self.make_request(url,
                                    u'POST',
                                    self.headers,
                                    not_found_ok=True,
                                    **kwargs)

        return data

    def delete_zone(self, zone_id, **kwargs):
        """
        Delete the zone for the given id.

        :param zone_id: the id of the zone to be deleted
        :return: nothing, will fail if the status code was not expected
        """
        url = urljoin(self.index_url, u'/zones/{0}'.format(zone_id))
        _, data = self.make_request(url,
                                    u'DELETE',
                                    self.headers,
                                    not_found_ok=True,
                                    **kwargs)

        return data

    def get_zone(self, zone_id, **kwargs):
        """
        Get a zone for the given zone id.

        :param zone_id: the id of the zone to retrieve
        :return: the zone, or will 404 if not found
        """
        url = urljoin(self.index_url, u'/zones/{0}'.format(zone_id))
        _, data = self.make_request(url,
                                    u'GET',
                                    self.headers,
                                    not_found_ok=True,
                                    **kwargs)

        return data

    def get_zone_history(self, zone_id, **kwargs):
        """
        Get the zone history for the given zone id.

        :param zone_id: the id of the zone to retrieve
        :return: the zone, or will 404 if not found
        """
        url = urljoin(self.index_url, u'/zones/{0}/history'.format(zone_id))

        _, data = self.make_request(url,
                                    u'GET',
                                    self.headers,
                                    not_found_ok=True,
                                    **kwargs)
        return data

    def get_zone_change(self, zone_change, **kwargs):
        """
        Get a zone change with the provided id.

        Unfortunately, there is no endpoint, so we have to get
        all zone history and parse.
        """
        zone_change_id = zone_change[u'id']
        change = None

        def change_id_match(possible_match):
            return possible_match[u'id'] == zone_change_id

        history = self.get_zone_history(zone_change[u'zone'][u'id'])
        if u'zoneChanges' in history:
            zone_changes = history[u'zoneChanges']
            matching_changes = filter(change_id_match, zone_changes)

            if matching_changes:
                change = matching_changes[0]

        return change

    def list_zone_changes(self,
                          zone_id,
                          start_from=None,
                          max_items=None,
                          **kwargs):
        """
        Get the zone changes for the given zone id.

        :param zone_id: the id of the zone to retrieve
        :param start_from: the start key of the page
        :param max_items: the page limit
        :return: the zone, or will 404 if not found
        """
        args = []
        if start_from:
            args.append(u'startFrom={0}'.format(start_from))
        if max_items is not None:
            args.append(u'maxItems={0}'.format(max_items))

        url = urljoin(
            self.index_url,
            u'/zones/{0}/changes'.format(zone_id)) + u'?' + u'&'.join(args)

        _, data = self.make_request(url,
                                    u'GET',
                                    self.headers,
                                    not_found_ok=True,
                                    **kwargs)
        return data

    def list_recordset_changes(self,
                               zone_id,
                               start_from=None,
                               max_items=None,
                               **kwargs):
        """
        Get the recordset changes for the given zone id.

        :param zone_id: the id of the zone to retrieve
        :param start_from: the start key of the page
        :param max_items: the page limit
        :return: the zone, or will 404 if not found
        """
        args = []
        if start_from:
            args.append(u'startFrom={0}'.format(start_from))
        if max_items is not None:
            args.append(u'maxItems={0}'.format(max_items))
        url = urljoin(self.index_url, u'/zones/{0}/recordsetchanges'.format(
            zone_id)) + u'?' + u'&'.join(args)

        _, data = self.make_request(url,
                                    u'GET',
                                    self.headers,
                                    not_found_ok=True,
                                    **kwargs)
        return data

    def list_zones(self,
                   name_filter=None,
                   start_from=None,
                   max_items=None,
                   **kwargs):
        """
        Get a list of zones that currently exist.

        :return: a list of zones
        """
        url = urljoin(self.index_url, u'/zones')

        query = []
        if name_filter:
            query.append(u'nameFilter=' + name_filter)

        if start_from:
            query.append(u'startFrom=' + str(start_from))

        if max_items:
            query.append(u'maxItems=' + str(max_items))

        if query:
            url = url + u'?' + u'&'.join(query)

        _, data = self.make_request(url, u'GET', self.headers, **kwargs)
        return data

    def create_recordset(self, recordset, **kwargs):
        """
        Create a new recordset.

        :param recordset: the recordset to be created
        :return: the content of the response
        """
        if recordset and u'name' in recordset:
            recordset[u'name'] = recordset[u'name'].replace(u'_', u'-')

        url = urljoin(self.index_url,
                      u'/zones/{0}/recordsets'.format(recordset[u'zoneId']))
        _, data = self.make_request(url, u'POST', self.headers,
                                    json.dumps(recordset), **kwargs)
        return data

    def delete_recordset(self, zone_id, rs_id, **kwargs):
        """
        Delete an existing recordset.

        :param zone_id: the zone id the recordset belongs to
        :param rs_id: the id of the recordset to be deleted
        :return: the content of the response
        """
        url = urljoin(self.index_url,
                      u'/zones/{0}/recordsets/{1}'.format(zone_id, rs_id))

        _, data = self.make_request(url,
                                    u'DELETE',
                                    self.headers,
                                    not_found_ok=True,
                                    **kwargs)
        return data

    def update_recordset(self, recordset, **kwargs):
        """
        Delete an existing recordset.

        :param recordset: the recordset to be updated
        :return: the content of the response
        """
        url = urljoin(
            self.index_url,
            u'/zones/{0}/recordsets/{1}'.format(recordset[u'zoneId'],
                                                recordset[u'id']))

        _, data = self.make_request(url,
                                    u'PUT',
                                    self.headers,
                                    json.dumps(recordset),
                                    not_found_ok=True,
                                    **kwargs)
        return data

    def get_recordset(self, zone_id, rs_id, **kwargs):
        """
        Get an existing recordset.

        :param zone_id: the zone id the recordset belongs to
        :param rs_id: the id of the recordset to be retrieved
        :return: the content of the response
        """
        url = urljoin(self.index_url,
                      u'/zones/{0}/recordsets/{1}'.format(zone_id, rs_id))

        _, data = self.make_request(url,
                                    u'GET',
                                    self.headers,
                                    None,
                                    not_found_ok=True,
                                    **kwargs)
        return data

    def get_recordset_change(self, zone_id, rs_id, change_id, **kwargs):
        """
        Get an existing recordset change.

        :param zone_id: the zone id the recordset belongs to
        :param rs_id: the id of the recordset to be retrieved
        :param change_id: the id of the change to be retrieved
        :return: the content of the response
        """
        url = urljoin(
            self.index_url, u'/zones/{0}/recordsets/{1}/changes/{2}'.format(
                zone_id, rs_id, change_id))

        _, data = self.make_request(url,
                                    u'GET',
                                    self.headers,
                                    None,
                                    not_found_ok=True,
                                    **kwargs)
        return data

    def list_recordsets(self,
                        zone_id,
                        start_from=None,
                        max_items=None,
                        record_name_filter=None,
                        **kwargs):
        """
        Retrieve all recordsets in a zone.

        :param zone_id: the zone to retrieve
        :param start_from: the start key of the page
        :param max_items: the page limit
        :param record_name_filter: only returns recordsets
        whose names contain filter string
        :return: the content of the response
        """
        args = []
        if start_from:
            args.append(u'startFrom={0}'.format(start_from))
        if max_items is not None:
            args.append(u'maxItems={0}'.format(max_items))
        if record_name_filter:
            args.append(u'recordNameFilter={0}'.format(record_name_filter))

        url = urljoin(
            self.index_url,
            u'/zones/{0}/recordsets'.format(zone_id)) + u'?' + u'&'.join(args)

        _, data = self.make_request(url, u'GET', self.headers, **kwargs)
        return data

    def create_batch_change(self, batch_change_input, **kwargs):
        """
        Create a new batch change.

        :param batch_change_input: the batchchange to be created
        :return: the content of the response
        """
        url = urljoin(self.index_url, u'/zones/batchrecordchanges')
        _, data = self.make_request(url, u'POST', self.headers,
                                    json.dumps(batch_change_input), **kwargs)
        return data

    def get_batch_change(self, batch_change_id, **kwargs):
        """
        Get an existing batch change.

        :param batch_change_id: the unique identifier of the batchchange
        :return: the content of the response
        """
        url = urljoin(self.index_url,
                      u'/zones/batchrecordchanges/{0}'.format(batch_change_id))
        _, data = self.make_request(url,
                                    u'GET',
                                    self.headers,
                                    None,
                                    not_found_ok=True,
                                    **kwargs)
        return data

    def list_batch_change_summaries(self,
                                    start_from=None,
                                    max_items=None,
                                    **kwargs):
        """
        Get list of user's batch change summaries.

        :return: the content of the response
        """
        args = []
        if start_from:
            args.append(u'startFrom={0}'.format(start_from))
        if max_items is not None:
            args.append(u'maxItems={0}'.format(max_items))

        url = urljoin(self.index_url,
                      u'/zones/batchrecordchanges') + u'?' + u'&'.join(args)

        _, data = self.make_request(url, u'GET', self.headers, **kwargs)
        return data

    def build_vinyldns_request(self,
                               method,
                               path,
                               body_data,
                               params=None,
                               **kwargs):
        """TODO: Add method docstring."""
        if isinstance(body_data, basestring):
            body_string = body_data
        else:
            body_string = json.dumps(body_data)

        new_headers = {u'X-Amz-Target': u'VinylDNS'}
        new_headers.update(kwargs.get(u'with_headers', dict()))

        suppress_headers = kwargs.get(u'suppress_headers', list())

        headers = self.build_headers(new_headers, suppress_headers)

        auth_header = self.signer.build_auth_header(method, path, headers,
                                                    body_string, params)
        headers[u'Authorization'] = auth_header

        return headers, body_string

    @staticmethod
    def build_headers(new_headers, suppressed_keys):
        """Construct HTTP headers for a request."""
        def canonical_header_name(field_name):
            return u'-'.join(word.capitalize()
                             for word in field_name.split(u'-'))

        import datetime
        now = datetime.datetime.utcnow()
        headers = {
            u'Content-Type': u'application/x-amz-json-1.0',
            u'Date': now.strftime(u'%a, %d %b %Y %H:%M:%S GMT'),
            u'X-Amz-Date': now.strftime(u'%Y%m%dT%H%M%SZ')
        }

        for k, v in iteritems(new_headers):
            headers[canonical_header_name(k)] = v

        for k in map(canonical_header_name, suppressed_keys):
            if k in headers:
                del headers[k]

        return headers

    def add_zone_acl_rule(self,
                          zone_id,
                          acl_rule,
                          sign_request=True,
                          **kwargs):
        """
        Put an acl rule on the zone.

        :param zone_id: The id of the zone to attach the acl rule to
        :param acl_rule: The acl rule contents
        :param sign_request: An indicator if we should sign the request;
        useful for testing auth
        :return: the content of the response
        """
        url = urljoin(self.index_url, '/zones/{0}/acl/rules'.format(zone_id))
        _, data = self.make_request(url,
                                    'PUT',
                                    self.headers,
                                    json.dumps(acl_rule),
                                    sign_request=sign_request,
                                    **kwargs)

        return data

    def delete_zone_acl_rule(self,
                             zone_id,
                             acl_rule,
                             sign_request=True,
                             **kwargs):
        """
        Delete an acl rule from the zone.

        :param zone_id: The id of the zone to remove the acl from
        :param acl_rule: The acl rule to remove
        :param sign_request:  An indicator if we should sign the request;
        useful for testing auth
        :return: the content of the response
        """
        url = urljoin(self.index_url, '/zones/{0}/acl/rules'.format(zone_id))
        _, data = self.make_request(url,
                                    'DELETE',
                                    self.headers,
                                    json.dumps(acl_rule),
                                    sign_request=sign_request,
                                    **kwargs)

        return data