--- /dev/null
+[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:<IP address of switch>]
+# <hostname>=<port> (1)
+# ssh_port=<ssh port> (2)
+# username=<credential username> (3)
+# password=<credential 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
--- /dev/null
+# 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')
--- /dev/null
+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
--- /dev/null
+# 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.
--- /dev/null
+# 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:
+# {('<device ipaddr>', '<keyword>'): '<value>', ...}
+#
+# 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]
--- /dev/null
+# 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'
--- /dev/null
+# 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)
--- /dev/null
+# 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.')
--- /dev/null
+# 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)
--- /dev/null
+# 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)
--- /dev/null
+# 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))
--- /dev/null
+# 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)
--- /dev/null
+# 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 ("<NexusPortBinding(%s,%s,%s,%s)>" %
+ (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
+ )
--- /dev/null
+# 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)
--- /dev/null
+# 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 = """
+ <config xmlns:xc="urn:ietf:params:xml:ns:netconf:base:1.0">
+ <configure>
+ <__XML__MODE__exec_configure>%s
+ </__XML__MODE__exec_configure>
+ </configure>
+ </config>
+"""
+
+CMD_VLAN_CONF_SNIPPET = """
+ <vlan>
+ <vlan-id-create-delete>
+ <__XML__PARAM_value>%s</__XML__PARAM_value>
+ <__XML__MODE_vlan>
+ <name>
+ <vlan-name>%s</vlan-name>
+ </name>
+ </__XML__MODE_vlan>
+ </vlan-id-create-delete>
+ </vlan>
+"""
+
+CMD_VLAN_ACTIVE_SNIPPET = """
+ <vlan>
+ <vlan-id-create-delete>
+ <__XML__PARAM_value>%s</__XML__PARAM_value>
+ <__XML__MODE_vlan>
+ <state>
+ <vstate>active</vstate>
+ </state>
+ </__XML__MODE_vlan>
+ </vlan-id-create-delete>
+ </vlan>
+"""
+
+CMD_VLAN_NO_SHUTDOWN_SNIPPET = """
+ <vlan>
+ <vlan-id-create-delete>
+ <__XML__PARAM_value>%s</__XML__PARAM_value>
+ <__XML__MODE_vlan>
+ <no>
+ <shutdown/>
+ </no>
+ </__XML__MODE_vlan>
+ </vlan-id-create-delete>
+ </vlan>
+"""
+
+CMD_NO_VLAN_CONF_SNIPPET = """
+ <no>
+ <vlan>
+ <vlan-id-create-delete>
+ <__XML__PARAM_value>%s</__XML__PARAM_value>
+ </vlan-id-create-delete>
+ </vlan>
+ </no>
+"""
+
+CMD_INT_VLAN_HEADER = """
+ <interface>
+ <ethernet>
+ <interface>%s</interface>
+ <__XML__MODE_if-ethernet-switch>
+ <switchport>
+ <trunk>
+ <allowed>
+ <vlan>"""
+
+CMD_VLAN_ID = """
+ <vlan_id>%s</vlan_id>"""
+
+CMD_VLAN_ADD_ID = """
+ <add>%s
+ </add>""" % CMD_VLAN_ID
+
+CMD_INT_VLAN_TRAILER = """
+ </vlan>
+ </allowed>
+ </trunk>
+ </switchport>
+ </__XML__MODE_if-ethernet-switch>
+ </ethernet>
+ </interface>
+"""
+
+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 = """
+ <interface>
+ <ethernet>
+ <interface>%s</interface>
+ <__XML__MODE_if-ethernet-switch>
+ <switchport></switchport>
+ <switchport>
+ <mode>
+ <trunk>
+ </trunk>
+ </mode>
+ </switchport>
+ </__XML__MODE_if-ethernet-switch>
+ </ethernet>
+ </interface>
+"""
+
+CMD_NO_SWITCHPORT = """
+ <interface>
+ <ethernet>
+ <interface>%s</interface>
+ <__XML__MODE_if-ethernet-switch>
+ <no>
+ <switchport>
+ </switchport>
+ </no>
+ </__XML__MODE_if-ethernet-switch>
+ </ethernet>
+ </interface>
+"""
+
+CMD_NO_VLAN_INT_SNIPPET = """
+ <interface>
+ <ethernet>
+ <interface>%s</interface>
+ <__XML__MODE_if-ethernet-switch>
+ <switchport></switchport>
+ <switchport>
+ <trunk>
+ <allowed>
+ <vlan>
+ <remove>
+ <vlan>%s</vlan>
+ </remove>
+ </vlan>
+ </allowed>
+ </trunk>
+ </switchport>
+ </__XML__MODE_if-ethernet-switch>
+ </ethernet>
+ </interface>
+"""
+
+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>
+"""
--- /dev/null
+# 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(['<no>', '<vlan>'])
+ )
+ 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(['<vlan>', '<remove>'])
+ )
+ self._assertExpectedHTTP(res.status_int, KeyError)
+
+ def test_nexus_delete_port_rollback(self):
+ """Test for proper rollback for nexus plugin delete port failure.
+
+ 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'
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
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