diff --git a/heat/engine/resources/openstack/neutron/address_scope.py b/heat/engine/resources/openstack/neutron/address_scope.py new file mode 100644 index 0000000000..bf06de59a4 --- /dev/null +++ b/heat/engine/resources/openstack/neutron/address_scope.py @@ -0,0 +1,104 @@ +# +# 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.resources.openstack.neutron import neutron +from heat.engine import support + + +class AddressScope(neutron.NeutronResource): + """A resource for Neutron address scope. + + This resource can be associated with multiple subnet pools + in a one-to-many relationship. The subnet pools under an + address scope must not overlap. + """ + + required_service_extension = 'address-scope' + + support_status = support.SupportStatus(version='6.0.0') + + PROPERTIES = ( + NAME, SHARED, TENANT_ID, IP_VERSION, + ) = ( + 'name', 'shared', 'tenant_id', 'ip_version', + ) + + properties_schema = { + NAME: properties.Schema( + properties.Schema.STRING, + _('The name for the address scope.'), + required=True, + update_allowed=True + ), + SHARED: properties.Schema( + properties.Schema.BOOLEAN, + _('Whether the address scope should be shared to other ' + 'tenants. Note that the default policy setting ' + 'restricts usage of this attribute to administrative ' + 'users only, and restricts changing of shared address scope ' + 'to unshared with update.'), + default=False, + update_allowed=True + ), + TENANT_ID: properties.Schema( + properties.Schema.STRING, + _('The owner tenant ID of the address scope. Only ' + 'administrative users can specify a tenant ID ' + 'other than their own.'), + constraints=[constraints.CustomConstraint('keystone.project')] + ), + IP_VERSION: properties.Schema( + properties.Schema.INTEGER, + _('Address family of the address scope, which is 4 or 6.'), + default=4, + constraints=[ + constraints.AllowedValues([4, 6]), + ] + ), + } + + def handle_create(self): + props = self.prepare_properties( + self.properties, + self.physical_resource_name()) + + address_scope = self.client().create_address_scope( + {'address_scope': props})['address_scope'] + self.resource_id_set(address_scope['id']) + + def handle_delete(self): + if self.resource_id is None: + return + + with self.client_plugin().ignore_not_found: + self.client().delete_address_scope(self.resource_id) + + def handle_update(self, json_snippet, tmpl_diff, prop_diff): + if prop_diff: + self.client().update_address_scope( + self.resource_id, + {'address_scope': prop_diff}) + + def _show_resource(self): + return self.client().show_address_scope( + self.resource_id)['address_scope'] + + +def resource_mapping(): + return { + 'OS::Neutron::AddressScope': AddressScope + } diff --git a/heat/tests/openstack/neutron/test_address_scope.py b/heat/tests/openstack/neutron/test_address_scope.py new file mode 100644 index 0000000000..18c8523bb3 --- /dev/null +++ b/heat/tests/openstack/neutron/test_address_scope.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. + +import mock + +from heat.common import template_format +from heat.engine.clients.os import neutron +from heat.engine.resources.openstack.neutron import address_scope +from heat.engine import rsrc_defn +from heat.engine import stack +from heat.engine import template +from heat.tests import common +from heat.tests import utils + +address_scope_template = ''' +heat_template_version: 2016-04-08 +description: This template to define a neutron address scope. +resources: + my_address_scope: + type: OS::Neutron::AddressScope + properties: + name: test_address_scope + shared: False + tenant_id: d66c74c01d6c41b9846088c1ad9634d0 +''' + + +class NeutronAddressScopeTest(common.HeatTestCase): + def setUp(self): + super(NeutronAddressScopeTest, self).setUp() + + utils.setup_dummy_db() + self.ctx = utils.dummy_context() + + tpl = template_format.parse(address_scope_template) + self.stack = stack.Stack( + self.ctx, + 'neutron_address_scope_test', + template.Template(tpl) + ) + + self.neutronclient = mock.MagicMock() + self.patchobject(neutron.NeutronClientPlugin, 'has_extension', + return_value=True) + self.my_address_scope = self.stack['my_address_scope'] + self.my_address_scope.client = mock.MagicMock( + return_value=self.neutronclient) + + def test_resource_mapping(self): + mapping = address_scope.resource_mapping() + self.assertEqual(address_scope.AddressScope, + mapping['OS::Neutron::AddressScope']) + self.assertIsInstance(self.my_address_scope, + address_scope.AddressScope) + + def test_address_scope_handle_create(self): + addrs = { + 'address_scope': { + 'name': 'test_address_scope', + 'id': '9c1eb3fe-7bba-479d-bd43-1d497e53c384', + 'tenant_id': 'd66c74c01d6c41b9846088c1ad9634d0', + 'shared': False, + 'ip_version': 4 + } + } + create_props = {'name': 'test_address_scope', + 'shared': False, + 'tenant_id': 'd66c74c01d6c41b9846088c1ad9634d0', + 'ip_version': 4} + + self.neutronclient.create_address_scope.return_value = addrs + self.my_address_scope.handle_create() + self.assertEqual('9c1eb3fe-7bba-479d-bd43-1d497e53c384', + self.my_address_scope.resource_id) + self.neutronclient.create_address_scope.assert_called_once_with( + {'address_scope': create_props} + ) + + def test_address_scope_handle_delete(self): + addrs_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151' + self.my_address_scope.resource_id = addrs_id + self.neutronclient.delete_address_scope.return_value = None + + self.assertIsNone(self.my_address_scope.handle_delete()) + self.neutronclient.delete_address_scope.assert_called_once_with( + self.my_address_scope.resource_id) + + def test_address_scope_handle_delete_not_found(self): + addrs_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151' + self.my_address_scope.resource_id = addrs_id + not_found = self.neutronclient.NotFound + self.neutronclient.delete_address_scope.side_effect = not_found + + self.assertIsNone(self.my_address_scope.handle_delete()) + self.neutronclient.delete_address_scope.assert_called_once_with( + self.my_address_scope.resource_id) + + def test_address_scope_handle_delete_resource_id_is_none(self): + self.my_address_scope.resource_id = None + self.assertIsNone(self.my_address_scope.handle_delete()) + self.assertEqual(0, + self.neutronclient.delete_address_scope.call_count) + + def test_address_scope_handle_update(self): + addrs_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151' + self.my_address_scope.resource_id = addrs_id + + props = { + 'name': 'new_name', + 'shared': True + } + + update_snippet = rsrc_defn.ResourceDefinition( + self.my_address_scope.name, + self.my_address_scope.type(), + props) + + self.my_address_scope.handle_update( + json_snippet=update_snippet, + tmpl_diff={}, + prop_diff=props) + + self.neutronclient.update_address_scope.assert_called_once_with( + addrs_id, {'address_scope': props}) + + def test_address_scope_get_attr(self): + self.my_address_scope.resource_id = 'addrs_id' + addrs = { + 'address_scope': { + 'name': 'test_addrs', + 'id': '9c1eb3fe-7bba-479d-bd43-1d497e53c384', + 'tenant_id': 'd66c74c01d6c41b9846088c1ad9634d0', + 'shared': True, + 'ip_version': 4 + } + } + self.neutronclient.show_address_scope.return_value = addrs + self.assertEqual(addrs['address_scope'], + self.my_address_scope.FnGetAtt('show')) + self.neutronclient.show_address_scope.assert_called_once_with( + self.my_address_scope.resource_id) diff --git a/releasenotes/notes/neutron-address-scope-ce234763e22c7449.yaml b/releasenotes/notes/neutron-address-scope-ce234763e22c7449.yaml new file mode 100644 index 0000000000..4ceb0ac2a9 --- /dev/null +++ b/releasenotes/notes/neutron-address-scope-ce234763e22c7449.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + A new ``OS::Neutron:AddressScope`` resource that helps in + managing the lifecycle of neutron address scope. + Availability of this resource depends on availability of + neutron ``address-scope`` API extension. This resource can + be associated with multiple subnet pools in a one-to-many + relationship. The subnet pools under an address scope must + not overlap. \ No newline at end of file