ML2 Mechanism Driver for Cisco Nexus

Port of the quantum/plugin/cisco/nexus plugin to run under the Modular
Layer 2 (ML2) infrastructure as defined in
https://blueprints.launchpad.net/quantum/+spec/ml2-mechanism-drivers

Implements blueprint ml2-md-cisco-nexus

Change-Id: Ifdd03bec554a08266de859387f1901858a3be4a1
This commit is contained in:
Rich Curran 2013-08-21 17:43:12 -04:00
parent f9e80c2c38
commit 3c164b9988
17 changed files with 1916 additions and 1 deletions

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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.

View File

@ -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]

View File

@ -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'

View File

@ -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)

View File

@ -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.')

View File

@ -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)

View File

@ -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)

View File

@ -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))

View File

@ -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)

View File

@ -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
)

View File

@ -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)

View File

@ -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>
"""

View File

@ -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'

View File

@ -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