From bcb9ea7ba4b4b1651f5ed430a2fb533dd2fdce6c Mon Sep 17 00:00:00 2001 From: Arvind Somya Date: Wed, 22 May 2013 10:27:48 -0700 Subject: [PATCH] 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 --- etc/quantum/plugins/cisco/cisco_plugins.ini | 1 + .../plugins/cisco/common/cisco_exceptions.py | 20 +++++ quantum/plugins/cisco/common/config.py | 2 + quantum/plugins/cisco/db/nexus_db_v2.py | 14 +++ .../plugins/cisco/models/virt_phy_sw_v2.py | 63 +++++++++++++ .../nexus/cisco_nexus_network_driver_v2.py | 31 ++++++- .../cisco/nexus/cisco_nexus_plugin_v2.py | 90 ++++++++++++++++++- .../cisco/nexus/cisco_nexus_snippets.py | 29 ++++++ quantum/tests/unit/cisco/test_nexus_plugin.py | 38 ++++++++ 9 files changed, 284 insertions(+), 4 deletions(-) diff --git a/etc/quantum/plugins/cisco/cisco_plugins.ini b/etc/quantum/plugins/cisco/cisco_plugins.ini index 2fbf99cd9..f9d8fdb3d 100644 --- a/etc/quantum/plugins/cisco/cisco_plugins.ini +++ b/etc/quantum/plugins/cisco/cisco_plugins.ini @@ -12,6 +12,7 @@ #model_class=quantum.plugins.cisco.models.virt_phy_sw_v2.VirtualPhysicalSwitchModelV2 #manager_class=quantum.plugins.cisco.segmentation.l2network_vlan_mgr_v2.L2NetworkVLANMgr #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 [CISCO_TEST] diff --git a/quantum/plugins/cisco/common/cisco_exceptions.py b/quantum/plugins/cisco/common/cisco_exceptions.py index f50e6050c..e33fa65e6 100644 --- a/quantum/plugins/cisco/common/cisco_exceptions.py +++ b/quantum/plugins/cisco/common/cisco_exceptions.py @@ -103,6 +103,11 @@ class NexusPortBindingNotFound(exceptions.QuantumException): 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): """PortVnic Binding already exists.""" message = _("PortVnic Binding %(port_id)s already exists") @@ -111,3 +116,18 @@ class PortVnicBindingAlreadyExists(exceptions.QuantumException): class PortVnicNotFound(exceptions.QuantumException): """PortVnic Binding 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') diff --git a/quantum/plugins/cisco/common/config.py b/quantum/plugins/cisco/common/config.py index af285a5ab..f8c94df8c 100644 --- a/quantum/plugins/cisco/common/config.py +++ b/quantum/plugins/cisco/common/config.py @@ -44,6 +44,8 @@ cisco_opts = [ help=_("Maximum Port Profile value")), cfg.StrOpt('max_networks', default='65568', help=_("Maximum Network value")), + cfg.BoolOpt('svi_round_robin', default=False, + help=_("Distribute SVI interfaces over all switches")), cfg.StrOpt('model_class', default='quantum.plugins.cisco.models.virt_phy_sw_v2.' 'VirtualPhysicalSwitchModelV2', diff --git a/quantum/plugins/cisco/db/nexus_db_v2.py b/quantum/plugins/cisco/db/nexus_db_v2.py index 4fd944993..a268937da 100644 --- a/quantum/plugins/cisco/db/nexus_db_v2.py +++ b/quantum/plugins/cisco/db/nexus_db_v2.py @@ -148,3 +148,17 @@ def get_port_switch_bindings(port_id, switch_ip): return binding except exc.NoResultFound: 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 diff --git a/quantum/plugins/cisco/models/virt_phy_sw_v2.py b/quantum/plugins/cisco/models/virt_phy_sw_v2.py index fe0bd9178..3642cd7d6 100644 --- a/quantum/plugins/cisco/models/virt_phy_sw_v2.py +++ b/quantum/plugins/cisco/models/virt_phy_sw_v2.py @@ -392,6 +392,69 @@ class VirtualPhysicalSwitchModelV2(quantum_plugin_base_v2.QuantumPluginBaseV2): 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): """For this model this method will be delegated to vswitch plugin.""" pass diff --git a/quantum/plugins/cisco/nexus/cisco_nexus_network_driver_v2.py b/quantum/plugins/cisco/nexus/cisco_nexus_network_driver_v2.py index a620afa97..b6d9f42d0 100644 --- a/quantum/plugins/cisco/nexus/cisco_nexus_network_driver_v2.py +++ b/quantum/plugins/cisco/nexus/cisco_nexus_network_driver_v2.py @@ -36,7 +36,7 @@ LOG = logging.getLogger(__name__) class CiscoNEXUSDriver(): """Nexus Driver Main Class.""" def __init__(self): - pass + self.connections = {} def _edit_config(self, mgr, target='running', config='', allowed_exc_strs=None): @@ -68,16 +68,21 @@ class CiscoNEXUSDriver(): def nxos_connect(self, nexus_host, nexus_ssh_port, nexus_user, nexus_password): """Make SSH connection to the Nexus Switch.""" + if getattr(self.connections.get(nexus_host), 'connected', None): + return self.connections[nexus_host] + try: - man = manager.connect(host=nexus_host, port=nexus_ssh_port, + man = manager.connect(host=nexus_host, + port=nexus_ssh_port, username=nexus_user, password=nexus_password) + self.connections[nexus_host] = man except Exception as e: # Raise a Quantum exception. Include a description of # the original ncclient exception. raise cexc.NexusConnectFailed(nexus_host=nexus_host, exc=e) - return man + return self.connections[nexus_host] def create_xml_snippet(self, cutomized_config): """Create XML snippet. @@ -227,3 +232,23 @@ class CiscoNEXUSDriver(): nexus_user, nexus_password) for ports in nexus_ports: 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) diff --git a/quantum/plugins/cisco/nexus/cisco_nexus_plugin_v2.py b/quantum/plugins/cisco/nexus/cisco_nexus_plugin_v2.py index b0c17366b..2ecf38197 100644 --- a/quantum/plugins/cisco/nexus/cisco_nexus_plugin_v2.py +++ b/quantum/plugins/cisco/nexus/cisco_nexus_plugin_v2.py @@ -117,6 +117,7 @@ class NexusPlugin(L2DevicePluginBase): _nexus_username, _nexus_password) self._client.enable_vlan_on_trunk_int(man, + _nexus_ip, port_id, vlan_id) vlan_enabled = True @@ -148,6 +149,91 @@ class NexusPlugin(L2DevicePluginBase): self._networks[net_id] = 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): """Delete network. @@ -205,7 +291,9 @@ class NexusPlugin(L2DevicePluginBase): try: # Delete this vlan from this switch _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, 'ssh_port']) _nexus_creds = self.get_credential(_nexus_ip) diff --git a/quantum/plugins/cisco/nexus/cisco_nexus_snippets.py b/quantum/plugins/cisco/nexus/cisco_nexus_snippets.py index a6a501853..cf7f6332f 100644 --- a/quantum/plugins/cisco/nexus/cisco_nexus_snippets.py +++ b/quantum/plugins/cisco/nexus/cisco_nexus_snippets.py @@ -183,3 +183,32 @@ FILTER_SHOW_VLAN_BRIEF_SNIPPET = """ """ + + +CMD_VLAN_SVI_SNIPPET = """ + + + %s + <__XML__MODE_vlan> + + + + +
+
%s
+
+
+ +
+
+""" + +CMD_NO_VLAN_SVI_SNIPPET = """ + + + + %s + + + +""" diff --git a/quantum/tests/unit/cisco/test_nexus_plugin.py b/quantum/tests/unit/cisco/test_nexus_plugin.py index 0e56c4fa6..8c1f07d9e 100644 --- a/quantum/tests/unit/cisco/test_nexus_plugin.py +++ b/quantum/tests/unit/cisco/test_nexus_plugin.py @@ -18,6 +18,7 @@ import mock from quantum.db import api as db from quantum.openstack.common import importutils 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.tests import base @@ -122,3 +123,40 @@ class TestCiscoNexusPlugin(base.BaseTestCase): INSTANCE, self.vlan_id) 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) -- 2.45.2