From: Rich Curran Date: Fri, 27 Sep 2013 15:35:08 +0000 (-0400) Subject: ML2 Cisco Nexus mech driver portbinding support X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=6925bd7e00d5c39d20450bcf41e848435b6d9830;p=openstack-build%2Fneutron-build.git ML2 Cisco Nexus mech driver portbinding support This commit adds portbinding extension support to the cisco nexus mechanism driver. Fixes bug: 1220878 Change-Id: I72003961b46190b82681b471f4f9cb5b11d3d068 --- diff --git a/etc/neutron/plugins/ml2/ml2_conf_cisco.ini b/etc/neutron/plugins/ml2/ml2_conf_cisco.ini index 69d531253..6b6f5a76d 100644 --- a/etc/neutron/plugins/ml2/ml2_conf_cisco.ini +++ b/etc/neutron/plugins/ml2/ml2_conf_cisco.ini @@ -10,6 +10,14 @@ # (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. # diff --git a/neutron/plugins/ml2/drivers/cisco/config.py b/neutron/plugins/ml2/drivers/cisco/config.py index c1d7ac1d3..5e15507ff 100644 --- a/neutron/plugins/ml2/drivers/cisco/config.py +++ b/neutron/plugins/ml2/drivers/cisco/config.py @@ -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.")), ] diff --git a/neutron/plugins/ml2/drivers/cisco/mech_cisco_nexus.py b/neutron/plugins/ml2/drivers/cisco/mech_cisco_nexus.py index 95addc1b8..2d2715d2c 100644 --- a/neutron/plugins/ml2/drivers/cisco/mech_cisco_nexus.py +++ b/neutron/plugins/ml2/drivers/cisco/mech_cisco_nexus.py @@ -17,9 +17,10 @@ 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) diff --git a/neutron/tests/unit/ml2/drivers/test_cisco_mech.py b/neutron/tests/unit/ml2/drivers/test_cisco_mech.py index ca20ec9f2..7652ec13d 100644 --- a/neutron/tests/unit/ml2/drivers/test_cisco_mech.py +++ b/neutron/tests/unit/ml2/drivers/test_cisco_mech.py @@ -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(['', '']) - ) - self._assertExpectedHTTP(res.status_int, - c_exc.NexusConfigFailed) + self.assertTrue(self._is_in_last_nexus_cfg(['', ''])) 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(['', '']) ) - 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, diff --git a/neutron/tests/unit/ml2/drivers/test_cisco_nexus.py b/neutron/tests/unit/ml2/drivers/test_cisco_nexus.py index 93b4758b8..5c9316abc 100644 --- a/neutron/tests/unit/ml2/drivers/test_cisco_nexus.py +++ b/neutron/tests/unit/ml2/drivers/test_cisco_nexus.py @@ -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."""