Adding SVI support to the Cisco Nexus plugin

Adding support to create vlan SVI gateways on Cisco Nexus hardware switches.

blueprint cisco-plugin-svi

Change-Id: I88516f3e67d51d213fa60f6ec9aee23f9ca4be97
This commit is contained in:
Arvind Somya 2013-05-22 10:27:48 -07:00
parent 08561f35ac
commit bcb9ea7ba4
9 changed files with 284 additions and 4 deletions

View File

@ -12,6 +12,7 @@
#model_class=quantum.plugins.cisco.models.virt_phy_sw_v2.VirtualPhysicalSwitchModelV2 #model_class=quantum.plugins.cisco.models.virt_phy_sw_v2.VirtualPhysicalSwitchModelV2
#manager_class=quantum.plugins.cisco.segmentation.l2network_vlan_mgr_v2.L2NetworkVLANMgr #manager_class=quantum.plugins.cisco.segmentation.l2network_vlan_mgr_v2.L2NetworkVLANMgr
#nexus_driver=quantum.plugins.cisco.tests.unit.v2.nexus.fake_nexus_driver.CiscoNEXUSFakeDriver #nexus_driver=quantum.plugins.cisco.tests.unit.v2.nexus.fake_nexus_driver.CiscoNEXUSFakeDriver
#svi_round_robin=False
# IMPORTANT: Comment out the following two lines for production deployments # IMPORTANT: Comment out the following two lines for production deployments
[CISCO_TEST] [CISCO_TEST]

View File

@ -103,6 +103,11 @@ class NexusPortBindingNotFound(exceptions.QuantumException):
super(NexusPortBindingNotFound, self).__init__(filters=filters) super(NexusPortBindingNotFound, self).__init__(filters=filters)
class NoNexusSviSwitch(exceptions.QuantumException):
"""No usable nexus switch found."""
message = _("No usable Nexus switch found to create SVI interface")
class PortVnicBindingAlreadyExists(exceptions.QuantumException): class PortVnicBindingAlreadyExists(exceptions.QuantumException):
"""PortVnic Binding already exists.""" """PortVnic Binding already exists."""
message = _("PortVnic Binding %(port_id)s already exists") message = _("PortVnic Binding %(port_id)s already exists")
@ -111,3 +116,18 @@ class PortVnicBindingAlreadyExists(exceptions.QuantumException):
class PortVnicNotFound(exceptions.QuantumException): class PortVnicNotFound(exceptions.QuantumException):
"""PortVnic Binding is not present.""" """PortVnic Binding is not present."""
message = _("PortVnic Binding %(port_id)s is not present") message = _("PortVnic Binding %(port_id)s is not present")
class SubnetNotSpecified(exceptions.QuantumException):
"""Subnet id not specified."""
message = _("No subnet_id specified for router gateway")
class SubnetInterfacePresent(exceptions.QuantumException):
"""Subnet SVI interface already exists."""
message = _("Subnet %(subnet_id)s has an interface on %(router_id)s")
class PortIdForNexusSvi(exceptions.QuantumException):
"""Port Id specified for Nexus SVI."""
message = _('Nexus hardware router gateway only uses Subnet Ids')

View File

@ -44,6 +44,8 @@ cisco_opts = [
help=_("Maximum Port Profile value")), help=_("Maximum Port Profile value")),
cfg.StrOpt('max_networks', default='65568', cfg.StrOpt('max_networks', default='65568',
help=_("Maximum Network value")), help=_("Maximum Network value")),
cfg.BoolOpt('svi_round_robin', default=False,
help=_("Distribute SVI interfaces over all switches")),
cfg.StrOpt('model_class', cfg.StrOpt('model_class',
default='quantum.plugins.cisco.models.virt_phy_sw_v2.' default='quantum.plugins.cisco.models.virt_phy_sw_v2.'
'VirtualPhysicalSwitchModelV2', 'VirtualPhysicalSwitchModelV2',

View File

@ -148,3 +148,17 @@ def get_port_switch_bindings(port_id, switch_ip):
return binding return binding
except exc.NoResultFound: except exc.NoResultFound:
return return
def get_nexussvi_bindings():
"""Lists nexus svi bindings."""
LOG.debug(_("get_nexussvi_bindings() called"))
session = db.get_session()
filters = {'port_id': 'router'}
bindings = (session.query(nexus_models_v2.NexusPortBinding).
filter_by(**filters).all())
if not bindings:
raise c_exc.NexusPortBindingNotFound(**filters)
return bindings

View File

@ -392,6 +392,69 @@ class VirtualPhysicalSwitchModelV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
return ovs_output[0] return ovs_output[0]
def add_router_interface(self, context, router_id, interface_info):
"""Add a router interface on a subnet.
Only invoke the Nexus plugin to create SVI if a Nexus
plugin is loaded, otherwise send it to the vswitch plugin
"""
nexus_driver = cfg.CONF.CISCO.nexus_driver
if nexus_driver.endswith('CiscoNEXUSDriver'):
LOG.debug(_("Nexus plugin loaded, creating SVI on switch"))
if 'subnet_id' not in interface_info:
raise cexc.SubnetNotSpecified()
if 'port_id' in interface_info:
raise cexc.PortIdForNexusSvi()
subnet = self.get_subnet(context, interface_info['subnet_id'])
gateway_ip = subnet['gateway_ip']
# Get gateway IP address and netmask
cidr = subnet['cidr']
netmask = cidr.split('/', 1)[1]
gateway_ip = gateway_ip + '/' + netmask
network_id = subnet['network_id']
vlan_id = self._get_segmentation_id(network_id)
vlan_name = conf.CISCO.vlan_name_prefix + str(vlan_id)
n_args = [vlan_name, vlan_id, subnet['id'], gateway_ip, router_id]
nexus_output = self._invoke_plugin_per_device(const.NEXUS_PLUGIN,
self._func_name(),
n_args)
return nexus_output
else:
LOG.debug(_("No Nexus plugin, sending to vswitch"))
n_args = [context, router_id, interface_info]
ovs_output = self._invoke_plugin_per_device(const.VSWITCH_PLUGIN,
self._func_name(),
n_args)
return ovs_output
def remove_router_interface(self, context, router_id, interface_info):
"""Remove a router interface.
Only invoke the Nexus plugin to delete SVI if a Nexus
plugin is loaded, otherwise send it to the vswitch plugin
"""
nexus_driver = cfg.CONF.CISCO.nexus_driver
if nexus_driver.endswith('CiscoNEXUSDriver'):
LOG.debug(_("Nexus plugin loaded, deleting SVI from switch"))
subnet = self.get_subnet(context, interface_info['subnet_id'])
network_id = subnet['network_id']
vlan_id = self._get_segmentation_id(network_id)
n_args = [vlan_id, router_id]
nexus_output = self._invoke_plugin_per_device(const.NEXUS_PLUGIN,
self._func_name(),
n_args)
return nexus_output
else:
LOG.debug(_("No Nexus plugin, sending to vswitch"))
n_args = [context, router_id, interface_info]
ovs_output = self._invoke_plugin_per_device(const.VSWITCH_PLUGIN,
self._func_name(),
n_args)
return ovs_output
def create_subnet(self, context, subnet): def create_subnet(self, context, subnet):
"""For this model this method will be delegated to vswitch plugin.""" """For this model this method will be delegated to vswitch plugin."""
pass pass

View File

@ -36,7 +36,7 @@ LOG = logging.getLogger(__name__)
class CiscoNEXUSDriver(): class CiscoNEXUSDriver():
"""Nexus Driver Main Class.""" """Nexus Driver Main Class."""
def __init__(self): def __init__(self):
pass self.connections = {}
def _edit_config(self, mgr, target='running', config='', def _edit_config(self, mgr, target='running', config='',
allowed_exc_strs=None): allowed_exc_strs=None):
@ -68,16 +68,21 @@ class CiscoNEXUSDriver():
def nxos_connect(self, nexus_host, nexus_ssh_port, nexus_user, def nxos_connect(self, nexus_host, nexus_ssh_port, nexus_user,
nexus_password): nexus_password):
"""Make SSH connection to the Nexus Switch.""" """Make SSH connection to the Nexus Switch."""
if getattr(self.connections.get(nexus_host), 'connected', None):
return self.connections[nexus_host]
try: try:
man = manager.connect(host=nexus_host, port=nexus_ssh_port, man = manager.connect(host=nexus_host,
port=nexus_ssh_port,
username=nexus_user, username=nexus_user,
password=nexus_password) password=nexus_password)
self.connections[nexus_host] = man
except Exception as e: except Exception as e:
# Raise a Quantum exception. Include a description of # Raise a Quantum exception. Include a description of
# the original ncclient exception. # the original ncclient exception.
raise cexc.NexusConnectFailed(nexus_host=nexus_host, exc=e) raise cexc.NexusConnectFailed(nexus_host=nexus_host, exc=e)
return man return self.connections[nexus_host]
def create_xml_snippet(self, cutomized_config): def create_xml_snippet(self, cutomized_config):
"""Create XML snippet. """Create XML snippet.
@ -227,3 +232,23 @@ class CiscoNEXUSDriver():
nexus_user, nexus_password) nexus_user, nexus_password)
for ports in nexus_ports: for ports in nexus_ports:
self.disable_vlan_on_trunk_int(man, ports, vlan_id) self.disable_vlan_on_trunk_int(man, ports, vlan_id)
def create_vlan_svi(self, vlan_id, nexus_host, nexus_user, nexus_password,
nexus_ssh_port, gateway_ip):
man = self.nxos_connect(nexus_host, int(nexus_ssh_port),
nexus_user, nexus_password)
confstr = snipp.CMD_VLAN_SVI_SNIPPET % (vlan_id, gateway_ip)
confstr = self.create_xml_snippet(confstr)
LOG.debug(_("NexusDriver: %s"), confstr)
man.edit_config(target='running', config=confstr)
def delete_vlan_svi(self, vlan_id, nexus_host, nexus_user, nexus_password,
nexus_ssh_port):
man = self.nxos_connect(nexus_host, int(nexus_ssh_port),
nexus_user, nexus_password)
confstr = snipp.CMD_NO_VLAN_SVI_SNIPPET % vlan_id
confstr = self.create_xml_snippet(confstr)
LOG.debug(_("NexusDriver: %s"), confstr)
man.edit_config(target='running', config=confstr)

View File

@ -117,6 +117,7 @@ class NexusPlugin(L2DevicePluginBase):
_nexus_username, _nexus_username,
_nexus_password) _nexus_password)
self._client.enable_vlan_on_trunk_int(man, self._client.enable_vlan_on_trunk_int(man,
_nexus_ip,
port_id, port_id,
vlan_id) vlan_id)
vlan_enabled = True vlan_enabled = True
@ -148,6 +149,91 @@ class NexusPlugin(L2DevicePluginBase):
self._networks[net_id] = new_net_dict self._networks[net_id] = new_net_dict
return new_net_dict return new_net_dict
def add_router_interface(self, vlan_name, vlan_id, subnet_id,
gateway_ip, router_id):
"""Create VLAN SVI on the Nexus switch."""
# Find a switch to create the SVI on
switch_ip = self._find_switch_for_svi()
if not switch_ip:
raise cisco_exc.NoNexusSwitch()
_nexus_ip = switch_ip
_nexus_ssh_port = self._nexus_switches[switch_ip, 'ssh_port']
_nexus_creds = self.get_credential(_nexus_ip)
_nexus_username = _nexus_creds['username']
_nexus_password = _nexus_creds['password']
# Check if this vlan exists on the switch already
try:
nxos_db.get_nexusvlan_binding(vlan_id, switch_ip)
except cisco_exc.NexusPortBindingNotFound:
# Create vlan and trunk vlan on the port
self._client.create_vlan(
vlan_name, str(vlan_id), _nexus_ip,
_nexus_username, _nexus_password,
[], _nexus_ssh_port, vlan_id)
# Check if a router interface has already been created
try:
nxos_db.get_nexusvm_binding(vlan_id, router_id)
raise cisco_exc.SubnetInterfacePresent(subnet_id=subnet_id,
router_id=router_id)
except cisco_exc.NexusPortBindingNotFound:
self._client.create_vlan_svi(vlan_id, _nexus_ip, _nexus_username,
_nexus_password, _nexus_ssh_port,
gateway_ip)
nxos_db.add_nexusport_binding('router', str(vlan_id),
switch_ip, router_id)
return True
def remove_router_interface(self, vlan_id, router_id):
"""Remove VLAN SVI from the Nexus Switch."""
# Grab switch_ip from database
row = nxos_db.get_nexusvm_binding(vlan_id, router_id)
# Delete the SVI interface from the switch
_nexus_ip = row['switch_ip']
_nexus_ssh_port = self._nexus_switches[_nexus_ip, 'ssh_port']
_nexus_creds = self.get_credential(_nexus_ip)
_nexus_username = _nexus_creds['username']
_nexus_password = _nexus_creds['password']
self._client.delete_vlan_svi(vlan_id, _nexus_ip, _nexus_username,
_nexus_password, _nexus_ssh_port)
# Invoke delete_port to delete this row
# And delete vlan if required
return self.delete_port(router_id, vlan_id)
def _find_switch_for_svi(self):
"""Get a switch to create the SVI on."""
LOG.debug(_("Grabbing a switch to create SVI"))
if conf.CISCO.svi_round_robin:
LOG.debug(_("Using round robin to create SVI"))
switch_dict = dict(
(switch_ip, 0) for switch_ip, _ in self._nexus_switches)
try:
bindings = nxos_db.get_nexussvi_bindings()
# Build a switch dictionary with weights
for binding in bindings:
switch_ip = binding.switch_ip
if switch_ip not in switch_dict:
switch_dict[switch_ip] = 1
else:
switch_dict[switch_ip] += 1
# Search for the lowest value in the dict
if switch_dict:
switch_ip = min(switch_dict.items(), key=switch_dict.get)
return switch_ip[0]
except cisco_exc.NexusPortBindingNotFound:
pass
LOG.debug(_("No round robin or zero weights, using first switch"))
# Return the first switch in the config
for switch_ip, attr in self._nexus_switches:
return switch_ip
def delete_network(self, tenant_id, net_id, **kwargs): def delete_network(self, tenant_id, net_id, **kwargs):
"""Delete network. """Delete network.
@ -205,7 +291,9 @@ class NexusPlugin(L2DevicePluginBase):
try: try:
# Delete this vlan from this switch # Delete this vlan from this switch
_nexus_ip = row['switch_ip'] _nexus_ip = row['switch_ip']
_nexus_ports = (row['port_id'],) _nexus_ports = ()
if row['port_id'] != 'router':
_nexus_ports = (row['port_id'],)
_nexus_ssh_port = (self._nexus_switches[_nexus_ip, _nexus_ssh_port = (self._nexus_switches[_nexus_ip,
'ssh_port']) 'ssh_port'])
_nexus_creds = self.get_credential(_nexus_ip) _nexus_creds = self.get_credential(_nexus_ip)

View File

@ -183,3 +183,32 @@ FILTER_SHOW_VLAN_BRIEF_SNIPPET = """
</vlan> </vlan>
</show> </show>
""" """
CMD_VLAN_SVI_SNIPPET = """
<interface>
<vlan>
<vlan>%s</vlan>
<__XML__MODE_vlan>
<no>
<shutdown/>
</no>
<ip>
<address>
<address>%s</address>
</address>
</ip>
</__XML__MODE_vlan>
</vlan>
</interface>
"""
CMD_NO_VLAN_SVI_SNIPPET = """
<no>
<interface>
<vlan>
<vlan>%s</vlan>
</vlan>
</interface>
</no>
"""

View File

@ -18,6 +18,7 @@ import mock
from quantum.db import api as db from quantum.db import api as db
from quantum.openstack.common import importutils from quantum.openstack.common import importutils
from quantum.plugins.cisco.common import cisco_constants as const from quantum.plugins.cisco.common import cisco_constants as const
from quantum.plugins.cisco.common import cisco_exceptions as cisco_exc
from quantum.plugins.cisco.nexus import cisco_nexus_plugin_v2 from quantum.plugins.cisco.nexus import cisco_nexus_plugin_v2
from quantum.tests import base from quantum.tests import base
@ -122,3 +123,40 @@ class TestCiscoNexusPlugin(base.BaseTestCase):
INSTANCE, self.vlan_id) INSTANCE, self.vlan_id)
self.assertEqual(expected_instance_id, INSTANCE) self.assertEqual(expected_instance_id, INSTANCE)
def test_nexus_add_remove_router_interface(self):
"""Tests addition of a router interface."""
vlan_name = self.vlan_name
vlan_id = self.vlan_id
gateway_ip = '10.0.0.1/24'
router_id = '00000R1'
subnet_id = '00001'
result = self._cisco_nexus_plugin.add_router_interface(vlan_name,
vlan_id,
subnet_id,
gateway_ip,
router_id)
self.assertTrue(result)
result = self._cisco_nexus_plugin.remove_router_interface(vlan_id,
router_id)
self.assertEqual(result, router_id)
def test_nexus_add_router_interface_fail(self):
"""Tests deletion of a router interface."""
vlan_name = self.vlan_name
vlan_id = self.vlan_id
gateway_ip = '10.0.0.1/24'
router_id = '00000R1'
subnet_id = '00001'
self._cisco_nexus_plugin.add_router_interface(vlan_name,
vlan_id,
subnet_id,
gateway_ip,
router_id)
self.assertRaises(
cisco_exc.SubnetInterfacePresent,
self._cisco_nexus_plugin.add_router_interface,
vlan_name, vlan_id, subnet_id, gateway_ip, router_id)