Add recordset_client's methods and tests to Designate tempest plugin

Partially-Implements: blueprint designate-tempest-plugin

Change-Id: I55ebc5210f7b1e50b59411658a1ae4d1f39a3ff4
diff --git a/designate_tempest_plugin/clients.py b/designate_tempest_plugin/clients.py
index d461a53..db1c61f 100644
--- a/designate_tempest_plugin/clients.py
+++ b/designate_tempest_plugin/clients.py
@@ -24,6 +24,8 @@
     QuotasClient
 from designate_tempest_plugin.services.dns.v2.json.zone_exports_client import \
     ZoneExportsClient
+from designate_tempest_plugin.services.dns.v2.json.recordset_client import \
+    RecordsetClient
 
 CONF = config.CONF
 
@@ -48,3 +50,5 @@
         self.quotas_client = QuotasClient(self.auth_provider, **params)
         self.zone_exports_client = ZoneExportsClient(self.auth_provider,
                                                      **params)
+        self.recordset_client = RecordsetClient(self.auth_provider,
+                                                **params)
diff --git a/designate_tempest_plugin/common/waiters.py b/designate_tempest_plugin/common/waiters.py
index 9563656..dbdbd5a 100644
--- a/designate_tempest_plugin/common/waiters.py
+++ b/designate_tempest_plugin/common/waiters.py
@@ -114,3 +114,37 @@
                 message = '(%s) %s' % (caller, message)
 
             raise lib_exc.TimeoutException(message)
+
+
+def wait_for_recordset_status(client, recordset_id, status):
+    """Waits for a recordset to reach the given status."""
+    LOG.info('Waiting for recordset %s to reach %s',
+             recordset_id, status)
+
+    _, recordset = client.show_recordset(recordset_id)
+    start = int(time.time())
+
+    while recordset['status'] != status:
+        time.sleep(client.build_interval)
+        _, recordset = client.show_recordset(recordset_id)
+        status_curr = recordset['status']
+        if status_curr == status:
+            LOG.info('Recordset %s reached %s', recordset_id, status)
+            return
+
+        if int(time.time()) - start >= client.build_timeout:
+            message = ('Recordset %(recordset_id)s failed to reach '
+                       'status=%(status) within the required time '
+                       '(%(timeout)s s). Current '
+                       'status: %(status_curr)s' %
+                       {'recordset_id': recordset_id,
+                        'status': status,
+                        'status_curr': status_curr,
+                        'timeout': client.build_timeout})
+
+            caller = misc_utils.find_test_caller()
+
+            if caller:
+                message = '(%s) %s' % (caller, message)
+
+            raise lib_exc.TimeoutException(message)
\ No newline at end of file
diff --git a/designate_tempest_plugin/data_utils.py b/designate_tempest_plugin/data_utils.py
index c77c4b1..322e506 100644
--- a/designate_tempest_plugin/data_utils.py
+++ b/designate_tempest_plugin/data_utils.py
@@ -11,12 +11,27 @@
 # 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 random
+
+import netaddr
 from oslo_log import log as logging
 from tempest.lib.common.utils import data_utils
 
 LOG = logging.getLogger(__name__)
 
 
+def rand_ip():
+    return ".".join(str(random.randrange(0, 256)) for _ in range(4))
+
+
+def rand_ipv6():
+    def hexes(n):
+        return "".join(random.choice("1234567890abcdef") for _ in range(n))
+    result = ":".join(hexes(4) for _ in range(8))
+    an = netaddr.IPAddress(result, version=6)
+    return an.format(netaddr.ipv6_compact)
+
+
 def rand_zone_name(name='', prefix=None, suffix='.com.'):
     """Generate a random zone name
     :param str name: The name that you want to include
@@ -80,3 +95,104 @@
             #     api_export_size or data_utils.rand_int_id(100, 999999),
         }
     }
+
+
+def rand_zone_data(name=None, email=None, ttl=None, description=None):
+    """Generate random zone data, with optional overrides
+
+    :return: A ZoneModel
+    """
+    if name is None:
+        name = rand_zone_name(prefix='testdomain', suffix='.com.')
+    if email is None:
+        email = ("admin@" + name).strip('.')
+    if description is None:
+        description = rand_zone_name(prefix='Description ', suffix='')
+    if ttl is None:
+        ttl = random.randint(1200, 8400),
+    return {
+        'name': name,
+        'email': email,
+        'ttl': random.randint(1200, 8400),
+        'description': description}
+
+
+def rand_recordset_data(record_type, zone_name, name=None, records=None,
+                        ttl=None):
+    """Generate random recordset data, with optional overrides
+
+    :return: A RecordsetModel
+    """
+    if name is None:
+        name = rand_zone_name(prefix=record_type, suffix='.' + zone_name)
+    if records is None:
+        records = [rand_ip()]
+    if ttl is None:
+        ttl = random.randint(1200, 8400)
+    return {
+        'type': record_type,
+        'name': name,
+        'records': records,
+        'ttl': ttl}
+
+
+def rand_a_recordset(zone_name, ip=None, **kwargs):
+    if ip is None:
+        ip = rand_ip()
+    return rand_recordset_data('A', zone_name, records=[ip], **kwargs)
+
+
+def rand_aaaa_recordset(zone_name, ip=None, **kwargs):
+    if ip is None:
+        ip = rand_ipv6()
+    return rand_recordset_data('AAAA', zone_name, records=[ip], **kwargs)
+
+
+def rand_cname_recordset(zone_name, cname=None, **kwargs):
+    if cname is None:
+        cname = zone_name
+    return rand_recordset_data('CNAME', zone_name, records=[cname], **kwargs)
+
+
+def rand_mx_recordset(zone_name, pref=None, host=None, **kwargs):
+    if pref is None:
+        pref = str(random.randint(0, 65535))
+    if host is None:
+        host = rand_zone_name(prefix='mail', suffix='.' + zone_name)
+    data = "{0} {1}".format(pref, host)
+    return rand_recordset_data('MX', zone_name, records=[data], **kwargs)
+
+
+def rand_spf_recordset(zone_name, data=None, **kwargs):
+    data = data or "v=spf1 +all"
+    return rand_recordset_data('SPF', zone_name, records=[data], **kwargs)
+
+
+def rand_srv_recordset(zone_name, data=None):
+    data = data or "10 0 8080 %s.%s" % (rand_zone_name(suffix=''), zone_name)
+    return rand_recordset_data('SRV', zone_name,
+                               name="_sip._tcp.%s" % zone_name,
+                               records=[data])
+
+
+def rand_sshfp_recordset(zone_name, algorithm_number=None,
+                         fingerprint_type=None, fingerprint=None,
+                         **kwargs):
+    algorithm_number = algorithm_number or 2
+    fingerprint_type = fingerprint_type or 1
+    fingerprint = fingerprint or \
+        "123456789abcdef67890123456789abcdef67890"
+
+    data = "%s %s %s" % (algorithm_number, fingerprint_type, fingerprint)
+    return rand_recordset_data('SSHFP', zone_name, records=[data], **kwargs)
+
+
+def rand_txt_recordset(zone_name, data=None, **kwargs):
+    data = data or "v=spf1 +all"
+    return rand_recordset_data('TXT', zone_name, records=[data], **kwargs)
+
+
+def wildcard_ns_recordset(zone_name):
+    name = "*.{0}".format(zone_name)
+    records = ["ns.example.com."]
+    return rand_recordset_data('NS', zone_name, name, records)
diff --git a/designate_tempest_plugin/services/dns/json/base.py b/designate_tempest_plugin/services/dns/json/base.py
index b53712f..ef07ec1 100644
--- a/designate_tempest_plugin/services/dns/json/base.py
+++ b/designate_tempest_plugin/services/dns/json/base.py
@@ -148,8 +148,26 @@
 
         return resp, self.deserialize(resp, body)
 
+    def _put_request(self, resource, uuid, object_dict, params=None):
+        """Updates the specified object using PUT request.
+        :param resource: The name of the REST resource, e.g., 'zones'.
+        :param uuid: Unique identifier of the object in UUID format.
+        :param object_dict: A Python dict that represents an object of the
+                            specified type
+        :param params: A Python dict that represents the query paramaters to
+                       include in the request URI.
+        :returns: Serialized object as a dictionary.
+        """
+        body = self.serialize(object_dict)
+        uri = self.get_uri(resource, uuid=uuid, params=params)
+        resp, body = self.put(uri, body=body)
+
+        self.expected_success([200, 202], resp.status)
+
+        return resp, self.deserialize(resp, body)
+
     def _update_request(self, resource, uuid, object_dict, params=None):
-        """Updates the specified object.
+        """Updates the specified object using PATCH request.
         :param resource: The name of the REST resource, e.g., 'zones'
         :param uuid: Unique identifier of the object in UUID format.
         :param object_dict: A Python dict that represents an object of the
diff --git a/designate_tempest_plugin/services/dns/v2/json/recordset_client.py b/designate_tempest_plugin/services/dns/v2/json/recordset_client.py
new file mode 100644
index 0000000..3bc9418
--- /dev/null
+++ b/designate_tempest_plugin/services/dns/v2/json/recordset_client.py
@@ -0,0 +1,107 @@
+# 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 RecordsetClient(base.DnsClientV2Base):
+    """API V2 Tempest REST client for Recordset API"""
+
+    @base.handle_errors
+    def create_recordset(self, zone_uuid, recordset_data,
+                         params=None, wait_until=False):
+        """Create a recordset for the specified zone.
+
+        :param zone_uuid: Unique identifier of the zone in UUID format..
+        :param recordset_data: A dictionary that represents the recordset
+                                data.
+        :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 created zone.
+        """
+        resp, body = self._create_request(
+            "/zones/{0}/recordsets".format(zone_uuid), params=params,
+            object_dict=recordset_data)
+
+        # Create Recordset should Return a HTTP 202
+        self.expected_success(202, resp.status)
+
+        if wait_until:
+            waiters.wait_for_recordset_status(self, body['id'], wait_until)
+
+        return resp, body
+
+    @base.handle_errors
+    def show_recordset(self, zone_uuid, recordset_uuid, params=None):
+        """Gets a specific recordset related to a specific zone.
+        :param zone_uuid: Unique identifier of the zone in UUID format.
+        :param recordset_uuid: Unique identifier of the recordset in
+                               UUID format.
+        :param params: A Python dict that represents the query paramaters to
+                       include in the request URI.
+        :return: Serialized recordset as a list.
+        """
+        return self._show_request(
+            'zones/{0}/recordsets'.format(zone_uuid), recordset_uuid,
+            params=params)
+
+    @base.handle_errors
+    def delete_recordset(self, zone_uuid, recordset_uuid, params=None):
+        """Deletes a recordset related to the specified zone UUID.
+        :param zone_uuid: The unique identifier of the zone.
+        :param recordset_uuid: The unique identifier of the record in
+                               uuid format.
+        :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/{0}/recordsets'.format(zone_uuid), recordset_uuid)
+
+        # Delete Recordset should Return a HTTP 202
+        self.expected_success(202, resp.status)
+
+        return resp, body
+
+    @base.handle_errors
+    def list_recordset(self, uuid, params=None):
+        """List recordsets related to the specified zone.
+        :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.
+        :return: Serialized recordset as a list.
+        """
+        return self._list_request(
+            'zones/{0}/recordsets'.format(uuid), params=params)
+
+    @base.handle_errors
+    def update_recordset(self, zone_uuid, recordset_uuid,
+                         recordset_model, params=None):
+        """Update the recordset related to the specified zone.
+        :param zone_uuid: Unique identifier of the zone in UUID format..
+        :param recordset_uuid: Unique identifier of the records in UUID format.
+        :param recordset_model: .
+        :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 created zone.
+        """
+        resp, body = self._put_request(
+            'zones/{0}/recordsets'.format(zone_uuid), recordset_uuid,
+            object_dict=recordset_model, params=params)
+
+        # Update Recordset should Return a HTTP 202
+        self.expected_success(202, resp.status)
+
+        return resp, body
diff --git a/designate_tempest_plugin/tests/api/v2/test_recordset.py b/designate_tempest_plugin/tests/api/v2/test_recordset.py
new file mode 100644
index 0000000..1c9d87d
--- /dev/null
+++ b/designate_tempest_plugin/tests/api/v2/test_recordset.py
@@ -0,0 +1,128 @@
+# 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
+from designate_tempest_plugin import data_utils
+
+LOG = logging.getLogger(__name__)
+
+
+class BaseRecordsetsTest(base.BaseDnsTest):
+    excluded_keys = ['created_at', 'updated_at', 'version', 'links',
+                     'type']
+
+
+class RecordsetsTest(BaseRecordsetsTest):
+    @classmethod
+    def setup_clients(cls):
+        super(RecordsetsTest, cls).setup_clients()
+
+        cls.client = cls.os.recordset_client
+        cls.zone_client = cls.os.zones_client
+
+    @test.attr(type='smoke')
+    @test.idempotent_id('631d74fd-6909-4684-a61b-5c4d2f92c3e7')
+    def test_create_recordset(self):
+        LOG.info('Create a zone')
+        _, zone = self.zone_client.create_zone()
+        self.addCleanup(self.zone_client.delete_zone, zone['id'])
+
+        recordset_data = data_utils.rand_recordset_data(
+            record_type='A', zone_name=zone['name'])
+
+        LOG.info('Create a Recordset')
+        resp, body = self.client.create_recordset(zone['id'], recordset_data)
+
+        LOG.info('Ensure we respond with PENDING')
+        self.assertEqual('PENDING', body['status'])
+
+    @test.attr(type='smoke')
+    @test.idempotent_id('5964f730-5546-46e6-9105-5030e9c492b2')
+    def test_list_recordsets(self):
+        LOG.info('Create a zone')
+        _, zone = self.zone_client.create_zone()
+        self.addCleanup(self.zone_client.delete_zone, zone['id'])
+
+        recordset_data = data_utils.rand_recordset_data(
+            record_type='A', zone_name=zone['name'])
+
+        LOG.info('Create a Recordset')
+        resp, body = self.client.create_recordset(zone['id'], recordset_data)
+
+        self.assertTrue(len(body) > 0)
+
+    @test.attr(type='smoke')
+    @test.idempotent_id('84c13cb2-9020-4c1e-aeb0-c348d9a70caa')
+    def test_show_recordsets(self):
+        LOG.info('Create a zone')
+        _, zone = self.zone_client.create_zone()
+        self.addCleanup(self.zone_client.delete_zone, zone['id'])
+
+        recordset_data = data_utils.rand_recordset_data(
+            record_type='A', zone_name=zone['name'])
+
+        LOG.info('Create a Recordset')
+        resp, body = self.client.create_recordset(zone['id'], recordset_data)
+
+        LOG.info('Re-Fetch the Recordset')
+        _, record = self.client.show_recordset(zone['id'], body['id'])
+
+        LOG.info('Ensure the fetched response matches the expected one')
+        self.assertExpected(body, record, self.excluded_keys)
+
+    @test.attr(type='smoke')
+    @test.idempotent_id('855399c1-8806-4ae5-aa31-cb8a6f35e218')
+    def test_delete_recordset(self):
+        LOG.info('Create a zone')
+        _, zone = self.zone_client.create_zone()
+        self.addCleanup(self.zone_client.delete_zone, zone['id'])
+
+        recordset_data = data_utils.rand_recordset_data(
+            record_type='A', zone_name=zone['name'])
+
+        LOG.info('Create a Recordset')
+        _, record = self.client.create_recordset(zone['id'], recordset_data)
+
+        LOG.info('Delete a Recordset')
+        _, body = self.client.delete_recordset(zone['id'], record['id'])
+
+        LOG.info('Ensure successful deletion of Recordset')
+        self.assertRaises(lib_exc.NotFound,
+            lambda: self.client.show_recordset(zone['id'], record['id']))
+
+    @test.attr(type='smoke')
+    @test.idempotent_id('8d41c85f-09f9-48be-a202-92d1bdf5c796')
+    def test_update_recordset(self):
+        LOG.info('Create a zone')
+        _, zone = self.zone_client.create_zone()
+        self.addCleanup(self.zone_client.delete_zone, zone['id'])
+
+        recordset_data = data_utils.rand_recordset_data(
+            record_type='A', zone_name=zone['name'])
+
+        LOG.info('Create a recordset')
+        _, record = self.client.create_recordset(zone['id'], recordset_data)
+
+        recordset_data = data_utils.rand_recordset_data(
+            record_type='A', zone_name=zone['name'], name=record['name'])
+
+        LOG.info('Update the recordset')
+        _, update = self.client.update_recordset(zone['id'],
+            record['id'], recordset_data)
+
+        self.assertEqual(record['name'], update['name'])
+        self.assertNotEqual(record['records'], update['records'])