diff --git a/vmware_nsx/common/exceptions.py b/vmware_nsx/common/exceptions.py index ad25019b6b..85ecea48d0 100644 --- a/vmware_nsx/common/exceptions.py +++ b/vmware_nsx/common/exceptions.py @@ -141,3 +141,7 @@ class StaleRevision(ManagerError): class NsxL2GWConnectionMappingNotFound(n_exc.NotFound): message = _('Unable to find mapping for L2 gateway connection: %(conn)s') + + +class NsxL2GWDeviceNotFound(n_exc.NotFound): + message = _('Unable to find logical L2 gateway device.') diff --git a/vmware_nsx/common/nsxv_constants.py b/vmware_nsx/common/nsxv_constants.py index b2d349403a..efa4ed8666 100644 --- a/vmware_nsx/common/nsxv_constants.py +++ b/vmware_nsx/common/nsxv_constants.py @@ -29,3 +29,6 @@ INTER_EDGE_PURPOSE = 'inter_edge_net' # etc INTERNAL_TENANT_ID = 'a1b2c3d4-e5f6-eeff-ffee-6f5e4d3c2b1a' + +# L2 gateway edge name prefix +L2_GATEWAY_EDGE = 'L2 bridging' diff --git a/vmware_nsx/plugins/nsx_v/vshield/edge_appliance_driver.py b/vmware_nsx/plugins/nsx_v/vshield/edge_appliance_driver.py index 451a81b45c..8297563463 100644 --- a/vmware_nsx/plugins/nsx_v/vshield/edge_appliance_driver.py +++ b/vmware_nsx/plugins/nsx_v/vshield/edge_appliance_driver.py @@ -1045,3 +1045,18 @@ class EdgeApplianceDriver(object): status_callback=self._retry_task, userdata=userdata) self.task_manager.add(task) + + def create_bridge(self, device_name, bridge): + try: + self.vcns.create_bridge(device_name, bridge) + except exceptions.VcnsApiException: + with excutils.save_and_reraise_exception(): + LOG.exception(_LE("Failed to create bridge in the %s"), + device_name) + + def delete_bridge(self, device_name): + try: + self.vcns.delete_bridge(device_name) + except exceptions.VcnsApiException: + LOG.exception(_LE("Failed to delete bridge in the %s"), + device_name) diff --git a/vmware_nsx/plugins/nsx_v/vshield/vcns.py b/vmware_nsx/plugins/nsx_v/vshield/vcns.py index fa196031dc..a53ec7aa53 100644 --- a/vmware_nsx/plugins/nsx_v/vshield/vcns.py +++ b/vmware_nsx/plugins/nsx_v/vshield/vcns.py @@ -61,6 +61,9 @@ DHCP_BINDING_RESOURCE = "bindings" # Syetem control constants SYSCTL_SERVICE = 'systemcontrol/config' +# L2 gateway constants +BRIDGE = "bridging/config" + def retry_upon_exception(exc, delay=500, max_delay=2000, max_attempts=cfg.CONF.nsxv.retries): @@ -489,6 +492,19 @@ class Vcns(object): if sg.find('name').text == sg_name: return sg.find('objectId').text + @retry_upon_exception(exceptions.VcnsApiException) + def create_bridge(self, edge_id, request): + """Create a bridge.""" + uri = self._build_uri_path(edge_id, BRIDGE) + return self.do_request(HTTP_PUT, uri, request, format='xml', + decode=False) + + @retry_upon_exception(exceptions.VcnsApiException) + def delete_bridge(self, edge_id): + """Delete a bridge.""" + uri = self._build_uri_path(edge_id, BRIDGE) + return self.do_request(HTTP_DELETE, uri, format='xml', decode=False) + def create_section(self, type, request): """Creates a layer 3 or layer 2 section in nsx rule table. diff --git a/vmware_nsx/services/l2gateway/nsx_v/__init__.py b/vmware_nsx/services/l2gateway/nsx_v/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vmware_nsx/services/l2gateway/nsx_v/driver.py b/vmware_nsx/services/l2gateway/nsx_v/driver.py new file mode 100644 index 0000000000..68eae76828 --- /dev/null +++ b/vmware_nsx/services/l2gateway/nsx_v/driver.py @@ -0,0 +1,163 @@ +# Copyright 2015 VMware, Inc. +# +# 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 networking_l2gw.db.l2gateway import l2gateway_db +from networking_l2gw.services.l2gateway.common import constants as l2gw_const +from networking_l2gw.services.l2gateway import exceptions as l2gw_exc +from neutron.common import exceptions as n_exc +from neutron.i18n import _LE +from neutron import manager +from oslo_log import log as logging +from oslo_utils import excutils +from oslo_utils import uuidutils + +from vmware_nsx.common import exceptions as nsx_exc +from vmware_nsx.common import nsxv_constants +from vmware_nsx.db import db as nsx_db +from vmware_nsx.db import nsxv_db +from vmware_nsx.plugins.nsx_v.vshield.common import exceptions + +LOG = logging.getLogger(__name__) + + +class NsxvL2GatewayDriver(l2gateway_db.L2GatewayMixin): + + def _core_plugin(self): + return manager.NeutronManager.get_plugin() + + @property + def _nsxv(self): + return self._core_plugin.nsx_v + + @property + def _edge_manager(self): + return self._core_plugin.edge_manager + + def _validate_device_list(self, devices): + # In NSX-v, one L2 gateway is mapped to one DLR. + # So we expect only one device to be configured as part of + # a L2 gateway resource. + if len(devices) != 1: + msg = _("Only a single device is supported for one L2 gateway") + raise n_exc.InvalidInput(error_message=msg) + + def _validate_interface_list(self, interfaces): + # In NSXv, interface is mapped to a vDS VLAN port group. + # Since HA is not supported, only one interface is expected + if len(interfaces) != 1: + msg = _("Only a single interface is supported for one L2 gateway") + raise n_exc.InvalidInput(error_message=msg) + if not self._nsxv.vcns.validate_network(interfaces[0]['name']): + msg = _("Configured interface not found") + raise n_exc.InvalidInput(error_message=msg) + + def create_l2_gateway(self, context, l2_gateway): + """Create a logical L2 gateway.""" + self._admin_check(context, 'CREATE') + gw = l2_gateway[self.gateway_resource] + devices = gw['devices'] + self._validate_device_list(devices) + interfaces = devices[0]['interfaces'] + self._validate_interface_list(interfaces) + # Create a dedicated DLR + try: + edge_id = self._create_l2_gateway_edge(context) + except nsx_exc.NsxL2GWDeviceNotFound: + LOG.exception(_LE("Failed to create backend device " + "for L2 gateway")) + raise + + devices[0]['device_name'] = edge_id + l2_gateway[self.gateway_resource]['devices'] = devices + return super(NsxvL2GatewayDriver, self).create_l2_gateway(context, + l2_gateway) + + def _create_l2_gateway_edge(self, context): + # Create a dedicated DLR + lrouter = {'name': nsxv_constants.L2_GATEWAY_EDGE, + 'id': uuidutils.generate_uuid()} + self._edge_manager.create_lrouter(context, + lrouter, lswitch=None, dist=True) + edge_binding = nsxv_db.get_nsxv_router_binding(context.session, + lrouter['id']) + if not edge_binding: + raise nsx_exc.NsxL2GWDeviceNotFound() + return edge_binding['edge_id'] + + def _get_device(self, context, l2gw_id): + devices = self._get_l2_gateway_devices(context, l2gw_id) + return devices[0] + + def create_l2_gateway_connection(self, context, l2_gateway_connection): + """Create a L2 gateway connection.""" + gw_connection = l2_gateway_connection.get(l2gw_const. + CONNECTION_RESOURCE_NAME) + l2gw_connection = super( + NsxvL2GatewayDriver, self).create_l2_gateway_connection( + context, l2_gateway_connection) + network_id = gw_connection.get('network_id') + virtual_wire = nsx_db.get_nsx_switch_ids(context.session, network_id) + l2gw_id = gw_connection.get(l2gw_const.L2GATEWAY_ID) + # In NSX-v, there will be only one device configured per L2 gateway. + # The name of the device shall carry the backend DLR. + device = self._get_device(context, l2gw_id) + device_name = device.get('device_name') + device_id = device.get('id') + interface = self._get_l2_gw_interfaces(context, device_id) + interface_name = interface[0].get("interface_name") + bridge_name = "bridge-" + uuidutils.generate_uuid() + bridge_dict = {"bridges": + {"bridge": + {"name": bridge_name, + "virtualWire": virtual_wire[0], + "dvportGroup": interface_name}}} + try: + self._nsxv.create_bridge(device_name, bridge_dict) + except exceptions.VcnsApiException: + with excutils.save_and_reraise_exception(): + super(NsxvL2GatewayDriver, self).delete_l2_gateway_connection( + context, l2gw_connection['id']) + LOG.exception(_LE("Failed to update NSX, " + "rolling back changes on neutron")) + return l2gw_connection + + def delete_l2_gateway_connection(self, context, l2_gateway_connection): + """Delete a L2 gateway connection.""" + self._admin_check(context, 'DELETE') + gw_connection = self.get_l2_gateway_connection(context, + l2_gateway_connection) + if not gw_connection: + raise l2gw_exc.L2GatewayConnectionNotFound( + l2_gateway_connection) + l2gw_id = gw_connection.get(l2gw_const.L2GATEWAY_ID) + device = self._get_device(context, l2gw_id) + device_name = device.get('device_name') + self._nsxv.delete_bridge(device_name) + return super(NsxvL2GatewayDriver, + self).delete_l2_gateway_connection(context, + l2_gateway_connection) + + def delete_l2_gateway(self, context, l2_gateway): + """Delete a L2 gateway.""" + self._admin_check(context, 'DELETE') + device = self._get_device(context, l2_gateway) + super(NsxvL2GatewayDriver, self).delete_l2_gateway(context, l2_gateway) + edge_id = device.get('device_name') + rtr_binding = nsxv_db.get_nsxv_router_binding_by_edge( + context.session, edge_id) + if rtr_binding: + self._edge_manager.delete_lrouter(context, + rtr_binding['router_id']) diff --git a/vmware_nsx/tests/unit/vmware/services/l2gateway/test_nsxv_driver.py b/vmware_nsx/tests/unit/vmware/services/l2gateway/test_nsxv_driver.py new file mode 100644 index 0000000000..89cd7dfff8 --- /dev/null +++ b/vmware_nsx/tests/unit/vmware/services/l2gateway/test_nsxv_driver.py @@ -0,0 +1,182 @@ +# Copyright 2015 VMware, Inc. +# 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. +import mock + +from neutron.common import exceptions as n_exc +from neutron import context +from neutron.tests import base + +from networking_l2gw.db.l2gateway import l2gateway_db +from vmware_nsx.common import exceptions as nsx_exc +from vmware_nsx.db import nsxv_db +from vmware_nsx.services.l2gateway.nsx_v import driver as nsx_v_driver + + +class TestL2gatewayDriver(base.BaseTestCase): + + def setUp(self): + super(TestL2gatewayDriver, self).setUp() + self.context = context.get_admin_context() + self.plugin = nsx_v_driver.NsxvL2GatewayDriver() + + def test_validate_device_with_multi_devices(self): + fake_l2gw_dict = {"l2_gateway": + {"tenant_id": "fake__tenant_id", + "name": "fake_l2gw", + "devices": [{"interfaces": + [{"name": "fake_inter"}], + "device_name": "fake_dev"}, + {"interfaces": + [{"name": "fake_inter_1"}], + "device_name": "fake_dev_1"}]}} + with mock.patch.object(l2gateway_db.L2GatewayMixin, '_admin_check'): + self.assertRaises(n_exc.InvalidInput, + self.plugin.create_l2_gateway, + self.context, fake_l2gw_dict) + + def test_validate_interface_with_multi_interfaces(self): + fake_l2gw_dict = {"l2_gateway": + {"tenant_id": "fake_tenant_id", + "name": "fake_l2gw", + "devices": [{"interfaces": + [{"name": "fake_inter_1"}, + {"name": "fake_inter_2"}], + "device_name": "fake_dev"}]}} + with mock.patch.object(l2gateway_db.L2GatewayMixin, '_admin_check'): + self.assertRaises(n_exc.InvalidInput, + self.plugin.create_l2_gateway, + self.context, fake_l2gw_dict) + + @mock.patch('vmware_nsx.services.l2gateway.' + 'nsx_v.driver.NsxvL2GatewayDriver._nsxv') + def test_validate_interface_with_invalid_interfaces(self, _nsxv): + fake_interfaces = [{"name": "fake_inter"}] + _nsxv.vcns.validate_network.return_value = False + self.assertRaises(n_exc.InvalidInput, + self.plugin._validate_interface_list, + fake_interfaces) + + @mock.patch('vmware_nsx.services.l2gateway.' + 'nsx_v.driver.NsxvL2GatewayDriver._edge_manager') + def test_create_gw_edge_failure(self, edge_manager): + with mock.patch.object(nsxv_db, + 'get_nsxv_router_binding', + return_value=None): + self.assertRaises(nsx_exc.NsxL2GWDeviceNotFound, + self.plugin._create_l2_gateway_edge, + self.context) + + @mock.patch('networking_l2gw.db.l2gateway.l2gateway_db.' + 'L2GatewayMixin._admin_check') + @mock.patch('vmware_nsx.services.l2gateway.' + 'nsx_v.driver.NsxvL2GatewayDriver._validate_device_list') + @mock.patch('vmware_nsx.services.l2gateway.' + 'nsx_v.driver.NsxvL2GatewayDriver._validate_interface_list') + @mock.patch('vmware_nsx.services.l2gateway.' + 'nsx_v.driver.NsxvL2GatewayDriver._create_l2_gateway_edge') + @mock.patch('networking_l2gw.db.l2gateway.l2gateway_db.' + 'L2GatewayMixin.create_l2_gateway') + def test_create_l2_gateway_failure(self, create_l2gw, _create_l2gw_edge, + val_inter, val_dev, _admin_check): + fake_l2gw_dict = {"l2_gateway": + {"tenant_id": "fake_teannt_id", + "name": "fake_l2gw", + "devices": [{"interfaces": + [{"name": "fake_inter"}], + "device_name": "fake_dev"}]}} + _create_l2gw_edge.side_effect = nsx_exc.NsxL2GWDeviceNotFound + self.assertRaises(nsx_exc.NsxL2GWDeviceNotFound, + self.plugin.create_l2_gateway, + self.context, fake_l2gw_dict) + + @mock.patch('networking_l2gw.db.l2gateway.l2gateway_db.' + 'L2GatewayMixin._admin_check') + @mock.patch('vmware_nsx.services.l2gateway.' + 'nsx_v.driver.NsxvL2GatewayDriver._validate_device_list') + @mock.patch('vmware_nsx.services.l2gateway.' + 'nsx_v.driver.NsxvL2GatewayDriver._validate_interface_list') + @mock.patch('vmware_nsx.services.l2gateway.' + 'nsx_v.driver.NsxvL2GatewayDriver._create_l2_gateway_edge') + @mock.patch('networking_l2gw.db.l2gateway.l2gateway_db.' + 'L2GatewayMixin.create_l2_gateway') + @mock.patch('vmware_nsx.services.l2gateway.' + 'nsx_v.driver.NsxvL2GatewayDriver._edge_manager') + def test_create_l2_gateway(self, edge_manager, create_l2gw, + _create_l2gw_edge, + val_inter, val_dev, _admin_check): + fake_l2gw_dict = {"l2_gateway": + {"tenant_id": "fake_teannt_id", + "name": "fake_l2gw", + "devices": [{"interfaces": + [{"name": "fake_inter"}], + "device_name": "fake_dev"}]}} + fake_devices = [{"interfaces": [{"name": "fake_inter"}], + "device_name": "fake_dev"}] + fake_interfaces = [{"name": "fake_inter"}] + _create_l2gw_edge.return_value = 'fake_dev' + self.plugin.create_l2_gateway(self.context, fake_l2gw_dict) + _admin_check.assert_called_with(self.context, 'CREATE') + val_dev.assert_called_with(fake_devices) + val_inter.assert_called_with(fake_interfaces) + create_l2gw.assert_called_with(self.context, fake_l2gw_dict) + + @mock.patch('networking_l2gw.db.l2gateway.l2gateway_db.' + 'L2GatewayMixin._admin_check') + @mock.patch('networking_l2gw.db.l2gateway.l2gateway_db.' + 'L2GatewayMixin.get_l2_gateway_connection') + @mock.patch('networking_l2gw.db.l2gateway.l2gateway_db.' + 'L2GatewayMixin.delete_l2_gateway_connection') + @mock.patch('vmware_nsx.services.l2gateway.' + 'nsx_v.driver.NsxvL2GatewayDriver._get_device') + @mock.patch('vmware_nsx.services.l2gateway.' + 'nsx_v.driver.NsxvL2GatewayDriver._nsxv') + def test_delete_l2_gateway_connection(self, nsxv, get_devices, del_conn, + get_conn, admin_check): + fake_conn_dict = {'l2_gateway_id': 'fake_l2gw_id'} + fake_device_dict = {'id': 'fake_dev_id', + 'device_name': 'fake_dev_name'} + get_conn.return_value = fake_conn_dict + get_devices.return_value = fake_device_dict + self.plugin.delete_l2_gateway_connection(self.context, fake_conn_dict) + admin_check.assert_called_with(self.context, 'DELETE') + get_conn.assert_called_with(self.context, fake_conn_dict) + get_devices.assert_called_with(self.context, 'fake_l2gw_id') + self.plugin._nsxv().del_bridge.asert_called_with('fake_dev_name') + del_conn.assert_called_with(self.context, fake_conn_dict) + + @mock.patch('networking_l2gw.db.l2gateway.l2gateway_db.' + 'L2GatewayMixin._admin_check') + @mock.patch('vmware_nsx.services.l2gateway.' + 'nsx_v.driver.NsxvL2GatewayDriver._get_device') + @mock.patch('vmware_nsx.db.' + 'nsxv_db.get_nsxv_router_binding_by_edge') + @mock.patch('networking_l2gw.db.l2gateway.l2gateway_db.' + 'L2GatewayMixin.delete_l2_gateway') + @mock.patch('vmware_nsx.services.l2gateway.' + 'nsx_v.driver.NsxvL2GatewayDriver._edge_manager') + def test_delete_l2_gateway(self, edge_manager, del_l2gw, get_nsxv_router, + get_devices, admin_check): + fake_device_dict = {"id": "fake_dev_id", + "device_name": "fake_edge_name", + "l2_gateway_id": "fake_l2gw_id"} + fake_rtr_binding = {"router_id": 'fake_router_id'} + get_devices.return_value = fake_device_dict + get_nsxv_router.return_value = fake_rtr_binding + self.plugin.delete_l2_gateway(self.context, 'fake_l2gw_id') + admin_check.assert_called_with(self.context, 'DELETE') + get_devices.assert_called_with(self.context, 'fake_l2gw_id') + del_l2gw.assert_called_with(self.context, 'fake_l2gw_id') + get_nsxv_router.assert_called_with(self.context.session, + "fake_edge_name") diff --git a/vmware_nsx/tests/unit/vmware/vshield/fake_vcns.py b/vmware_nsx/tests/unit/vmware/vshield/fake_vcns.py index 880ef6fbac..69c78334da 100644 --- a/vmware_nsx/tests/unit/vmware/vshield/fake_vcns.py +++ b/vmware_nsx/tests/unit/vmware/vshield/fake_vcns.py @@ -304,6 +304,24 @@ class FakeVcns(object): response = '' return (header, response) + def create_bridge(self, edge_id, request): + if edge_id not in self._edges: + raise Exception(_("Edge %s does not exist") % edge_id) + header = { + 'status': 204 + } + response = '' + return (header, response) + + def delete_bridge(self, edge_id): + if edge_id not in self._edges: + raise Exception(_("Edge %s does not exist") % edge_id) + header = { + 'status': 204 + } + response = '' + return (header, response) + def get_nat_config(self, edge_id): if edge_id not in self._edges: raise Exception(_("Edge %s does not exist") % edge_id)