]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
ML2 Cisco Nexus mech driver portbinding support
authorRich Curran <rcurran@cisco.com>
Fri, 27 Sep 2013 15:35:08 +0000 (11:35 -0400)
committerRich Curran <rcurran@cisco.com>
Sat, 28 Sep 2013 16:54:53 +0000 (12:54 -0400)
This commit adds portbinding extension support to
the cisco nexus mechanism driver.

Fixes bug: 1220878

Change-Id: I72003961b46190b82681b471f4f9cb5b11d3d068

etc/neutron/plugins/ml2/ml2_conf_cisco.ini
neutron/plugins/ml2/drivers/cisco/config.py
neutron/plugins/ml2/drivers/cisco/mech_cisco_nexus.py
neutron/tests/unit/ml2/drivers/test_cisco_mech.py
neutron/tests/unit/ml2/drivers/test_cisco_nexus.py

index 69d5312533b03f3aa5399eccc77ef6605a37cbae..6b6f5a76de96f3428dc07b40503f491329e2c09e 100644 (file)
 # (BoolOpt) A flag to enable round robin scheduling of routers for SVI.
 # svi_round_robin = False
 
+#
+# (StrOpt) The name of the physical_network managed via the Cisco Nexus Switch.
+# This string value must be present in the ml2_conf.ini network_vlan_ranges
+# variable.
+#
+# managed_physical_network =
+# Example: managed_physical_network = physnet1
+
 # Cisco Nexus Switch configurations.
 # Each switch to be managed by Openstack Neutron must be configured here.
 #
index c1d7ac1d37ff58486bc3fd492e42ba5032a8d3b2..5e15507ffe36673b0937b400ab0d149c15faf9fb 100644 (file)
@@ -21,6 +21,8 @@ ml2_cisco_opts = [
                help=_("VLAN Name prefix")),
     cfg.BoolOpt('svi_round_robin', default=False,
                 help=_("Distribute SVI interfaces over all switches")),
+    cfg.StrOpt('managed_physical_network', default=None,
+               help=_("The physical network managed by the switches.")),
 ]
 
 
index 95addc1b8fb926e21bdf19bdefc2e398a8089490..2d2715d2c09a826137f00ad6f93d70f22d7f5e68 100644 (file)
 ML2 Mechanism Driver for Cisco Nexus platforms.
 """
 
-from novaclient.v1_1 import client as nova_client
 from oslo.config import cfg
 
+from neutron.common import constants as n_const
+from neutron.extensions import portbindings
 from neutron.openstack.common import excutils
 from neutron.openstack.common import log as logging
 from neutron.plugins.ml2 import driver_api as api
@@ -50,12 +51,22 @@ class CiscoNexusMechanismDriver(api.MechanismDriver):
         # Initialize credential store after database initialization
         cred.Store.initialize()
 
-    def _get_vlanid(self, port_context):
-        """Return the VLAN ID (segmentation ID) for this network."""
-        # NB: Currently only a single physical network is supported.
-        network_context = port_context.network
-        network_segments = network_context.network_segments
-        return network_segments[0]['segmentation_id']
+    def _valid_network_segment(self, segment):
+        return (cfg.CONF.ml2_cisco.managed_physical_network is None or
+                cfg.CONF.ml2_cisco.managed_physical_network ==
+                segment[api.PHYSICAL_NETWORK])
+
+    def _get_vlanid(self, context):
+        segment = context.bound_segment
+        if (segment and segment[api.NETWORK_TYPE] == 'vlan' and
+            self._valid_network_segment(segment)):
+            return context.bound_segment.get(api.SEGMENTATION_ID)
+
+    def _is_deviceowner_compute(self, port):
+        return port['device_owner'].startswith('compute')
+
+    def _is_status_active(self, port):
+        return port['status'] == n_const.PORT_STATUS_ACTIVE
 
     def _get_credential(self, nexus_ip):
         """Return credential information for a given Nexus IP address.
@@ -128,51 +139,24 @@ class CiscoNexusMechanismDriver(api.MechanismDriver):
                     self.driver.disable_vlan_on_trunk_int(switch_ip, vlan_id,
                                                           port_id)
 
-    # TODO(rcurran) Temporary access to host_id. When available use
-    # port-binding to access host name.
-    def _get_instance_host(self, instance_id):
-        keystone_conf = cfg.CONF.keystone_authtoken
-        keystone_auth_url = '%s://%s:%s/v2.0/' % (keystone_conf.auth_protocol,
-                                                  keystone_conf.auth_host,
-                                                  keystone_conf.auth_port)
-        nc = nova_client.Client(keystone_conf.admin_user,
-                                keystone_conf.admin_password,
-                                keystone_conf.admin_tenant_name,
-                                keystone_auth_url,
-                                no_cache=True)
-        serv = nc.servers.get(instance_id)
-        host = serv.__getattr__('OS-EXT-SRV-ATTR:host')
-
-        return host
-
-    def _invoke_nexus_on_port_event(self, context, instance_id):
-        """Prepare variables for call to nexus switch."""
+    def _invoke_nexus_on_port_event(self, context):
         vlan_id = self._get_vlanid(context)
-        host = self._get_instance_host(instance_id)
-
-        # Trunk segmentation id for only this host
-        vlan_name = cfg.CONF.ml2_cisco.vlan_name_prefix + str(vlan_id)
-        self._manage_port(vlan_name, vlan_id, host, instance_id)
-
-    def create_port_postcommit(self, context):
-        """Create port post-database commit event."""
-        port = context.current
-        instance_id = port['device_id']
-        device_owner = port['device_owner']
+        host_id = context.current.get(portbindings.HOST_ID)
 
-        if instance_id and device_owner != 'network:dhcp':
-            self._invoke_nexus_on_port_event(context, instance_id)
+        if vlan_id and host_id:
+            vlan_name = cfg.CONF.ml2_cisco.vlan_name_prefix + str(vlan_id)
+            instance_id = context.current.get('device_id')
+            self._manage_port(vlan_name, vlan_id, host_id, instance_id)
+        else:
+            LOG.debug(_("Vlan ID %(vlan_id)s or Host ID %(host_id)s missing."),
+                      {'vlan_id': vlan_id, 'host_id': host_id})
 
     def update_port_postcommit(self, context):
         """Update port post-database commit event."""
         port = context.current
-        old_port = context.original
-        old_device = old_port['device_id']
-        instance_id = port['device_id'] if 'device_id' in port else ""
 
-        # Check if there's a new device_id
-        if instance_id and not old_device:
-            self._invoke_nexus_on_port_event(context, instance_id)
+        if self._is_deviceowner_compute(port) and self._is_status_active(port):
+            self._invoke_nexus_on_port_event(context)
 
     def delete_port_precommit(self, context):
         """Delete port pre-database commit event.
@@ -180,10 +164,17 @@ class CiscoNexusMechanismDriver(api.MechanismDriver):
         Delete port bindings from the database and scan whether the network
         is still required on the interfaces trunked.
         """
+
+        if not self._is_deviceowner_compute(context.current):
+            return
+
         port = context.current
         device_id = port['device_id']
         vlan_id = self._get_vlanid(context)
 
+        if not vlan_id or not device_id:
+            return
+
         # Delete DB row for this port
         try:
             row = nxos_db.get_nexusvm_binding(vlan_id, device_id)
index ca20ec9f22efb78a896d170e24cac5e6b5cd1957..7652ec13dc776bd874fdfabd0f8a690406002096 100644 (file)
@@ -19,7 +19,9 @@ import mock
 import webob.exc as wexc
 
 from neutron.api.v2 import base
+from neutron.common import constants as n_const
 from neutron import context
+from neutron.extensions import portbindings
 from neutron.manager import NeutronManager
 from neutron.openstack.common import log as logging
 from neutron.plugins.ml2 import config as ml2_config
@@ -61,7 +63,7 @@ class CiscoML2MechanismTestCase(test_db_plugin.NeutronDbPluginV2TestCase):
 
         # Configure the ML2 mechanism drivers and network types
         ml2_opts = {
-            'mechanism_drivers': ['cisco_nexus', 'logger', 'test'],
+            'mechanism_drivers': ['cisco_nexus'],
             'tenant_network_types': ['vlan'],
         }
         for opt, val in ml2_opts.items():
@@ -94,11 +96,23 @@ class CiscoML2MechanismTestCase(test_db_plugin.NeutronDbPluginV2TestCase):
                           '_import_ncclient',
                           return_value=self.mock_ncclient).start()
 
-        # Use COMP_HOST_NAME as the compute node host name.
-        mock_host = mock.patch.object(
+        # Mock port values for 'status' and 'binding:segmenation_id'
+        mock_status = mock.patch.object(
             mech_cisco_nexus.CiscoNexusMechanismDriver,
-            '_get_instance_host').start()
-        mock_host.return_value = COMP_HOST_NAME
+            '_is_status_active').start()
+        mock_status.return_value = n_const.PORT_STATUS_ACTIVE
+
+        def _mock_get_vlanid(context):
+            port = context.current
+            if port['device_id'] == DEVICE_ID_1:
+                return VLAN_START
+            else:
+                return VLAN_START + 1
+
+        mock_vlanid = mock.patch.object(
+            mech_cisco_nexus.CiscoNexusMechanismDriver,
+            '_get_vlanid').start()
+        mock_vlanid.side_effect = _mock_get_vlanid
 
         super(CiscoML2MechanismTestCase, self).setUp(ML2_PLUGIN)
 
@@ -147,48 +161,62 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
                        test_db_plugin.TestPortsV2):
 
     @contextlib.contextmanager
-    def _create_port_res(self, name='myname', cidr=CIDR_1,
-                         device_id=DEVICE_ID_1, do_delete=True):
+    def _create_resources(self, name='myname', cidr=CIDR_1,
+                          device_id=DEVICE_ID_1,
+                          host_id=COMP_HOST_NAME,
+                          expected_exception=None):
         """Create network, subnet, and port resources for test cases.
 
-        Create a network, subnet, and port, yield the result,
+        Create a network, subnet, port and then update port, yield the result,
         then delete the port, subnet, and network.
 
-        :param name: Name of network to be created
-        :param cidr: cidr address of subnetwork to be created
-        :param device_id: Device ID to use for port to be created
-        :param do_delete: If set to True, delete the port at the
-                          end of testing
+        :param name: Name of network to be created.
+        :param cidr: cidr address of subnetwork to be created.
+        :param device_id: Device ID to use for port to be created/updated.
+        :param host_id: Host ID to use for port create/update.
+        :param expected_exception: Expected HTTP code.
 
         """
+        ctx = context.get_admin_context()
         with self.network(name=name) as network:
             with self.subnet(network=network, cidr=cidr) as subnet:
                 net_id = subnet['subnet']['network_id']
-                res = self._create_port(self.fmt, net_id,
-                                        device_id=device_id)
+                args = (portbindings.HOST_ID, 'device_id', 'device_owner',
+                        'admin_state_up')
+                port_dict = {portbindings.HOST_ID: host_id,
+                             'device_id': device_id,
+                             'device_owner': 'compute:none',
+                             'admin_state_up': True}
+
+                res = self._create_port(self.fmt, net_id, arg_list=args,
+                                        context=ctx, **port_dict)
                 port = self.deserialize(self.fmt, res)
+
+                expected_exception = self._expectedHTTP(expected_exception)
+                data = {'port': port_dict}
+                self._update('ports', port['port']['id'], data,
+                             expected_code=expected_exception,
+                             neutron_context=ctx)
+
                 try:
-                    yield res
+                    yield port
                 finally:
-                    if do_delete:
-                        self._delete('ports', port['port']['id'])
+                    self._delete('ports', port['port']['id'])
 
-    def _assertExpectedHTTP(self, status, exc):
-        """Confirm that an HTTP status corresponds to an expected exception.
+    def _expectedHTTP(self, exc):
+        """Map a Cisco exception to the HTTP status equivalent.
 
-        Confirm that an HTTP status which has been returned for an
-        neutron API request matches the HTTP status corresponding
-        to an expected exception.
-
-        :param status: HTTP status
-        :param exc: Expected exception
+        :param exc: Expected Cisco exception
 
         """
-        if exc in base.FAULT_MAP:
+        if exc == None:
+            expected_http = wexc.HTTPOk.code
+        elif exc in base.FAULT_MAP:
             expected_http = base.FAULT_MAP[exc].code
         else:
             expected_http = wexc.HTTPInternalServerError.code
-        self.assertEqual(status, expected_http)
+
+        return expected_http
 
     def test_create_ports_bulk_emulated_plugin_failure(self):
         real_has_attr = hasattr
@@ -264,10 +292,11 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
         the command staring sent to the switch contains the keyword 'add'.
 
         """
-        with self._create_port_res(name='net1', cidr=CIDR_1):
+        with self._create_resources(name='net1', cidr=CIDR_1):
             self.assertTrue(self._is_in_last_nexus_cfg(['allowed', 'vlan']))
             self.assertFalse(self._is_in_last_nexus_cfg(['add']))
-            with self._create_port_res(name='net2', cidr=CIDR_2):
+            with self._create_resources(name='net2', device_id=DEVICE_ID_2,
+                                        cidr=CIDR_2):
                 self.assertTrue(
                     self._is_in_last_nexus_cfg(['allowed', 'vlan', 'add']))
 
@@ -281,9 +310,7 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
         """
         with self._patch_ncclient('connect.side_effect',
                                   AttributeError):
-            with self._create_port_res(do_delete=False) as res:
-                self._assertExpectedHTTP(res.status_int,
-                                         c_exc.NexusConnectFailed)
+            self._create_resources(expected_exception=c_exc.NexusConnectFailed)
 
     def test_nexus_config_fail(self):
         """Test a Nexus switch configuration failure.
@@ -296,9 +323,7 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
         with self._patch_ncclient(
             'connect.return_value.edit_config.side_effect',
             AttributeError):
-            with self._create_port_res(do_delete=False) as res:
-                self._assertExpectedHTTP(res.status_int,
-                                         c_exc.NexusConfigFailed)
+            self._create_resources(expected_exception=c_exc.NexusConfigFailed)
 
     def test_nexus_extended_vlan_range_failure(self):
         """Test that extended VLAN range config errors are ignored.
@@ -316,8 +341,7 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
         with self._patch_ncclient(
             'connect.return_value.edit_config.side_effect',
             mock_edit_config_a):
-            with self._create_port_res(name='myname') as res:
-                self.assertEqual(res.status_int, wexc.HTTPCreated.code)
+            self._create_resources(name='myname')
 
         def mock_edit_config_b(target, config):
             if all(word in config for word in ['no', 'shutdown']):
@@ -326,8 +350,7 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
         with self._patch_ncclient(
             'connect.return_value.edit_config.side_effect',
             mock_edit_config_b):
-            with self._create_port_res(name='myname') as res:
-                self.assertEqual(res.status_int, wexc.HTTPCreated.code)
+            self._create_resources(name='myname')
 
     def test_nexus_vlan_config_rollback(self):
         """Test rollback following Nexus VLAN state config failure.
@@ -344,14 +367,12 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
         with self._patch_ncclient(
             'connect.return_value.edit_config.side_effect',
             mock_edit_config):
-            with self._create_port_res(name='myname', do_delete=False) as res:
+            with self._create_resources(
+                    name='myname',
+                    expected_exception=c_exc.NexusConfigFailed):
                 # Confirm that the last configuration sent to the Nexus
                 # switch was deletion of the VLAN.
-                self.assertTrue(
-                    self._is_in_last_nexus_cfg(['<no>', '<vlan>'])
-                )
-                self._assertExpectedHTTP(res.status_int,
-                                         c_exc.NexusConfigFailed)
+                self.assertTrue(self._is_in_last_nexus_cfg(['<no>', '<vlan>']))
 
     def test_nexus_host_not_configured(self):
         """Test handling of a NexusComputeHostNotConfigured exception.
@@ -360,12 +381,9 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
         a fictitious host name during port creation.
 
         """
-        with mock.patch.object(mech_cisco_nexus.CiscoNexusMechanismDriver,
-                               '_get_instance_host') as mock_get_host:
-            mock_get_host.return_value = 'fictitious_host'
-            with self._create_port_res(do_delete=False) as res:
-                self._assertExpectedHTTP(res.status_int,
-                                         c_exc.NexusComputeHostNotConfigured)
+        self._create_resources(
+            host_id='fake_host',
+            expected_exception=c_exc.NexusComputeHostNotConfigured)
 
     def test_nexus_bind_fail_rollback(self):
         """Test for proper rollback following add Nexus DB binding failure.
@@ -378,13 +396,12 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
         with mock.patch.object(nexus_db_v2,
                                'add_nexusport_binding',
                                side_effect=KeyError):
-            with self._create_port_res(do_delete=False) as res:
+            with self._create_resources(expected_exception=KeyError):
                 # Confirm that the last configuration sent to the Nexus
                 # switch was a removal of vlan from the test interface.
                 self.assertTrue(
                     self._is_in_last_nexus_cfg(['<vlan>', '<remove>'])
                 )
-                self._assertExpectedHTTP(res.status_int, KeyError)
 
     def test_nexus_delete_port_rollback(self):
         """Test for proper rollback for nexus plugin delete port failure.
@@ -394,10 +411,7 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
         nexus switch during a delete_port operation.
 
         """
-        with self._create_port_res() as res:
-
-            port = self.deserialize(self.fmt, res)
-
+        with self._create_resources() as port:
             # Check that there is only one binding in the nexus database
             # for this VLAN/nexus switch.
             start_rows = nexus_db_v2.get_nexusvlan_binding(VLAN_START,
index 93b4758b880d4a41dbe8690ddebf94e1265ba762..5c9316abc083af8c5f567dc18129448da28ae6b6 100644 (file)
@@ -17,8 +17,11 @@ import collections
 import mock
 import testtools
 
+from neutron.common import constants as n_const
 from neutron.db import api as db
+from neutron.extensions import portbindings
 from neutron.openstack.common import importutils
+from neutron.plugins.ml2 import driver_api as api
 from neutron.plugins.ml2.drivers.cisco import constants
 from neutron.plugins.ml2.drivers.cisco import exceptions
 from neutron.plugins.ml2.drivers.cisco import mech_cisco_nexus
@@ -43,6 +46,8 @@ VLAN_ID_2 = 265
 VLAN_ID_PC = 268
 DEVICE_OWNER = 'compute:test'
 NEXUS_SSH_PORT = '22'
+PORT_STATE = n_const.PORT_STATUS_ACTIVE
+NETWORK_TYPE = 'vlan'
 NEXUS_DRIVER = ('neutron.plugins.ml2.drivers.cisco.'
                 'nexus_network_driver.CiscoNexusDriver')
 
@@ -52,7 +57,8 @@ class FakeNetworkContext(object):
     """Network context for testing purposes only."""
 
     def __init__(self, segment_id):
-        self._network_segments = [{'segmentation_id': segment_id}]
+        self._network_segments = {api.SEGMENTATION_ID: segment_id,
+                                  api.NETWORK_TYPE: NETWORK_TYPE}
 
     @property
     def network_segments(self):
@@ -63,12 +69,15 @@ class FakePortContext(object):
 
     """Port context for testing purposes only."""
 
-    def __init__(self, device_id, network_context):
+    def __init__(self, device_id, host_name, network_context):
         self._port = {
+            'status': PORT_STATE,
             'device_id': device_id,
-            'device_owner': DEVICE_OWNER
+            'device_owner': DEVICE_OWNER,
+            portbindings.HOST_ID: host_name
         }
         self._network = network_context
+        self._segment = network_context.network_segments
 
     @property
     def current(self):
@@ -78,6 +87,10 @@ class FakePortContext(object):
     def network(self):
         return self._network
 
+    @property
+    def bound_segment(self):
+        return self._segment
+
 
 class TestCiscoNexusDevice(base.BaseTestCase):
 
@@ -166,26 +179,22 @@ class TestCiscoNexusDevice(base.BaseTestCase):
         vlan_id = port_config.vlan_id
 
         network_context = FakeNetworkContext(vlan_id)
-        port_context = FakePortContext(instance_id, network_context)
-
-        with mock.patch.object(mech_cisco_nexus.CiscoNexusMechanismDriver,
-                               '_get_instance_host') as mock_host:
-            mock_host.return_value = host_name
-
-            self._cisco_mech_driver.create_port_postcommit(port_context)
-            bindings = nexus_db_v2.get_nexusport_binding(nexus_port,
-                                                         vlan_id,
-                                                         nexus_ip_addr,
-                                                         instance_id)
-            self.assertEqual(len(bindings), 1)
-
-            self._cisco_mech_driver.delete_port_precommit(port_context)
-            with testtools.ExpectedException(
-                    exceptions.NexusPortBindingNotFound):
-                nexus_db_v2.get_nexusport_binding(nexus_port,
-                                                  vlan_id,
-                                                  nexus_ip_addr,
-                                                  instance_id)
+        port_context = FakePortContext(instance_id, host_name,
+                                       network_context)
+
+        self._cisco_mech_driver.update_port_postcommit(port_context)
+        bindings = nexus_db_v2.get_nexusport_binding(nexus_port,
+                                                     vlan_id,
+                                                     nexus_ip_addr,
+                                                     instance_id)
+        self.assertEqual(len(bindings), 1)
+
+        self._cisco_mech_driver.delete_port_precommit(port_context)
+        with testtools.ExpectedException(exceptions.NexusPortBindingNotFound):
+            nexus_db_v2.get_nexusport_binding(nexus_port,
+                                              vlan_id,
+                                              nexus_ip_addr,
+                                              instance_id)
 
     def test_create_delete_ports(self):
         """Tests creation and deletion of two new virtual Ports."""