Merge "ML2 Cisco Nexus mech driver portbinding support"
This commit is contained in:
commit
c299539db5
@ -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.
|
||||
#
|
||||
|
@ -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.")),
|
||||
]
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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."""
|
||||
|
Loading…
Reference in New Issue
Block a user