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:
parent
f9e80c2c38
commit
3c164b9988
36
etc/neutron/plugins/ml2/ml2_conf_cisco.ini
Normal file
36
etc/neutron/plugins/ml2/ml2_conf_cisco.ini
Normal 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
|
@ -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')
|
19
neutron/plugins/ml2/drivers/cisco/README
Normal file
19
neutron/plugins/ml2/drivers/cisco/README
Normal 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
|
14
neutron/plugins/ml2/drivers/cisco/__init__.py
Normal file
14
neutron/plugins/ml2/drivers/cisco/__init__.py
Normal 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.
|
63
neutron/plugins/ml2/drivers/cisco/config.py
Normal file
63
neutron/plugins/ml2/drivers/cisco/config.py
Normal 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]
|
48
neutron/plugins/ml2/drivers/cisco/constants.py
Normal file
48
neutron/plugins/ml2/drivers/cisco/constants.py
Normal 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'
|
71
neutron/plugins/ml2/drivers/cisco/credentials_v2.py
Normal file
71
neutron/plugins/ml2/drivers/cisco/credentials_v2.py
Normal 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)
|
78
neutron/plugins/ml2/drivers/cisco/exceptions.py
Normal file
78
neutron/plugins/ml2/drivers/cisco/exceptions.py
Normal 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.')
|
221
neutron/plugins/ml2/drivers/cisco/mech_cisco_nexus.py
Normal file
221
neutron/plugins/ml2/drivers/cisco/mech_cisco_nexus.py
Normal 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)
|
115
neutron/plugins/ml2/drivers/cisco/network_db_v2.py
Normal file
115
neutron/plugins/ml2/drivers/cisco/network_db_v2.py
Normal 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)
|
31
neutron/plugins/ml2/drivers/cisco/network_models_v2.py
Normal file
31
neutron/plugins/ml2/drivers/cisco/network_models_v2.py
Normal 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))
|
149
neutron/plugins/ml2/drivers/cisco/nexus_db_v2.py
Normal file
149
neutron/plugins/ml2/drivers/cisco/nexus_db_v2.py
Normal 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)
|
45
neutron/plugins/ml2/drivers/cisco/nexus_models_v2.py
Normal file
45
neutron/plugins/ml2/drivers/cisco/nexus_models_v2.py
Normal 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
|
||||
)
|
215
neutron/plugins/ml2/drivers/cisco/nexus_network_driver.py
Normal file
215
neutron/plugins/ml2/drivers/cisco/nexus_network_driver.py
Normal 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)
|
200
neutron/plugins/ml2/drivers/cisco/nexus_snippets.py
Normal file
200
neutron/plugins/ml2/drivers/cisco/nexus_snippets.py
Normal 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>
|
||||
"""
|
539
neutron/tests/unit/ml2/drivers/test_cisco_mech.py
Normal file
539
neutron/tests/unit/ml2/drivers/test_cisco_mech.py
Normal 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'
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user