From ce5ef3d17e0aea789ef759ed358e3b241826519a Mon Sep 17 00:00:00 2001 From: Kanagaraj Manickam Date: Mon, 22 Jun 2015 16:29:48 +0530 Subject: [PATCH] Designate Domain resource Adds resource plugin for designate Domain implements blueprint heat-designate-resoure Change-Id: I47625096ce70b1df8959ae8e3149bb7c672ae15c --- heat/engine/clients/os/designate.py | 13 ++ .../resources/openstack/designate/__init__.py | 0 .../resources/openstack/designate/domain.py | 127 ++++++++++++++ heat/tests/clients/test_designate_client.py | 49 ++++++ heat/tests/openstack/designate/test_domain.py | 164 ++++++++++++++++++ 5 files changed, 353 insertions(+) create mode 100644 heat/engine/resources/openstack/designate/__init__.py create mode 100644 heat/engine/resources/openstack/designate/domain.py create mode 100644 heat/tests/openstack/designate/test_domain.py diff --git a/heat/engine/clients/os/designate.py b/heat/engine/clients/os/designate.py index bb81604f24..1f7ee9b0a6 100644 --- a/heat/engine/clients/os/designate.py +++ b/heat/engine/clients/os/designate.py @@ -13,6 +13,7 @@ from designateclient import exceptions from designateclient import v1 as client +from designateclient.v1 import domains from heat.common import exception as heat_exception from heat.engine.clients import client_plugin @@ -51,6 +52,18 @@ class DesignateClientPlugin(client_plugin.ClientPlugin): raise heat_exception.EntityNotFound(entity='Designate Domain', name=domain_id_or_name) + def domain_create(self, **kwargs): + domain = domains.Domain(**kwargs) + return self.client().domains.create(domain) + + def domain_update(self, **kwargs): + # Designate mandates to pass the Domain object with updated properties + domain = self.client().domains.get(kwargs['id']) + for key in kwargs.keys(): + setattr(domain, key, kwargs[key]) + + return self.client().domains.update(domain) + class DesignateDomainConstraint(constraints.BaseCustomConstraint): diff --git a/heat/engine/resources/openstack/designate/__init__.py b/heat/engine/resources/openstack/designate/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/heat/engine/resources/openstack/designate/domain.py b/heat/engine/resources/openstack/designate/domain.py new file mode 100644 index 0000000000..895a625d28 --- /dev/null +++ b/heat/engine/resources/openstack/designate/domain.py @@ -0,0 +1,127 @@ +# +# 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 heat.common.i18n import _ +from heat.engine import attributes +from heat.engine import constraints +from heat.engine import properties +from heat.engine import resource +from heat.engine import support + + +class DesignateDomain(resource.Resource): + """Heat Template Resource for Designate Domain.""" + + support_status = support.SupportStatus( + version='5.0.0') + + PROPERTIES = ( + NAME, TTL, DESCRIPTION, EMAIL + ) = ( + 'name', 'ttl', 'description', 'email' + ) + + ATTRIBUTES = ( + SERIAL, + ) = ( + 'serial', + ) + + properties_schema = { + # Based on RFC 1035, length of name is set to max of 255 + NAME: properties.Schema( + properties.Schema.STRING, + _('Domain name.'), + required=True, + constraints=[constraints.Length(max=255)] + ), + # Based on RFC 1035, range for ttl is set to 0 to signed 32 bit number + TTL: properties.Schema( + properties.Schema.INTEGER, + _('Time To Live (Seconds).'), + update_allowed=True, + constraints=[constraints.Range(min=0, + max=2147483647)] + ), + # designate mandates to the max length of 160 for description + DESCRIPTION: properties.Schema( + properties.Schema.STRING, + _('Description of domain.'), + update_allowed=True, + constraints=[constraints.Length(max=160)] + ), + EMAIL: properties.Schema( + properties.Schema.STRING, + _('Domain email.'), + update_allowed=True, + required=True + ) + } + + attributes_schema = { + SERIAL: attributes.Schema( + _("DNS domain serial."), + type=attributes.Schema.STRING + ), + } + + default_client_name = 'designate' + + def handle_create(self): + args = dict( + name=self.properties[self.NAME], + email=self.properties[self.EMAIL], + description=self.properties[self.DESCRIPTION], + ttl=self.properties[self.TTL] + ) + + domain = self.client_plugin().domain_create(**args) + + self.resource_id_set(domain.id) + + def handle_update(self, + json_snippet=None, + tmpl_diff=None, + prop_diff=None): + args = dict() + + if prop_diff.get(self.EMAIL): + args['email'] = prop_diff.get(self.EMAIL) + + if prop_diff.get(self.TTL): + args['ttl'] = prop_diff.get(self.TTL) + + if prop_diff.get(self.DESCRIPTION): + args['description'] = prop_diff.get(self.DESCRIPTION) + + if len(args.keys()) > 0: + args['id'] = self.resource_id + self.client_plugin().domain_update(**args) + + def handle_delete(self): + if self.resource_id is not None: + try: + self.client().domains.delete(self.resource_id) + except Exception as ex: + self.client_plugin().ignore_not_found(ex) + + def _resolve_attribute(self, name): + if name == self.SERIAL: + domain = self.client().domains.get(self.resource_id) + return domain.serial + + +def resource_mapping(): + return { + 'OS::Designate::Domain': DesignateDomain + } diff --git a/heat/tests/clients/test_designate_client.py b/heat/tests/clients/test_designate_client.py index 9a89e90e5f..0d6731005b 100644 --- a/heat/tests/clients/test_designate_client.py +++ b/heat/tests/clients/test_designate_client.py @@ -154,3 +154,52 @@ class DesignateClientPluginDomainTest(common.HeatTestCase): self._client.domains.get.assert_called_once_with( self.sample_name) self._client.domains.list.assert_called_once_with() + + @mock.patch.object(client.DesignateClientPlugin, 'client') + @mock.patch('designateclient.v1.domains.Domain') + def test_domain_create(self, mock_domain, client_designate): + self._client.domains.create.return_value = None + client_designate.return_value = self._client + + domain = dict( + name='test-domain.com', + description='updated description', + ttl=4200, + email='xyz@test-domain.com' + ) + + mock_sample_domain = mock.Mock() + mock_domain.return_value = mock_sample_domain + + self.client_plugin.domain_create(**domain) + + # Make sure domain entity is created with right arguments + mock_domain.assert_called_once_with(**domain) + self._client.domains.create.assert_called_once_with( + mock_sample_domain) + + @mock.patch.object(client.DesignateClientPlugin, 'client') + def test_domain_update(self, client_designate): + self._client.domains.update.return_value = None + mock_domain = self._get_mock_domain() + self._client.domains.get.return_value = mock_domain + + client_designate.return_value = self._client + + domain = dict( + id='sample-id', + description='updated description', + ttl=4200, + email='xyz@test-domain.com' + ) + + self.client_plugin.domain_update(**domain) + + self._client.domains.get.assert_called_once_with( + mock_domain.id) + + for key in domain.keys(): + setattr(mock_domain, key, domain[key]) + + self._client.domains.update.assert_called_once_with( + mock_domain) diff --git a/heat/tests/openstack/designate/test_domain.py b/heat/tests/openstack/designate/test_domain.py new file mode 100644 index 0000000000..8c0147fc5b --- /dev/null +++ b/heat/tests/openstack/designate/test_domain.py @@ -0,0 +1,164 @@ +# +# 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 mock + +from designateclient import exceptions as designate_exception + +from heat.engine.resources.openstack.designate import domain +from heat.engine import stack +from heat.engine import template +from heat.tests import common +from heat.tests import utils + + +sample_template = { + 'heat_template_version': '2015-04-30', + 'resources': { + 'test_resource': { + 'type': 'OS::Designate::Domain', + 'properties': { + 'name': 'test-domain.com', + 'description': 'Test domain', + 'ttl': 3600, + 'email': 'abc@test-domain.com' + } + } + } +} + +RESOURCE_TYPE = 'OS::Designate::Domain' + + +class DesignateDomainTest(common.HeatTestCase): + + def setUp(self): + super(DesignateDomainTest, self).setUp() + + self.ctx = utils.dummy_context() + + self.stack = stack.Stack( + self.ctx, 'test_stack', + template.Template(sample_template) + ) + + self.test_resource = self.stack['test_resource'] + + # Mock client plugin + self.test_client_plugin = mock.MagicMock() + self.test_resource.client_plugin = mock.MagicMock( + return_value=self.test_client_plugin) + + # Mock client + self.test_client = mock.MagicMock() + self.test_resource.client = mock.MagicMock( + return_value=self.test_client) + + def _get_mock_resource(self): + value = mock.MagicMock() + value.id = '477e8273-60a7-4c41-b683-fdb0bc7cd152' + value.serial = '1434596972' + + return value + + def test_resource_handle_create(self): + mock_domain_create = self.test_client_plugin.domain_create + mock_resource = self._get_mock_resource() + mock_domain_create.return_value = mock_resource + + # validate the properties + self.assertEqual( + 'test-domain.com', + self.test_resource.properties.get(domain.DesignateDomain.NAME)) + self.assertEqual( + 'Test domain', + self.test_resource.properties.get( + domain.DesignateDomain.DESCRIPTION)) + self.assertEqual( + 3600, + self.test_resource.properties.get(domain.DesignateDomain.TTL)) + self.assertEqual( + 'abc@test-domain.com', + self.test_resource.properties.get(domain.DesignateDomain.EMAIL)) + + self.test_resource.data_set = mock.Mock() + self.test_resource.handle_create() + + args = dict( + name='test-domain.com', + description='Test domain', + ttl=3600, + email='abc@test-domain.com' + ) + + mock_domain_create.assert_called_once_with(**args) + + # validate physical resource id + self.assertEqual(mock_resource.id, self.test_resource.resource_id) + + def test_resource_handle_update(self): + mock_domain_update = self.test_client_plugin.domain_update + self.test_resource.resource_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151' + + prop_diff = {domain.DesignateDomain.EMAIL: 'xyz@test-domain.com', + domain.DesignateDomain.DESCRIPTION: 'updated description', + domain.DesignateDomain.TTL: 4200} + + self.test_resource.handle_update(json_snippet=None, + tmpl_diff=None, + prop_diff=prop_diff) + + args = dict( + id=self.test_resource.resource_id, + description='updated description', + ttl=4200, + email='xyz@test-domain.com' + ) + mock_domain_update.assert_called_once_with(**args) + + def test_resource_handle_delete(self): + mock_domain_delete = self.test_client.domains.delete + self.test_resource.resource_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151' + mock_domain_delete.return_value = None + + self.assertIsNone(self.test_resource.handle_delete()) + mock_domain_delete.assert_called_once_with( + self.test_resource.resource_id + ) + + def test_resource_handle_delete_resource_id_is_none(self): + self.test_resource.resource_id = None + self.assertIsNone(self.test_resource.handle_delete()) + + def test_resource_handle_delete_not_found(self): + mock_domain_delete = self.test_client.domains.delete + mock_domain_delete.side_effect = designate_exception.NotFound + self.assertIsNone(self.test_resource.handle_delete()) + + def test_resolve_attributes(self): + mock_domain = self._get_mock_resource() + self.test_resource.resource_id = mock_domain.id + self.test_client.domains.get.return_value = mock_domain + self.assertEqual(mock_domain.serial, + self.test_resource._resolve_attribute( + domain.DesignateDomain.SERIAL + )) + self.test_client.domains.get.assert_called_once_with( + self.test_resource.resource_id + ) + + def test_resource_mapping(self): + mapping = domain.resource_mapping() + self.assertEqual(1, len(mapping)) + self.assertEqual(domain.DesignateDomain, mapping[RESOURCE_TYPE]) + self.assertIsInstance(self.test_resource, domain.DesignateDomain)