diff --git a/gcloud/dns/changes.py b/gcloud/dns/changes.py new file mode 100644 index 000000000000..29a42f318abc --- /dev/null +++ b/gcloud/dns/changes.py @@ -0,0 +1,260 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Define API ResourceRecordSets.""" + +import datetime + +import six + +from gcloud._helpers import UTC +from gcloud._helpers import _RFC3339_MICROS +from gcloud.exceptions import NotFound +from gcloud.dns.resource_record_set import ResourceRecordSet + + +class Changes(object): + """Changes are bundled additions / deletions of DNS resource records. + + Changes are owned by a :class:`gcloud.dns.zone.ManagedZone` instance. + + See: + https://cloud.google.com/dns/api/v1/changes + + :type zone: :class:`gcloud.dns.zone.ManagedZone` + :param zone: A zone which holds one or more record sets. + """ + + def __init__(self, zone): + self.zone = zone + self._properties = {} + self._additions = self._deletions = () + + @classmethod + def from_api_repr(cls, resource, zone): + """Factory: construct a change set given its API representation + + :type resource: dict + :param resource: change set representation returned from the API + + :type zone: :class:`gcloud.dns.zone.ManagedZone` + :param zone: A zone which holds zero or more change sets. + + :rtype: :class:`gcloud.dns.changes.Changes` + :returns: RRS parsed from ``resource``. + """ + changes = cls(zone=zone) + changes._set_properties(resource) + return changes + + def _set_properties(self, resource): + """Helper method for :meth:`from_api_repr`, :meth:`create`, etc. + + :type resource: dict + :param resource: change set representation returned from the API + """ + resource = resource.copy() + self._additions = tuple([ + ResourceRecordSet.from_api_repr(added_res, self.zone) + for added_res in resource.pop('additions', ())]) + self._deletions = tuple([ + ResourceRecordSet.from_api_repr(added_res, self.zone) + for added_res in resource.pop('deletions', ())]) + self._properties = resource + + @property + def path(self): + """URL path for change set APIs. + + :rtype: string + :returns: the path based on project, zone, and change set names. + """ + return '/projects/%s/managedZones/%s/changes/%s' % ( + self.zone.project, self.zone.name, self.name) + + @property + def name(self): + """Name of the change set. + + :rtype: string or ``NoneType`` + :returns: Name, as set by the back-end, or None. + """ + return self._properties.get('id') + + @name.setter + def name(self, value): + """Update name of the change set. + + :type value: string + :param value: New name for the changeset. + """ + if not isinstance(value, six.string_types): + raise ValueError("Pass a string") + self._properties['id'] = value + + @property + def status(self): + """Status of the change set. + + :rtype: string or ``NoneType`` + :returns: Status, as set by the back-end, or None. + """ + return self._properties.get('status') + + @property + def started(self): + """Time when the change set was started. + + :rtype: ``datetime.datetime`` or ``NoneType`` + :returns: Time, as set by the back-end, or None. + """ + stamp = self._properties.get('startTime') + if stamp is not None: + return datetime.datetime.strptime(stamp, _RFC3339_MICROS).replace( + tzinfo=UTC) + + @property + def additions(self): + """Resource record sets to be added to the zone. + + :rtype: sequence of + :class:`gcloud.dns.resource_record_set.ResourceRecordSet'. + :returns: record sets appended via :meth:`add_record_set` + """ + return self._additions + + @property + def deletions(self): + """Resource record sets to be deleted from the zone. + + :rtype: sequence of + :class:`gcloud.dns.resource_record_set.ResourceRecordSet'. + :returns: record sets appended via :meth:`delete_record_set` + """ + return self._deletions + + def add_record_set(self, record_set): + """Append a record set to the 'additions' for the change set. + + :type record_set: + :class:`gcloud.dns.resource_record_set.ResourceRecordSet' + :param record_set: the record set to append + + :raises: ``ValueError`` if ``record_set`` is not of the required type. + """ + if not isinstance(record_set, ResourceRecordSet): + raise ValueError("Pass a ResourceRecordSet") + self._additions += (record_set,) + + def delete_record_set(self, record_set): + """Append a record set to the 'deletions' for the change set. + + :type record_set: + :class:`gcloud.dns.resource_record_set.ResourceRecordSet' + :param record_set: the record set to append + + :raises: ``ValueError`` if ``record_set`` is not of the required type. + """ + if not isinstance(record_set, ResourceRecordSet): + raise ValueError("Pass a ResourceRecordSet") + self._deletions += (record_set,) + + def _require_client(self, client): + """Check client or verify over-ride. + + :type client: :class:`gcloud.dns.client.Client` or ``NoneType`` + :param client: the client to use. If not passed, falls back to the + ``client`` stored on the current zone. + + :rtype: :class:`gcloud.dns.client.Client` + :returns: The client passed in or the currently bound client. + """ + if client is None: + client = self.zone._client + return client + + def _build_resource(self): + """Generate a resource for ``create``.""" + additions = [{ + 'name': added.name, + 'type': added.record_type, + 'ttl': str(added.ttl), + 'rrdatas': added.rrdatas, + } for added in self.additions] + + deletions = [{ + 'name': deleted.name, + 'type': deleted.record_type, + 'ttl': str(deleted.ttl), + 'rrdatas': deleted.rrdatas, + } for deleted in self.deletions] + + return { + 'additions': additions, + 'deletions': deletions, + } + + def create(self, client=None): + """API call: create the change set via a POST request + + See: + https://cloud.google.com/dns/api/v1/changes/create + + :type client: :class:`gcloud.dns.client.Client` or ``NoneType`` + :param client: the client to use. If not passed, falls back to the + ``client`` stored on the current zone. + """ + if len(self.additions) == 0 and len(self.deletions) == 0: + raise ValueError("No record sets added or deleted") + client = self._require_client(client) + path = '/projects/%s/managedZones/%s/changes' % ( + self.zone.project, self.zone.name) + api_response = client.connection.api_request( + method='POST', path=path, data=self._build_resource()) + self._set_properties(api_response) + + def exists(self, client=None): + """API call: test for the existence of the change set via a GET request + + See + https://cloud.google.com/dns/api/v1/changes/get + + :type client: :class:`gcloud.dns.client.Client` or ``NoneType`` + :param client: the client to use. If not passed, falls back to the + ``client`` stored on the current zone. + """ + client = self._require_client(client) + try: + client.connection.api_request(method='GET', path=self.path, + query_params={'fields': 'id'}) + except NotFound: + return False + else: + return True + + def reload(self, client=None): + """API call: refresh zone properties via a GET request + + See + https://cloud.google.com/dns/api/v1/changes/get + + :type client: :class:`gcloud.dns.client.Client` or ``NoneType`` + :param client: the client to use. If not passed, falls back to the + ``client`` stored on the current zone. + """ + client = self._require_client(client) + + api_response = client.connection.api_request( + method='GET', path=self.path) + self._set_properties(api_response) diff --git a/gcloud/dns/resource_record_set.py b/gcloud/dns/resource_record_set.py index b53fe8ceebea..dbd95b3b17c5 100644 --- a/gcloud/dns/resource_record_set.py +++ b/gcloud/dns/resource_record_set.py @@ -18,7 +18,7 @@ class ResourceRecordSet(object): """ResourceRecordSets are DNS resource records. - RRS are contained wihin a :class:`gcloud.dns.zone.ManagedZone` instance. + RRS are owned by a :class:`gcloud.dns.zone.ManagedZone` instance. See: https://cloud.google.com/dns/api/v1/resourceRecordSets diff --git a/gcloud/dns/test_changes.py b/gcloud/dns/test_changes.py new file mode 100644 index 000000000000..aa402a610665 --- /dev/null +++ b/gcloud/dns/test_changes.py @@ -0,0 +1,346 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest2 + + +class TestChanges(unittest2.TestCase): + PROJECT = 'project' + ZONE_NAME = 'example.com' + CHANGES_NAME = 'changeset_id' + + def _getTargetClass(self): + from gcloud.dns.changes import Changes + return Changes + + def _makeOne(self, *args, **kw): + return self._getTargetClass()(*args, **kw) + + def _setUpConstants(self): + from gcloud._helpers import UTC + from gcloud._helpers import _NOW + self.WHEN = _NOW().replace(tzinfo=UTC) + + def _makeResource(self): + from gcloud._helpers import _RFC3339_MICROS + when_str = self.WHEN.strftime(_RFC3339_MICROS) + return { + 'kind': 'dns#change', + 'id': self.CHANGES_NAME, + 'startTime': when_str, + 'status': 'done', + 'additions': [ + {'name': 'test.example.com', + 'type': 'CNAME', + 'ttl': '3600', + 'rrdatas': ['www.example.com']}, + ], + 'deletions': [ + {'name': 'test.example.com', + 'type': 'CNAME', + 'ttl': '86400', + 'rrdatas': ['other.example.com']}, + ], + } + + def _verifyResourceProperties(self, changes, resource, zone): + import datetime + from gcloud._helpers import UTC + from gcloud._helpers import _RFC3339_MICROS + self.assertEqual(changes.name, resource['id']) + started = datetime.datetime.strptime( + resource['startTime'], _RFC3339_MICROS).replace(tzinfo=UTC) + self.assertEqual(changes.started, started) + self.assertEqual(changes.status, resource['status']) + + r_additions = resource.get('additions', ()) + self.assertEqual(len(changes.additions), len(r_additions)) + for found, expected in zip(changes.additions, r_additions): + self.assertEqual(found.name, expected['name']) + self.assertEqual(found.record_type, expected['type']) + self.assertEqual(found.ttl, int(expected['ttl'])) + self.assertEqual(found.rrdatas, expected['rrdatas']) + self.assertTrue(found.zone is zone) + + r_deletions = resource.get('deletions', ()) + self.assertEqual(len(changes.deletions), len(r_deletions)) + for found, expected in zip(changes.deletions, r_deletions): + self.assertEqual(found.name, expected['name']) + self.assertEqual(found.record_type, expected['type']) + self.assertEqual(found.ttl, int(expected['ttl'])) + self.assertEqual(found.rrdatas, expected['rrdatas']) + self.assertTrue(found.zone is zone) + + def test_ctor(self): + zone = _Zone() + + changes = self._makeOne(zone) + + self.assertTrue(changes.zone is zone) + self.assertEqual(changes.name, None) + self.assertEqual(changes.status, None) + self.assertEqual(changes.started, None) + self.assertEqual(list(changes.additions), []) + self.assertEqual(list(changes.deletions), []) + + def test_from_api_repr_missing_additions_deletions(self): + self._setUpConstants() + RESOURCE = self._makeResource() + del RESOURCE['additions'] + del RESOURCE['deletions'] + zone = _Zone() + klass = self._getTargetClass() + + changes = klass.from_api_repr(RESOURCE, zone=zone) + + self._verifyResourceProperties(changes, RESOURCE, zone) + + def test_from_api_repr(self): + self._setUpConstants() + RESOURCE = self._makeResource() + zone = _Zone() + klass = self._getTargetClass() + + changes = klass.from_api_repr(RESOURCE, zone=zone) + + self._verifyResourceProperties(changes, RESOURCE, zone) + + def test_name_setter_bad_value(self): + zone = _Zone() + changes = self._makeOne(zone) + with self.assertRaises(ValueError): + changes.name = 12345 + + def test_name_setter(self): + zone = _Zone() + changes = self._makeOne(zone) + changes.name = 'NAME' + self.assertEqual(changes.name, 'NAME') + + def test_add_record_set_invalid_value(self): + zone = _Zone() + changes = self._makeOne(zone) + + with self.assertRaises(ValueError): + changes.add_record_set(object()) + + def test_add_record_set(self): + from gcloud.dns.resource_record_set import ResourceRecordSet + zone = _Zone() + changes = self._makeOne(zone) + rrs = ResourceRecordSet('test.example.com', 'CNAME', 3600, + ['www.example.com'], zone) + changes.add_record_set(rrs) + self.assertEqual(list(changes.additions), [rrs]) + + def test_delete_record_set_invalid_value(self): + zone = _Zone() + changes = self._makeOne(zone) + + with self.assertRaises(ValueError): + changes.delete_record_set(object()) + + def test_delete_record_set(self): + from gcloud.dns.resource_record_set import ResourceRecordSet + zone = _Zone() + changes = self._makeOne(zone) + rrs = ResourceRecordSet('test.example.com', 'CNAME', 3600, + ['www.example.com'], zone) + changes.delete_record_set(rrs) + self.assertEqual(list(changes.deletions), [rrs]) + + def test_create_wo_additions_or_deletions(self): + self._setUpConstants() + RESOURCE = self._makeResource() + conn = _Connection(RESOURCE) + client = _Client(project=self.PROJECT, connection=conn) + zone = _Zone(client) + changes = self._makeOne(zone) + + with self.assertRaises(ValueError): + changes.create() + + self.assertEqual(len(conn._requested), 0) + + def test_create_w_bound_client(self): + from gcloud.dns.resource_record_set import ResourceRecordSet + self._setUpConstants() + RESOURCE = self._makeResource() + PATH = 'projects/%s/managedZones/%s/changes' % ( + self.PROJECT, self.ZONE_NAME) + conn = _Connection(RESOURCE) + client = _Client(project=self.PROJECT, connection=conn) + zone = _Zone(client) + changes = self._makeOne(zone) + changes.add_record_set(ResourceRecordSet( + 'test.example.com', 'CNAME', 3600, ['www.example.com'], zone)) + changes.delete_record_set(ResourceRecordSet( + 'test.example.com', 'CNAME', 86400, ['other.example.com'], zone)) + + changes.create() + + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'POST') + self.assertEqual(req['path'], '/%s' % PATH) + SENT = { + 'additions': RESOURCE['additions'], + 'deletions': RESOURCE['deletions'], + } + self.assertEqual(req['data'], SENT) + self._verifyResourceProperties(changes, RESOURCE, zone) + + def test_create_w_alternate_client(self): + from gcloud.dns.resource_record_set import ResourceRecordSet + self._setUpConstants() + RESOURCE = self._makeResource() + PATH = 'projects/%s/managedZones/%s/changes' % ( + self.PROJECT, self.ZONE_NAME) + conn1 = _Connection() + client1 = _Client(project=self.PROJECT, connection=conn1) + conn2 = _Connection(RESOURCE) + client2 = _Client(project=self.PROJECT, connection=conn2) + zone = _Zone(client1) + changes = self._makeOne(zone) + changes.add_record_set(ResourceRecordSet( + 'test.example.com', 'CNAME', 3600, ['www.example.com'], zone)) + changes.delete_record_set(ResourceRecordSet( + 'test.example.com', 'CNAME', 86400, ['other.example.com'], zone)) + + changes.create(client=client2) + + self.assertEqual(len(conn1._requested), 0) + self.assertEqual(len(conn2._requested), 1) + req = conn2._requested[0] + self.assertEqual(req['method'], 'POST') + self.assertEqual(req['path'], '/%s' % PATH) + SENT = { + 'additions': RESOURCE['additions'], + 'deletions': RESOURCE['deletions'], + } + self.assertEqual(req['data'], SENT) + self._verifyResourceProperties(changes, RESOURCE, zone) + + def test_exists_miss_w_bound_client(self): + PATH = 'projects/%s/managedZones/%s/changes/%s' % ( + self.PROJECT, self.ZONE_NAME, self.CHANGES_NAME) + self._setUpConstants() + conn = _Connection() + client = _Client(project=self.PROJECT, connection=conn) + zone = _Zone(client) + changes = self._makeOne(zone) + changes.name = self.CHANGES_NAME + + self.assertFalse(changes.exists()) + + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'GET') + self.assertEqual(req['path'], '/%s' % PATH) + self.assertEqual(req['query_params'], {'fields': 'id'}) + + def test_exists_hit_w_alternate_client(self): + PATH = 'projects/%s/managedZones/%s/changes/%s' % ( + self.PROJECT, self.ZONE_NAME, self.CHANGES_NAME) + conn1 = _Connection() + client1 = _Client(project=self.PROJECT, connection=conn1) + conn2 = _Connection({}) + client2 = _Client(project=self.PROJECT, connection=conn2) + zone = _Zone(client1) + changes = self._makeOne(zone) + changes.name = self.CHANGES_NAME + + self.assertTrue(changes.exists(client=client2)) + + self.assertEqual(len(conn1._requested), 0) + self.assertEqual(len(conn2._requested), 1) + req = conn2._requested[0] + self.assertEqual(req['method'], 'GET') + self.assertEqual(req['path'], '/%s' % PATH) + self.assertEqual(req['query_params'], {'fields': 'id'}) + + def test_reload_w_bound_client(self): + PATH = 'projects/%s/managedZones/%s/changes/%s' % ( + self.PROJECT, self.ZONE_NAME, self.CHANGES_NAME) + self._setUpConstants() + RESOURCE = self._makeResource() + conn = _Connection(RESOURCE) + client = _Client(project=self.PROJECT, connection=conn) + zone = _Zone(client) + changes = self._makeOne(zone) + changes.name = self.CHANGES_NAME + + changes.reload() + + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'GET') + self.assertEqual(req['path'], '/%s' % PATH) + self._verifyResourceProperties(changes, RESOURCE, zone) + + def test_reload_w_alternate_client(self): + PATH = 'projects/%s/managedZones/%s/changes/%s' % ( + self.PROJECT, self.ZONE_NAME, self.CHANGES_NAME) + self._setUpConstants() + RESOURCE = self._makeResource() + conn1 = _Connection() + client1 = _Client(project=self.PROJECT, connection=conn1) + conn2 = _Connection(RESOURCE) + client2 = _Client(project=self.PROJECT, connection=conn2) + zone = _Zone(client1) + changes = self._makeOne(zone) + changes.name = self.CHANGES_NAME + + changes.reload(client=client2) + + self.assertEqual(len(conn1._requested), 0) + self.assertEqual(len(conn2._requested), 1) + req = conn2._requested[0] + self.assertEqual(req['method'], 'GET') + self.assertEqual(req['path'], '/%s' % PATH) + self._verifyResourceProperties(changes, RESOURCE, zone) + + +class _Zone(object): + + def __init__(self, client=None, project=TestChanges.PROJECT, + name=TestChanges.ZONE_NAME): + self._client = client + self.project = project + self.name = name + + +class _Client(object): + + def __init__(self, project='project', connection=None): + self.project = project + self.connection = connection + + +class _Connection(object): + + def __init__(self, *responses): + self._responses = responses + self._requested = [] + + def api_request(self, **kw): + from gcloud.exceptions import NotFound + self._requested.append(kw) + + try: + response, self._responses = self._responses[0], self._responses[1:] + except: + raise NotFound('miss') + else: + return response diff --git a/gcloud/dns/test_zone.py b/gcloud/dns/test_zone.py index 760bc372b3df..7fc26fef2976 100644 --- a/gcloud/dns/test_zone.py +++ b/gcloud/dns/test_zone.py @@ -415,6 +415,163 @@ def test_list_resource_record_sets_explicit(self): self.assertEqual(req['query_params'], {'maxResults': 3, 'pageToken': TOKEN}) + def test_list_changes_defaults(self): + from gcloud._helpers import _RFC3339_MICROS + from gcloud.dns.changes import Changes + from gcloud.dns.resource_record_set import ResourceRecordSet + self._setUpConstants() + PATH = 'projects/%s/managedZones/%s/changes' % ( + self.PROJECT, self.ZONE_NAME) + TOKEN = 'TOKEN' + NAME_1 = 'www.example.com' + TYPE_1 = 'A' + TTL_1 = '86400' + RRDATAS_1 = ['123.45.67.89'] + NAME_2 = 'alias.example.com' + TYPE_2 = 'CNAME' + TTL_2 = '3600' + RRDATAS_2 = ['www.example.com'] + CHANGES_NAME = 'changeset_id' + DATA = { + 'nextPageToken': TOKEN, + 'changes': [{ + 'kind': 'dns#change', + 'id': CHANGES_NAME, + 'status': 'pending', + 'startTime': self.WHEN.strftime(_RFC3339_MICROS), + 'additions': [ + {'kind': 'dns#resourceRecordSet', + 'name': NAME_1, + 'type': TYPE_1, + 'ttl': TTL_1, + 'rrdatas': RRDATAS_1}], + 'deletions': [ + {'kind': 'dns#change', + 'name': NAME_2, + 'type': TYPE_2, + 'ttl': TTL_2, + 'rrdatas': RRDATAS_2}], + }] + } + conn = _Connection(DATA) + client = _Client(project=self.PROJECT, connection=conn) + zone = self._makeOne(self.ZONE_NAME, self.DNS_NAME, client) + + changes, token = zone.list_changes() + + self.assertEqual(len(changes), len(DATA['changes'])) + for found, expected in zip(changes, DATA['changes']): + self.assertTrue(isinstance(found, Changes)) + self.assertEqual(found.name, CHANGES_NAME) + self.assertEqual(found.status, 'pending') + self.assertEqual(found.started, self.WHEN) + + self.assertEqual(len(found.additions), len(expected['additions'])) + for found_rr, expected_rr in zip(found.additions, + expected['additions']): + self.assertTrue(isinstance(found_rr, ResourceRecordSet)) + self.assertEqual(found_rr.name, expected_rr['name']) + self.assertEqual(found_rr.record_type, expected_rr['type']) + self.assertEqual(found_rr.ttl, int(expected_rr['ttl'])) + self.assertEqual(found_rr.rrdatas, expected_rr['rrdatas']) + + self.assertEqual(len(found.deletions), len(expected['deletions'])) + for found_rr, expected_rr in zip(found.deletions, + expected['deletions']): + self.assertTrue(isinstance(found_rr, ResourceRecordSet)) + self.assertEqual(found_rr.name, expected_rr['name']) + self.assertEqual(found_rr.record_type, expected_rr['type']) + self.assertEqual(found_rr.ttl, int(expected_rr['ttl'])) + self.assertEqual(found_rr.rrdatas, expected_rr['rrdatas']) + + self.assertEqual(token, TOKEN) + + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'GET') + self.assertEqual(req['path'], '/%s' % PATH) + + def test_list_changes_explicit(self): + from gcloud._helpers import _RFC3339_MICROS + from gcloud.dns.changes import Changes + from gcloud.dns.resource_record_set import ResourceRecordSet + self._setUpConstants() + PATH = 'projects/%s/managedZones/%s/changes' % ( + self.PROJECT, self.ZONE_NAME) + TOKEN = 'TOKEN' + NAME_1 = 'www.example.com' + TYPE_1 = 'A' + TTL_1 = '86400' + RRDATAS_1 = ['123.45.67.89'] + NAME_2 = 'alias.example.com' + TYPE_2 = 'CNAME' + TTL_2 = '3600' + RRDATAS_2 = ['www.example.com'] + CHANGES_NAME = 'changeset_id' + DATA = { + 'changes': [{ + 'kind': 'dns#change', + 'id': CHANGES_NAME, + 'status': 'pending', + 'startTime': self.WHEN.strftime(_RFC3339_MICROS), + 'additions': [ + {'kind': 'dns#resourceRecordSet', + 'name': NAME_1, + 'type': TYPE_1, + 'ttl': TTL_1, + 'rrdatas': RRDATAS_1}], + 'deletions': [ + {'kind': 'dns#change', + 'name': NAME_2, + 'type': TYPE_2, + 'ttl': TTL_2, + 'rrdatas': RRDATAS_2}], + }] + } + conn1 = _Connection() + client1 = _Client(project=self.PROJECT, connection=conn1) + conn2 = _Connection(DATA) + client2 = _Client(project=self.PROJECT, connection=conn2) + zone = self._makeOne(self.ZONE_NAME, self.DNS_NAME, client1) + + changes, token = zone.list_changes( + max_results=3, page_token=TOKEN, client=client2) + + self.assertEqual(len(changes), len(DATA['changes'])) + for found, expected in zip(changes, DATA['changes']): + self.assertTrue(isinstance(found, Changes)) + self.assertEqual(found.name, CHANGES_NAME) + self.assertEqual(found.status, 'pending') + self.assertEqual(found.started, self.WHEN) + + self.assertEqual(len(found.additions), len(expected['additions'])) + for found_rr, expected_rr in zip(found.additions, + expected['additions']): + self.assertTrue(isinstance(found_rr, ResourceRecordSet)) + self.assertEqual(found_rr.name, expected_rr['name']) + self.assertEqual(found_rr.record_type, expected_rr['type']) + self.assertEqual(found_rr.ttl, int(expected_rr['ttl'])) + self.assertEqual(found_rr.rrdatas, expected_rr['rrdatas']) + + self.assertEqual(len(found.deletions), len(expected['deletions'])) + for found_rr, expected_rr in zip(found.deletions, + expected['deletions']): + self.assertTrue(isinstance(found_rr, ResourceRecordSet)) + self.assertEqual(found_rr.name, expected_rr['name']) + self.assertEqual(found_rr.record_type, expected_rr['type']) + self.assertEqual(found_rr.ttl, int(expected_rr['ttl'])) + self.assertEqual(found_rr.rrdatas, expected_rr['rrdatas']) + + self.assertEqual(token, None) + + self.assertEqual(len(conn1._requested), 0) + self.assertEqual(len(conn2._requested), 1) + req = conn2._requested[0] + self.assertEqual(req['method'], 'GET') + self.assertEqual(req['path'], '/%s' % PATH) + self.assertEqual(req['query_params'], + {'maxResults': 3, 'pageToken': TOKEN}) + class _Client(object): diff --git a/gcloud/dns/zone.py b/gcloud/dns/zone.py index 4ed9eea65ac2..420dadd52cbc 100644 --- a/gcloud/dns/zone.py +++ b/gcloud/dns/zone.py @@ -17,6 +17,7 @@ from gcloud._helpers import _datetime_from_microseconds from gcloud.exceptions import NotFound +from gcloud.dns.changes import Changes from gcloud.dns.resource_record_set import ResourceRecordSet @@ -312,3 +313,46 @@ def list_resource_record_sets(self, max_results=None, page_token=None, zones = [ResourceRecordSet.from_api_repr(resource, self) for resource in resp['rrsets']] return zones, resp.get('nextPageToken') + + def list_changes(self, max_results=None, page_token=None, client=None): + """List change sets for this zone. + + See: + https://cloud.google.com/dns/api/v1/resourceRecordSets/list + + :type max_results: int + :param max_results: maximum number of zones to return, If not + passed, defaults to a value set by the API. + + :type page_token: string + :param page_token: opaque marker for the next "page" of zones. If + not passed, the API will return the first page of + zones. + + :type client: :class:`gcloud.dns.client.Client` or ``NoneType`` + :param client: the client to use. If not passed, falls back to the + ``client`` stored on the current zone. + + :rtype: tuple, (list, str) + :returns: list of + :class:`gcloud.dns.resource_record_set.ResourceRecordSet`, + plus a "next page token" string: if the token is not None, + indicates that more zones can be retrieved with another + call (pass that value as ``page_token``). + """ + params = {} + + if max_results is not None: + params['maxResults'] = max_results + + if page_token is not None: + params['pageToken'] = page_token + + path = '/projects/%s/managedZones/%s/changes' % ( + self.project, self.name) + client = self._require_client(client) + conn = client.connection + resp = conn.api_request(method='GET', path=path, query_params=params) + zones = [Changes.from_api_repr(resource, self) + for resource in resp['changes']] + return zones, resp.get('nextPageToken')