From 34d7ffcd43d51071383f80d338f185ad8c8271f8 Mon Sep 17 00:00:00 2001 From: Kanagaraj Manickam Date: Wed, 29 Apr 2015 12:16:04 +0530 Subject: [PATCH] Resource plug-in for keystone endpoint Adds contrib resource plug-in for keystone endpoint implements blueprint keystone-resource-service-endpoint Change-Id: If0d493df5b8a74a8c0e01c3856028663dfa30fb5 --- .../heat_keystone/resources/endpoint.py | 151 ++++++++ .../heat_keystone/tests/test_endpoint.py | 324 ++++++++++++++++++ 2 files changed, 475 insertions(+) create mode 100644 contrib/heat_keystone/heat_keystone/resources/endpoint.py create mode 100644 contrib/heat_keystone/heat_keystone/tests/test_endpoint.py diff --git a/contrib/heat_keystone/heat_keystone/resources/endpoint.py b/contrib/heat_keystone/heat_keystone/resources/endpoint.py new file mode 100644 index 000000000..9c5d3328e --- /dev/null +++ b/contrib/heat_keystone/heat_keystone/resources/endpoint.py @@ -0,0 +1,151 @@ +# +# 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 constraints +from heat.engine import properties +from heat.engine import resource +from heat.engine import support + + +class KeystoneEndpoint(resource.Resource): + """Heat Template Resource for Keystone Service Endpoint.""" + + support_status = support.SupportStatus( + version='2015.2', + message=_('Supported versions: keystone v3')) + + PROPERTIES = ( + NAME, REGION, SERVICE, INTERFACE, SERVICE_URL + ) = ( + 'name', 'region', 'service', 'interface', 'url' + ) + + properties_schema = { + NAME: properties.Schema( + properties.Schema.STRING, + _('Name of keystone endpoint.'), + update_allowed=True + ), + REGION: properties.Schema( + properties.Schema.STRING, + _('Name or Id of keystone region.'), + update_allowed=True + ), + SERVICE: properties.Schema( + properties.Schema.STRING, + _('Name or Id of keystone service.'), + update_allowed=True, + required=True, + constraints=[constraints.CustomConstraint('keystone.service')] + ), + INTERFACE: properties.Schema( + properties.Schema.STRING, + _('Interface type of keystone service endpoint.'), + update_allowed=True, + required=True, + constraints=[constraints.AllowedValues( + ['public', 'internal', 'admin'] + )] + ), + SERVICE_URL: properties.Schema( + properties.Schema.STRING, + _('URL of keystone service endpoint.'), + update_allowed=True, + required=True + ) + } + + def _create_endpoint(self, + service, + interface, + url, + region=None, + name=None): + return self.keystone().client.endpoints.create( + region=region, + service=service, + interface=interface, + url=url, + name=name) + + def _delete_endpoint(self, endpoint_id): + return self.keystone().client.endpoints.delete(endpoint_id) + + def _update_endpoint(self, + endpoint_id, + new_region=None, + new_service=None, + new_interface=None, + new_url=None, + new_name=None): + return self.keystone().client.endpoints.update( + endpoint=endpoint_id, + region=new_region, + service=new_service, + interface=new_interface, + url=new_url, + name=new_name) + + def handle_create(self): + region = self.properties.get(self.REGION) + service = self.properties.get(self.SERVICE) + interface = self.properties.get(self.INTERFACE) + url = self.properties.get(self.SERVICE_URL) + name = (self.properties.get(self.NAME) or + self.physical_resource_name()) + + endpoint = self._create_endpoint( + region=region, + service=service, + interface=interface, + url=url, + name=name + ) + + self.resource_id_set(endpoint.id) + + def handle_update(self, + json_snippet=None, + tmpl_diff=None, + prop_diff=None): + region = prop_diff.get(self.REGION) + service = prop_diff.get(self.SERVICE) + interface = prop_diff.get(self.INTERFACE) + url = prop_diff.get(self.SERVICE_URL) + name = None + if self.NAME in prop_diff: + name = (prop_diff.get(self.NAME) or + self.physical_resource_name()) + + self._update_endpoint( + endpoint_id=self.resource_id, + new_region=region, + new_interface=interface, + new_service=service, + new_url=url, + new_name=name + ) + + def handle_delete(self): + if self.resource_id is not None: + try: + self._delete_endpoint(endpoint_id=self.resource_id) + except Exception as ex: + self.client_plugin('keystone').ignore_not_found(ex) + + +def resource_mapping(): + return { + 'OS::Keystone::Endpoint': KeystoneEndpoint + } diff --git a/contrib/heat_keystone/heat_keystone/tests/test_endpoint.py b/contrib/heat_keystone/heat_keystone/tests/test_endpoint.py new file mode 100644 index 000000000..77e8f3423 --- /dev/null +++ b/contrib/heat_keystone/heat_keystone/tests/test_endpoint.py @@ -0,0 +1,324 @@ +# +# 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 heat.engine import constraints +from heat.engine import properties +from heat.engine import resource +from heat.engine import stack +from heat.engine import template +from heat.tests import common +from heat.tests import utils + +from ..resources import endpoint # noqa + +keystone_endpoint_template = { + 'heat_template_version': '2015-04-30', + 'resources': { + 'test_endpoint': { + 'type': 'OS::Keystone::Endpoint', + 'properties': { + 'service': 'heat', + 'region': 'RegionOne', + 'interface': 'public', + 'url': 'http://127.0.0.1:8004/v1/tenant-id', + 'name': 'endpoint_foo' + } + } + } +} + +RESOURCE_TYPE = 'OS::Keystone::Endpoint' + + +class KeystoneEndpointTest(common.HeatTestCase): + def setUp(self): + super(KeystoneEndpointTest, self).setUp() + + self.ctx = utils.dummy_context() + + # For unit testing purpose. Register resource provider explicitly. + resource._register_class(RESOURCE_TYPE, + endpoint.KeystoneEndpoint) + + self.stack = stack.Stack( + self.ctx, 'test_stack_keystone', + template.Template(keystone_endpoint_template) + ) + + self.test_endpoint = self.stack['test_endpoint'] + + # Mock client + self.keystoneclient = mock.MagicMock() + self.test_endpoint.keystone = mock.MagicMock() + self.test_endpoint.keystone.return_value = self.keystoneclient + self.endpoints = self.keystoneclient.client.endpoints + + # Mock client plugin + keystone_client_plugin = mock.MagicMock() + self.test_endpoint.client_plugin = mock.MagicMock() + self.test_endpoint.client_plugin.return_value = keystone_client_plugin + + def _get_mock_endpoint(self): + value = mock.MagicMock() + value.id = '477e8273-60a7-4c41-b683-fdb0bc7cd152' + + return value + + def test_endpoint_handle_create(self): + mock_endpoint = self._get_mock_endpoint() + self.endpoints.create.return_value = mock_endpoint + + # validate the properties + self.assertEqual( + 'heat', + self.test_endpoint.properties.get( + endpoint.KeystoneEndpoint.SERVICE)) + self.assertEqual( + 'public', + self.test_endpoint.properties.get( + endpoint.KeystoneEndpoint.INTERFACE)) + self.assertEqual( + 'RegionOne', + self.test_endpoint.properties.get( + endpoint.KeystoneEndpoint.REGION)) + self.assertEqual( + 'http://127.0.0.1:8004/v1/tenant-id', + self.test_endpoint.properties.get( + endpoint.KeystoneEndpoint.SERVICE_URL)) + self.assertEqual( + 'endpoint_foo', + self.test_endpoint.properties.get( + endpoint.KeystoneEndpoint.NAME)) + + self.test_endpoint.handle_create() + + # validate endpoint creation + self.endpoints.create.assert_called_once_with( + service='heat', + url='http://127.0.0.1:8004/v1/tenant-id', + interface='public', + region='RegionOne', + name='endpoint_foo') + + # validate physical resource id + self.assertEqual(mock_endpoint.id, self.test_endpoint.resource_id) + + def test_endpoint_handle_update(self): + self.test_endpoint.resource_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151' + + prop_diff = {endpoint.KeystoneEndpoint.REGION: 'RegionTwo', + endpoint.KeystoneEndpoint.INTERFACE: 'internal', + endpoint.KeystoneEndpoint.SERVICE: 'updated_id', + endpoint.KeystoneEndpoint.SERVICE_URL: + 'http://127.0.0.1:8004/v2/tenant-id', + endpoint.KeystoneEndpoint.NAME: + 'endpoint_foo_updated'} + + self.test_endpoint.handle_update(json_snippet=None, + tmpl_diff=None, + prop_diff=prop_diff) + + self.endpoints.update.assert_called_once_with( + endpoint=self.test_endpoint.resource_id, + region=prop_diff[endpoint.KeystoneEndpoint.REGION], + interface=prop_diff[endpoint.KeystoneEndpoint.INTERFACE], + service=prop_diff[endpoint.KeystoneEndpoint.SERVICE], + url=prop_diff[endpoint.KeystoneEndpoint.SERVICE_URL], + name=prop_diff[endpoint.KeystoneEndpoint.NAME] + ) + + def test_endpoint_handle_update_default(self): + self.test_endpoint.resource_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151' + self.test_endpoint.physical_resource_name = mock.MagicMock() + self.test_endpoint.physical_resource_name.return_value = 'foo' + + # Name is reset to None, so default to physical resource name + prop_diff = {endpoint.KeystoneEndpoint.NAME: None} + + self.test_endpoint.handle_update(json_snippet=None, + tmpl_diff=None, + prop_diff=prop_diff) + + # validate default name to physical resource name + self.endpoints.update.assert_called_once_with( + endpoint=self.test_endpoint.resource_id, + region=None, + interface=None, + service=None, + url=None, + name='foo' + ) + + def test_endpoint_handle_delete(self): + self.test_endpoint.resource_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151' + self.endpoints.delete.return_value = None + + self.assertIsNone(self.test_endpoint.handle_delete()) + self.endpoints.delete.assert_called_once_with( + self.test_endpoint.resource_id + ) + + def test_endpoint_handle_delete_resource_id_is_none(self): + self.test_endpoint.resource_id = None + self.assertIsNone(self.test_endpoint.handle_delete()) + + def test_endpoint_handle_delete_not_found(self): + exc = self.keystoneclient.NotFound + self.endpoints.delete.side_effect = exc + + self.assertIsNone(self.test_endpoint.handle_delete()) + + def test_resource_mapping(self): + mapping = endpoint.resource_mapping() + self.assertEqual(1, len(mapping)) + self.assertEqual(endpoint.KeystoneEndpoint, mapping[RESOURCE_TYPE]) + self.assertIsInstance(self.test_endpoint, endpoint.KeystoneEndpoint) + + def test_properties_title(self): + property_title_map = { + endpoint.KeystoneEndpoint.SERVICE: 'service', + endpoint.KeystoneEndpoint.REGION: 'region', + endpoint.KeystoneEndpoint.INTERFACE: 'interface', + endpoint.KeystoneEndpoint.SERVICE_URL: 'url', + endpoint.KeystoneEndpoint.NAME: 'name' + } + + for actual_title, expected_title in property_title_map.items(): + self.assertEqual( + expected_title, + actual_title, + 'KeystoneEndpoint PROPERTIES(%s) title modified.' % + actual_title) + + def test_property_service_validate_schema(self): + schema = (endpoint.KeystoneEndpoint.properties_schema[ + endpoint.KeystoneEndpoint.SERVICE]) + self.assertTrue( + schema.update_allowed, + 'update_allowed for property %s is modified' % + endpoint.KeystoneEndpoint.SERVICE) + + self.assertTrue( + schema.required, + 'required for property %s is modified' % + endpoint.KeystoneEndpoint.SERVICE) + + self.assertEqual(properties.Schema.STRING, + schema.type, + 'type for property %s is modified' % + endpoint.KeystoneEndpoint.SERVICE) + + self.assertEqual('Name or Id of keystone service.', + schema.description, + 'description for property %s is modified' % + endpoint.KeystoneEndpoint.SERVICE) + + # Make sure, SERVICE is of keystone.service custom constrain type + self.assertEqual(1, len(schema.constraints)) + keystone_service_constrain = schema.constraints[0] + self.assertIsInstance(keystone_service_constrain, + constraints.CustomConstraint) + self.assertEqual('keystone.service', + keystone_service_constrain.name) + + def test_property_region_validate_schema(self): + schema = (endpoint.KeystoneEndpoint.properties_schema[ + endpoint.KeystoneEndpoint.REGION]) + self.assertTrue( + schema.update_allowed, + 'update_allowed for property %s is modified' % + endpoint.KeystoneEndpoint.REGION) + + self.assertEqual(properties.Schema.STRING, + schema.type, + 'type for property %s is modified' % + endpoint.KeystoneEndpoint.REGION) + + self.assertEqual('Name or Id of keystone region.', + schema.description, + 'description for property %s is modified' % + endpoint.KeystoneEndpoint.REGION) + + def test_property_interface_validate_schema(self): + schema = (endpoint.KeystoneEndpoint.properties_schema[ + endpoint.KeystoneEndpoint.INTERFACE]) + self.assertTrue( + schema.update_allowed, + 'update_allowed for property %s is modified' % + endpoint.KeystoneEndpoint.INTERFACE) + + self.assertTrue( + schema.required, + 'required for property %s is modified' % + endpoint.KeystoneEndpoint.INTERFACE) + + self.assertEqual(properties.Schema.STRING, + schema.type, + 'type for property %s is modified' % + endpoint.KeystoneEndpoint.INTERFACE) + + self.assertEqual('Interface type of keystone service endpoint.', + schema.description, + 'description for property %s is modified' % + endpoint.KeystoneEndpoint.INTERFACE) + + # Make sure INTERFACE valid constrains + self.assertEqual(1, len(schema.constraints)) + allowed_constrain = schema.constraints[0] + self.assertIsInstance(allowed_constrain, + constraints.AllowedValues) + self.assertEqual(('public', 'internal', 'admin'), + allowed_constrain.allowed) + + def test_property_service_url_validate_schema(self): + schema = (endpoint.KeystoneEndpoint.properties_schema[ + endpoint.KeystoneEndpoint.SERVICE_URL]) + self.assertTrue( + schema.update_allowed, + 'update_allowed for property %s is modified' % + endpoint.KeystoneEndpoint.SERVICE_URL) + + self.assertTrue( + schema.required, + 'required for property %s is modified' % + endpoint.KeystoneEndpoint.SERVICE_URL) + + self.assertEqual(properties.Schema.STRING, + schema.type, + 'type for property %s is modified' % + endpoint.KeystoneEndpoint.SERVICE_URL) + + self.assertEqual('URL of keystone service endpoint.', + schema.description, + 'description for property %s is modified' % + endpoint.KeystoneEndpoint.SERVICE_URL) + + def test_property_name_validate_schema(self): + schema = (endpoint.KeystoneEndpoint.properties_schema[ + endpoint.KeystoneEndpoint.NAME]) + self.assertTrue( + schema.update_allowed, + 'update_allowed for property %s is modified' % + endpoint.KeystoneEndpoint.NAME) + + self.assertEqual(properties.Schema.STRING, + schema.type, + 'type for property %s is modified' % + endpoint.KeystoneEndpoint.NAME) + + self.assertEqual('Name of keystone endpoint.', + schema.description, + 'description for property %s is modified' % + endpoint.KeystoneEndpoint.NAME)