Merge "ML2 Cisco Nexus mech driver portbinding support"

This commit is contained in:
Jenkins 2013-09-29 14:21:31 +00:00 committed by Gerrit Code Review
commit c299539db5
5 changed files with 148 additions and 124 deletions

View File

@ -10,6 +10,14 @@
# (BoolOpt) A flag to enable round robin scheduling of routers for SVI. # (BoolOpt) A flag to enable round robin scheduling of routers for SVI.
# svi_round_robin = False # 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. # Cisco Nexus Switch configurations.
# Each switch to be managed by Openstack Neutron must be configured here. # Each switch to be managed by Openstack Neutron must be configured here.
# #

View File

@ -21,6 +21,8 @@ ml2_cisco_opts = [
help=_("VLAN Name prefix")), help=_("VLAN Name prefix")),
cfg.BoolOpt('svi_round_robin', default=False, cfg.BoolOpt('svi_round_robin', default=False,
help=_("Distribute SVI interfaces over all switches")), help=_("Distribute SVI interfaces over all switches")),
cfg.StrOpt('managed_physical_network', default=None,
help=_("The physical network managed by the switches.")),
] ]

View File

@ -17,9 +17,10 @@
ML2 Mechanism Driver for Cisco Nexus platforms. ML2 Mechanism Driver for Cisco Nexus platforms.
""" """
from novaclient.v1_1 import client as nova_client
from oslo.config import cfg 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 excutils
from neutron.openstack.common import log as logging from neutron.openstack.common import log as logging
from neutron.plugins.ml2 import driver_api as api from neutron.plugins.ml2 import driver_api as api
@ -50,12 +51,22 @@ class CiscoNexusMechanismDriver(api.MechanismDriver):
# Initialize credential store after database initialization # Initialize credential store after database initialization
cred.Store.initialize() cred.Store.initialize()
def _get_vlanid(self, port_context): def _valid_network_segment(self, segment):
"""Return the VLAN ID (segmentation ID) for this network.""" return (cfg.CONF.ml2_cisco.managed_physical_network is None or
# NB: Currently only a single physical network is supported. cfg.CONF.ml2_cisco.managed_physical_network ==
network_context = port_context.network segment[api.PHYSICAL_NETWORK])
network_segments = network_context.network_segments
return network_segments[0]['segmentation_id'] 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): def _get_credential(self, nexus_ip):
"""Return credential information for a given Nexus IP address. """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, self.driver.disable_vlan_on_trunk_int(switch_ip, vlan_id,
port_id) port_id)
# TODO(rcurran) Temporary access to host_id. When available use def _invoke_nexus_on_port_event(self, context):
# 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."""
vlan_id = self._get_vlanid(context) vlan_id = self._get_vlanid(context)
host = self._get_instance_host(instance_id) host_id = context.current.get(portbindings.HOST_ID)
# Trunk segmentation id for only this host if vlan_id and host_id:
vlan_name = cfg.CONF.ml2_cisco.vlan_name_prefix + str(vlan_id) vlan_name = cfg.CONF.ml2_cisco.vlan_name_prefix + str(vlan_id)
self._manage_port(vlan_name, vlan_id, host, instance_id) instance_id = context.current.get('device_id')
self._manage_port(vlan_name, vlan_id, host_id, instance_id)
def create_port_postcommit(self, context): else:
"""Create port post-database commit event.""" LOG.debug(_("Vlan ID %(vlan_id)s or Host ID %(host_id)s missing."),
port = context.current {'vlan_id': vlan_id, 'host_id': host_id})
instance_id = port['device_id']
device_owner = port['device_owner']
if instance_id and device_owner != 'network:dhcp':
self._invoke_nexus_on_port_event(context, instance_id)
def update_port_postcommit(self, context): def update_port_postcommit(self, context):
"""Update port post-database commit event.""" """Update port post-database commit event."""
port = context.current 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 self._is_deviceowner_compute(port) and self._is_status_active(port):
if instance_id and not old_device: self._invoke_nexus_on_port_event(context)
self._invoke_nexus_on_port_event(context, instance_id)
def delete_port_precommit(self, context): def delete_port_precommit(self, context):
"""Delete port pre-database commit event. """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 Delete port bindings from the database and scan whether the network
is still required on the interfaces trunked. is still required on the interfaces trunked.
""" """
if not self._is_deviceowner_compute(context.current):
return
port = context.current port = context.current
device_id = port['device_id'] device_id = port['device_id']
vlan_id = self._get_vlanid(context) vlan_id = self._get_vlanid(context)
if not vlan_id or not device_id:
return
# Delete DB row for this port # Delete DB row for this port
try: try:
row = nxos_db.get_nexusvm_binding(vlan_id, device_id) row = nxos_db.get_nexusvm_binding(vlan_id, device_id)

View File

@ -19,7 +19,9 @@ import mock
import webob.exc as wexc import webob.exc as wexc
from neutron.api.v2 import base from neutron.api.v2 import base
from neutron.common import constants as n_const
from neutron import context from neutron import context
from neutron.extensions import portbindings
from neutron.manager import NeutronManager from neutron.manager import NeutronManager
from neutron.openstack.common import log as logging from neutron.openstack.common import log as logging
from neutron.plugins.ml2 import config as ml2_config 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 # Configure the ML2 mechanism drivers and network types
ml2_opts = { ml2_opts = {
'mechanism_drivers': ['cisco_nexus', 'logger', 'test'], 'mechanism_drivers': ['cisco_nexus'],
'tenant_network_types': ['vlan'], 'tenant_network_types': ['vlan'],
} }
for opt, val in ml2_opts.items(): for opt, val in ml2_opts.items():
@ -94,11 +96,23 @@ class CiscoML2MechanismTestCase(test_db_plugin.NeutronDbPluginV2TestCase):
'_import_ncclient', '_import_ncclient',
return_value=self.mock_ncclient).start() return_value=self.mock_ncclient).start()
# Use COMP_HOST_NAME as the compute node host name. # Mock port values for 'status' and 'binding:segmenation_id'
mock_host = mock.patch.object( mock_status = mock.patch.object(
mech_cisco_nexus.CiscoNexusMechanismDriver, mech_cisco_nexus.CiscoNexusMechanismDriver,
'_get_instance_host').start() '_is_status_active').start()
mock_host.return_value = COMP_HOST_NAME 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) super(CiscoML2MechanismTestCase, self).setUp(ML2_PLUGIN)
@ -147,48 +161,62 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
test_db_plugin.TestPortsV2): test_db_plugin.TestPortsV2):
@contextlib.contextmanager @contextlib.contextmanager
def _create_port_res(self, name='myname', cidr=CIDR_1, def _create_resources(self, name='myname', cidr=CIDR_1,
device_id=DEVICE_ID_1, do_delete=True): device_id=DEVICE_ID_1,
host_id=COMP_HOST_NAME,
expected_exception=None):
"""Create network, subnet, and port resources for test cases. """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. then delete the port, subnet, and network.
:param name: Name of network to be created :param name: Name of network to be created.
:param cidr: cidr address of subnetwork 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 device_id: Device ID to use for port to be created/updated.
:param do_delete: If set to True, delete the port at the :param host_id: Host ID to use for port create/update.
end of testing :param expected_exception: Expected HTTP code.
""" """
ctx = context.get_admin_context()
with self.network(name=name) as network: with self.network(name=name) as network:
with self.subnet(network=network, cidr=cidr) as subnet: with self.subnet(network=network, cidr=cidr) as subnet:
net_id = subnet['subnet']['network_id'] net_id = subnet['subnet']['network_id']
res = self._create_port(self.fmt, net_id, args = (portbindings.HOST_ID, 'device_id', 'device_owner',
device_id=device_id) '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) 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: try:
yield res yield port
finally: finally:
if do_delete:
self._delete('ports', port['port']['id']) self._delete('ports', port['port']['id'])
def _assertExpectedHTTP(self, status, exc): def _expectedHTTP(self, exc):
"""Confirm that an HTTP status corresponds to an expected exception. """Map a Cisco exception to the HTTP status equivalent.
Confirm that an HTTP status which has been returned for an :param exc: Expected Cisco exception
neutron API request matches the HTTP status corresponding
to an expected exception.
:param status: HTTP status
:param exc: Expected 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 expected_http = base.FAULT_MAP[exc].code
else: else:
expected_http = wexc.HTTPInternalServerError.code expected_http = wexc.HTTPInternalServerError.code
self.assertEqual(status, expected_http)
return expected_http
def test_create_ports_bulk_emulated_plugin_failure(self): def test_create_ports_bulk_emulated_plugin_failure(self):
real_has_attr = hasattr real_has_attr = hasattr
@ -264,10 +292,11 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
the command staring sent to the switch contains the keyword 'add'. 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.assertTrue(self._is_in_last_nexus_cfg(['allowed', 'vlan']))
self.assertFalse(self._is_in_last_nexus_cfg(['add'])) 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.assertTrue(
self._is_in_last_nexus_cfg(['allowed', 'vlan', 'add'])) self._is_in_last_nexus_cfg(['allowed', 'vlan', 'add']))
@ -281,9 +310,7 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
""" """
with self._patch_ncclient('connect.side_effect', with self._patch_ncclient('connect.side_effect',
AttributeError): AttributeError):
with self._create_port_res(do_delete=False) as res: self._create_resources(expected_exception=c_exc.NexusConnectFailed)
self._assertExpectedHTTP(res.status_int,
c_exc.NexusConnectFailed)
def test_nexus_config_fail(self): def test_nexus_config_fail(self):
"""Test a Nexus switch configuration failure. """Test a Nexus switch configuration failure.
@ -296,9 +323,7 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
with self._patch_ncclient( with self._patch_ncclient(
'connect.return_value.edit_config.side_effect', 'connect.return_value.edit_config.side_effect',
AttributeError): AttributeError):
with self._create_port_res(do_delete=False) as res: self._create_resources(expected_exception=c_exc.NexusConfigFailed)
self._assertExpectedHTTP(res.status_int,
c_exc.NexusConfigFailed)
def test_nexus_extended_vlan_range_failure(self): def test_nexus_extended_vlan_range_failure(self):
"""Test that extended VLAN range config errors are ignored. """Test that extended VLAN range config errors are ignored.
@ -316,8 +341,7 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
with self._patch_ncclient( with self._patch_ncclient(
'connect.return_value.edit_config.side_effect', 'connect.return_value.edit_config.side_effect',
mock_edit_config_a): mock_edit_config_a):
with self._create_port_res(name='myname') as res: self._create_resources(name='myname')
self.assertEqual(res.status_int, wexc.HTTPCreated.code)
def mock_edit_config_b(target, config): def mock_edit_config_b(target, config):
if all(word in config for word in ['no', 'shutdown']): if all(word in config for word in ['no', 'shutdown']):
@ -326,8 +350,7 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
with self._patch_ncclient( with self._patch_ncclient(
'connect.return_value.edit_config.side_effect', 'connect.return_value.edit_config.side_effect',
mock_edit_config_b): mock_edit_config_b):
with self._create_port_res(name='myname') as res: self._create_resources(name='myname')
self.assertEqual(res.status_int, wexc.HTTPCreated.code)
def test_nexus_vlan_config_rollback(self): def test_nexus_vlan_config_rollback(self):
"""Test rollback following Nexus VLAN state config failure. """Test rollback following Nexus VLAN state config failure.
@ -344,14 +367,12 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
with self._patch_ncclient( with self._patch_ncclient(
'connect.return_value.edit_config.side_effect', 'connect.return_value.edit_config.side_effect',
mock_edit_config): 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 # Confirm that the last configuration sent to the Nexus
# switch was deletion of the VLAN. # switch was deletion of the VLAN.
self.assertTrue( self.assertTrue(self._is_in_last_nexus_cfg(['<no>', '<vlan>']))
self._is_in_last_nexus_cfg(['<no>', '<vlan>'])
)
self._assertExpectedHTTP(res.status_int,
c_exc.NexusConfigFailed)
def test_nexus_host_not_configured(self): def test_nexus_host_not_configured(self):
"""Test handling of a NexusComputeHostNotConfigured exception. """Test handling of a NexusComputeHostNotConfigured exception.
@ -360,12 +381,9 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
a fictitious host name during port creation. a fictitious host name during port creation.
""" """
with mock.patch.object(mech_cisco_nexus.CiscoNexusMechanismDriver, self._create_resources(
'_get_instance_host') as mock_get_host: host_id='fake_host',
mock_get_host.return_value = 'fictitious_host' expected_exception=c_exc.NexusComputeHostNotConfigured)
with self._create_port_res(do_delete=False) as res:
self._assertExpectedHTTP(res.status_int,
c_exc.NexusComputeHostNotConfigured)
def test_nexus_bind_fail_rollback(self): def test_nexus_bind_fail_rollback(self):
"""Test for proper rollback following add Nexus DB binding failure. """Test for proper rollback following add Nexus DB binding failure.
@ -378,13 +396,12 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
with mock.patch.object(nexus_db_v2, with mock.patch.object(nexus_db_v2,
'add_nexusport_binding', 'add_nexusport_binding',
side_effect=KeyError): 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 # Confirm that the last configuration sent to the Nexus
# switch was a removal of vlan from the test interface. # switch was a removal of vlan from the test interface.
self.assertTrue( self.assertTrue(
self._is_in_last_nexus_cfg(['<vlan>', '<remove>']) self._is_in_last_nexus_cfg(['<vlan>', '<remove>'])
) )
self._assertExpectedHTTP(res.status_int, KeyError)
def test_nexus_delete_port_rollback(self): def test_nexus_delete_port_rollback(self):
"""Test for proper rollback for nexus plugin delete port failure. """Test for proper rollback for nexus plugin delete port failure.
@ -394,10 +411,7 @@ class TestCiscoPortsV2(CiscoML2MechanismTestCase,
nexus switch during a delete_port operation. nexus switch during a delete_port operation.
""" """
with self._create_port_res() as res: with self._create_resources() as port:
port = self.deserialize(self.fmt, res)
# Check that there is only one binding in the nexus database # Check that there is only one binding in the nexus database
# for this VLAN/nexus switch. # for this VLAN/nexus switch.
start_rows = nexus_db_v2.get_nexusvlan_binding(VLAN_START, start_rows = nexus_db_v2.get_nexusvlan_binding(VLAN_START,

View File

@ -17,8 +17,11 @@ import collections
import mock import mock
import testtools import testtools
from neutron.common import constants as n_const
from neutron.db import api as db from neutron.db import api as db
from neutron.extensions import portbindings
from neutron.openstack.common import importutils 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 constants
from neutron.plugins.ml2.drivers.cisco import exceptions from neutron.plugins.ml2.drivers.cisco import exceptions
from neutron.plugins.ml2.drivers.cisco import mech_cisco_nexus from neutron.plugins.ml2.drivers.cisco import mech_cisco_nexus
@ -43,6 +46,8 @@ VLAN_ID_2 = 265
VLAN_ID_PC = 268 VLAN_ID_PC = 268
DEVICE_OWNER = 'compute:test' DEVICE_OWNER = 'compute:test'
NEXUS_SSH_PORT = '22' NEXUS_SSH_PORT = '22'
PORT_STATE = n_const.PORT_STATUS_ACTIVE
NETWORK_TYPE = 'vlan'
NEXUS_DRIVER = ('neutron.plugins.ml2.drivers.cisco.' NEXUS_DRIVER = ('neutron.plugins.ml2.drivers.cisco.'
'nexus_network_driver.CiscoNexusDriver') 'nexus_network_driver.CiscoNexusDriver')
@ -52,7 +57,8 @@ class FakeNetworkContext(object):
"""Network context for testing purposes only.""" """Network context for testing purposes only."""
def __init__(self, segment_id): 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 @property
def network_segments(self): def network_segments(self):
@ -63,12 +69,15 @@ class FakePortContext(object):
"""Port context for testing purposes only.""" """Port context for testing purposes only."""
def __init__(self, device_id, network_context): def __init__(self, device_id, host_name, network_context):
self._port = { self._port = {
'status': PORT_STATE,
'device_id': device_id, 'device_id': device_id,
'device_owner': DEVICE_OWNER 'device_owner': DEVICE_OWNER,
portbindings.HOST_ID: host_name
} }
self._network = network_context self._network = network_context
self._segment = network_context.network_segments
@property @property
def current(self): def current(self):
@ -78,6 +87,10 @@ class FakePortContext(object):
def network(self): def network(self):
return self._network return self._network
@property
def bound_segment(self):
return self._segment
class TestCiscoNexusDevice(base.BaseTestCase): class TestCiscoNexusDevice(base.BaseTestCase):
@ -166,13 +179,10 @@ class TestCiscoNexusDevice(base.BaseTestCase):
vlan_id = port_config.vlan_id vlan_id = port_config.vlan_id
network_context = FakeNetworkContext(vlan_id) network_context = FakeNetworkContext(vlan_id)
port_context = FakePortContext(instance_id, network_context) port_context = FakePortContext(instance_id, host_name,
network_context)
with mock.patch.object(mech_cisco_nexus.CiscoNexusMechanismDriver, self._cisco_mech_driver.update_port_postcommit(port_context)
'_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, bindings = nexus_db_v2.get_nexusport_binding(nexus_port,
vlan_id, vlan_id,
nexus_ip_addr, nexus_ip_addr,
@ -180,8 +190,7 @@ class TestCiscoNexusDevice(base.BaseTestCase):
self.assertEqual(len(bindings), 1) self.assertEqual(len(bindings), 1)
self._cisco_mech_driver.delete_port_precommit(port_context) self._cisco_mech_driver.delete_port_precommit(port_context)
with testtools.ExpectedException( with testtools.ExpectedException(exceptions.NexusPortBindingNotFound):
exceptions.NexusPortBindingNotFound):
nexus_db_v2.get_nexusport_binding(nexus_port, nexus_db_v2.get_nexusport_binding(nexus_port,
vlan_id, vlan_id,
nexus_ip_addr, nexus_ip_addr,