]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
ML2 Mechanism Driver for Cisco Nexus
authorRich Curran <rcurran@cisco.com>
Wed, 21 Aug 2013 21:43:12 +0000 (17:43 -0400)
committerRich Curran <rcurran@cisco.com>
Wed, 4 Sep 2013 18:10:33 +0000 (14:10 -0400)
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

17 files changed:
etc/neutron/plugins/ml2/ml2_conf_cisco.ini [new file with mode: 0644]
neutron/db/migration/alembic_migrations/versions/51b4de912379_cisco_nexus_ml2_mech.py [new file with mode: 0755]
neutron/plugins/ml2/drivers/cisco/README [new file with mode: 0644]
neutron/plugins/ml2/drivers/cisco/__init__.py [new file with mode: 0644]
neutron/plugins/ml2/drivers/cisco/config.py [new file with mode: 0644]
neutron/plugins/ml2/drivers/cisco/constants.py [new file with mode: 0644]
neutron/plugins/ml2/drivers/cisco/credentials_v2.py [new file with mode: 0644]
neutron/plugins/ml2/drivers/cisco/exceptions.py [new file with mode: 0644]
neutron/plugins/ml2/drivers/cisco/mech_cisco_nexus.py [new file with mode: 0644]
neutron/plugins/ml2/drivers/cisco/network_db_v2.py [new file with mode: 0644]
neutron/plugins/ml2/drivers/cisco/network_models_v2.py [new file with mode: 0644]
neutron/plugins/ml2/drivers/cisco/nexus_db_v2.py [new file with mode: 0644]
neutron/plugins/ml2/drivers/cisco/nexus_models_v2.py [new file with mode: 0644]
neutron/plugins/ml2/drivers/cisco/nexus_network_driver.py [new file with mode: 0644]
neutron/plugins/ml2/drivers/cisco/nexus_snippets.py [new file with mode: 0644]
neutron/tests/unit/ml2/drivers/test_cisco_mech.py [new file with mode: 0644]
setup.cfg

diff --git a/etc/neutron/plugins/ml2/ml2_conf_cisco.ini b/etc/neutron/plugins/ml2/ml2_conf_cisco.ini
new file mode 100644 (file)
index 0000000..69d5312
--- /dev/null
@@ -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:<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
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 (executable)
index 0000000..f6038fd
--- /dev/null
@@ -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 (file)
index 0000000..5960e95
--- /dev/null
@@ -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 (file)
index 0000000..788cea1
--- /dev/null
@@ -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 (file)
index 0000000..cf47dc4
--- /dev/null
@@ -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:
+# {('<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]
diff --git a/neutron/plugins/ml2/drivers/cisco/constants.py b/neutron/plugins/ml2/drivers/cisco/constants.py
new file mode 100644 (file)
index 0000000..df78eee
--- /dev/null
@@ -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 (file)
index 0000000..ea172aa
--- /dev/null
@@ -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 (file)
index 0000000..c431f9b
--- /dev/null
@@ -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 (file)
index 0000000..95addc1
--- /dev/null
@@ -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 (file)
index 0000000..46ed79c
--- /dev/null
@@ -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 (file)
index 0000000..8725edb
--- /dev/null
@@ -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 (file)
index 0000000..1fac6b6
--- /dev/null
@@ -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 (file)
index 0000000..ce7c416
--- /dev/null
@@ -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 ("<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
+        )
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 (file)
index 0000000..936c2bb
--- /dev/null
@@ -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 (file)
index 0000000..b30c7e6
--- /dev/null
@@ -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 = """
+      <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>
+"""
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 (file)
index 0000000..ca20ec9
--- /dev/null
@@ -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(['<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'
index 5bc7f36c591f3927eaa14581f4de42cd8bebf5d4..7c5a63427725ab6a615b83e5ac8a77f3b82b6e5a 100644 (file)
--- 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