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
This commit is contained in:
Rich Curran 2013-09-27 11:35:08 -04:00
parent 10614aabd3
commit 6925bd7e00
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.
# 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.
#

View 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.")),
]

View File

@ -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)
host_id = context.current.get(portbindings.HOST_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']
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)

View 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,

View 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)
port_context = FakePortContext(instance_id, host_name,
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.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.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)
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."""