Add zones_export_client's methods and tests to Designate tempest plugin
Partially-Implements: blueprint designate-tempest-plugin
Change-Id: Iff03a53842ac4e44ed720163695a35a74b970768
diff --git a/designate_tempest_plugin/clients.py b/designate_tempest_plugin/clients.py
index d042f65..d461a53 100644
--- a/designate_tempest_plugin/clients.py
+++ b/designate_tempest_plugin/clients.py
@@ -22,6 +22,8 @@
BlacklistsClient
from designate_tempest_plugin.services.dns.admin.json.quotas_client import \
QuotasClient
+from designate_tempest_plugin.services.dns.v2.json.zone_exports_client import \
+ ZoneExportsClient
CONF = config.CONF
@@ -44,3 +46,5 @@
**params)
self.blacklists_client = BlacklistsClient(self.auth_provider, **params)
self.quotas_client = QuotasClient(self.auth_provider, **params)
+ self.zone_exports_client = ZoneExportsClient(self.auth_provider,
+ **params)
diff --git a/designate_tempest_plugin/common/models.py b/designate_tempest_plugin/common/models.py
new file mode 100644
index 0000000..e69bd09
--- /dev/null
+++ b/designate_tempest_plugin/common/models.py
@@ -0,0 +1,81 @@
+"""
+Copyright 2015 Rackspace
+
+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.
+"""
+
+
+class ZoneFile(object):
+
+ def __init__(self, origin, ttl, records):
+ self.origin = origin
+ self.ttl = ttl
+ self.records = records
+
+ def __str__(self):
+ return str(self.__dict__)
+
+ def __repr__(self):
+ return str(self)
+
+ def __eq__(self, other):
+ return self.__dict__ == other.__dict__
+
+ @classmethod
+ def from_text(cls, text):
+ """Return a ZoneFile from a string containing the zone file contents"""
+ # filter out empty lines and strip all leading/trailing whitespace.
+ # this assumes no multiline records
+ lines = [x.strip() for x in text.split('\n') if x.strip()]
+
+ assert lines[0].startswith('$ORIGIN')
+ assert lines[1].startswith('$TTL')
+
+ return ZoneFile(
+ origin=lines[0].split(' ')[1],
+ ttl=int(lines[1].split(' ')[1]),
+ records=[ZoneFileRecord.from_text(x) for x in lines[2:]],
+ )
+
+
+class ZoneFileRecord(object):
+
+ def __init__(self, name, type, data):
+ self.name = str(name)
+ self.type = str(type)
+ self.data = str(data)
+
+ def __str__(self):
+ return str(self.__dict__)
+
+ def __repr__(self):
+ return str(self)
+
+ def __eq__(self, other):
+ return self.__dict__ == other.__dict__
+
+ def __hash__(self):
+ return hash(tuple(sorted(self.__dict__.items())))
+
+ @classmethod
+ def from_text(cls, text):
+ """Create a ZoneFileRecord from a line of text of a zone file, like:
+
+ mydomain.com. IN NS ns1.example.com.
+ """
+ # assumes records don't have a TTL between the name and the class.
+ # assumes no parentheses in the record, all on a single line.
+ parts = [x for x in text.split(' ', 4) if x.strip()]
+ name, rclass, rtype, data = parts
+ assert rclass == 'IN'
+ return cls(name=name, type=rtype, data=data)
diff --git a/designate_tempest_plugin/services/dns/json/base.py b/designate_tempest_plugin/services/dns/json/base.py
index 9068c78..b53712f 100644
--- a/designate_tempest_plugin/services/dns/json/base.py
+++ b/designate_tempest_plugin/services/dns/json/base.py
@@ -16,8 +16,10 @@
from oslo_log import log as logging
from oslo_serialization import jsonutils as json
from tempest.lib.common import rest_client
+from tempest.lib import exceptions as lib_exc
from six.moves.urllib import parse as urllib
+from designate_tempest_plugin.common.models import ZoneFile
LOG = logging.getLogger(__name__)
@@ -50,8 +52,13 @@
def serialize(self, object_dict):
return json.dumps(object_dict)
- def deserialize(self, object_str):
- return json.loads(object_str)
+ def deserialize(self, resp, object_str):
+ if 'application/json' in resp['content-type']:
+ return json.loads(object_str)
+ elif 'text/dns' in resp['content-type']:
+ return ZoneFile.from_text(object_str)
+ else:
+ raise lib_exc.InvalidContentType()
def expected_success(self, expected_code, read_code):
# the base class method does not check correctly if read_code is not
@@ -84,7 +91,7 @@
uuid=uuid,
params=params)
- def _create_request(self, resource, object_dict, params=None,
+ def _create_request(self, resource, object_dict=None, params=None,
headers=None, extra_headers=False):
"""Create an object of the specified type.
:param resource: The name of the REST resource, e.g., 'zones'.
@@ -108,9 +115,9 @@
extra_headers=extra_headers)
self.expected_success([201, 202], resp.status)
- return resp, self.deserialize(body)
+ return resp, self.deserialize(resp, body)
- def _show_request(self, resource, uuid, params=None):
+ def _show_request(self, resource, uuid, headers=None, params=None):
"""Gets a specific object of the specified type.
:param resource: The name of the REST resource, e.g., 'zones'.
:param uuid: Unique identifier of the object in UUID format.
@@ -120,11 +127,11 @@
"""
uri = self.get_uri(resource, uuid=uuid, params=params)
- resp, body = self.get(uri)
+ resp, body = self.get(uri, headers=headers)
self.expected_success(200, resp.status)
- return resp, self.deserialize(body)
+ return resp, self.deserialize(resp, body)
def _list_request(self, resource, params=None):
"""Gets a list of objects.
@@ -139,7 +146,7 @@
self.expected_success(200, resp.status)
- return resp, self.deserialize(body)
+ return resp, self.deserialize(resp, body)
def _update_request(self, resource, uuid, object_dict, params=None):
"""Updates the specified object.
@@ -158,7 +165,7 @@
self.expected_success([200, 202], resp.status)
- return resp, self.deserialize(body)
+ return resp, self.deserialize(resp, body)
def _delete_request(self, resource, uuid, params=None):
"""Deletes the specified object.
@@ -173,6 +180,6 @@
resp, body = self.delete(uri)
self.expected_success([202, 204], resp.status)
if resp.status == 202:
- body = self.deserialize(body)
+ body = self.deserialize(resp, body)
return resp, body
diff --git a/designate_tempest_plugin/services/dns/v2/json/zone_exports_client.py b/designate_tempest_plugin/services/dns/v2/json/zone_exports_client.py
new file mode 100644
index 0000000..15c8339
--- /dev/null
+++ b/designate_tempest_plugin/services/dns/v2/json/zone_exports_client.py
@@ -0,0 +1,93 @@
+# Copyright 2016 NEC Corporation. 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.
+
+from designate_tempest_plugin.common import waiters
+from designate_tempest_plugin.services.dns.v2.json import base
+
+
+class ZoneExportsClient(base.DnsClientV2Base):
+
+ @base.handle_errors
+ def create_zone_export(self, uuid, params=None, wait_until=False):
+ """Create a zone export.
+ :param uuid: Unique identifier of the zone in UUID format.
+ :param params: A Python dict that represents the query paramaters to
+ include in the request URI.
+ :param wait_until: Block until the exported zone reaches the
+ desired status
+ :return: Serialized imported zone as a dictionary.
+ """
+
+ export_uri = 'zones/{0}/tasks/export'.format(uuid)
+ resp, body = self._create_request(
+ export_uri, params=params)
+
+ # Create Zone Export should Return a HTTP 202
+ self.expected_success(202, resp.status)
+
+ if wait_until:
+ waiters.wait_for_zone_export_status(self, body['id'], wait_until)
+
+ return resp, body
+
+ @base.handle_errors
+ def show_zone_export_records(self, uuid, params=None):
+ """Gets a specific exported zone.
+ :param uuid: Unique identifier of the exported zone in UUID format.
+ :param params: A Python dict that represents the query paramaters to
+ include in the request URI.
+ :return: Serialized exported zone as a dictionary.
+ """
+ return self._show_request(
+ 'zones/tasks/exports', uuid, params=params)
+
+ @base.handle_errors
+ def show_zone_exported(self, uuid, params=None):
+ """Gets a specific exported zone.
+ :param uuid: Unique identifier of the exported zone in UUID format.
+ :param params: A Python dict that represents the query paramaters to
+ include in the request URI.
+ :return: Serialized exported zone as a dictionary.
+ """
+ headers = {'Accept': 'text/dns'}
+
+ return self._show_request(
+ 'zones/tasks/exports/{0}/export'.format(uuid),
+ headers=headers, params=params)
+
+ @base.handle_errors
+ def list_zones_exports(self, params=None):
+ """Gets all the exported zones
+ :param params: A Python dict that represents the query paramaters to
+ include in the request URI.
+ :return: Serialized exported zone as a list.
+ """
+ return self._list_request(
+ 'zones/tasks/exports', params=params)
+
+ @base.handle_errors
+ def delete_zone_export(self, uuid, params=None):
+ """Deletes an exported zone having the specified UUID.
+ :param uuid: The unique identifier of the exported zone.
+ :param params: A Python dict that represents the query paramaters to
+ include in the request URI.
+ :return: A tuple with the server response and the response body.
+ """
+ resp, body = self._delete_request(
+ 'zones/tasks/exports', uuid, params=params)
+
+ # Delete Zone export should Return a HTTP 204
+ self.expected_success(204, resp.status)
+
+ return resp, body
diff --git a/designate_tempest_plugin/tests/api/v2/test_zones_exports.py b/designate_tempest_plugin/tests/api/v2/test_zones_exports.py
new file mode 100644
index 0000000..d11bb64
--- /dev/null
+++ b/designate_tempest_plugin/tests/api/v2/test_zones_exports.py
@@ -0,0 +1,97 @@
+# Copyright 2016 NEC Corporation. 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.
+
+from oslo_log import log as logging
+from tempest import test
+from tempest.lib import exceptions as lib_exc
+
+from designate_tempest_plugin.tests import base
+
+LOG = logging.getLogger(__name__)
+
+
+class BaseZoneExportsTest(base.BaseDnsTest):
+ excluded_keys = ['created_at', 'updated_at', 'version', 'links',
+ 'status', 'location']
+
+
+class ZonesExportTest(BaseZoneExportsTest):
+ @classmethod
+ def setup_clients(cls):
+ super(ZonesExportTest, cls).setup_clients()
+
+ cls.zone_client = cls.os.zones_client
+ cls.client = cls.os.zone_exports_client
+
+ @test.attr(type='smoke')
+ @test.idempotent_id('2dd8a9a0-98a2-4bf6-bb51-286583b30f40')
+ def test_create_zone_export(self):
+ LOG.info('Create a zone')
+ _, zone = self.zone_client.create_zone()
+ self.addCleanup(self.zone_client.delete_zone, zone['id'])
+
+ LOG.info('Create a zone export')
+ _, zone_export = self.client.create_zone_export(zone['id'])
+
+ LOG.info('Ensure we respond with PENDING')
+ self.assertEqual('PENDING', zone_export['status'])
+
+ @test.attr(type='smoke')
+ @test.idempotent_id('2d29a2a9-1941-4b7e-9d8a-ad6c2140ea68')
+ def test_show_zone_export(self):
+ LOG.info('Create a zone')
+ _, zone = self.zone_client.create_zone()
+ self.addCleanup(self.zone_client.delete_zone, zone['id'])
+
+ LOG.info('Create a zone export')
+ resp, zone_export = self.client.create_zone_export(zone['id'])
+
+ LOG.info('Re-Fetch the zone export records')
+ _, body = self.client.show_zone_export_records(str(zone_export['id']))
+
+ LOG.info('Ensure the fetched response matches the records of '
+ 'exported zone')
+ self.assertExpected(zone_export, body, self.excluded_keys)
+
+ @test.attr(type='smoke')
+ @test.idempotent_id('97234f00-8bcb-43f8-84dd-874f8bc4a80e')
+ def test_delete_zone_export(self):
+ LOG.info('Create a zone')
+ _, zone = self.zone_client.create_zone()
+ self.addCleanup(self.zone_client.delete_zone, zone['id'],
+ ignore_errors=lib_exc.NotFound)
+
+ LOG.info('Create a zone export')
+ _, zone_export = self.client.create_zone_export(zone['id'])
+
+ LOG.info('Delete the exported zone')
+ _, body = self.client.delete_zone_export(zone_export['id'])
+
+ LOG.info('Ensure the exported zone has been successfully deleted')
+ self.assertRaises(lib_exc.NotFound,
+ lambda: self.client.show_zone_export_records(zone_export['id']))
+
+ @test.attr(type='smoke')
+ @test.idempotent_id('476bfdfe-58c8-46e2-b376-8403c0fff440')
+ def test_list_zones_exports(self):
+ LOG.info('Create a zone')
+ _, zone = self.zone_client.create_zone()
+ self.addCleanup(self.zone_client.delete_zone, zone['id'])
+
+ _, export = self.client.create_zone_export(zone['id'])
+
+ LOG.info('List zones exports')
+ _, body = self.client.list_zones_exports()
+
+ self.assertTrue(len(body['exports']) > 0)