From ff5ebec12cb35b8a6fd29993cfa3303f0cda2f6d Mon Sep 17 00:00:00 2001 From: Abhishek Raut Date: Thu, 18 Aug 2016 22:04:15 -0700 Subject: [PATCH] NSXv3: Add support for trunk service driver This patch adds support for trunk extensions in the NSXv3 plugin. Now you can create trunk and subports which map to CIF on the backend. i.e. trunk port <-> parent port and subport <-> child port on the backend. If backend fails to update subports, the status of trunk will be set to ERROR. Use OSC commands for trunk CRUD operations. For e.g. Create trunk with a subport: openstack network trunk create --parent-port --subport port=child-port,segmentation-type=vlan,segmentation-id=200 TRUNK_NAME Delete trunk: openstack network trunk delete TRUNK_NAME Change-Id: Iedd47d868d803ca8c52856554885fd7d14668924 --- ...xv3-add-trunk-driver-925ad1205972cbdf.yaml | 7 + vmware_nsx/common/nsx_constants.py | 3 + vmware_nsx/plugins/nsx_v3/plugin.py | 4 + vmware_nsx/services/trunk/README.rst | 12 ++ vmware_nsx/services/trunk/__init__.py | 0 vmware_nsx/services/trunk/nsx_v3/__init__.py | 0 vmware_nsx/services/trunk/nsx_v3/driver.py | 188 ++++++++++++++++++ .../tests/unit/services/trunk/__init__.py | 0 .../unit/services/trunk/test_nsxv3_driver.py | 183 +++++++++++++++++ 9 files changed, 397 insertions(+) create mode 100644 releasenotes/notes/nsxv3-add-trunk-driver-925ad1205972cbdf.yaml create mode 100644 vmware_nsx/services/trunk/README.rst create mode 100644 vmware_nsx/services/trunk/__init__.py create mode 100644 vmware_nsx/services/trunk/nsx_v3/__init__.py create mode 100644 vmware_nsx/services/trunk/nsx_v3/driver.py create mode 100644 vmware_nsx/tests/unit/services/trunk/__init__.py create mode 100644 vmware_nsx/tests/unit/services/trunk/test_nsxv3_driver.py diff --git a/releasenotes/notes/nsxv3-add-trunk-driver-925ad1205972cbdf.yaml b/releasenotes/notes/nsxv3-add-trunk-driver-925ad1205972cbdf.yaml new file mode 100644 index 0000000000..f103c60647 --- /dev/null +++ b/releasenotes/notes/nsxv3-add-trunk-driver-925ad1205972cbdf.yaml @@ -0,0 +1,7 @@ +--- +prelude: > + Support VLAN-aware-VM feature in NSXv3 plugin. +features: + - Trunk driver for NSXv3 plugin which allows creation of trunk ports + and subports which subsequently create parent port and child ports + relationship in the backend. diff --git a/vmware_nsx/common/nsx_constants.py b/vmware_nsx/common/nsx_constants.py index 53388b1f0f..b5b91624de 100644 --- a/vmware_nsx/common/nsx_constants.py +++ b/vmware_nsx/common/nsx_constants.py @@ -59,3 +59,6 @@ BRIDGE_ENDPOINT = "BRIDGEENDPOINT" # NSX service type SERVICE_DHCP = "dhcp" + +# NSXv3 CORE PLUGIN PATH +VMWARE_NSX_V3_PLUGIN_NAME = 'vmware_nsx.plugin.NsxV3Plugin' diff --git a/vmware_nsx/plugins/nsx_v3/plugin.py b/vmware_nsx/plugins/nsx_v3/plugin.py index e9cbfe9654..d90b53c1ba 100644 --- a/vmware_nsx/plugins/nsx_v3/plugin.py +++ b/vmware_nsx/plugins/nsx_v3/plugin.py @@ -94,6 +94,7 @@ from vmware_nsx.nsxlib.v3 import router from vmware_nsx.nsxlib.v3 import security from vmware_nsx.services.qos.common import utils as qos_com_utils from vmware_nsx.services.qos.nsx_v3 import utils as qos_utils +from vmware_nsx.services.trunk.nsx_v3 import driver as trunk_driver LOG = log.getLogger(__name__) @@ -215,6 +216,9 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, # translate configured transport zones/rotuers names to uuid self._translate_configured_names_2_uuids() + # Register NSXv3 trunk driver to support trunk extensions + self.trunk_driver = trunk_driver.NsxV3TrunkDriver.create(self) + def _init_nsx_profiles(self): LOG.debug("Initializing NSX v3 port spoofguard switching profile") # XXX improve logic to avoid requiring setting this to none. diff --git a/vmware_nsx/services/trunk/README.rst b/vmware_nsx/services/trunk/README.rst new file mode 100644 index 0000000000..da1b176a12 --- /dev/null +++ b/vmware_nsx/services/trunk/README.rst @@ -0,0 +1,12 @@ +========================================= + Enabling NSX trunk driver using DevStack +========================================= + +1. Download DevStack + +2. Enable trunk service and configure following flags in ``local.conf``:: + + [[local]|[localrc]] + # Trunk plugin NSXv3 driver config + ENABLED_SERVICES+=,q-trunk + Q_SERVICE_PLUGIN_CLASSES=trunk diff --git a/vmware_nsx/services/trunk/__init__.py b/vmware_nsx/services/trunk/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vmware_nsx/services/trunk/nsx_v3/__init__.py b/vmware_nsx/services/trunk/nsx_v3/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vmware_nsx/services/trunk/nsx_v3/driver.py b/vmware_nsx/services/trunk/nsx_v3/driver.py new file mode 100644 index 0000000000..fd16d53ce4 --- /dev/null +++ b/vmware_nsx/services/trunk/nsx_v3/driver.py @@ -0,0 +1,188 @@ +# Copyright 2016 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 oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import excutils + +from neutron.callbacks import events +from neutron.callbacks import registry +from neutron.extensions import portbindings +from neutron.services.trunk import constants as trunk_consts +from neutron.services.trunk.drivers import base + +from vmware_nsx._i18n import _LE +from vmware_nsx.common import nsx_constants as nsx_consts +from vmware_nsx.common import utils as nsx_utils +from vmware_nsx.db import db as nsx_db +from vmware_nsx.nsxlib.v3 import exceptions as nsxlib_exc +from vmware_nsx.nsxlib.v3 import resources as nsx_resources + +LOG = logging.getLogger(__name__) + +SUPPORTED_INTERFACES = ( + portbindings.VIF_TYPE_OVS, +) +SUPPORTED_SEGMENTATION_TYPES = ( + trunk_consts.VLAN, +) + + +class NsxV3TrunkHandler(object): + """Class to handle trunk events.""" + + def __init__(self, plugin_driver): + self.plugin_driver = plugin_driver + + #TODO(abhiraut): Refactor nsxlib code and reuse here. + def _build_switching_profile_ids(self, profiles): + switch_profile_ids = [] + for profile in profiles: + switch_profile = nsx_resources.SwitchingProfileTypeId( + profile_type=profile['key'], + profile_id=profile['value']) + switch_profile_ids.append(switch_profile) + return switch_profile_ids + + def _update_port_at_backend(self, context, parent_port_id, subport): + # Retrieve the child port details + child_port = self.plugin_driver.get_port(context, subport.port_id) + # Retrieve the logical port ID based on the child port's neutron ID + nsx_child_port_id = nsx_db.get_nsx_switch_and_port_id( + session=context.session, neutron_id=subport.port_id)[1] + # Retrieve child logical port from the backend + try: + nsx_child_port = self.plugin_driver._port_client.get( + nsx_child_port_id) + except nsxlib_exc.ResourceNotFound: + with excutils.save_and_reraise_exception(): + LOG.error(_LE("Child port %s not found on the backend. " + "Setting trunk status to ERROR."), + nsx_child_port_id) + # Build address bindings and switch profiles otherwise backend will + # clear that information during port update + address_bindings = self.plugin_driver._build_address_bindings( + child_port) + switching_profile_ids = self._build_switching_profile_ids( + nsx_child_port.get('switching_profile_ids', [])) + attachment_type = None + seg_id = None + if parent_port_id: + # Set properties for VLAN trunking + if subport.segmentation_type == nsx_utils.NsxV3NetworkTypes.VLAN: + attachment_type = nsx_consts.ATTACHMENT_CIF + seg_id = subport.segmentation_id + else: + # Unset the parent port properties from child port + attachment_type = nsx_consts.ATTACHMENT_VIF + seg_id = None + # Update logical port in the backend to set/unset parent port + try: + self.plugin_driver._port_client.update( + lport_id=nsx_child_port.get('id'), + vif_uuid=subport.port_id, + name=nsx_child_port.get('display_name'), + admin_state=nsx_child_port.get('admin_state'), + address_bindings=address_bindings, + switch_profile_ids=switching_profile_ids, + attachment_type=attachment_type, + parent_vif_id=parent_port_id, + parent_tag=seg_id) + except nsxlib_exc.ManagerError as e: + with excutils.save_and_reraise_exception(): + LOG.error(_LE("Unable to update subport for attachment " + "type. Setting trunk status to ERROR. " + "Exception is %s"), e) + + def _set_subports(self, context, parent_port_id, subports): + for subport in subports: + # Update port with parent port for backend. + self._update_port_at_backend(context, parent_port_id, subport) + + def _unset_subports(self, context, subports): + for subport in subports: + # Update port and remove parent port attachment in the backend + self._update_port_at_backend( + context=context, parent_port_id=None, subport=subport) + + def trunk_created(self, context, trunk): + try: + if trunk.sub_ports: + self._set_subports(context, trunk.port_id, trunk.sub_ports) + trunk.update(status=trunk_consts.ACTIVE_STATUS) + except (nsxlib_exc.ManagerError, nsxlib_exc.ResourceNotFound): + trunk.update(status=trunk_consts.ERROR_STATUS) + + def trunk_deleted(self, context, trunk): + self._unset_subports(context, trunk.sub_ports) + + def subports_added(self, context, trunk, subports): + try: + self._set_subports(context, trunk.port_id, subports) + trunk.update(status=trunk_consts.ACTIVE_STATUS) + except (nsxlib_exc.ManagerError, nsxlib_exc.ResourceNotFound): + trunk.update(status=trunk_consts.ERROR_STATUS) + + def subports_deleted(self, context, trunk, subports): + try: + self._unset_subports(context, subports) + except (nsxlib_exc.ManagerError, nsxlib_exc.ResourceNotFound): + trunk.update(status=trunk_consts.ERROR_STATUS) + + def trunk_event(self, resource, event, trunk_plugin, payload): + if event == events.AFTER_CREATE: + self.trunk_created(payload.context, payload.current_trunk) + elif event == events.AFTER_DELETE: + self.trunk_deleted(payload.context, payload.original_trunk) + + def subport_event(self, resource, event, trunk_plugin, payload): + if event == events.AFTER_CREATE: + self.subports_added( + payload.context, payload.original_trunk, payload.subports) + elif event == events.AFTER_DELETE: + self.subports_deleted( + payload.context, payload.original_trunk, payload.subports) + + +class NsxV3TrunkDriver(base.DriverBase): + """Driver to implement neutron's trunk extensions.""" + + @property + def is_loaded(self): + try: + return nsx_consts.VMWARE_NSX_V3_PLUGIN_NAME == cfg.CONF.core_plugin + except cfg.NoSuchOptError: + return False + + @classmethod + def create(cls, plugin_driver): + cls.plugin_driver = plugin_driver + return cls(nsx_consts.VMWARE_NSX_V3_PLUGIN_NAME, SUPPORTED_INTERFACES, + SUPPORTED_SEGMENTATION_TYPES, + agent_type=None, can_trunk_bound_port=False) + + def register(self, resource, event, trigger, **kwargs): + super(NsxV3TrunkDriver, self).register( + resource, event, trigger, **kwargs) + self._handler = NsxV3TrunkHandler(self.plugin_driver) + for event in (events.AFTER_CREATE, events.AFTER_DELETE): + registry.subscribe(self._handler.trunk_event, + trunk_consts.TRUNK, + event) + registry.subscribe(self._handler.subport_event, + trunk_consts.SUBPORTS, + event) + LOG.debug("VMware NSXv3 trunk driver initialized.") diff --git a/vmware_nsx/tests/unit/services/trunk/__init__.py b/vmware_nsx/tests/unit/services/trunk/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vmware_nsx/tests/unit/services/trunk/test_nsxv3_driver.py b/vmware_nsx/tests/unit/services/trunk/test_nsxv3_driver.py new file mode 100644 index 0000000000..ccfe89345b --- /dev/null +++ b/vmware_nsx/tests/unit/services/trunk/test_nsxv3_driver.py @@ -0,0 +1,183 @@ +# Copyright (c) 2016 VMware, Inc. +# +# 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 import context +from neutron.tests import base + +from oslo_config import cfg +from oslo_utils import importutils + +from vmware_nsx.common import nsx_constants +from vmware_nsx.services.trunk.nsx_v3 import driver as trunk_driver +from vmware_nsx.tests.unit.nsx_v3 import test_constants as test_consts +from vmware_nsx.tests.unit.nsx_v3 import test_plugin as test_nsx_v3_plugin + + +class TestNsxV3TrunkHandler(test_nsx_v3_plugin.NsxV3PluginTestCaseMixin, + base.BaseTestCase): + def setUp(self): + super(TestNsxV3TrunkHandler, self).setUp() + self.context = context.get_admin_context() + self.core_plugin = importutils.import_object(test_consts.PLUGIN_NAME) + self.handler = trunk_driver.NsxV3TrunkHandler(self.core_plugin) + self.handler._update_port_at_backend = mock.Mock() + self.trunk_1 = mock.Mock() + self.trunk_1.port_id = "parent_port_1" + + self.trunk_2 = mock.Mock() + self.trunk_2.port_id = "parent_port_2" + + self.sub_port_1 = mock.Mock() + self.sub_port_1.segmentation_id = 40 + self.sub_port_1.trunk_id = "trunk-1" + self.sub_port_1.port_id = "sub_port_1" + + self.sub_port_2 = mock.Mock() + self.sub_port_2.segmentation_id = 41 + self.sub_port_2.trunk_id = "trunk-2" + self.sub_port_2.port_id = "sub_port_2" + + self.sub_port_3 = mock.Mock() + self.sub_port_3.segmentation_id = 43 + self.sub_port_3.trunk_id = "trunk-2" + self.sub_port_3.port_id = "sub_port_3" + + def test_trunk_created(self): + # Create trunk with no subport + self.trunk_1.sub_ports = [] + self.handler.trunk_created(self.context, self.trunk_1) + self.handler._update_port_at_backend.assert_not_called() + + # Create trunk with 1 subport + self.trunk_1.sub_ports = [self.sub_port_1] + self.handler.trunk_created(self.context, self.trunk_1) + self.handler._update_port_at_backend.assert_called_with( + self.context, + self.trunk_1.port_id, + self.sub_port_1) + + # Create trunk with multiple subports + self.trunk_2.sub_ports = [self.sub_port_2, self.sub_port_3] + self.handler.trunk_created(self.context, self.trunk_2) + calls = [mock.call._update_port_at_backend( + self.context, + self.trunk_2.port_id, + self.sub_port_2), + mock.call._update_port_at_backend( + self.context, + self.trunk_2.port_id, + self.sub_port_3)] + self.handler._update_port_at_backend.assert_has_calls( + calls, any_order=True) + + def test_trunk_deleted(self): + # Delete trunk with no subport + self.trunk_1.sub_ports = [] + self.handler.trunk_deleted(self.context, self.trunk_1) + self.handler._update_port_at_backend.assert_not_called() + + # Delete trunk with 1 subport + self.trunk_1.sub_ports = [self.sub_port_1] + self.handler.trunk_deleted(self.context, self.trunk_1) + self.handler._update_port_at_backend.assert_called_with( + context=self.context, + parent_port_id=None, + subport=self.sub_port_1) + + # Delete trunk with multiple subports + self.trunk_2.sub_ports = [self.sub_port_2, self.sub_port_3] + self.handler.trunk_deleted(self.context, self.trunk_2) + calls = [mock.call._update_port_at_backend( + context=self.context, + parent_port_id=None, + subport=self.sub_port_2), + mock.call._update_port_at_backend( + context=self.context, + parent_port_id=None, + subport=self.sub_port_3)] + self.handler._update_port_at_backend.assert_has_calls( + calls, any_order=True) + + def test_subports_added(self): + # Update trunk with no subport + sub_ports = [] + self.handler.subports_added(self.context, self.trunk_1, sub_ports) + self.handler._update_port_at_backend.assert_not_called() + + # Update trunk with 1 subport + sub_ports = [self.sub_port_1] + self.handler.subports_added(self.context, self.trunk_1, sub_ports) + self.handler._update_port_at_backend.assert_called_with( + self.context, + self.trunk_1.port_id, + self.sub_port_1) + + # Update trunk with multiple subports + sub_ports = [self.sub_port_2, self.sub_port_3] + self.handler.subports_added(self.context, self.trunk_2, sub_ports) + calls = [mock.call._update_port_at_backend( + self.context, + self.trunk_2.port_id, + self.sub_port_2), + mock.call._update_port_at_backend( + self.context, + self.trunk_2.port_id, + self.sub_port_3)] + self.handler._update_port_at_backend.assert_has_calls( + calls, any_order=True) + + def test_subports_deleted(self): + # Update trunk to remove no subport + sub_ports = [] + self.handler.subports_deleted(self.context, self.trunk_1, sub_ports) + self.handler._update_port_at_backend.assert_not_called() + + # Update trunk to remove 1 subport + sub_ports = [self.sub_port_1] + self.handler.subports_deleted(self.context, self.trunk_1, sub_ports) + self.handler._update_port_at_backend.assert_called_with( + context=self.context, + parent_port_id=None, + subport=self.sub_port_1) + + # Update trunk to remove multiple subports + sub_ports = [self.sub_port_2, self.sub_port_3] + self.handler.subports_deleted(self.context, self.trunk_2, sub_ports) + calls = [mock.call._update_port_at_backend( + context=self.context, + parent_port_id=None, + subport=self.sub_port_2), + mock.call._update_port_at_backend( + context=self.context, + parent_port_id=None, + subport=self.sub_port_3)] + self.handler._update_port_at_backend.assert_has_calls( + calls, any_order=True) + + +class TestNsxV3TrunkDriver(base.BaseTestCase): + def setUp(self): + super(TestNsxV3TrunkDriver, self).setUp() + + def test_is_loaded(self): + driver = trunk_driver.NsxV3TrunkDriver.create(mock.Mock()) + cfg.CONF.set_override('core_plugin', + nsx_constants.VMWARE_NSX_V3_PLUGIN_NAME) + self.assertTrue(driver.is_loaded) + + cfg.CONF.set_override('core_plugin', 'not_vmware_nsx_plugin') + self.assertFalse(driver.is_loaded)