diff --git a/doc/source/devstack.rst b/doc/source/devstack.rst index a835ebd229..f34a439ec3 100644 --- a/doc/source/devstack.rst +++ b/doc/source/devstack.rst @@ -305,6 +305,15 @@ Add octavia and python-octaviaclient repos as external repositories and configur [controller_worker] network_driver = allowed_address_pairs_driver +Trunk Driver +~~~~~~~~~~~~ + +Enable trunk service and configure following flags in ``local.conf``:: + + [[local]|[localrc]] + # Trunk plugin NSX-P driver config + ENABLED_SERVICES+=,q-trunk + Q_SERVICE_PLUGIN_CLASSES+=,trunk Neutron VPNaaS ~~~~~~~~~~~~~~ diff --git a/vmware_nsx/plugins/nsx_p/plugin.py b/vmware_nsx/plugins/nsx_p/plugin.py index 1f971dc47b..5427d9e4f1 100644 --- a/vmware_nsx/plugins/nsx_p/plugin.py +++ b/vmware_nsx/plugins/nsx_p/plugin.py @@ -92,6 +92,7 @@ from vmware_nsx.services.lbaas.octavia import octavia_listener from vmware_nsx.services.qos.common import utils as qos_com_utils from vmware_nsx.services.qos.nsx_v3 import driver as qos_driver from vmware_nsx.services.qos.nsx_v3 import pol_utils as qos_utils +from vmware_nsx.services.trunk.nsx_p import driver as trunk_driver from vmware_nsxlib.v3 import exceptions as nsx_lib_exc from vmware_nsxlib.v3 import nsx_constants as nsxlib_consts @@ -232,6 +233,9 @@ class NsxPolicyPlugin(nsx_plugin_common.NsxPluginV3Base): # Init QoS qos_driver.register(qos_utils.PolicyQosNotificationsHandler()) + # Register NSXP trunk driver to support trunk extensions + self.trunk_driver = trunk_driver.NsxpTrunkDriver.create(self) + registry.subscribe(self.spawn_complete, resources.PROCESS, events.AFTER_SPAWN) diff --git a/vmware_nsx/services/trunk/nsx_p/__init__.py b/vmware_nsx/services/trunk/nsx_p/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vmware_nsx/services/trunk/nsx_p/driver.py b/vmware_nsx/services/trunk/nsx_p/driver.py new file mode 100644 index 0000000000..7a40d952e7 --- /dev/null +++ b/vmware_nsx/services/trunk/nsx_p/driver.py @@ -0,0 +1,241 @@ +# Copyright 2019 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.services.trunk.drivers import base +from neutron_lib.api.definitions import portbindings +from neutron_lib.callbacks import events +from neutron_lib.callbacks import registry +from neutron_lib.callbacks import resources +from neutron_lib.services.trunk import constants as trunk_consts + +from vmware_nsx._i18n import _ +from vmware_nsx.common import exceptions as nsx_exc +from vmware_nsx.common import utils as nsx_utils +from vmware_nsx.extensions import projectpluginmap +from vmware_nsxlib.v3 import exceptions as nsxlib_exc +from vmware_nsxlib.v3.policy import constants as p_constants +from vmware_nsxlib.v3 import utils as nsxlib_utils + +LOG = logging.getLogger(__name__) + +SUPPORTED_INTERFACES = ( + portbindings.VIF_TYPE_OVS, +) +SUPPORTED_SEGMENTATION_TYPES = ( + trunk_consts.SEGMENTATION_TYPE_VLAN, +) + +DRIVER_NAME = 'vmware_nsxp_trunk' +TRUNK_ID_TAG_NAME = 'os-neutron-trunk-id' + + +class NsxpTrunkHandler(object): + def __init__(self, plugin_driver): + self.plugin_driver = plugin_driver + + def _get_port_tags_and_network(self, context, port_id): + port = self.plugin_driver.get_port(context, port_id) + segment_id = self.plugin_driver._get_network_nsx_segment_id( + context, port['network_id']) + lport = self.plugin_driver.nsxpolicy.segment_port.get( + segment_id, port_id) + + return segment_id, lport.get('tags', []) + + def _update_tags(self, port_id, tags, tags_update, is_delete=False): + if is_delete: + tags = [tag for tag in tags if tag not in tags_update] + else: + for tag in tags: + for tag_u in tags_update: + if tag_u['scope'] == tag['scope']: + tag['tag'] = tag_u['tag'] + tags_update.remove(tag_u) + break + + tags.extend( + [tag for tag in tags_update if tag not in tags]) + if len(tags) > nsxlib_utils.MAX_TAGS: + LOG.warning("Cannot add external tags to port %s: " + "too many tags", port_id) + return tags + + def _set_subports(self, context, parent_port_id, subports): + for subport in subports: + # Update port with parent port for backend. + + # Set properties for VLAN trunking + if subport.segmentation_type == nsx_utils.NsxV3NetworkTypes.VLAN: + seg_id = subport.segmentation_id + else: + msg = (_("Cannot create a subport %s with no segmentation" + " id") % subport.port_id) + LOG.error(msg) + raise nsx_exc.NsxPluginException(err_msg=msg) + + tags_update = [{'scope': TRUNK_ID_TAG_NAME, + 'tag': subport.trunk_id}] + + segment_id, tags = self._get_port_tags_and_network( + context, subport.port_id) + + tags = self._update_tags( + subport.port_id, tags, tags_update, is_delete=False) + + # Update logical port in the backend to set/unset parent port + try: + self.plugin_driver.nsxpolicy.segment_port.attach( + segment_id, + subport.port_id, + p_constants.ATTACHMENT_CHILD, + subport.port_id, + context_id=parent_port_id, + traffic_tag=seg_id, + tags=tags) + + except nsxlib_exc.ManagerError as e: + with excutils.save_and_reraise_exception(): + LOG.error("Unable to update subport for attachment " + "type. Exception is %s", e) + + def _unset_subports(self, context, subports): + for subport in subports: + # Update port and remove parent port attachment in the backend + # Unset the parent port properties from child port + + tags_update = [{'scope': TRUNK_ID_TAG_NAME, + 'tag': subport.trunk_id}] + + segment_id, tags = self._get_port_tags_and_network( + context, subport.port_id) + + tags = self._update_tags( + subport.port_id, tags, tags_update, is_delete=True) + + # Update logical port in the backend to set/unset parent port + try: + self.plugin_driver.nsxpolicy.segment_port.detach( + segment_id, subport.port_id, tags=tags) + + except nsxlib_exc.ManagerError as e: + with excutils.save_and_reraise_exception(): + LOG.error("Unable to update subport for attachment " + "type. Exception is %s", e) + + def trunk_created(self, context, trunk): + tags_update = [{'scope': TRUNK_ID_TAG_NAME, 'tag': trunk.id}] + segment_id, tags = self._get_port_tags_and_network( + context, trunk.port_id) + + tags = self._update_tags( + trunk.port_id, tags, tags_update, is_delete=False) + + try: + self.plugin_driver.nsxpolicy.segment_port.attach( + segment_id, + trunk.port_id, + vif_id=trunk.port_id, + attachment_type=p_constants.ATTACHMENT_PARENT, + tags=tags) + except Exception as e: + with excutils.save_and_reraise_exception(): + LOG.error("Parent port attachment for trunk %(trunk)s failed " + "with error %(e)s", {'trunk': trunk.id, 'e': e}) + + if trunk.sub_ports: + self.subports_added(context, trunk, trunk.sub_ports) + + def trunk_deleted(self, context, trunk): + tags_update = [{'scope': TRUNK_ID_TAG_NAME, 'tag': trunk.id}] + + segment_id, tags = self._get_port_tags_and_network( + context, trunk.port_id) + + tags = self._update_tags( + trunk.port_id, tags, tags_update, is_delete=True) + + try: + self.plugin_driver.nsxpolicy.segment_port.detach( + segment_id, trunk.port_id, tags=tags) + except Exception as e: + with excutils.save_and_reraise_exception(): + LOG.error("Parent port detachment for trunk %(trunk)s failed " + "with error %(e)s", {'trunk': trunk.id, 'e': e}) + + self.subports_deleted(context, trunk, trunk.sub_ports) + + def subports_added(self, context, trunk, subports): + try: + self._set_subports(context, trunk.port_id, subports) + trunk.update(status=trunk_consts.TRUNK_ACTIVE_STATUS) + except (nsxlib_exc.ManagerError, nsxlib_exc.ResourceNotFound): + trunk.update(status=trunk_consts.TRUNK_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.TRUNK_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 NsxpTrunkDriver(base.DriverBase): + """Driver to implement neutron's trunk extensions.""" + + @property + def is_loaded(self): + try: + plugin_type = self.plugin_driver.plugin_type() + return plugin_type == projectpluginmap.NsxPlugins.NSX_P + except cfg.NoSuchOptError: + return False + + @classmethod + def create(cls, plugin_driver): + cls.plugin_driver = plugin_driver + return cls(DRIVER_NAME, SUPPORTED_INTERFACES, + SUPPORTED_SEGMENTATION_TYPES, + agent_type=None, can_trunk_bound_port=True) + + @registry.receives(resources.TRUNK_PLUGIN, [events.AFTER_INIT]) + def register(self, resource, event, trigger, payload=None): + super(NsxpTrunkDriver, self).register( + resource, event, trigger, payload=payload) + self._handler = NsxpTrunkHandler(self.plugin_driver) + for event in (events.AFTER_CREATE, events.AFTER_DELETE): + registry.subscribe(self._handler.trunk_event, + resources.TRUNK, + event) + registry.subscribe(self._handler.subport_event, + resources.SUBPORTS, + event) + LOG.debug("VMware NSXP trunk driver initialized.") diff --git a/vmware_nsx/tests/unit/services/trunk/test_nsxp_driver.py b/vmware_nsx/tests/unit/services/trunk/test_nsxp_driver.py new file mode 100644 index 0000000000..bf4cd47f62 --- /dev/null +++ b/vmware_nsx/tests/unit/services/trunk/test_nsxp_driver.py @@ -0,0 +1,262 @@ +# 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.tests import base + +from neutron_lib import context +from oslo_utils import importutils + +from vmware_nsx.extensions import projectpluginmap +from vmware_nsx.services.trunk.nsx_p import driver as trunk_driver +from vmware_nsx.tests.unit.nsx_p import test_plugin as test_nsx_p_plugin + +PLUGIN_NAME = 'vmware_nsx.plugins.nsx_p.plugin.NsxPolicyPlugin' + + +class TestNsxpTrunkHandler(test_nsx_p_plugin.NsxPPluginTestCaseMixin, + base.BaseTestCase): + + def _get_port_tags_and_network(self, context, port_id): + return 'net_' + port_id[-1:], [] + + def setUp(self): + super(TestNsxpTrunkHandler, self).setUp() + self.context = context.get_admin_context() + self.core_plugin = importutils.import_object(PLUGIN_NAME) + self.handler = trunk_driver.NsxpTrunkHandler(self.core_plugin) + self.handler._get_port_tags_and_network = mock.Mock( + side_effect=self._get_port_tags_and_network) + self.trunk_1 = mock.Mock() + self.trunk_1.port_id = "parent_port_1" + self.trunk_1.id = "trunk_1_id" + + self.trunk_2 = mock.Mock() + self.trunk_2.port_id = "parent_port_2" + + self.sub_port_a = mock.Mock() + self.sub_port_a.segmentation_id = 40 + self.sub_port_a.trunk_id = "trunk-1" + self.sub_port_a.port_id = "sub_port_a" + self.sub_port_a.segmentation_type = 'vlan' + + self.sub_port_b = mock.Mock() + self.sub_port_b.segmentation_id = 41 + self.sub_port_b.trunk_id = "trunk-2" + self.sub_port_b.port_id = "sub_port_b" + self.sub_port_b.segmentation_type = 'vlan' + + self.sub_port_c = mock.Mock() + self.sub_port_c.segmentation_id = 43 + self.sub_port_c.trunk_id = "trunk-2" + self.sub_port_c.port_id = "sub_port_c" + self.sub_port_c.segmentation_type = 'vlan' + + def test_trunk_created(self): + # Create trunk with no subport + self.trunk_1.sub_ports = [] + with mock.patch.object( + self.handler.plugin_driver.nsxpolicy.segment_port, + 'attach') as m_attach: + self.handler.trunk_created(self.context, self.trunk_1) + m_attach.assert_called_with( + 'net_1', self.trunk_1.port_id, attachment_type='PARENT', + tags=[{'tag': self.trunk_1.id, + 'scope': 'os-neutron-trunk-id'}], + vif_id=self.trunk_1.port_id) + + # Create trunk with 1 subport + self.trunk_1.sub_ports = [self.sub_port_a] + with mock.patch.object( + self.handler.plugin_driver.nsxpolicy.segment_port, + 'attach') as m_attach: + self.handler.trunk_created(self.context, self.trunk_1) + calls = [ + mock.call.m_attach( + 'net_1', self.trunk_1.port_id, attachment_type='PARENT', + tags=[{'tag': self.trunk_1.id, + 'scope': 'os-neutron-trunk-id'}], + vif_id=self.trunk_1.port_id), + mock.call.m_attach( + 'net_a', self.sub_port_a.port_id, 'CHILD', + self.sub_port_a.port_id, + context_id=self.trunk_1.port_id, + tags=[{'tag': self.sub_port_a.trunk_id, + 'scope': 'os-neutron-trunk-id'}], + traffic_tag=self.sub_port_a.segmentation_id)] + m_attach.assert_has_calls(calls, any_order=True) + + # Create trunk with multiple subports + self.trunk_2.sub_ports = [self.sub_port_b, self.sub_port_c] + with mock.patch.object( + self.handler.plugin_driver.nsxpolicy.segment_port, + 'attach') as m_attach: + self.handler.trunk_created(self.context, self.trunk_2) + calls = [ + mock.call.m_attach( + 'net_2', self.trunk_2.port_id, attachment_type='PARENT', + tags=[{'tag': self.trunk_2.id, + 'scope': 'os-neutron-trunk-id'}], + vif_id=self.trunk_2.port_id), + mock.call.m_attach( + 'net_b', self.sub_port_b.port_id, 'CHILD', + self.sub_port_b.port_id, + context_id=self.trunk_2.port_id, + tags=[{'tag': self.sub_port_b.trunk_id, + 'scope': 'os-neutron-trunk-id'}], + traffic_tag=self.sub_port_b.segmentation_id), + mock.call.m_attach( + 'net_c', self.sub_port_c.port_id, 'CHILD', + self.sub_port_c.port_id, + context_id=self.trunk_2.port_id, + tags=[{'tag': self.sub_port_c.trunk_id, + 'scope': 'os-neutron-trunk-id'}], + traffic_tag=self.sub_port_c.segmentation_id)] + m_attach.assert_has_calls(calls, any_order=True) + + def test_trunk_deleted(self): + # Delete trunk with no subport + self.trunk_1.sub_ports = [] + with mock.patch.object( + self.handler.plugin_driver.nsxpolicy.segment_port, + 'detach') as m_detach: + self.handler.trunk_deleted(self.context, self.trunk_1) + m_detach.assert_called_with( + 'net_1', self.trunk_1.port_id, tags=mock.ANY) + + # Delete trunk with 1 subport + self.trunk_1.sub_ports = [self.sub_port_a] + with mock.patch.object( + self.handler.plugin_driver.nsxpolicy.segment_port, + 'detach') as m_detach: + self.handler.trunk_deleted(self.context, self.trunk_1) + calls = [ + mock.call.m_detach( + 'net_1', self.trunk_1.port_id, tags=mock.ANY), + mock.call.m_detach( + 'net_a', self.sub_port_a.port_id, tags=mock.ANY)] + m_detach.assert_has_calls(calls, any_order=True) + + # Delete trunk with multiple subports + self.trunk_2.sub_ports = [self.sub_port_b, self.sub_port_c] + with mock.patch.object( + self.handler.plugin_driver.nsxpolicy.segment_port, + 'detach') as m_detach: + self.handler.trunk_deleted(self.context, self.trunk_2) + calls = [ + mock.call.m_detach( + 'net_2', self.trunk_2.port_id, tags=mock.ANY), + mock.call.m_detach( + 'net_b', self.sub_port_b.port_id, tags=mock.ANY), + mock.call.m_detach( + 'net_c', self.sub_port_c.port_id, tags=mock.ANY)] + m_detach.assert_has_calls(calls, any_order=True) + + def test_subports_added(self): + # Update trunk with no subport + sub_ports = [] + with mock.patch.object( + self.handler.plugin_driver.nsxpolicy.segment_port, + 'attach') as m_attach: + self.handler.subports_added(self.context, self.trunk_1, sub_ports) + m_attach.assert_not_called() + + # Update trunk with 1 subport + sub_ports = [self.sub_port_a] + with mock.patch.object( + self.handler.plugin_driver.nsxpolicy.segment_port, + 'attach') as m_attach: + self.handler.subports_added(self.context, self.trunk_1, sub_ports) + m_attach.assert_called_with( + 'net_a', self.sub_port_a.port_id, 'CHILD', + self.sub_port_a.port_id, + context_id=self.trunk_1.port_id, + tags=[{'tag': self.sub_port_a.trunk_id, + 'scope': 'os-neutron-trunk-id'}], + traffic_tag=self.sub_port_a.segmentation_id) + + # Update trunk with multiple subports + sub_ports = [self.sub_port_b, self.sub_port_c] + with mock.patch.object( + self.handler.plugin_driver.nsxpolicy.segment_port, + 'attach') as m_attach: + self.handler.subports_added(self.context, self.trunk_2, sub_ports) + calls = [ + mock.call.m_attach( + 'net_b', self.sub_port_b.port_id, 'CHILD', + self.sub_port_b.port_id, + context_id=self.trunk_2.port_id, + tags=[{'tag': self.sub_port_b.trunk_id, + 'scope': 'os-neutron-trunk-id'}], + traffic_tag=self.sub_port_b.segmentation_id), + mock.call.m_attach( + 'net_c', self.sub_port_c.port_id, 'CHILD', + self.sub_port_c.port_id, + context_id=self.trunk_2.port_id, + tags=[{'tag': self.sub_port_c.trunk_id, + 'scope': 'os-neutron-trunk-id'}], + traffic_tag=self.sub_port_c.segmentation_id)] + m_attach.assert_has_calls(calls, any_order=True) + + def test_subports_deleted(self): + # Update trunk to remove no subport + sub_ports = [] + with mock.patch.object( + self.handler.plugin_driver.nsxpolicy.segment_port, + 'detach') as m_detach: + self.handler.subports_deleted( + self.context, self.trunk_1, sub_ports) + m_detach.assert_not_called() + + # Update trunk to remove 1 subport + sub_ports = [self.sub_port_a] + with mock.patch.object( + self.handler.plugin_driver.nsxpolicy.segment_port, + 'detach') as m_detach: + self.handler.subports_deleted( + self.context, self.trunk_1, sub_ports) + m_detach.assert_called_with( + 'net_a', self.sub_port_a.port_id, tags=mock.ANY) + + # Update trunk to remove multiple subports + sub_ports = [self.sub_port_b, self.sub_port_c] + with mock.patch.object( + self.handler.plugin_driver.nsxpolicy.segment_port, + 'detach') as m_detach: + self.handler.subports_deleted( + self.context, self.trunk_2, sub_ports) + calls = [ + mock.call.m_detach( + 'net_b', self.sub_port_b.port_id, tags=mock.ANY), + mock.call.m_detach( + 'net_c', self.sub_port_c.port_id, tags=mock.ANY)] + m_detach.assert_has_calls(calls, any_order=True) + + +class TestNsxpTrunkDriver(base.BaseTestCase): + def setUp(self): + super(TestNsxpTrunkDriver, self).setUp() + + def test_is_loaded(self): + core_plugin = mock.Mock() + driver = trunk_driver.NsxpTrunkDriver.create(core_plugin) + with mock.patch.object(core_plugin, 'plugin_type', + return_value=projectpluginmap.NsxPlugins.NSX_P): + self.assertTrue(driver.is_loaded) + + with mock.patch.object(core_plugin, 'plugin_type', + return_value=projectpluginmap.NsxPlugins.NSX_T): + self.assertFalse(driver.is_loaded)