From 3c164b99883ba93567e3bfae6014e2bac9b03e4a Mon Sep 17 00:00:00 2001 From: Rich Curran Date: Wed, 21 Aug 2013 17:43:12 -0400 Subject: [PATCH] ML2 Mechanism Driver for Cisco Nexus Port of the quantum/plugin/cisco/nexus plugin to run under the Modular Layer 2 (ML2) infrastructure as defined in https://blueprints.launchpad.net/quantum/+spec/ml2-mechanism-drivers Implements blueprint ml2-md-cisco-nexus Change-Id: Ifdd03bec554a08266de859387f1901858a3be4a1 --- etc/neutron/plugins/ml2/ml2_conf_cisco.ini | 36 ++ .../51b4de912379_cisco_nexus_ml2_mech.py | 68 +++ neutron/plugins/ml2/drivers/cisco/README | 19 + neutron/plugins/ml2/drivers/cisco/__init__.py | 14 + neutron/plugins/ml2/drivers/cisco/config.py | 63 ++ .../plugins/ml2/drivers/cisco/constants.py | 48 ++ .../ml2/drivers/cisco/credentials_v2.py | 71 +++ .../plugins/ml2/drivers/cisco/exceptions.py | 78 +++ .../ml2/drivers/cisco/mech_cisco_nexus.py | 221 +++++++ .../ml2/drivers/cisco/network_db_v2.py | 115 ++++ .../ml2/drivers/cisco/network_models_v2.py | 31 + .../plugins/ml2/drivers/cisco/nexus_db_v2.py | 149 +++++ .../ml2/drivers/cisco/nexus_models_v2.py | 45 ++ .../ml2/drivers/cisco/nexus_network_driver.py | 215 +++++++ .../ml2/drivers/cisco/nexus_snippets.py | 200 +++++++ .../tests/unit/ml2/drivers/test_cisco_mech.py | 539 ++++++++++++++++++ setup.cfg | 5 +- 17 files changed, 1916 insertions(+), 1 deletion(-) create mode 100644 etc/neutron/plugins/ml2/ml2_conf_cisco.ini create mode 100755 neutron/db/migration/alembic_migrations/versions/51b4de912379_cisco_nexus_ml2_mech.py create mode 100644 neutron/plugins/ml2/drivers/cisco/README create mode 100644 neutron/plugins/ml2/drivers/cisco/__init__.py create mode 100644 neutron/plugins/ml2/drivers/cisco/config.py create mode 100644 neutron/plugins/ml2/drivers/cisco/constants.py create mode 100644 neutron/plugins/ml2/drivers/cisco/credentials_v2.py create mode 100644 neutron/plugins/ml2/drivers/cisco/exceptions.py create mode 100644 neutron/plugins/ml2/drivers/cisco/mech_cisco_nexus.py create mode 100644 neutron/plugins/ml2/drivers/cisco/network_db_v2.py create mode 100644 neutron/plugins/ml2/drivers/cisco/network_models_v2.py create mode 100644 neutron/plugins/ml2/drivers/cisco/nexus_db_v2.py create mode 100644 neutron/plugins/ml2/drivers/cisco/nexus_models_v2.py create mode 100644 neutron/plugins/ml2/drivers/cisco/nexus_network_driver.py create mode 100644 neutron/plugins/ml2/drivers/cisco/nexus_snippets.py create mode 100644 neutron/tests/unit/ml2/drivers/test_cisco_mech.py diff --git a/etc/neutron/plugins/ml2/ml2_conf_cisco.ini b/etc/neutron/plugins/ml2/ml2_conf_cisco.ini new file mode 100644 index 0000000000..69d5312533 --- /dev/null +++ b/etc/neutron/plugins/ml2/ml2_conf_cisco.ini @@ -0,0 +1,36 @@ +[ml2_cisco] + +# (StrOpt) A short prefix to prepend to the VLAN number when creating a +# VLAN interface. For example, if an interface is being created for +# VLAN 2001 it will be named 'q-2001' using the default prefix. +# +# vlan_name_prefix = q- +# Example: vlan_name_prefix = vnet- + +# (BoolOpt) A flag to enable round robin scheduling of routers for SVI. +# svi_round_robin = False + +# Cisco Nexus Switch configurations. +# Each switch to be managed by Openstack Neutron must be configured here. +# +# Cisco Nexus Switch Format. +# [ml2_mech_cisco_nexus:] +# = (1) +# ssh_port= (2) +# username= (3) +# password= (4) +# +# (1) For each host connected to a port on the switch, specify the hostname +# and the Nexus physical port (interface) it is connected to. +# (2) The TCP port for connecting via SSH to manage the switch. This is +# port number 22 unless the switch has been configured otherwise. +# (3) The username for logging into the switch to manage it. +# (4) The password for logging into the switch to manage it. +# +# Example: +# [ml2_mech_cisco_nexus:1.1.1.1] +# compute1=1/1 +# compute2=1/2 +# ssh_port=22 +# username=admin +# password=mySecretPassword diff --git a/neutron/db/migration/alembic_migrations/versions/51b4de912379_cisco_nexus_ml2_mech.py b/neutron/db/migration/alembic_migrations/versions/51b4de912379_cisco_nexus_ml2_mech.py new file mode 100755 index 0000000000..f6038fd3e6 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/51b4de912379_cisco_nexus_ml2_mech.py @@ -0,0 +1,68 @@ +# Copyright 2013 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +"""Cisco Nexus ML2 mechanism driver + +Revision ID: 51b4de912379 +Revises: 66a59a7f516 +Create Date: 2013-08-20 15:31:40.553634 + +""" + +# revision identifiers, used by Alembic. +revision = '51b4de912379' +down_revision = '66a59a7f516' + +migration_for_plugins = [ + 'neutron.plugins.ml2.plugin.Ml2Plugin' +] + +from alembic import op +import sqlalchemy as sa + +from neutron.db import migration + + +def upgrade(active_plugins=None, options=None): + if not migration.should_run(active_plugins, migration_for_plugins): + return + + op.create_table( + 'cisco_ml2_nexusport_bindings', + sa.Column('binding_id', sa.Integer(), nullable=False), + sa.Column('port_id', sa.String(length=255), nullable=True), + sa.Column('vlan_id', sa.Integer(), autoincrement=False, + nullable=False), + sa.Column('switch_ip', sa.String(length=255), nullable=True), + sa.Column('instance_id', sa.String(length=255), nullable=True), + sa.PrimaryKeyConstraint('binding_id'), + ) + op.create_table( + 'cisco_ml2_credentials', + sa.Column('credential_id', sa.String(length=255), nullable=True), + sa.Column('tenant_id', sa.String(length=255), nullable=False), + sa.Column('credential_name', sa.String(length=255), nullable=False), + sa.Column('user_name', sa.String(length=255), nullable=True), + sa.Column('password', sa.String(length=255), nullable=True), + sa.PrimaryKeyConstraint('tenant_id', 'credential_name'), + ) + + +def downgrade(active_plugins=None, options=None): + if not migration.should_run(active_plugins, migration_for_plugins): + return + + op.drop_table('cisco_ml2_credentials') + op.drop_table('cisco_ml2_nexusport_bindings') diff --git a/neutron/plugins/ml2/drivers/cisco/README b/neutron/plugins/ml2/drivers/cisco/README new file mode 100644 index 0000000000..5960e951da --- /dev/null +++ b/neutron/plugins/ml2/drivers/cisco/README @@ -0,0 +1,19 @@ +Neutron ML2 Cisco Mechanism Drivers README + +Cisco mechanism drivers implement the ML2 driver APIs for managing +Cisco devices. + +Notes: +The initial version of the Cisco Nexus driver supports only the +VLAN network type on a single physical network. + +Provider networks are not currently supported. + +The Cisco Nexus mechanism driver's database may have duplicate entries also +found in the core ML2 database. Since the Cisco Nexus DB code is a port from +the plugins/cisco implementation this duplication will remain until the +plugins/cisco code is deprecated. + + +For more details on using Cisco Nexus switches under ML2 please refer to: +http://wiki.openstack.org/wiki/Neutron/ML2/MechCiscoNexus diff --git a/neutron/plugins/ml2/drivers/cisco/__init__.py b/neutron/plugins/ml2/drivers/cisco/__init__.py new file mode 100644 index 0000000000..788cea1f70 --- /dev/null +++ b/neutron/plugins/ml2/drivers/cisco/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2013 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. diff --git a/neutron/plugins/ml2/drivers/cisco/config.py b/neutron/plugins/ml2/drivers/cisco/config.py new file mode 100644 index 0000000000..cf47dc4430 --- /dev/null +++ b/neutron/plugins/ml2/drivers/cisco/config.py @@ -0,0 +1,63 @@ +# Copyright (c) 2013 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo.config import cfg + + +ml2_cisco_opts = [ + cfg.StrOpt('vlan_name_prefix', default='q-', + help=_("VLAN Name prefix")), + cfg.BoolOpt('svi_round_robin', default=False, + help=_("Distribute SVI interfaces over all switches")), +] + + +cfg.CONF.register_opts(ml2_cisco_opts, "ml2_cisco") + +# +# Format for ml2_conf_cisco.ini 'ml2_mech_cisco_nexus' is: +# {('', ''): '', ...} +# +# Example: +# {('1.1.1.1', 'username'): 'admin', +# ('1.1.1.1', 'password'): 'mySecretPassword', +# ('1.1.1.1', 'compute1'): '1/1', ...} +# + + +class ML2MechCiscoConfig(object): + """ML2 Mechanism Driver Cisco Configuration class.""" + nexus_dict = {} + + def __init__(self): + self._create_ml2_mech_device_cisco_dictionary() + + def _create_ml2_mech_device_cisco_dictionary(self): + """Create the ML2 device cisco dictionary. + + Read data from the ml2_conf_cisco.ini device supported sections. + """ + multi_parser = cfg.MultiConfigParser() + read_ok = multi_parser.read(cfg.CONF.config_file) + + if len(read_ok) != len(cfg.CONF.config_file): + raise cfg.Error("Some config files were not parsed properly") + + for parsed_file in multi_parser.parsed: + for parsed_item in parsed_file.keys(): + dev_id, sep, dev_ip = parsed_item.partition(':') + if dev_id.lower() == 'ml2_mech_cisco_nexus': + for dev_key, value in parsed_file[parsed_item].items(): + self.nexus_dict[dev_ip, dev_key] = value[0] diff --git a/neutron/plugins/ml2/drivers/cisco/constants.py b/neutron/plugins/ml2/drivers/cisco/constants.py new file mode 100644 index 0000000000..df78eee9ea --- /dev/null +++ b/neutron/plugins/ml2/drivers/cisco/constants.py @@ -0,0 +1,48 @@ +# Copyright 2011 OpenStack Foundation. +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + + +# Attachment attributes +INSTANCE_ID = 'instance_id' +TENANT_ID = 'tenant_id' +TENANT_NAME = 'tenant_name' +HOST_NAME = 'host_name' + +# Network attributes +NET_ID = 'id' +NET_NAME = 'name' +NET_VLAN_ID = 'vlan_id' +NET_VLAN_NAME = 'vlan_name' +NET_PORTS = 'ports' + +# Network types +NETWORK_TYPE_FLAT = 'flat' +NETWORK_TYPE_LOCAL = 'local' +NETWORK_TYPE_VLAN = 'vlan' +NETWORK_TYPE_NONE = 'none' + +CREDENTIAL_USERNAME = 'user_name' +CREDENTIAL_PASSWORD = 'password' + +USERNAME = 'username' +PASSWORD = 'password' + +NETWORK_ADMIN = 'network_admin' + +NETWORK = 'network' +PORT = 'port' +CONTEXT = 'context' +SUBNET = 'subnet' diff --git a/neutron/plugins/ml2/drivers/cisco/credentials_v2.py b/neutron/plugins/ml2/drivers/cisco/credentials_v2.py new file mode 100644 index 0000000000..ea172aabc5 --- /dev/null +++ b/neutron/plugins/ml2/drivers/cisco/credentials_v2.py @@ -0,0 +1,71 @@ +# Copyright (c) 2013 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +from neutron.plugins.ml2.drivers.cisco import config as config +from neutron.plugins.ml2.drivers.cisco import constants as const +from neutron.plugins.ml2.drivers.cisco import exceptions as cexc +from neutron.plugins.ml2.drivers.cisco import network_db_v2 as cdb + + +TENANT = const.NETWORK_ADMIN + + +class Store(object): + """ML2 Cisco Mechanism Driver Credential Store.""" + + @staticmethod + def initialize(): + _nexus_dict = config.ML2MechCiscoConfig.nexus_dict + for ipaddr, keyword in _nexus_dict.keys(): + if keyword == const.USERNAME: + try: + cdb.add_credential(TENANT, ipaddr, + _nexus_dict[ipaddr, const.USERNAME], + _nexus_dict[ipaddr, const.PASSWORD]) + except cexc.CredentialAlreadyExists: + # We are quietly ignoring this, since it only happens + # if this class module is loaded more than once, in which + # case, the credentials are already populated + pass + + @staticmethod + def put_credential(cred_name, username, password): + """Set the username and password.""" + cdb.add_credential(TENANT, cred_name, username, password) + + @staticmethod + def get_username(cred_name): + """Get the username.""" + credential = cdb.get_credential_name(TENANT, cred_name) + return credential[const.CREDENTIAL_USERNAME] + + @staticmethod + def get_password(cred_name): + """Get the password.""" + credential = cdb.get_credential_name(TENANT, cred_name) + return credential[const.CREDENTIAL_PASSWORD] + + @staticmethod + def get_credential(cred_name): + """Get the username and password.""" + credential = cdb.get_credential_name(TENANT, cred_name) + return {const.USERNAME: credential[const.CREDENTIAL_USERNAME], + const.PASSWORD: credential[const.CREDENTIAL_PASSWORD]} + + @staticmethod + def delete_credential(cred_name): + """Delete a credential.""" + cdb.remove_credential(TENANT, cred_name) diff --git a/neutron/plugins/ml2/drivers/cisco/exceptions.py b/neutron/plugins/ml2/drivers/cisco/exceptions.py new file mode 100644 index 0000000000..c431f9b2d6 --- /dev/null +++ b/neutron/plugins/ml2/drivers/cisco/exceptions.py @@ -0,0 +1,78 @@ +# Copyright (c) 2013 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Exceptions used by Cisco ML2 mechanism drivers.""" + +from neutron.common import exceptions + + +class CredentialNotFound(exceptions.NeutronException): + """Credential with this ID cannot be found.""" + message = _("Credential %(credential_id)s could not be found.") + + +class CredentialNameNotFound(exceptions.NeutronException): + """Credential Name could not be found.""" + message = _("Credential %(credential_name)s could not be found.") + + +class CredentialAlreadyExists(exceptions.NeutronException): + """Credential ID already exists.""" + message = _("Credential %(credential_id)s already exists " + "for tenant %(tenant_id)s.") + + +class NexusComputeHostNotConfigured(exceptions.NeutronException): + """Connection to compute host is not configured.""" + message = _("Connection to %(host)s is not configured.") + + +class NexusConnectFailed(exceptions.NeutronException): + """Failed to connect to Nexus switch.""" + message = _("Unable to connect to Nexus %(nexus_host)s. Reason: %(exc)s.") + + +class NexusConfigFailed(exceptions.NeutronException): + """Failed to configure Nexus switch.""" + message = _("Failed to configure Nexus: %(config)s. Reason: %(exc)s.") + + +class NexusPortBindingNotFound(exceptions.NeutronException): + """NexusPort Binding is not present.""" + message = _("Nexus Port Binding (%(filters)s) is not present") + + def __init__(self, **kwargs): + filters = ','.join('%s=%s' % i for i in kwargs.items()) + super(NexusPortBindingNotFound, self).__init__(filters=filters) + + +class NoNexusSviSwitch(exceptions.NeutronException): + """No usable nexus switch found.""" + message = _("No usable Nexus switch found to create SVI interface.") + + +class SubnetNotSpecified(exceptions.NeutronException): + """Subnet id not specified.""" + message = _("No subnet_id specified for router gateway.") + + +class SubnetInterfacePresent(exceptions.NeutronException): + """Subnet SVI interface already exists.""" + message = _("Subnet %(subnet_id)s has an interface on %(router_id)s.") + + +class PortIdForNexusSvi(exceptions.NeutronException): + """Port Id specified for Nexus SVI.""" + message = _('Nexus hardware router gateway only uses Subnet Ids.') diff --git a/neutron/plugins/ml2/drivers/cisco/mech_cisco_nexus.py b/neutron/plugins/ml2/drivers/cisco/mech_cisco_nexus.py new file mode 100644 index 0000000000..95addc1b8f --- /dev/null +++ b/neutron/plugins/ml2/drivers/cisco/mech_cisco_nexus.py @@ -0,0 +1,221 @@ +# Copyright 2013 OpenStack Foundation +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +ML2 Mechanism Driver for Cisco Nexus platforms. +""" + +from novaclient.v1_1 import client as nova_client +from oslo.config import cfg + +from neutron.openstack.common import excutils +from neutron.openstack.common import log as logging +from neutron.plugins.ml2 import driver_api as api +from neutron.plugins.ml2.drivers.cisco import config as conf +from neutron.plugins.ml2.drivers.cisco import credentials_v2 as cred +from neutron.plugins.ml2.drivers.cisco import exceptions as excep +from neutron.plugins.ml2.drivers.cisco import nexus_db_v2 as nxos_db +from neutron.plugins.ml2.drivers.cisco import nexus_network_driver + +LOG = logging.getLogger(__name__) + + +class CiscoNexusMechanismDriver(api.MechanismDriver): + + """Cisco Nexus ML2 Mechanism Driver.""" + + def initialize(self): + # Create ML2 device dictionary from ml2_conf.ini entries. + conf.ML2MechCiscoConfig() + + # Extract configuration parameters from the configuration file. + self._nexus_switches = conf.ML2MechCiscoConfig.nexus_dict + LOG.debug(_("nexus_switches found = %s"), self._nexus_switches) + + self.credentials = {} + self.driver = nexus_network_driver.CiscoNexusDriver() + + # 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 _get_credential(self, nexus_ip): + """Return credential information for a given Nexus IP address. + + If credential doesn't exist then also add to local dictionary. + """ + if nexus_ip not in self.credentials: + _nexus_username = cred.Store.get_username(nexus_ip) + _nexus_password = cred.Store.get_password(nexus_ip) + self.credentials[nexus_ip] = { + 'username': _nexus_username, + 'password': _nexus_password + } + return self.credentials[nexus_ip] + + def _manage_port(self, vlan_name, vlan_id, host, instance): + """Called during create and update port events. + + Create a VLAN in the appropriate switch/port and configure the + appropriate interfaces for this VLAN. + """ + + # Grab the switch IP and port for this host + for switch_ip, attr in self._nexus_switches: + if str(attr) == str(host): + port_id = self._nexus_switches[switch_ip, attr] + break + else: + raise excep.NexusComputeHostNotConfigured(host=host) + + # Check if this network is already in the DB + vlan_created = False + vlan_trunked = False + + try: + nxos_db.get_port_vlan_switch_binding(port_id, vlan_id, switch_ip) + except excep.NexusPortBindingNotFound: + # Check for vlan/switch binding + try: + nxos_db.get_nexusvlan_binding(vlan_id, switch_ip) + except excep.NexusPortBindingNotFound: + # Create vlan and trunk vlan on the port + LOG.debug(_("Nexus: create & trunk vlan %s"), vlan_name) + self.driver.create_and_trunk_vlan(switch_ip, vlan_id, + vlan_name, port_id) + vlan_created = True + vlan_trunked = True + else: + # Only trunk vlan on the port + LOG.debug(_("Nexus: trunk vlan %s"), vlan_name) + self.driver.enable_vlan_on_trunk_int(switch_ip, vlan_id, + port_id) + vlan_trunked = True + + try: + nxos_db.add_nexusport_binding(port_id, str(vlan_id), + switch_ip, instance) + except Exception: + with excutils.save_and_reraise_exception(): + # Add binding failed, roll back any vlan creation/enabling + if vlan_created and vlan_trunked: + LOG.debug(_("Nexus: delete & untrunk vlan %s"), vlan_name) + self.driver.delete_and_untrunk_vlan(switch_ip, vlan_id, + port_id) + elif vlan_created: + LOG.debug(_("Nexus: delete vlan %s"), vlan_name) + self.driver.delete_vlan(switch_ip, vlan_id) + elif vlan_trunked: + LOG.debug(_("Nexus: untrunk vlan %s"), vlan_name) + 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.""" + vlan_id = self._get_vlanid(context) + host = self._get_instance_host(instance_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) + + 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) + + def delete_port_precommit(self, context): + """Delete port pre-database commit event. + + Delete port bindings from the database and scan whether the network + is still required on the interfaces trunked. + """ + port = context.current + device_id = port['device_id'] + vlan_id = self._get_vlanid(context) + + # Delete DB row for this port + try: + row = nxos_db.get_nexusvm_binding(vlan_id, device_id) + except excep.NexusPortBindingNotFound: + return + + switch_ip = row.switch_ip + nexus_port = None + if row.port_id != 'router': + nexus_port = row.port_id + + nxos_db.remove_nexusport_binding(row.port_id, row.vlan_id, + row.switch_ip, row.instance_id) + + # Check for any other bindings with the same vlan_id and switch_ip + try: + nxos_db.get_nexusvlan_binding(row.vlan_id, row.switch_ip) + except excep.NexusPortBindingNotFound: + try: + # Delete this vlan from this switch + if nexus_port: + self.driver.disable_vlan_on_trunk_int(switch_ip, + row.vlan_id, + nexus_port) + self.driver.delete_vlan(switch_ip, row.vlan_id) + except Exception: + # The delete vlan operation on the Nexus failed, + # so this delete_port request has failed. For + # consistency, roll back the Nexus database to what + # it was before this request. + with excutils.save_and_reraise_exception(): + nxos_db.add_nexusport_binding(row.port_id, + row.vlan_id, + row.switch_ip, + row.instance_id) diff --git a/neutron/plugins/ml2/drivers/cisco/network_db_v2.py b/neutron/plugins/ml2/drivers/cisco/network_db_v2.py new file mode 100644 index 0000000000..46ed79c8cf --- /dev/null +++ b/neutron/plugins/ml2/drivers/cisco/network_db_v2.py @@ -0,0 +1,115 @@ +# Copyright (c) 2013 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +from sqlalchemy.orm import exc + +from neutron.db import api as db +from neutron.openstack.common import log as logging +from neutron.openstack.common import uuidutils +from neutron.plugins.ml2.drivers.cisco import exceptions as c_exc +from neutron.plugins.ml2.drivers.cisco import network_models_v2 +from neutron.plugins.ml2.drivers.cisco import nexus_models_v2 # noqa + + +LOG = logging.getLogger(__name__) + + +def get_all_credentials(tenant_id): + """Lists all the creds for a tenant.""" + session = db.get_session() + return (session.query(network_models_v2.Credential). + filter_by(tenant_id=tenant_id).all()) + + +def get_credential(tenant_id, credential_id): + """Lists the creds for given a cred_id and tenant_id.""" + session = db.get_session() + try: + cred = (session.query(network_models_v2.Credential). + filter_by(tenant_id=tenant_id). + filter_by(credential_id=credential_id).one()) + return cred + except exc.NoResultFound: + raise c_exc.CredentialNotFound(credential_id=credential_id, + tenant_id=tenant_id) + + +def get_credential_name(tenant_id, credential_name): + """Lists the creds for given a cred_name and tenant_id.""" + session = db.get_session() + try: + cred = (session.query(network_models_v2.Credential). + filter_by(tenant_id=tenant_id). + filter_by(credential_name=credential_name).one()) + return cred + except exc.NoResultFound: + raise c_exc.CredentialNameNotFound(credential_name=credential_name, + tenant_id=tenant_id) + + +def add_credential(tenant_id, credential_name, user_name, password): + """Adds a qos to tenant association.""" + session = db.get_session() + try: + cred = (session.query(network_models_v2.Credential). + filter_by(tenant_id=tenant_id). + filter_by(credential_name=credential_name).one()) + raise c_exc.CredentialAlreadyExists(credential_name=credential_name, + tenant_id=tenant_id) + except exc.NoResultFound: + cred = network_models_v2.Credential( + credential_id=uuidutils.generate_uuid(), + tenant_id=tenant_id, + credential_name=credential_name, + user_name=user_name, + password=password) + session.add(cred) + session.flush() + return cred + + +def remove_credential(tenant_id, credential_id): + """Removes a credential from a tenant.""" + session = db.get_session() + try: + cred = (session.query(network_models_v2.Credential). + filter_by(tenant_id=tenant_id). + filter_by(credential_id=credential_id).one()) + session.delete(cred) + session.flush() + return cred + except exc.NoResultFound: + pass + + +def update_credential(tenant_id, credential_id, + new_user_name=None, new_password=None): + """Updates a credential for a tenant.""" + session = db.get_session() + try: + cred = (session.query(network_models_v2.Credential). + filter_by(tenant_id=tenant_id). + filter_by(credential_id=credential_id).one()) + if new_user_name: + cred["user_name"] = new_user_name + if new_password: + cred["password"] = new_password + session.merge(cred) + session.flush() + return cred + except exc.NoResultFound: + raise c_exc.CredentialNotFound(credential_id=credential_id, + tenant_id=tenant_id) diff --git a/neutron/plugins/ml2/drivers/cisco/network_models_v2.py b/neutron/plugins/ml2/drivers/cisco/network_models_v2.py new file mode 100644 index 0000000000..8725edb7b4 --- /dev/null +++ b/neutron/plugins/ml2/drivers/cisco/network_models_v2.py @@ -0,0 +1,31 @@ +# Copyright (c) 2013 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import sqlalchemy as sa + +from neutron.db import model_base + + +class Credential(model_base.BASEV2): + """Represents credentials for a tenant to control Cisco switches.""" + + __tablename__ = 'cisco_ml2_credentials' + + credential_id = sa.Column(sa.String(255)) + tenant_id = sa.Column(sa.String(255), primary_key=True) + credential_name = sa.Column(sa.String(255), primary_key=True) + user_name = sa.Column(sa.String(255)) + password = sa.Column(sa.String(255)) diff --git a/neutron/plugins/ml2/drivers/cisco/nexus_db_v2.py b/neutron/plugins/ml2/drivers/cisco/nexus_db_v2.py new file mode 100644 index 0000000000..1fac6b610f --- /dev/null +++ b/neutron/plugins/ml2/drivers/cisco/nexus_db_v2.py @@ -0,0 +1,149 @@ +# Copyright (c) 2013 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import sqlalchemy.orm.exc as sa_exc + +import neutron.db.api as db +from neutron.openstack.common import log as logging +from neutron.plugins.ml2.drivers.cisco import exceptions as c_exc +from neutron.plugins.ml2.drivers.cisco import nexus_models_v2 + + +LOG = logging.getLogger(__name__) + + +def get_nexusport_binding(port_id, vlan_id, switch_ip, instance_id): + """Lists a nexusport binding.""" + LOG.debug(_("get_nexusport_binding() called")) + return _lookup_all_nexus_bindings(port_id=port_id, + vlan_id=vlan_id, + switch_ip=switch_ip, + instance_id=instance_id) + + +def get_nexusvlan_binding(vlan_id, switch_ip): + """Lists a vlan and switch binding.""" + LOG.debug(_("get_nexusvlan_binding() called")) + return _lookup_all_nexus_bindings(vlan_id=vlan_id, switch_ip=switch_ip) + + +def add_nexusport_binding(port_id, vlan_id, switch_ip, instance_id): + """Adds a nexusport binding.""" + LOG.debug(_("add_nexusport_binding() called")) + session = db.get_session() + binding = nexus_models_v2.NexusPortBinding(port_id=port_id, + vlan_id=vlan_id, + switch_ip=switch_ip, + instance_id=instance_id) + session.add(binding) + session.flush() + return binding + + +def remove_nexusport_binding(port_id, vlan_id, switch_ip, instance_id): + """Removes a nexusport binding.""" + LOG.debug(_("remove_nexusport_binding() called")) + session = db.get_session() + binding = _lookup_all_nexus_bindings(session=session, + vlan_id=vlan_id, + switch_ip=switch_ip, + port_id=port_id, + instance_id=instance_id) + for bind in binding: + session.delete(bind) + session.flush() + return binding + + +def update_nexusport_binding(port_id, new_vlan_id): + """Updates nexusport binding.""" + if not new_vlan_id: + LOG.warning(_("update_nexusport_binding called with no vlan")) + return + LOG.debug(_("update_nexusport_binding called")) + session = db.get_session() + binding = _lookup_one_nexus_binding(session=session, port_id=port_id) + binding.vlan_id = new_vlan_id + session.merge(binding) + session.flush() + return binding + + +def get_nexusvm_binding(vlan_id, instance_id): + """Lists nexusvm bindings.""" + LOG.debug(_("get_nexusvm_binding() called")) + return _lookup_first_nexus_binding(instance_id=instance_id, + vlan_id=vlan_id) + + +def get_port_vlan_switch_binding(port_id, vlan_id, switch_ip): + """Lists nexusvm bindings.""" + LOG.debug(_("get_port_vlan_switch_binding() called")) + return _lookup_all_nexus_bindings(port_id=port_id, + switch_ip=switch_ip, + vlan_id=vlan_id) + + +def get_port_switch_bindings(port_id, switch_ip): + """List all vm/vlan bindings on a Nexus switch port.""" + LOG.debug(_("get_port_switch_bindings() called, " + "port:'%(port_id)s', switch:'%(switch_ip)s'"), + {'port_id': port_id, 'switch_ip': switch_ip}) + try: + return _lookup_all_nexus_bindings(port_id=port_id, + switch_ip=switch_ip) + except c_exc.NexusPortBindingNotFound: + pass + + +def get_nexussvi_bindings(): + """Lists nexus svi bindings.""" + LOG.debug(_("get_nexussvi_bindings() called")) + return _lookup_all_nexus_bindings(port_id='router') + + +def _lookup_nexus_bindings(query_type, session=None, **bfilter): + """Look up 'query_type' Nexus bindings matching the filter. + + :param query_type: 'all', 'one' or 'first' + :param session: db session + :param bfilter: filter for bindings query + :return: bindings if query gave a result, else + raise NexusPortBindingNotFound. + """ + if session is None: + session = db.get_session() + query_method = getattr(session.query( + nexus_models_v2.NexusPortBinding).filter_by(**bfilter), query_type) + try: + bindings = query_method() + if bindings: + return bindings + except sa_exc.NoResultFound: + pass + raise c_exc.NexusPortBindingNotFound(**bfilter) + + +def _lookup_all_nexus_bindings(session=None, **bfilter): + return _lookup_nexus_bindings('all', session, **bfilter) + + +def _lookup_one_nexus_binding(session=None, **bfilter): + return _lookup_nexus_bindings('one', session, **bfilter) + + +def _lookup_first_nexus_binding(session=None, **bfilter): + return _lookup_nexus_bindings('first', session, **bfilter) diff --git a/neutron/plugins/ml2/drivers/cisco/nexus_models_v2.py b/neutron/plugins/ml2/drivers/cisco/nexus_models_v2.py new file mode 100644 index 0000000000..ce7c416632 --- /dev/null +++ b/neutron/plugins/ml2/drivers/cisco/nexus_models_v2.py @@ -0,0 +1,45 @@ +# Copyright (c) 2013 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import sqlalchemy as sa + +from neutron.db import model_base + + +class NexusPortBinding(model_base.BASEV2): + """Represents a binding of VM's to nexus ports.""" + + __tablename__ = "cisco_ml2_nexusport_bindings" + + binding_id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) + port_id = sa.Column(sa.String(255)) + vlan_id = sa.Column(sa.Integer, nullable=False) + switch_ip = sa.Column(sa.String(255)) + instance_id = sa.Column(sa.String(255)) + + def __repr__(self): + """Just the binding, without the id key.""" + return ("" % + (self.port_id, self.vlan_id, self.switch_ip, self.instance_id)) + + def __eq__(self, other): + """Compare only the binding, without the id key.""" + return ( + self.port_id == other.port_id and + self.vlan_id == other.vlan_id and + self.switch_ip == other.switch_ip and + self.instance_id == other.instance_id + ) diff --git a/neutron/plugins/ml2/drivers/cisco/nexus_network_driver.py b/neutron/plugins/ml2/drivers/cisco/nexus_network_driver.py new file mode 100644 index 0000000000..936c2bb015 --- /dev/null +++ b/neutron/plugins/ml2/drivers/cisco/nexus_network_driver.py @@ -0,0 +1,215 @@ +# Copyright 2013 OpenStack Foundation +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Implements a Nexus-OS NETCONF over SSHv2 API Client +""" + +from neutron.openstack.common import excutils +from neutron.openstack.common import importutils +from neutron.openstack.common import log as logging +from neutron.plugins.ml2.drivers.cisco import config as conf +from neutron.plugins.ml2.drivers.cisco import constants as const +from neutron.plugins.ml2.drivers.cisco import credentials_v2 as cred +from neutron.plugins.ml2.drivers.cisco import exceptions as cexc +from neutron.plugins.ml2.drivers.cisco import nexus_db_v2 +from neutron.plugins.ml2.drivers.cisco import nexus_snippets as snipp + +LOG = logging.getLogger(__name__) + + +class CiscoNexusDriver(object): + """Nexus Driver Main Class.""" + def __init__(self): + self.ncclient = None + self.nexus_switches = conf.ML2MechCiscoConfig.nexus_dict + self.credentials = {} + self.connections = {} + + def _import_ncclient(self): + """Import the NETCONF client (ncclient) module. + + The ncclient module is not installed as part of the normal Neutron + distributions. It is imported dynamically in this module so that + the import can be mocked, allowing unit testing without requiring + the installation of ncclient. + + """ + return importutils.import_module('ncclient.manager') + + def _edit_config(self, nexus_host, target='running', config='', + allowed_exc_strs=None): + """Modify switch config for a target config type. + + :param nexus_host: IP address of switch to configure + :param target: Target config type + :param config: Configuration string in XML format + :param allowed_exc_strs: Exceptions which have any of these strings + as a subset of their exception message + (str(exception)) can be ignored + + :raises: NexusConfigFailed + + """ + if not allowed_exc_strs: + allowed_exc_strs = [] + mgr = self.nxos_connect(nexus_host) + try: + mgr.edit_config(target, config=config) + except Exception as e: + for exc_str in allowed_exc_strs: + if exc_str in str(e): + break + else: + # Raise a Neutron exception. Include a description of + # the original ncclient exception. + raise cexc.NexusConfigFailed(config=config, exc=e) + + def get_credential(self, nexus_ip): + """Return credential information for a given Nexus IP address. + + If credential doesn't exist then also add to local dictionary. + """ + if nexus_ip not in self.credentials: + nexus_username = cred.Store.get_username(nexus_ip) + nexus_password = cred.Store.get_password(nexus_ip) + self.credentials[nexus_ip] = { + const.USERNAME: nexus_username, + const.PASSWORD: nexus_password + } + return self.credentials[nexus_ip] + + def nxos_connect(self, nexus_host): + """Make SSH connection to the Nexus Switch.""" + if getattr(self.connections.get(nexus_host), 'connected', None): + return self.connections[nexus_host] + + if not self.ncclient: + self.ncclient = self._import_ncclient() + nexus_ssh_port = int(self.nexus_switches[nexus_host, 'ssh_port']) + nexus_creds = self.get_credential(nexus_host) + nexus_user = nexus_creds[const.USERNAME] + nexus_password = nexus_creds[const.PASSWORD] + try: + man = self.ncclient.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 Neutron exception. Include a description of + # the original ncclient exception. + raise cexc.NexusConnectFailed(nexus_host=nexus_host, exc=e) + + return self.connections[nexus_host] + + def create_xml_snippet(self, customized_config): + """Create XML snippet. + + Creates the Proper XML structure for the Nexus Switch Configuration. + """ + conf_xml_snippet = snipp.EXEC_CONF_SNIPPET % (customized_config) + return conf_xml_snippet + + def create_vlan(self, nexus_host, vlanid, vlanname): + """Create a VLAN on Nexus Switch given the VLAN ID and Name.""" + confstr = self.create_xml_snippet( + snipp.CMD_VLAN_CONF_SNIPPET % (vlanid, vlanname)) + LOG.debug(_("NexusDriver: %s"), confstr) + self._edit_config(nexus_host, target='running', config=confstr) + + # Enable VLAN active and no-shutdown states. Some versions of + # Nexus switch do not allow state changes for the extended VLAN + # range (1006-4094), but these errors can be ignored (default + # values are appropriate). + for snippet in [snipp.CMD_VLAN_ACTIVE_SNIPPET, + snipp.CMD_VLAN_NO_SHUTDOWN_SNIPPET]: + try: + confstr = self.create_xml_snippet(snippet % vlanid) + self._edit_config( + nexus_host, + target='running', + config=confstr, + allowed_exc_strs=["Can't modify state for extended", + "Command is only allowed on VLAN"]) + except cexc.NexusConfigFailed: + with excutils.save_and_reraise_exception(): + self.delete_vlan(nexus_host, vlanid) + + def delete_vlan(self, nexus_host, vlanid): + """Delete a VLAN on Nexus Switch given the VLAN ID.""" + confstr = snipp.CMD_NO_VLAN_CONF_SNIPPET % vlanid + confstr = self.create_xml_snippet(confstr) + self._edit_config(nexus_host, target='running', config=confstr) + + def enable_port_trunk(self, nexus_host, interface): + """Enable trunk mode an interface on Nexus Switch.""" + confstr = snipp.CMD_PORT_TRUNK % (interface) + confstr = self.create_xml_snippet(confstr) + LOG.debug(_("NexusDriver: %s"), confstr) + self._edit_config(nexus_host, target='running', config=confstr) + + def disable_switch_port(self, nexus_host, interface): + """Disable trunk mode an interface on Nexus Switch.""" + confstr = snipp.CMD_NO_SWITCHPORT % (interface) + confstr = self.create_xml_snippet(confstr) + LOG.debug(_("NexusDriver: %s"), confstr) + self._edit_config(nexus_host, target='running', config=confstr) + + def enable_vlan_on_trunk_int(self, nexus_host, vlanid, interface): + """Enable a VLAN on a trunk interface.""" + # If one or more VLANs are already configured on this interface, + # include the 'add' keyword. + if nexus_db_v2.get_port_switch_bindings(interface, nexus_host): + snippet = snipp.CMD_INT_VLAN_ADD_SNIPPET + else: + snippet = snipp.CMD_INT_VLAN_SNIPPET + confstr = snippet % (interface, vlanid) + confstr = self.create_xml_snippet(confstr) + LOG.debug(_("NexusDriver: %s"), confstr) + self._edit_config(nexus_host, target='running', config=confstr) + + def disable_vlan_on_trunk_int(self, nexus_host, vlanid, interface): + """Disable a VLAN on a trunk interface.""" + confstr = snipp.CMD_NO_VLAN_INT_SNIPPET % (interface, vlanid) + confstr = self.create_xml_snippet(confstr) + LOG.debug(_("NexusDriver: %s"), confstr) + self._edit_config(nexus_host, target='running', config=confstr) + + def create_and_trunk_vlan(self, nexus_host, vlan_id, vlan_name, + nexus_port): + """Create VLAN and trunk it on the specified ports.""" + self.create_vlan(nexus_host, vlan_id, vlan_name) + LOG.debug(_("NexusDriver created VLAN: %s"), vlan_id) + if nexus_port: + self.enable_vlan_on_trunk_int(nexus_host, vlan_id, nexus_port) + + def delete_and_untrunk_vlan(self, nexus_host, vlan_id, nexus_port): + """Delete VLAN and untrunk it from the specified ports.""" + self.delete_vlan(nexus_host, vlan_id) + if nexus_port: + self.disable_vlan_on_trunk_int(nexus_host, vlan_id, nexus_port) + + def create_vlan_svi(self, nexus_host, vlan_id, gateway_ip): + confstr = snipp.CMD_VLAN_SVI_SNIPPET % (vlan_id, gateway_ip) + confstr = self.create_xml_snippet(confstr) + LOG.debug(_("NexusDriver: %s"), confstr) + self._edit_config(nexus_host, target='running', config=confstr) + + def delete_vlan_svi(self, nexus_host, vlan_id): + confstr = snipp.CMD_NO_VLAN_SVI_SNIPPET % vlan_id + confstr = self.create_xml_snippet(confstr) + LOG.debug(_("NexusDriver: %s"), confstr) + self._edit_config(nexus_host, target='running', config=confstr) diff --git a/neutron/plugins/ml2/drivers/cisco/nexus_snippets.py b/neutron/plugins/ml2/drivers/cisco/nexus_snippets.py new file mode 100644 index 0000000000..b30c7e6387 --- /dev/null +++ b/neutron/plugins/ml2/drivers/cisco/nexus_snippets.py @@ -0,0 +1,200 @@ +# Copyright 2013 OpenStack Foundation. +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +""" +Cisco Nexus-OS XML-based configuration snippets. +""" + +import logging + + +LOG = logging.getLogger(__name__) + + +# The following are standard strings, messages used to communicate with Nexus. +EXEC_CONF_SNIPPET = """ + + + <__XML__MODE__exec_configure>%s + + + +""" + +CMD_VLAN_CONF_SNIPPET = """ + + + <__XML__PARAM_value>%s + <__XML__MODE_vlan> + + %s + + + + +""" + +CMD_VLAN_ACTIVE_SNIPPET = """ + + + <__XML__PARAM_value>%s + <__XML__MODE_vlan> + + active + + + + +""" + +CMD_VLAN_NO_SHUTDOWN_SNIPPET = """ + + + <__XML__PARAM_value>%s + <__XML__MODE_vlan> + + + + + + +""" + +CMD_NO_VLAN_CONF_SNIPPET = """ + + + + <__XML__PARAM_value>%s + + + +""" + +CMD_INT_VLAN_HEADER = """ + + + %s + <__XML__MODE_if-ethernet-switch> + + + + """ + +CMD_VLAN_ID = """ + %s""" + +CMD_VLAN_ADD_ID = """ + %s + """ % CMD_VLAN_ID + +CMD_INT_VLAN_TRAILER = """ + + + + + + + +""" + +CMD_INT_VLAN_SNIPPET = (CMD_INT_VLAN_HEADER + + CMD_VLAN_ID + + CMD_INT_VLAN_TRAILER) + +CMD_INT_VLAN_ADD_SNIPPET = (CMD_INT_VLAN_HEADER + + CMD_VLAN_ADD_ID + + CMD_INT_VLAN_TRAILER) + +CMD_PORT_TRUNK = """ + + + %s + <__XML__MODE_if-ethernet-switch> + + + + + + + + + + +""" + +CMD_NO_SWITCHPORT = """ + + + %s + <__XML__MODE_if-ethernet-switch> + + + + + + + +""" + +CMD_NO_VLAN_INT_SNIPPET = """ + + + %s + <__XML__MODE_if-ethernet-switch> + + + + + + + %s + + + + + + + + +""" + +CMD_VLAN_SVI_SNIPPET = """ + + + %s + <__XML__MODE_vlan> + + + + +
+
%s
+
+
+ +
+
+""" + +CMD_NO_VLAN_SVI_SNIPPET = """ + + + + %s + + + +""" diff --git a/neutron/tests/unit/ml2/drivers/test_cisco_mech.py b/neutron/tests/unit/ml2/drivers/test_cisco_mech.py new file mode 100644 index 0000000000..ca20ec9f22 --- /dev/null +++ b/neutron/tests/unit/ml2/drivers/test_cisco_mech.py @@ -0,0 +1,539 @@ +# Copyright (c) 2012 OpenStack Foundation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import contextlib +import mock + +import webob.exc as wexc + +from neutron.api.v2 import base +from neutron import context +from neutron.manager import NeutronManager +from neutron.openstack.common import log as logging +from neutron.plugins.ml2 import config as ml2_config +from neutron.plugins.ml2.drivers.cisco import config as cisco_config +from neutron.plugins.ml2.drivers.cisco import exceptions as c_exc +from neutron.plugins.ml2.drivers.cisco import mech_cisco_nexus +from neutron.plugins.ml2.drivers.cisco import nexus_db_v2 +from neutron.plugins.ml2.drivers.cisco import nexus_network_driver +from neutron.plugins.ml2.drivers import type_vlan as vlan_config +from neutron.tests.unit import test_db_plugin + +LOG = logging.getLogger(__name__) +ML2_PLUGIN = 'neutron.plugins.ml2.plugin.Ml2Plugin' +PHYS_NET = 'physnet1' +COMP_HOST_NAME = 'testhost' +VLAN_START = 1000 +VLAN_END = 1100 +NEXUS_IP_ADDR = '1.1.1.1' +CIDR_1 = '10.0.0.0/24' +CIDR_2 = '10.0.1.0/24' +DEVICE_ID_1 = '11111111-1111-1111-1111-111111111111' +DEVICE_ID_2 = '22222222-2222-2222-2222-222222222222' + + +class CiscoML2MechanismTestCase(test_db_plugin.NeutronDbPluginV2TestCase): + + def setUp(self): + """Configure for end-to-end neutron testing using a mock ncclient. + + This setup includes: + - Configure the ML2 plugin to use VLANs in the range of 1000-1100. + - Configure the Cisco mechanism driver to use an imaginary switch + at NEXUS_IP_ADDR. + - Create a mock NETCONF client (ncclient) for the Cisco mechanism + driver + + """ + self.addCleanup(mock.patch.stopall) + + # Configure the ML2 mechanism drivers and network types + ml2_opts = { + 'mechanism_drivers': ['cisco_nexus', 'logger', 'test'], + 'tenant_network_types': ['vlan'], + } + for opt, val in ml2_opts.items(): + ml2_config.cfg.CONF.set_override(opt, val, 'ml2') + self.addCleanup(ml2_config.cfg.CONF.reset) + + # Configure the ML2 VLAN parameters + phys_vrange = ':'.join([PHYS_NET, str(VLAN_START), str(VLAN_END)]) + vlan_config.cfg.CONF.set_override('network_vlan_ranges', + [phys_vrange], + 'ml2_type_vlan') + self.addCleanup(vlan_config.cfg.CONF.reset) + + # Configure the Cisco Nexus mechanism driver + nexus_config = { + (NEXUS_IP_ADDR, 'username'): 'admin', + (NEXUS_IP_ADDR, 'password'): 'mySecretPassword', + (NEXUS_IP_ADDR, 'ssh_port'): 22, + (NEXUS_IP_ADDR, COMP_HOST_NAME): '1/1'} + nexus_patch = mock.patch.dict( + cisco_config.ML2MechCiscoConfig.nexus_dict, + nexus_config) + nexus_patch.start() + self.addCleanup(nexus_patch.stop) + + # The NETCONF client module is not included in the DevStack + # distribution, so mock this module for unit testing. + self.mock_ncclient = mock.Mock() + mock.patch.object(nexus_network_driver.CiscoNexusDriver, + '_import_ncclient', + return_value=self.mock_ncclient).start() + + # Use COMP_HOST_NAME as the compute node host name. + mock_host = mock.patch.object( + mech_cisco_nexus.CiscoNexusMechanismDriver, + '_get_instance_host').start() + mock_host.return_value = COMP_HOST_NAME + + super(CiscoML2MechanismTestCase, self).setUp(ML2_PLUGIN) + + self.port_create_status = 'DOWN' + + @contextlib.contextmanager + def _patch_ncclient(self, attr, value): + """Configure an attribute on the mock ncclient module. + + This method can be used to inject errors by setting a side effect + or a return value for an ncclient method. + + :param attr: ncclient attribute (typically method) to be configured. + :param value: Value to be configured on the attribute. + + """ + # Configure attribute. + config = {attr: value} + self.mock_ncclient.configure_mock(**config) + # Continue testing + yield + # Unconfigure attribute + config = {attr: None} + self.mock_ncclient.configure_mock(**config) + + def _is_in_last_nexus_cfg(self, words): + """Confirm last config sent to Nexus contains specified keywords.""" + last_cfg = (self.mock_ncclient.connect.return_value. + edit_config.mock_calls[-1][2]['config']) + return all(word in last_cfg for word in words) + + +class TestCiscoBasicGet(CiscoML2MechanismTestCase, + test_db_plugin.TestBasicGet): + + pass + + +class TestCiscoV2HTTPResponse(CiscoML2MechanismTestCase, + test_db_plugin.TestV2HTTPResponse): + + pass + + +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): + """Create network, subnet, and port resources for test cases. + + Create a network, subnet, and 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 + + """ + 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) + port = self.deserialize(self.fmt, res) + try: + yield res + finally: + if do_delete: + self._delete('ports', port['port']['id']) + + def _assertExpectedHTTP(self, status, exc): + """Confirm that an HTTP status corresponds to an expected exception. + + 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 + + """ + if exc in base.FAULT_MAP: + expected_http = base.FAULT_MAP[exc].code + else: + expected_http = wexc.HTTPInternalServerError.code + self.assertEqual(status, expected_http) + + def test_create_ports_bulk_emulated_plugin_failure(self): + real_has_attr = hasattr + + #ensures the API chooses the emulation code path + def fakehasattr(item, attr): + if attr.endswith('__native_bulk_support'): + return False + return real_has_attr(item, attr) + + with mock.patch('__builtin__.hasattr', + new=fakehasattr): + plugin_obj = NeutronManager.get_plugin() + orig = plugin_obj.create_port + with mock.patch.object(plugin_obj, + 'create_port') as patched_plugin: + + def side_effect(*args, **kwargs): + return self._do_side_effect(patched_plugin, orig, + *args, **kwargs) + + patched_plugin.side_effect = side_effect + with self.network() as net: + res = self._create_port_bulk(self.fmt, 2, + net['network']['id'], + 'test', + True) + # Expect an internal server error as we injected a fault + self._validate_behavior_on_bulk_failure( + res, + 'ports', + wexc.HTTPInternalServerError.code) + + def test_create_ports_bulk_native(self): + if self._skip_native_bulk: + self.skipTest("Plugin does not support native bulk port create") + + def test_create_ports_bulk_emulated(self): + if self._skip_native_bulk: + self.skipTest("Plugin does not support native bulk port create") + + def test_create_ports_bulk_native_plugin_failure(self): + if self._skip_native_bulk: + self.skipTest("Plugin does not support native bulk port create") + ctx = context.get_admin_context() + with self.network() as net: + plugin_obj = NeutronManager.get_plugin() + orig = plugin_obj.create_port + with mock.patch.object(plugin_obj, + 'create_port') as patched_plugin: + + def side_effect(*args, **kwargs): + return self._do_side_effect(patched_plugin, orig, + *args, **kwargs) + + patched_plugin.side_effect = side_effect + res = self._create_port_bulk(self.fmt, 2, net['network']['id'], + 'test', True, context=ctx) + # We expect an internal server error as we injected a fault + self._validate_behavior_on_bulk_failure( + res, + 'ports', + wexc.HTTPInternalServerError.code) + + def test_nexus_enable_vlan_cmd(self): + """Verify the syntax of the command to enable a vlan on an intf. + + Confirm that for the first VLAN configured on a Nexus interface, + the command string sent to the switch does not contain the + keyword 'add'. + + Confirm that for the second VLAN configured on a Nexus interface, + the command staring sent to the switch contains the keyword 'add'. + + """ + with self._create_port_res(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): + self.assertTrue( + self._is_in_last_nexus_cfg(['allowed', 'vlan', 'add'])) + + def test_nexus_connect_fail(self): + """Test failure to connect to a Nexus switch. + + While creating a network, subnet, and port, simulate a connection + failure to a nexus switch. Confirm that the expected HTTP code + is returned for the create port operation. + + """ + 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) + + def test_nexus_config_fail(self): + """Test a Nexus switch configuration failure. + + While creating a network, subnet, and port, simulate a nexus + switch configuration error. Confirm that the expected HTTP code + is returned for the create port operation. + + """ + 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) + + def test_nexus_extended_vlan_range_failure(self): + """Test that extended VLAN range config errors are ignored. + + Some versions of Nexus switch do not allow state changes for + the extended VLAN range (1006-4094), but these errors can be + ignored (default values are appropriate). Test that such errors + are ignored by the Nexus plugin. + + """ + def mock_edit_config_a(target, config): + if all(word in config for word in ['state', 'active']): + raise Exception("Can't modify state for extended") + + 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) + + def mock_edit_config_b(target, config): + if all(word in config for word in ['no', 'shutdown']): + raise Exception("Command is only allowed on VLAN") + + 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) + + def test_nexus_vlan_config_rollback(self): + """Test rollback following Nexus VLAN state config failure. + + Test that the Cisco Nexus plugin correctly deletes the VLAN + on the Nexus switch when the 'state active' command fails (for + a reason other than state configuration change is rejected + for the extended VLAN range). + + """ + def mock_edit_config(target, config): + if all(word in config for word in ['state', 'active']): + raise ValueError + 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: + # Confirm that the last configuration sent to the Nexus + # switch was deletion of the VLAN. + self.assertTrue( + self._is_in_last_nexus_cfg(['', '']) + ) + self._assertExpectedHTTP(res.status_int, + c_exc.NexusConfigFailed) + + def test_nexus_host_not_configured(self): + """Test handling of a NexusComputeHostNotConfigured exception. + + Test the Cisco NexusComputeHostNotConfigured exception by using + 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) + + def test_nexus_bind_fail_rollback(self): + """Test for proper rollback following add Nexus DB binding failure. + + Test that the Cisco Nexus plugin correctly rolls back the vlan + configuration on the Nexus switch when add_nexusport_binding fails + within the plugin's create_port() method. + + """ + with mock.patch.object(nexus_db_v2, + 'add_nexusport_binding', + side_effect=KeyError): + with self._create_port_res(do_delete=False) as res: + # 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(['', '']) + ) + self._assertExpectedHTTP(res.status_int, KeyError) + + def test_nexus_delete_port_rollback(self): + """Test for proper rollback for nexus plugin delete port failure. + + Test for rollback (i.e. restoration) of a VLAN entry in the + nexus database whenever the nexus plugin fails to reconfigure the + nexus switch during a delete_port operation. + + """ + with self._create_port_res() as res: + + port = self.deserialize(self.fmt, res) + + # 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, + NEXUS_IP_ADDR) + self.assertEqual(len(start_rows), 1) + + # Simulate a Nexus switch configuration error during + # port deletion. + with self._patch_ncclient( + 'connect.return_value.edit_config.side_effect', + AttributeError): + self._delete('ports', port['port']['id'], + wexc.HTTPInternalServerError.code) + + # Confirm that the binding has been restored (rolled back). + end_rows = nexus_db_v2.get_nexusvlan_binding(VLAN_START, + NEXUS_IP_ADDR) + self.assertEqual(start_rows, end_rows) + + +class TestCiscoNetworksV2(CiscoML2MechanismTestCase, + test_db_plugin.TestNetworksV2): + + def test_create_networks_bulk_emulated_plugin_failure(self): + real_has_attr = hasattr + + def fakehasattr(item, attr): + if attr.endswith('__native_bulk_support'): + return False + return real_has_attr(item, attr) + + plugin_obj = NeutronManager.get_plugin() + orig = plugin_obj.create_network + #ensures the API choose the emulation code path + with mock.patch('__builtin__.hasattr', + new=fakehasattr): + with mock.patch.object(plugin_obj, + 'create_network') as patched_plugin: + def side_effect(*args, **kwargs): + return self._do_side_effect(patched_plugin, orig, + *args, **kwargs) + patched_plugin.side_effect = side_effect + res = self._create_network_bulk(self.fmt, 2, 'test', True) + LOG.debug("response is %s" % res) + # We expect an internal server error as we injected a fault + self._validate_behavior_on_bulk_failure( + res, + 'networks', + wexc.HTTPInternalServerError.code) + + def test_create_networks_bulk_native_plugin_failure(self): + if self._skip_native_bulk: + self.skipTest("Plugin does not support native bulk network create") + plugin_obj = NeutronManager.get_plugin() + orig = plugin_obj.create_network + with mock.patch.object(plugin_obj, + 'create_network') as patched_plugin: + + def side_effect(*args, **kwargs): + return self._do_side_effect(patched_plugin, orig, + *args, **kwargs) + + patched_plugin.side_effect = side_effect + res = self._create_network_bulk(self.fmt, 2, 'test', True) + # We expect an internal server error as we injected a fault + self._validate_behavior_on_bulk_failure( + res, + 'networks', + wexc.HTTPInternalServerError.code) + + +class TestCiscoSubnetsV2(CiscoML2MechanismTestCase, + test_db_plugin.TestSubnetsV2): + + def test_create_subnets_bulk_emulated_plugin_failure(self): + real_has_attr = hasattr + + #ensures the API choose the emulation code path + def fakehasattr(item, attr): + if attr.endswith('__native_bulk_support'): + return False + return real_has_attr(item, attr) + + with mock.patch('__builtin__.hasattr', + new=fakehasattr): + plugin_obj = NeutronManager.get_plugin() + orig = plugin_obj.create_subnet + with mock.patch.object(plugin_obj, + 'create_subnet') as patched_plugin: + + def side_effect(*args, **kwargs): + self._do_side_effect(patched_plugin, orig, + *args, **kwargs) + + patched_plugin.side_effect = side_effect + with self.network() as net: + res = self._create_subnet_bulk(self.fmt, 2, + net['network']['id'], + 'test') + # We expect an internal server error as we injected a fault + self._validate_behavior_on_bulk_failure( + res, + 'subnets', + wexc.HTTPInternalServerError.code) + + def test_create_subnets_bulk_native_plugin_failure(self): + if self._skip_native_bulk: + self.skipTest("Plugin does not support native bulk subnet create") + plugin_obj = NeutronManager.get_plugin() + orig = plugin_obj.create_subnet + with mock.patch.object(plugin_obj, + 'create_subnet') as patched_plugin: + def side_effect(*args, **kwargs): + return self._do_side_effect(patched_plugin, orig, + *args, **kwargs) + + patched_plugin.side_effect = side_effect + with self.network() as net: + res = self._create_subnet_bulk(self.fmt, 2, + net['network']['id'], + 'test') + + # We expect an internal server error as we injected a fault + self._validate_behavior_on_bulk_failure( + res, + 'subnets', + wexc.HTTPInternalServerError.code) + + +class TestCiscoPortsV2XML(TestCiscoPortsV2): + fmt = 'xml' + + +class TestCiscoNetworksV2XML(TestCiscoNetworksV2): + fmt = 'xml' + + +class TestCiscoSubnetsV2XML(TestCiscoSubnetsV2): + fmt = 'xml' diff --git a/setup.cfg b/setup.cfg index 5bc7f36c59..7c5a634277 100644 --- a/setup.cfg +++ b/setup.cfg @@ -49,8 +49,10 @@ data_files = etc/neutron/plugins/linuxbridge = etc/neutron/plugins/linuxbridge/linuxbridge_conf.ini etc/neutron/plugins/metaplugin = etc/neutron/plugins/metaplugin/metaplugin.ini etc/neutron/plugins/midonet = etc/neutron/plugins/midonet/midonet.ini - etc/neutron/plugins/ml2 = etc/neutron/plugins/ml2/ml2_conf.ini + etc/neutron/plugins/ml2 = + etc/neutron/plugins/ml2/ml2_conf.ini etc/neutron/plugins/ml2/ml2_conf_arista.ini + etc/neutron/plugins/ml2/ml2_conf_cisco.ini etc/neutron/plugins/mlnx = etc/neutron/plugins/mlnx/mlnx_conf.ini etc/neutron/plugins/nec = etc/neutron/plugins/nec/nec.ini etc/neutron/plugins/nicira = etc/neutron/plugins/nicira/nvp.ini @@ -124,6 +126,7 @@ neutron.ml2.mechanism_drivers = hyperv = neutron.plugins.ml2.drivers.mech_hyperv:HypervMechanismDriver ncs = neutron.plugins.ml2.drivers.mechanism_ncs:NCSMechanismDriver arista = neutron.plugins.ml2.drivers.mech_arista.mechanism_arista:AristaDriver + cisco_nexus = neutron.plugins.ml2.drivers.cisco.mech_cisco_nexus:CiscoNexusMechanismDriver [build_sphinx] all_files = 1