diff --git a/heat/engine/resources/openstack/neutron/l2_gateway.py b/heat/engine/resources/openstack/neutron/l2_gateway.py new file mode 100644 index 0000000000..fb32654689 --- /dev/null +++ b/heat/engine/resources/openstack/neutron/l2_gateway.py @@ -0,0 +1,158 @@ +# Copyright 2018 Ericsson +# +# 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 collections +import six + +from heat.common.i18n import _ +from heat.engine import properties +from heat.engine.resources.openstack.neutron import neutron +from heat.engine import support + + +class L2Gateway(neutron.NeutronResource): + """A resource for managing Neutron L2 Gateways. + + The are a number of use cases that can be addressed by an L2 Gateway API. + Most notably in cloud computing environments, a typical use case is + bridging the virtual with the physical. Translate this to Neutron and the + OpenStack world, and this means relying on L2 Gateway capabilities to + extend Neutron logical (overlay) networks into physical (provider) + networks that are outside the OpenStack realm. + """ + + required_service_extension = 'l2-gateway' + + entity = 'l2_gateway' + + support_status = support.SupportStatus(version='12.0.0') + + PROPERTIES = ( + NAME, DEVICES, + ) = ( + 'name', 'devices', + ) + + _DEVICE_KEYS = ( + DEVICE_NAME, INTERFACES, + ) = ( + 'device_name', 'interfaces', + ) + + _INTERFACE_KEYS = ( + INTERFACE_NAME, SEGMENTATION_ID, + ) = ( + 'name', 'segmentation_id', + ) + + _interface_schema = { + INTERFACE_NAME: properties.Schema( + properties.Schema.STRING, + _('The name of the interface on the gateway device.'), + required=True + ), + SEGMENTATION_ID: properties.Schema( + properties.Schema.LIST, + _('A list of segmentation ids of the interface.') + ), + } + + _device_schema = { + DEVICE_NAME: properties.Schema( + properties.Schema.STRING, + _('The name of the gateway device.'), + required=True + ), + INTERFACES: properties.Schema( + properties.Schema.LIST, + _('List of gateway device interfaces.'), + schema=properties.Schema( + properties.Schema.MAP, + schema=_interface_schema + ), + required=True + ) + } + + properties_schema = { + NAME: properties.Schema( + properties.Schema.STRING, + _('A symbolic name for the l2-gateway, ' + 'which is not required to be unique.'), + required=True, + update_allowed=True + ), + DEVICES: properties.Schema( + properties.Schema.LIST, + _('List of gateway devices.'), + schema=properties.Schema( + properties.Schema.MAP, + schema=_device_schema + ), + required=True, + update_allowed=True + ), + } + + @staticmethod + def _remove_none_value_props(props): + if isinstance(props, collections.Mapping): + return dict((k, L2Gateway._remove_none_value_props(v)) for k, v + in props.items() if v is not None) + elif (isinstance(props, collections.Sequence) and + not isinstance(props, six.string_types)): + return list(L2Gateway._remove_none_value_props(l) for l in props + if l is not None) + return props + + @staticmethod + def prepare_properties(properties, name): + # Overrides method from base class NeutronResource to ensure None + # values are removed from all levels of value_specs. + + # TODO(neatherweb): move this recursive check for None to + # prepare_properties in NeutronResource + props = L2Gateway._remove_none_value_props(dict(properties)) + + if 'name' in properties: + props.setdefault('name', name) + + return props + + def handle_create(self): + props = self.prepare_properties( + self.properties, + self.physical_resource_name()) + l2gw = self.client().create_l2_gateway( + {'l2_gateway': props})['l2_gateway'] + self.resource_id_set(l2gw['id']) + + def handle_delete(self): + if self.resource_id is None: + return + with self.client_plugin().ignore_not_found: + self.client().delete_l2_gateway(self.resource_id) + + def handle_update(self, json_snippet, tmpl_diff, prop_diff): + if prop_diff: + self.prepare_update_properties(prop_diff) + prop_diff = L2Gateway._remove_none_value_props(prop_diff) + self.client().update_l2_gateway( + self.resource_id, {'l2_gateway': prop_diff}) + + +def resource_mapping(): + return { + 'OS::Neutron::L2Gateway': L2Gateway, + } diff --git a/heat/tests/openstack/neutron/test_neutron_l2_gateway.py b/heat/tests/openstack/neutron/test_neutron_l2_gateway.py new file mode 100644 index 0000000000..dcf2930f2c --- /dev/null +++ b/heat/tests/openstack/neutron/test_neutron_l2_gateway.py @@ -0,0 +1,312 @@ +# Copyright 2018 Ericsson +# +# 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 neutronclient.common import exceptions +from neutronclient.v2_0 import client as neutronclient +import six + +from heat.common import template_format +from heat.engine.clients.os import neutron +from heat.engine import scheduler +from heat.tests import common +from heat.tests import utils + +try: + from networking_l2gw.services.l2gateway.exceptions import ( + L2GatewaySegmentationRequired) # noqa +except ImportError: + class L2GatewaySegmentationRequired(exceptions.NeutronException): + message = ("L2 gateway segmentation id must be consistent for all " + "the interfaces") + + +class NeutronL2GatewayTest(common.HeatTestCase): + test_template = ''' + heat_template_version: queens + description: Template to test L2Gateway Neutron resource + resources: + l2gw: + type: OS::Neutron::L2Gateway + properties: + name: L2GW01 + devices: + - device_name: switch01 + interfaces: + - name: eth0 + - name: eth1 + ''' + + test_template_update = ''' + heat_template_version: queens + description: Template to test L2Gateway Neutron resource + resources: + l2gw: + type: OS::Neutron::L2Gateway + properties: + name: L2GW01 + devices: + - device_name: switch01 + interfaces: + - name: eth0 + - name: eth1 + - device_name: switch02 + interfaces: + - name: eth5 + - name: eth6 + ''' + + test_template_with_seg = ''' + heat_template_version: queens + description: Template to test L2Gateway Neutron resource + resources: + l2gw: + type: OS::Neutron::L2Gateway + properties: + name: L2GW01 + devices: + - device_name: switch01 + interfaces: + - name: eth0 + segmentation_id: + - 101 + - 102 + - 103 + - name: eth1 + segmentation_id: + - 101 + - 102 + - 103 + ''' + + test_template_invalid_seg = ''' + heat_template_version: queens + description: Template to test L2Gateway Neutron resource + resources: + l2gw: + type: OS::Neutron::L2Gateway + properties: + name: L2GW01 + devices: + - device_name: switch01 + interfaces: + - name: eth0 + segmentation_id: + - 101 + - 102 + - 103 + - name: eth1 + ''' + + mock_create_req = { + "l2_gateway": { + "name": "L2GW01", + "devices": [{ + "device_name": "switch01", + "interfaces": [ + {"name": "eth0"}, + {"name": "eth1"} + ] + }] + }} + mock_create_reply = { + "l2_gateway": { + "name": "L2GW01", + "id": "d3590f37-b072-4358-9719-71964d84a31c", + "tenant_id": "7ea656c7c9b8447494f33b0bc741d9e6", + "devices": [{ + "device_name": "switch01", + "interfaces": [ + {"name": "eth0"}, + {"name": "eth1"} + ] + }] + }} + + mock_update_req = { + "l2_gateway": { + "devices": [{ + "device_name": "switch01", + "interfaces": [ + {"name": "eth0"}, + {"name": "eth1"} + ] + }, { + "device_name": "switch02", + "interfaces": [ + {"name": "eth5"}, + {"name": "eth6"}] + }] + }} + + mock_update_reply = { + "l2_gateway": { + "name": "L2GW01", + "id": "d3590f37-b072-4358-9719-71964d84a31c", + "tenant_id": "7ea656c7c9b8447494f33b0bc741d9e6", + "devices": [{ + "device_name": "switch01", + "interfaces": [ + {"name": "eth0"}, + {"name": "eth1"}] + }, { + "device_name": "switch02", + "interfaces": [ + {"name": "eth5"}, + {"name": "eth6"}] + }] + }} + + mock_create_with_seg_req = { + "l2_gateway": { + "name": "L2GW01", + "devices": [{ + "device_name": "switch01", + "interfaces": [ + {"name": "eth0", + "segmentation_id": [101, 102, 103]}, + {"name": "eth1", + "segmentation_id": [101, 102, 103]} + ] + }] + }} + mock_create_with_seg_reply = { + "l2_gateway": { + "name": "L2GW01", + "id": "d3590f37-b072-4358-9719-71964d84a31c", + "tenant_id": "7ea656c7c9b8447494f33b0bc741d9e6", + "devices": [{ + "device_name": "switch01", + "interfaces": [ + {"name": "eth0", + "segmentation_id": ["101", "102", "103"]}, + {"name": "eth1", + "segmentation_id": ["101", "102", "103"]} + ] + }] + }} + + mock_create_invalid_seg_req = { + "l2_gateway": { + "name": "L2GW01", + "devices": [{ + "device_name": "switch01", + "interfaces": [ + {"name": "eth0", + "segmentation_id": [101, 102, 103]}, + {"name": "eth1"} + ] + }] + }} + mock_create_invalid_seg_reply = { + "l2_gateway": { + "name": "L2GW01", + "id": "d3590f37-b072-4358-9719-71964d84a31c", + "tenant_id": "7ea656c7c9b8447494f33b0bc741d9e6", + "devices": [{ + "device_name": "switch01", + "interfaces": [ + {"name": "eth0", + "segmentation_id": ["101", "102", "103"]}, + {"name": "eth1"} + ] + }] + }} + + def setUp(self): + super(NeutronL2GatewayTest, self).setUp() + self.mockclient = mock.MagicMock() + self.patchobject(neutronclient, 'Client', return_value=self.mockclient) + + self.patchobject(neutron.NeutronClientPlugin, 'has_extension', + return_value=True) + + def _create_l2_gateway(self, hot, reply): + # stack create + self.mockclient.create_l2_gateway.return_value = reply + self.mockclient.show_l2_gateway.return_value = reply + template = template_format.parse(hot) + self.stack = utils.parse_stack(template) + scheduler.TaskRunner(self.stack.create)() + self.l2gw_resource = self.stack['l2gw'] + + def test_l2_gateway_create(self): + self._create_l2_gateway(self.test_template, self.mock_create_reply) + self.assertIsNone(self.l2gw_resource.validate()) + self.assertEqual((self.l2gw_resource.CREATE, + self.l2gw_resource.COMPLETE), + self.l2gw_resource.state) + self.assertEqual('d3590f37-b072-4358-9719-71964d84a31c', + self.l2gw_resource.FnGetRefId()) + self.mockclient.create_l2_gateway.assert_called_once_with( + self.mock_create_req) + + def test_l2_gateway_update(self): + self._create_l2_gateway(self.test_template, self.mock_create_reply) + # update l2_gateway with 2nd device + self.mockclient.update_l2_gateway.return_value = self.mock_update_reply + self.mockclient.show_l2_gateway.return_value = self.mock_update_reply + updated_tmpl = template_format.parse(self.test_template_update) + updated_stack = utils.parse_stack(updated_tmpl) + self.stack.update(updated_stack) + ud_l2gw_resource = self.stack['l2gw'] + self.assertIsNone(ud_l2gw_resource.validate()) + self.assertEqual((ud_l2gw_resource.UPDATE, ud_l2gw_resource.COMPLETE), + ud_l2gw_resource.state) + self.assertEqual('d3590f37-b072-4358-9719-71964d84a31c', + ud_l2gw_resource.FnGetRefId()) + self.mockclient.update_l2_gateway.assert_called_once_with( + 'd3590f37-b072-4358-9719-71964d84a31c', + self.mock_update_req) + + def test_l2_gateway_create_with_seg(self): + # test with segmentation_id in template + self._create_l2_gateway(self.test_template_with_seg, + self.mock_create_with_seg_reply) + self.assertIsNone(self.l2gw_resource.validate()) + self.assertEqual((self.l2gw_resource.CREATE, + self.l2gw_resource.COMPLETE), + self.l2gw_resource.state) + self.assertEqual('d3590f37-b072-4358-9719-71964d84a31c', + self.l2gw_resource.FnGetRefId()) + self.mockclient.create_l2_gateway.assert_called_once_with( + self.mock_create_with_seg_req) + + def test_l2_gateway_create_invalid_seg(self): + # test failure when segmentation_id is not consistent across + # all interfaces + self.mockclient.create_l2_gateway.side_effect = ( + L2GatewaySegmentationRequired()) + template = template_format.parse(self.test_template_invalid_seg) + self.stack = utils.parse_stack(template) + scheduler.TaskRunner(self.stack.create)() + self.l2gw_resource = self.stack['l2gw'] + self.assertIsNone(self.l2gw_resource.validate()) + self.assertEqual( + six.text_type('Resource CREATE failed: ' + 'L2GatewaySegmentationRequired: resources.l2gw: ' + 'L2 gateway segmentation id must be consistent for ' + 'all the interfaces'), + self.stack.status_reason) + self.assertEqual((self.l2gw_resource.CREATE, + self.l2gw_resource.FAILED), + self.l2gw_resource.state) + self.mockclient.create_l2_gateway.assert_called_once_with( + self.mock_create_invalid_seg_req) + + def test_l2_gateway_delete(self): + self._create_l2_gateway(self.test_template, self.mock_create_reply) + self.stack.delete() + self.mockclient.delete_l2_gateway.assert_called_with( + 'd3590f37-b072-4358-9719-71964d84a31c') diff --git a/releasenotes/notes/neutron-l2gw-support-9fbb690bb5648f76.yaml b/releasenotes/notes/neutron-l2gw-support-9fbb690bb5648f76.yaml new file mode 100644 index 0000000000..8d229efa5c --- /dev/null +++ b/releasenotes/notes/neutron-l2gw-support-9fbb690bb5648f76.yaml @@ -0,0 +1,6 @@ +--- +features: + - New resource ``OS::Neutron::L2Gateway`` to allow management of Neutron + Layer2 Gateway. This resource provides life-cycle management of layer2 + gateway instances. The resource depends on the Neutron ``l2-gateway`` + extension.