Cisco DFA ML2 Mechanism Driver

Part 1:
This commit contains changes to support ML2 mechanism driver for Cisco
DFA. For more details please see the blueprint which has more description and
link to document with requirements.

Part 2: Changes in OVS neutron agent.
(http://review.openstack.org/110065)

Part 3: DFA extension driver.
(http://review.openstack.org/111761)

Part 4: DFA config profile service plugin.
(http://review.openstack.org/111863)

Change-Id: Ib53b6705948e1ed75059b85d8809562d9bb63f65
Partially Implements: blueprint ml2-mechanism-driver-for-cisco-dfa
This commit is contained in:
Nader Lahouti 2014-06-27 18:09:19 -07:00
parent 2ae77d0329
commit 3680fd61c9
21 changed files with 1855 additions and 1 deletions

View File

@ -20,6 +20,7 @@
# Example: mechanism_drivers = cisco,logger
# Example: mechanism_drivers = openvswitch,brocade
# Example: mechanism_drivers = linuxbridge,brocade
# Example: mechanism_drivers = openvswitch,cisco_dfa
[ml2_type_flat]
# (ListOpt) List of physical_network names with which flat networks

View File

@ -112,3 +112,17 @@
# encap=vlan-100
# cidr_exposed=10.10.40.2/16
# gateway_ip=10.10.40.1
[ml2_cisco_dfa]
# (StrOpt) IP address of Cisco DCNM (Data Center Network Manager).
# dcnm_ip = 1.1.1.1
#
# (StrOpt) User login name for DCNM.
# dcnm_user = username
#
# (StrOpt) Login password for DCNM.
# dcnm_password = password
#
# (StrOpt) Gateway MAC address when forwarding mode in created config profile
# is proxy mode.
# gateway_mac = 00:01:02:03:04:05

View File

@ -0,0 +1,58 @@
# Copyright 2014 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 DFA Mechanism Driver
Revision ID: 469426cd2173
Revises: 32f3915891fd
Create Date: 2014-06-28 01:13:04.152945
"""
# revision identifiers, used by Alembic.
revision = '469426cd2173'
down_revision = '32f3915891fd'
from alembic import op
import sqlalchemy as sa
def upgrade(active_plugins=None, options=None):
op.create_table(
'cisco_dfa_config_profiles',
sa.Column('id', sa.String(36)),
sa.Column('name', sa.String(255)),
sa.Column('forwarding_mode', sa.String(32)),
sa.PrimaryKeyConstraint('id'))
op.create_table(
'cisco_dfa_config_profile_bindings',
sa.Column('network_id', sa.String(36)),
sa.Column('cfg_profile_id', sa.String(36)),
sa.ForeignKeyConstraint(['network_id'], ['networks.id'],
ondelete='CASCADE'),
sa.PrimaryKeyConstraint('network_id', 'cfg_profile_id'))
op.create_table(
'cisco_dfa_project_cache',
sa.Column('project_id', sa.String(36)),
sa.Column('project_name', sa.String(255)),
sa.PrimaryKeyConstraint('project_id'))
def downgrade(active_plugins=None, options=None):
op.drop_table('cisco_dfa_project_cache')
op.drop_table('cisco_dfa_config_profile_bindings')
op.drop_table('cisco_dfa_config_profiles')

View File

@ -1 +1 @@
32f3915891fd
469426cd2173

View File

@ -59,6 +59,7 @@ from neutron.plugins.ml2.drivers.arista import db # noqa
from neutron.plugins.ml2.drivers.brocade.db import ( # noqa
models as ml2_brocade_models)
from neutron.plugins.ml2.drivers.cisco.apic import apic_model # noqa
from neutron.plugins.ml2.drivers.cisco.dfa import dfa_models_v2 # noqa
from neutron.plugins.ml2.drivers.cisco.nexus import ( # noqa
nexus_models_v2 as ml2_nexus_models_v2)
from neutron.plugins.ml2.drivers import type_flat # noqa

View File

@ -0,0 +1,109 @@
# Copyright 2014 Cisco Systems, Inc.
# 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 models_v2
from neutron.plugins.ml2.drivers.cisco.dfa import constants as dfac
from neutron.plugins.ml2.drivers.cisco.dfa import dfa_exceptions as dexc
from neutron.plugins.ml2.drivers.cisco.dfa import dfa_models_v2
def get_network_profile_binding(session, net_id):
"""Retrieve network and config profile binding."""
try:
return (session.query(dfa_models_v2.ConfigProfileBinding).
filter_by(network_id=net_id).one())
except (exc.NoResultFound, exc.MultipleResultsFound):
pass
def add_dfa_cfg_profile_binding(session, netid, cpid):
"""Add new entry to the config profile binding database."""
try:
if cpid == dfac.DEFAULT_CFG_PROFILE_ID:
# The config profile is not provided when creating network.
# Use 'defaultNetworkL2Profile' as default config profile.
cfgp_name = 'defaultNetworkL2Profile'
cfgp_entry = (session.query(dfa_models_v2.ConfigProfile).
filter_by(name=cfgp_name).one())
cpid = cfgp_entry.id
binding = dfa_models_v2.ConfigProfileBinding(network_id=netid,
cfg_profile_id=cpid)
session.add(binding)
except (exc.NoResultFound, exc.MultipleResultsFound):
raise dexc.ConfigProfileNotFound(network_id=netid)
def get_network_entry(session, netid):
"""Retrieve network information."""
try:
return (session.query(models_v2.Network).
filter_by(id=netid).one())
except (exc.NoResultFound, exc.MultipleResultsFound):
raise dexc.NetworkNotFound(network_id=netid)
def get_config_profile_name(db_session, netid):
"""Retrieve configuration profile for a network."""
try:
cfgpobj = dfa_models_v2.ConfigProfileBinding
cfgp = db_session.query(cfgpobj).filter_by(network_id=netid).one()
cfgid = cfgp.cfg_profile_id
except (exc.NoResultFound, exc.MultipleResultsFound):
raise dexc.ConfigProfileNotFound(network_id=netid)
try:
cfgp_entry = db_session.query(
dfa_models_v2.ConfigProfile).filter_by(id=cfgid).one()
except (exc.NoResultFound, exc.MultipleResultsFound):
raise dexc.ConfigProfileIdNotFound(profile_id=cfgid)
return cfgp_entry.name
def get_config_profile_fwd_mode(db_session, network_id):
"""Retrieve configuration profile for a network."""
try:
cfgp = (db_session.query(dfa_models_v2.ConfigProfileBinding).
filter_by(network_id=network_id).one())
cfgid = cfgp.cfg_profile_id
except (exc.NoResultFound, exc.MultipleResultsFound):
raise dexc.ConfigProfileNotFound(network_id=network_id)
try:
cfgp_entry = db_session.query(
dfa_models_v2.ConfigProfile).filter_by(id=cfgid).one()
return cfgp_entry.forwarding_mode
except (exc.NoResultFound, exc.MultipleResultsFound):
raise dexc.ConfigProfileIdNotFound(profile_id=cfgid)
def delete_dfa_cfg_profile_binding(db_session, network_id):
"""Delete an entry from the config profile binding database."""
try:
with db_session.begin(subtransactions=True):
entry = (db_session.query(dfa_models_v2.ConfigProfileBinding).
filter_by(network_id=network_id).one())
db_session.delete(entry)
except (exc.NoResultFound, exc.MultipleResultsFound):
raise dexc.ConfigProfileNotFound(network_id=network_id)

View File

@ -0,0 +1,303 @@
# Copyright 2014 Cisco Systems, Inc.
# 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
import requests
from neutron.openstack.common import jsonutils
from neutron.openstack.common import log as logging
from neutron.plugins.ml2.drivers.cisco.dfa import dfa_exceptions as dexc
LOG = logging.getLogger(__name__)
class DFARESTClient(object):
"""DFA client class that provides APIs to interact with DCNM."""
def __init__(self):
self._ip = cfg.CONF.ml2_cisco_dfa.dcnm_ip
self._user = cfg.CONF.ml2_cisco_dfa.dcnm_user
self._pwd = cfg.CONF.ml2_cisco_dfa.dcnm_password
if (not self._ip) or (not self._user) or (not self._pwd):
msg = _("[DFARESTClient] Input DCNM IP, user name or password"
"parameter is not specified")
raise ValueError(msg)
# url timeout: 10 seconds
self._TIMEOUT_RESPONSE = 10
# urls
net_url = 'http://%s/' % self._ip
net_url += 'rest/auto-config/organizations/%s/partitions/%s/networks'
self._create_network_url = net_url
cfg_url = 'http://%s/rest/auto-config/profiles' % self._ip
self._cfg_profile_list_url = cfg_url
cfg_url += '/%s'
self._cfg_profile_get_url = cfg_url
self._org_url = 'http://%s/rest/auto-config/organizations' % self._ip
tmp_url = 'http://%s/rest/auto-config/organizations/' % self._ip
tmp_url += '%s/partitions'
self._create_part_url = tmp_url
self._del_org_url = self._org_url + '/%s'
self._del_part = self._org_url + '/%s/partitions/%s'
self._del_network_url = (self._org_url +
'/%s/partitions/%s/networks/segment/%s')
self._login_url = 'http://%s/rest/logon' % (self._ip)
self._logout_url = 'http://%s/rest/logout' % (self._ip)
self._exp_time = 100000
self._resp_ok = 200
def _create_network(self, network_info):
"""Send create network request to DCNM.
:network_info: network parameters to be created on DCNM
"""
url = self._create_network_url % (network_info['partitionName'],
network_info['partitionName'])
payload = network_info
LOG.info(_('url %(url)s payload %(payload)s'),
{'url': url, 'payload': payload})
return (self._send_request('POST', url, payload, 'network'))
def _config_profile_get(self, thisprofile):
"""Get information of a config profile from DCNM.
:thisprofile: network config profile in request
"""
url = self._cfg_profile_get_url % (thisprofile)
payload = {}
res = self._send_request('GET', url, payload, 'config-profile')
return res.json()
def _config_profile_list(self):
"""Get list of supported config profile from DCNM."""
url = self._cfg_profile_list_url
payload = {}
res = self._send_request('GET', url, payload, 'config-profile')
return res.json()
def _create_org(self, name, desc):
"""Create organization on the DCNM.
:name: Name of organization
:desc: Description of organization
"""
url = self._org_url
payload = {
"organizationName": name,
"description": name if len(desc) == 0 else desc,
"orchestrationSource": "Openstack Controller"}
return (self._send_request('POST', url, payload, 'organization'))
def _create_partition(self, org_name, part_name, desc):
"""Send Create partition request to the DCNM.
:org_name: name of organization
:part_name: name of partition
:desc: description of partition
"""
url = self._create_part_url % (org_name)
payload = {
"partitionName": part_name,
"description": part_name if len(desc) == 0 else desc,
"organizationName": org_name}
return (self._send_request('POST', url, payload, 'partition'))
def _delete_org(self, org_name):
"""Send organization delete request to DCNM.
:org_name: name of organization to be deleted
"""
url = self._del_org_url % (org_name)
self._send_request('DELETE', url, '', 'organization')
def _delete_partition(self, org_name, partition_name):
"""Send partition delete request to DCNM.
:partition_name: name of partition to be deleted
"""
url = self._del_part % (org_name, partition_name)
self._send_request('DELETE', url, '', 'partition')
def _delete_network(self, network_info):
"""Send network delete request to DCNM.
:partition_name: name of partition to be deleted
"""
org_name = network_info.get('organizationName', '')
part_name = network_info.get('partitionName', '')
segment_id = network_info['segmentId']
url = self._del_network_url % (org_name, part_name, segment_id)
self._send_request('DELETE', url, '', 'network')
def _login(self):
"""Login request to DCNM."""
url_login = self._login_url
expiration_time = self._exp_time
payload = {'expirationTime': expiration_time}
self._req_headers = {'Accept': 'application/json',
'Content-Type': 'application/json; charset=UTF-8'}
res = requests.post(url_login,
data=jsonutils.dumps(payload),
headers=self._req_headers,
auth=(self._user, self._pwd),
timeout=self._TIMEOUT_RESPONSE)
session_id = ''
if res and res.status_code == self._resp_ok:
session_id = res.json().get('Dcnm-Token')
self._req_headers.update({'Dcnm-Token': session_id})
def _logout(self):
"""Logout request to DCNM."""
url_logout = self._logout_url
requests.post(url_logout,
headers=self._req_headers,
timeout=self._TIMEOUT_RESPONSE)
def _send_request(self, operation, url, payload, desc):
"""Send request to DCNM."""
res = None
try:
payload_json = None
if payload and payload != '':
payload_json = jsonutils.dumps(payload)
self._login()
desc_lookup = {'POST': ' creation', 'PUT': ' update',
'DELETE': ' deletion', 'GET': ' get'}
res = requests.request(operation, url, data=payload_json,
headers=self._req_headers,
timeout=self._TIMEOUT_RESPONSE)
desc += desc_lookup.get(operation, operation.lower())
LOG.info(_("DCNM-send_request: %(desc)s %(url)s %(pld)s"),
{'desc': desc, 'url': url, 'pld': payload})
self._logout()
except (requests.HTTPError, requests.Timeout,
requests.ConnectionError) as e:
LOG.exception(_('Error during request'))
raise dexc.DFAClientRequestFailed(reason=e)
return res
def _check_for_supported_profile(self, thisprofile):
"""Filter those profiles that are not currently supported."""
return (thisprofile.endswith('Ipv4TfProfile') or
thisprofile.endswith('Ipv4EfProfile') or
'defaultNetworkL2Profile' in thisprofile)
def config_profile_list(self):
"""Return config profile list from DCNM."""
profile_list = []
these_profiles = []
these_profiles = self._config_profile_list()
profile_list = [q for p in these_profiles for q in
[p.get('profileName')]
if self._check_for_supported_profile(q)]
return profile_list
def config_profile_fwding_mode_get(self, profile_name):
"""Return forwarding mode of given config profile."""
profile_params = self._config_profile_get(profile_name)
fwd_cli = 'fabric forwarding mode proxy-gateway'
if fwd_cli in profile_params['configCommands']:
return 'proxy-gateway'
else:
return 'anycast-gateway'
def create_network(self, tenant_name, network, subnet):
"""Create network on the DCNM.
:tenant_name: name of tenant the network belongs to
:network: network parameters
:subnet: subnet parameters of the network
"""
network_info = {}
seg_id = str(network.provider__segmentation_id)
subnet_ip_mask = subnet.cidr.split('/')
gw_ip = subnet.gateway_ip
cfg_args = [
"$segmentId=" + seg_id,
"$netMaskLength=" + subnet_ip_mask[1],
"$gatewayIpAddress=" + gw_ip,
"$networkName=" + network.name,
"$vlanId=0",
"$vrfName=" + tenant_name + ':' + tenant_name
]
cfg_args = ';'.join(cfg_args)
ip_range = ','.join(["%s-%s" % (p['start'], p['end']) for p in
subnet.allocation_pools])
dhcp_scopes = {'ipRange': ip_range,
'subnet': subnet.cidr,
'gateway': gw_ip}
network_info = {"segmentId": seg_id,
"vlanId": "0",
"mobilityDomainId": "None",
"profileName": network.config_profile,
"networkName": network.name,
"configArg": cfg_args,
"organizationName": tenant_name,
"partitionName": tenant_name,
"description": network.name,
"dhcpScope": dhcp_scopes}
LOG.debug("Create %s network in DCNM." % network_info)
self._create_network(network_info)
def delete_network(self, tenant_name, network):
"""Delete network on the DCNM.
:tenant_name: name of tenant the network belongs to
:network: object that contains network parameters
"""
network_info = {}
seg_id = network.provider__segmentation_id
network_info = {
'organizationName': tenant_name,
'partitionName': tenant_name,
'segmentId': seg_id,
}
LOG.debug("Delete %s network in DCNM." % network_info)
self._delete_network(network_info)
def delete_tenant(self, tenant_name):
"""Delete tenant on the DCNM.
:tenant_name: name of tenant to be deleted.
"""
self._delete_partition(tenant_name, tenant_name)
self._delete_org(tenant_name)
def create_project(self, org_name, desc=None):
"""Create project on the DCNM.
:org_name: name of organization to be created
:desc: string that describes organization
"""
desc = desc or org_name
self._create_org(org_name, desc)
self._create_partition(org_name, org_name, desc)

View File

@ -0,0 +1,53 @@
# Copyright 2014 Cisco Systems, Inc.
# 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_dfa_opts = [
cfg.StrOpt('dcnm_ip', default='0.0.0.0',
help=_("IP address of DCNM.")),
cfg.StrOpt('dcnm_user', default='user',
help=_("User login name for DCNM.")),
cfg.StrOpt('dcnm_password', default='password',
secret=True,
help=_("Login password for DCNM.")),
cfg.StrOpt('gateway_mac', default='00:00:DE:AD:BE:EF',
help=_("Gateway mac address when using proxy mode.")),
]
cfg.CONF.register_opts(ml2_cisco_dfa_opts, "ml2_cisco_dfa")
class CiscoDFAConfig(object):
"""Cisco DFA Mechanism Driver Configuration class."""
dfa_cfg = {}
def __init__(self):
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(_("Failed to read config files %(file)s") %
{'file': cfg.CONF.config_file})
for parsed_file in multi_parser.parsed:
for parsed_item in parsed_file.keys():
for key, value in parsed_file[parsed_item].items():
if parsed_item == 'mech_driver_agent':
self.dfa_cfg[key] = value

View File

@ -0,0 +1,22 @@
# Copyright 2014 Cisco Systems, Inc.
# 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 uuid
CISCO_DFA_MECH_DRVR_NAME = 'cisco_dfa'
DEFAULT_CFG_PROFILE_ID = str(uuid.UUID(int=0))
CONFIG_PROFILE_ID = 'dfa:cfg_profile_id'

View File

@ -0,0 +1,63 @@
# Copyright 2014 Cisco Systems, Inc.
# 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 DFA ML2 mechanism drivers."""
from neutron.common import exceptions
class NetworkNotFound(exceptions.NotFound):
"""Network cannot be found."""
message = _("Network %(network_id)s could not be found.")
class ConfigProfileNotFound(exceptions.NotFound):
"""Config Profile cannot be found."""
message = _("Config profile for network %(network_id)s"
" could not be found.")
class ConfigProfileFwdModeNotFound(exceptions.NotFound):
"""Config Profile forwarding mode cannot be found."""
message = _("Forwarding Mode for network %(network_id)s"
" could not be found.")
class ConfigProfileIdNotFound(exceptions.NotFound):
"""Config Profile ID cannot be found."""
message = _("Config Profile %(profile_id)s could not be found.")
class ConfigProfileNameNotFound(exceptions.NotFound):
"""Config Profile name cannot be found."""
message = _("Config Profile %(name)s could not be found.")
class ProjectIdNotFound(exceptions.NotFound):
"""Project ID cannot be found."""
message = _("Project ID %(project_id)s could not be found.")
class DFAClientRequestFailed(exceptions.ServiceUnavailable):
"""Request to DCNM failed."""
message = _("Request to DCNM failed: %(reason)s.")

View File

@ -0,0 +1,140 @@
# Copyright 2014 Cisco Systems, Inc.
# 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.
#
"""
This file provides a wrapper to novaclient API, for getting the instacne's
information such as display_name.
"""
from keystoneclient.v2_0 import client as keyc
from neutron.openstack.common import log as logging
from novaclient import exceptions as nexc
from novaclient.v1_1 import client as nova_client
LOG = logging.getLogger(__name__)
class DFAInstanceAPI(object):
"""This class provides API to get information for a given instance."""
def __init__(self, cfg):
self._tenant_name = cfg.CONF.keystone_authtoken.admin_tenant_name
self._user_name = cfg.CONF.keystone_authtoken.admin_user
self._admin_password = cfg.CONF.keystone_authtoken.admin_password
self._TIMEOUT_RESPONSE = 10
self._token = None
self._project_id = None
self._auth_url = None
self._token_id = None
self._token = None
self._novaclnt = None
self._url = cfg.CONF.nova_admin_auth_url
self._inst_info_cache = {}
def _create_token(self):
"""Create new token for using novaclient API."""
ks = keyc.Client(username=self._user_name,
password=self._admin_password,
tenant_name=self._tenant_name,
auth_url=self._url)
result = ks.authenticate()
if result:
access = ks.auth_ref
token = access.get('token')
self._token_id = token['id']
self._project_id = token['tenant'].get('id')
service_catalog = access.get('serviceCatalog')
for sc in service_catalog:
if sc['type'] == "compute" and sc['name'] == 'nova':
endpoints = sc['endpoints']
for endp in endpoints:
self._auth_url = endp['adminURL']
LOG.info(_('_create_token: token = %s'), token)
# Create nova client.
self._novaclnt = self._create_nova_client()
return token
else:
# Failed request.
LOG.error(_('Failed to send token create request.'))
def _create_nova_client(self):
"""Creates nova client object."""
try:
clnt = nova_client.Client(self._user_name,
self._token_id,
self._project_id,
self._auth_url,
insecure=False,
cacert=None)
clnt.client.auth_token = self._token_id
clnt.client.management_url = self._auth_url
return clnt
except nexc.Unauthorized:
thismsg = (_('Failed to get novaclient:Unauthorised '
'%(proj)s %(user)s') % {'proj': self.project_id,
'user': self._user_name})
raise nexc.ClientException(thismsg)
except nexc.AuthorizationFailure as err:
raise nexc.ClientException(_("Failed to get novaclient %s") % err)
def _get_instances_for_project(self, project_id):
"""Return all instances for a given project.
:project_id: UUID of project (tenant)
"""
search_opts = {'marker': None,
'all_tenants': True,
'project_id': project_id}
self._create_token()
try:
servers = self._novaclnt.servers.list(True, search_opts)
LOG.debug('_get_instances_for_project: servers=%s' % servers)
return servers
except nexc.Unauthorized:
emsg = (_('Failed to get novaclient:Unauthorised '
'project_id=%(proj)s user=%(user)s'),
{'proj': self.project_id, 'name': self._user_name})
LOG.exception(emsg)
raise nexc.ClientException(emsg)
except nexc.AuthorizationFailure as err:
emsg = _("Failed to get novaclient %s")
LOG.exception(emsg % err)
raise nexc.ClientException(emsg % err)
def get_instance_for_uuid(self, uuid, project_id):
"""Return instance name for given uuid of an instance and project.
:uuid: Instance's UUID
:project_id: UUID of project (tenant)
"""
instance_name = None
instance_name = self._inst_info_cache.get((uuid, project_id))
if instance_name:
return instance_name
instances = self._get_instances_for_project(project_id)
for inst in instances:
if inst.id.replace('-', '') == uuid:
LOG.debug('get_instance_for_uuid: name=%s' % inst.name)
instance_name = inst.name
self._inst_info_cache[(uuid, project_id)] = instance_name
return instance_name
return instance_name

View File

@ -0,0 +1,49 @@
# Copyright 2014 Cisco Systems, Inc.
# 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.common import rpc as n_rpc
from neutron.common import topics
class RpcCallbacks(n_rpc.RpcCallback):
RPC_API_VERSION = '1.1'
def __init__(self, notifier):
self._nofifier = notifier
super(RpcCallbacks, self).__init__()
class MechDriversAgentNotifierApi(n_rpc.RpcProxy):
"""Agent side of the cisco DFA mechanism driver rpc API.
API version history:
1.0 - Initial version.
"""
BASE_RPC_API_VERSION = '1.0'
def __init__(self, topic, agt_topic_tbl):
super(MechDriversAgentNotifierApi, self).__init__(
topic=topic, default_version=self.BASE_RPC_API_VERSION)
self.topic_dfa_update = topics.get_topic_name(topic,
agt_topic_tbl,
topics.UPDATE)
def send_vm_info(self, context, vm_info):
self.fanout_cast(context,
self.make_msg('send_vm_info', vm_info=vm_info),
topic=self.topic_dfa_update)

View File

@ -0,0 +1,59 @@
# Copyright 2014 Cisco Systems, Inc.
# 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.db import model_base
import sqlalchemy as sa
class ConfigProfile(model_base.BASEV2):
"""Cisco DFA network configuration profile.
'id' - UUID and is localy generated,
'name' - profile name coming form DCNM.
"""
__tablename__ = 'cisco_dfa_config_profiles'
id = sa.Column(sa.String(36), primary_key=True)
name = sa.Column(sa.String(255))
forwarding_mode = sa.Column(sa.String(32))
class ConfigProfileBinding(model_base.BASEV2):
"""Represents a binding of Network to Config Profile.
netwrok_id - Network UUID,
cfg_profile_id - UUID of config profile.
"""
__tablename__ = 'cisco_dfa_config_profile_bindings'
network_id = sa.Column(sa.String(36),
sa.ForeignKey('networks.id', ondelete="CASCADE"),
primary_key=True)
cfg_profile_id = sa.Column(sa.String(36), primary_key=True)
class ProjectNameCache(model_base.BASEV2):
"""Cache project name and project ID for Cisco DFA.
project_id - project UUID,
project_name - project name.
"""
__tablename__ = 'cisco_dfa_project_cache'
project_id = sa.Column(sa.String(36),
primary_key=True)
project_name = sa.Column(sa.String(255))

View File

@ -0,0 +1,277 @@
# Copyright (c) 2014 Cisco Systems
# 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 DFA platforms.
"""
import eventlet
from oslo.config import cfg
from neutron.common import exceptions as n_exc
from neutron.common import rpc as n_rpc
from neutron.common import topics
from neutron.extensions import portbindings
from neutron.openstack.common import log as logging
from neutron.plugins.ml2.common import exceptions as ml2_exc
from neutron.plugins.ml2 import driver_api as api
from neutron.plugins.ml2.drivers.cisco.dfa import cfg_profile_db_v2
from neutron.plugins.ml2.drivers.cisco.dfa import cisco_dfa_rest
from neutron.plugins.ml2.drivers.cisco.dfa import config
from neutron.plugins.ml2.drivers.cisco.dfa import constants as dfa_const
from neutron.plugins.ml2.drivers.cisco.dfa import dfa_exceptions as dexc
from neutron.plugins.ml2.drivers.cisco.dfa import dfa_instance_api
from neutron.plugins.ml2.drivers.cisco.dfa import dfa_mech_driver_rpc as drpc
from neutron.plugins.ml2.drivers.cisco.dfa import project_events
from neutron.plugins.ml2.drivers.cisco.dfa import projects_cache_db_v2
LOG = logging.getLogger(__name__)
class SubnetObj(object):
"""Represents a subnet object.
The information in the object will be used when creating a subnet on
the DCNM.
"""
def __init__(self, subnet):
self.allocation_pools = subnet['allocation_pools']
self.host_routes = subnet['host_routes']
self.cidr = subnet['cidr']
self.id = subnet['id']
self.name = subnet['name']
self.enable_dhcp = subnet['enable_dhcp']
self.network_id = subnet['network_id']
self.tenant_id = subnet['tenant_id']
self.dns_nameservers = subnet['dns_nameservers']
self.gateway_ip = subnet['gateway_ip']
self.ip_version = subnet['ip_version']
self.shared = subnet['shared']
class NetworkObj(object):
"""Represents a network object.
The information in this object will be used when creating a network on
the DCNM.
"""
def __init__(self, net, segid, cfgp=None):
self.provider__segmentation_id = segid
self.tenant_id = net['tenant_id']
self.name = net['name']
self.config_profile = cfgp
self.id = net['id']
class CiscoDfaMechanismDriver(api.MechanismDriver):
"""Cisco DFA ML2 Mechanism Driver."""
def initialize(self):
# Initialize the config
self._dfa_cfg = config.CiscoDFAConfig().dfa_cfg
# Initialize DCNM client.
self._dcnm_client = cisco_dfa_rest.DFARESTClient()
# Initialize project creation/deletion events object.
# This will be used to get notification from keystone when
# a tenant (i.e. project) is created or deleted.
self._keys = project_events.EventsHandler('keystone',
self._dcnm_client)
# Spawn a task, to process notification queue for keystone events.
eventlet.spawn(self._process_keystone_events)
# Initialize nova client wrapper. It will be used to get more
# information for an instance.
self._inst_api = dfa_instance_api.DFAInstanceAPI(cfg)
# Initialize mechanism driver RPC.
self._setup_mechdrv_rpc()
# Initialize project info object.
self.projects_cache_db_v2 = projects_cache_db_v2.ProjectsInfoCache()
self._ctask_sleep_interval = 60
def _get_agent_topic(self):
"""Read the mech_driver_agent section from the config file."""
mech_drvr_rpc = self._dfa_cfg.get('mech_driver_rpc')
if mech_drvr_rpc is None:
return
self._agent_topic = ''
self._mech_drv_topic = ''
for val in mech_drvr_rpc:
if len(val) > 0:
if val.split(':')[0] != dfa_const.CISCO_DFA_MECH_DRVR_NAME:
continue
try:
self._mech_drv_topic = val.split(':')[1]
self._agent_topic = val.split(':')[2]
except IndexError:
emsg = _('No topics is defined for %s mechanism driver')
LOG.error(emsg % dfa_const.CISCO_DFA_MECH_DRVR_NAME)
return
def _setup_mechdrv_rpc(self):
"""Setup RPC for this mechanism driver."""
self._get_agent_topic()
if not self._agent_topic or not self._mech_drv_topic:
LOG.debug('Mechanism Driver notifer is not initialized')
return
self.dfa_notifier = drpc.MechDriversAgentNotifierApi(topics.AGENT,
self._agent_topic)
self.endpoints = [drpc.RpcCallbacks(self.dfa_notifier)]
self.topic = self._mech_drv_topic
self.conn = n_rpc.create_connection(new=True)
self.conn.create_consumer(self.topic, self.endpoints, fanout=False)
self.conn.consume_in_threads()
def _process_keystone_events(self):
"""Task to process notification from keystone.
The handler processes events such as creation and deletion of projects
sent by keystone.
"""
self._keys.event_handler()
def create_network_postcommit(self, context):
# Check if the tenant is valid.
projid = context.current.get('tenant_id')
if not self._keys.is_valid_project(projid):
return
# Check if network id exists in the config profile DB. If not,
# exception should be raised.
net_id = context.current.get('id')
res = cfg_profile_db_v2.get_network_profile_binding(
context._plugin_context.session, net_id)
if not res:
cfgp_id = context.current.get(dfa_const.CONFIG_PROFILE_ID)
msg = (_("Failed to create network. Config Profile id %s"
" does not exist.") % cfgp_id)
raise n_exc.BadRequest(resource='network', msg=msg)
# Get the project name. If project name does not exist, an exception
# will be raised.
self.projects_cache_db_v2.get_project_name(projid)
def delete_network_postcommit(self, context):
projid = context.current.get('tenant_id')
if not self._keys.is_valid_project(projid):
return
segid = context.current.get('provider:segmentation_id')
tenant_name = context._plugin_context.tenant_name
net = NetworkObj(context.current, segid)
try:
self._dcnm_client.delete_network(tenant_name, net)
except dexc.DFAClientRequestFailed as ex:
emsg = _('Failed to create network %(net)s. Error:%(err)s.')
LOG.error(emsg % {'net': net.name, 'err': ex})
raise ml2_exc.MechanismDriverError
def create_subnet_postcommit(self, context):
projid = context.current.get('tenant_id')
if not self._keys.is_valid_project(projid):
return
subnet = context.current
if subnet['name'] == 'private-subnet':
emsg = _("%s is default subnet and no need to create it in DCNM.")
LOG.info(emsg % subnet['name'])
return
session = context._plugin_context.session
netid = context.current['network_id']
network_entry = cfg_profile_db_v2.get_network_entry(session, netid)
tenant_name = context._plugin_context.tenant_name
segid = self.projects_cache_db_v2.get_network_segid(netid)
cfgp_name = cfg_profile_db_v2.get_config_profile_name(session, netid)
snet = SubnetObj(context.current)
net = NetworkObj(network_entry, int(segid), cfgp_name)
try:
self._dcnm_client.create_network(tenant_name, net, snet)
except dexc.DFAClientRequestFailed as ex:
emsg = _('Failed to create network %(net)s. Error:%(err)s.')
LOG.error(emsg % {'net': net.name, 'err': ex})
raise ml2_exc.MechanismDriverError
def update_port_postcommit(self, context):
projid = context.current.get('tenant_id')
if not self._keys.is_valid_project(projid):
return
session = context._plugin_context.session
self.device_id = context.current.get('device_id').replace('-', '')
tenant_id = context.current.get('tenant_id')
netid = context.current.get('network_id')
self.inst_name = self._inst_api.get_instance_for_uuid(self.device_id,
tenant_id)
self.fwd_mode = cfg_profile_db_v2.get_config_profile_fwd_mode(session,
netid)
self.segid = self.projects_cache_db_v2.get_network_segid(netid)
self.mac = context.current.get('mac_address')
self.ip = (context.current.get('fixed_ips')[0]['ip_address']
if context.current.get('fixed_ips') else None)
vm_info = {
'status': 'up',
'ip': self.ip,
'mac': self.mac,
'segid': self.segid,
'inst_name': self.inst_name,
'inst_uuid': self.device_id,
'host': context.current.get(portbindings.HOST_ID),
'port_id': context.current.get('id'),
'network_id': context.current.get('network_id'),
'oui_type': 'cisco',
}
if self.inst_name:
self.dfa_notifier.send_vm_info(context._plugin_context, vm_info)
LOG.debug("update_port_postcommit : %s" % vm_info)
def delete_port_postcommit(self, context):
session = context._plugin_context.session
self.device_id = context.current.get('device_id').replace('-', '')
tenant_id = context.current.get('tenant_id')
netid = context.current.get('network_id')
self.inst_name = self._inst_api.get_instance_for_uuid(self.device_id,
tenant_id)
self.fwd_mode = cfg_profile_db_v2.get_config_profile_fwd_mode(session,
netid)
self.segid = self.projects_cache_db_v2.get_network_segid(netid)
self.mac = context.current.get('mac_address')
self.ip = (context.current.get('fixed_ips')[0]['ip_address']
if context.current.get('fixed_ips') else None)
vm_info = {
'status': 'down',
'ip': self.ip,
'mac': self.mac,
'segid': self.segid,
'inst_name': self.inst_name,
'inst_uuid': self.device_id,
'host': context.current.get(portbindings.HOST_ID),
'port_id': context.current.get('id'),
'network_id': context.current.get('network_id'),
'oui_type': 'cisco',
}
if self.inst_name:
self.dfa_notifier.send_vm_info(context._plugin_context, vm_info)
LOG.debug("delete_port_postcommit : %s" % vm_info)

View File

@ -0,0 +1,184 @@
# Copyright 2014 Cisco Systems, Inc.
# 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 keystoneclient.v3 import client
from oslo.config import cfg
from oslo import messaging
from neutron.openstack.common import excutils
from neutron.openstack.common import log as logging
from neutron.plugins.ml2.drivers.cisco.dfa import dfa_exceptions as dexc
from neutron.plugins.ml2.drivers.cisco.dfa import projects_cache_db_v2
LOG = logging.getLogger(__name__)
notif_params = {
'keystone': {
'admin_token': 'ADMIN',
'admin_endpoint': 'http://localhost:%(admin_port)s/',
'admin_port': '35357',
'default_notification_level': 'INFO',
'notification_topics': 'notifications',
'control_exchange': 'openstack',
}
}
proj_exceptions_list = [
'admin', 'service', 'invisible_to_admin', 'demo', 'alt_demo']
class NotificationEndpoint(object):
def __init__(self, evnt_hndlr):
self._event_hndlr = evnt_hndlr
def info(self, ctxt, publisher_id, event_type, payload, metadata):
self._event_hndlr.callback(event_type, payload)
class EventsHandler(projects_cache_db_v2.ProjectsInfoCache):
"""This class defines methods to listen and process the project events."""
def __init__(self, ser_name, dcnm_client):
self._keystone = None
self._service = ser_name
self._notif_params = {}
self._set_notif_params()
self._dcnm_client = dcnm_client
self.events_handler = {
'identity.project.created': self.project_create_event,
'identity.project.deleted': self.project_delete_event,
'identity.user.created': self.no_op_event,
'identity.user.deleted': self.no_op_event,
}
def no_op_event(self, keyc, project_id, dcnmc):
pass
def project_create_event(self, keyc, project_id, dcnmc):
"""Create a project on the DCNM.
:param keyc: keystoneclient object
:param project_id: UUID of the project
:param dcnmc: DCNM client object
"""
proj = keyc.projects.get(project_id)
proj_name = proj.name
desc = proj.description
LOG.debug("project_create_event: %(proj)s %(proj_name)s %(desc)s." %
{'proj': proj, 'proj_name': proj_name, 'desc': desc})
if proj_name not in proj_exceptions_list:
try:
dcnmc.create_project(proj_name, desc)
except dexc.DFAClientConnectionFailed as ex:
with excutils.save_and_reraise_exception():
LOG.exception(_('Failed to create %(proj)s. '
'Error:%(err)s.'),
{'proj': proj_name, 'err': ex})
proj_info = {'project_id': project_id,
'project_name': proj_name}
self.create_projects_cache_db(proj_info)
def project_delete_event(self, keyc, project_id, dcnmc):
"""Delete a project on the DCNM.
:param keyc: keystoneclient object
:param project_id: UUID of the project
:param dcnmc: DCNM client object
"""
try:
proj_info = self.delete_projects_cache_db(project_id)
LOG.debug("project_delete_event: proj_info: %s." % proj_info)
dcnmc.delete_tenant(proj_info.project_name)
except dexc.ProjectIdNotFound:
with excutils.save_and_reraise_exception():
LOG.exception(_("Failed to delete %(id)s"), {'id': project_id})
except dexc.DFAClientConnectionFailed:
with excutils.save_and_reraise_exception():
LOG.exception(_("Failed to delete %(proj)s in DCNM."),
{'proj': proj_info.project_name})
def _set_notif_params(self):
"""Read notification parameters from the config file."""
self._notif_params.update(notif_params[self._service])
temp_db = {}
cfgfile = cfg.find_config_files(self._service)
multi_parser = cfg.MultiConfigParser()
cfgr = multi_parser.read(cfgfile)
if len(cfgr) == 0:
LOG.error(_("Failed to read %s."), cfgfile)
return
for parsed_file in multi_parser.parsed:
for parsed_item in parsed_file.keys():
for key, value in parsed_file[parsed_item].items():
if key in self._notif_params:
val = notif_params[self._service].get(key)
if val != value[0]:
temp_db[key] = value[0]
self._notif_params.update(temp_db)
self._token = self.get_notif_params().get('admin_token')
_endpoint = self.get_notif_params().get('admin_endpoint')
self._endpoint_url = _endpoint % self.get_notif_params() + 'v3/'
self._keystone = client.Client(token=self._token,
endpoint=self._endpoint_url)
def callback(self, event_type, payload):
"""Callback method for processing events in notification queue.
:param event_type: event type in the notification queue such as
identity.project.created, identity.project.deleted.
:param payload: Contains information of an event
"""
try:
event = event_type
if event in self.events_handler:
project_id = payload['resource_info']
self.events_handler[event](self._keystone, project_id,
self._dcnm_client)
except KeyError:
LOG.error(_('event_type %s does not have payload/resource_info '
'key'), event)
def event_handler(self):
"""Prepare connection and channels for listenning to the events."""
topicname = self.get_notif_params().get('notification_topics')
transport = messaging.get_transport(cfg.CONF)
targets = [messaging.Target(topic=topicname)]
endpoints = [NotificationEndpoint(self)]
server = messaging.get_notification_listener(transport, targets,
endpoints)
server.start()
server.wait()
def get_notif_params(self):
"""Return notification parameters."""
return self._notif_params
def is_valid_project(self, project_id):
"""Check the validity of project.
:param project_id: UUID of project
:returns: True if project is valid.
"""
proj = self._keystone.projects.get(project_id)
proj_name = proj.name
if proj_name in proj_exceptions_list:
LOG.debug("Project %s is not created by user." % proj_name)
return False
return True

View File

@ -0,0 +1,95 @@
# Copyright 2014 Cisco Systems, Inc.
# 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
import neutron.db.api as db
from neutron.plugins.ml2 import db as ml2db
from neutron.plugins.ml2.drivers.cisco.dfa import dfa_exceptions as dexc
from neutron.plugins.ml2.drivers.cisco.dfa import dfa_models_v2
class ProjectsInfoCache(object):
"""Project DB API."""
def _get_project_entry(self, db_session, pid):
"""Get a project entry from the table.
:param db_session: database session object
:param pid: project ID
"""
try:
return db_session.query(
dfa_models_v2.ProjectNameCache).filter_by(project_id=pid).one()
except exc.NoResultFound:
raise dexc.ProjectIdNotFound(project_id=pid)
def create_projects_cache_db(self, proj_info):
"""Create an entry in the database.
:param proj_info: dictionary that contains information of the project
"""
db_session = db.get_session()
with db_session.begin(subtransactions=True):
projid = proj_info["project_id"]
projname = proj_info["project_name"]
thisproj = dfa_models_v2.ProjectNameCache(project_id=projid,
project_name=projname)
db_session.add(thisproj)
return thisproj
def delete_projects_cache_db(self, proj_id):
"""Delete a project from the table.
:param proj_id: UUID of the project
"""
db_session = db.get_session()
thisproj = None
with db_session.begin(subtransactions=True):
thisproj = self._get_project_entry(db_session, proj_id)
db_session.delete(thisproj)
return thisproj
def get_project_name(self, proj_id):
"""Returns project's name.
:param proj_id: UUID of the project
"""
db_session = db.get_session()
with db_session.begin(subtransactions=True):
thisproj = self._get_project_entry(db_session, proj_id)
return thisproj.project_name
def update_projects_cache_db(self, pid, proj_info):
"""Update projects DB.
:param pid: project ID
:param proj_info: dictionary that contains information of the project
"""
db_session = db.get_session()
with db_session.begin(subtransactions=True):
thisproj = self._get_project_entry(db_session, pid)
thisproj.update(proj_info)
def get_network_segid(self, sid):
"""Get network segmentation id.
:param sid: requested segment id
"""
db_session = db.get_session()
seg_entry = ml2db.get_network_segments(db_session, sid)
return seg_entry[0]['segmentation_id']

View File

@ -0,0 +1,153 @@
# Copyright 2014 Cisco Systems, Inc.
# 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 mock
from oslo.config import cfg
from neutron.plugins.ml2.drivers.cisco.dfa import cisco_dfa_rest as dc
from neutron.plugins.ml2.drivers.cisco.dfa import config # noqa
from neutron.tests import base
"""This file includes test cases for cisco_dfa_rest.py."""
FAKE_DCNM_IP = '1.1.1.1'
FAKE_DCNM_USERNAME = 'dcnmuser'
FAKE_DCNM_PASSWORD = 'dcnmpass'
org_url = 'http://%s/rest/auto-config/organizations'
part_url = 'http://%s/rest/auto-config/organizations/%s/partitions'
net_url = 'http://%s/rest/auto-config/organizations/%s/partitions/%s/networks'
del_net_url = ('http://%s/rest/auto-config/organizations/%s/partitions/%s/'
'networks/segment/%s')
class TestNetwork(object):
provider__segmentation_id = 123456
name = 'cisco_test_network'
config_profile = 'defaultL2ConfigProfile'
class TestCiscoDFAClient(base.BaseTestCase):
"""Test cases for DFARESTClient."""
def setUp(self):
# Declare the test resource.
super(TestCiscoDFAClient, self).setUp()
dcnm_cfg = {'dcnm_ip': FAKE_DCNM_IP,
'dcnm_user': FAKE_DCNM_USERNAME,
'dcnm_password': FAKE_DCNM_PASSWORD}
for k, v in dcnm_cfg.items():
cfg.CONF.set_override(k, v, 'ml2_cisco_dfa')
self.dcnm_client = dc.DFARESTClient()
mock.patch.object(self.dcnm_client, '_send_request').start()
self.testnetwork = TestNetwork()
def test_create_org(self):
"""Test create organization."""
org_name = 'Test_Project'
url = org_url % (cfg.CONF.ml2_cisco_dfa.dcnm_ip)
payload = {'organizationName': org_name,
'description': org_name,
'orchestrationSource': 'Openstack Controller'}
self.dcnm_client._create_org(org_name, org_name)
self.dcnm_client._send_request.assert_called_with('POST', url,
payload,
'organization')
def test_create_partition(self):
"""Test create partition."""
org_name = 'Cisco'
part_name = 'Lab'
url = part_url % (cfg.CONF.ml2_cisco_dfa.dcnm_ip, org_name)
payload = {'partitionName': part_name,
'description': org_name,
'organizationName': org_name}
self.dcnm_client._create_partition(org_name, part_name, org_name)
self.dcnm_client._send_request.assert_called_with('POST', url,
payload,
'partition')
def test_create_project(self):
"""Test create project."""
org_name = 'Cisco'
self.dcnm_client.create_project(org_name)
call_cnt = self.dcnm_client._send_request.call_count
self.assertEqual(2, call_cnt)
def test_create_network(self):
"""Test create network."""
network_info = {}
cfg_args = []
seg_id = str(self.testnetwork.provider__segmentation_id)
config_profile = self.testnetwork.config_profile
network_name = self.testnetwork.name
tenant_name = 'Cisco'
url = net_url % (cfg.CONF.ml2_cisco_dfa.dcnm_ip, tenant_name,
tenant_name)
cfg_args.append("$segmentId=" + seg_id)
cfg_args.append("$netMaskLength=16")
cfg_args.append("$gatewayIpAddress=30.31.32.1")
cfg_args.append("$networkName=" + network_name)
cfg_args.append("$vlanId=0")
cfg_args.append("$vrfName=%s:%s" % (tenant_name, tenant_name))
cfg_args = ';'.join(cfg_args)
dhcp_scopes = {'ipRange': '10.11.12.14-10.11.12.254',
'subnet': '10.11.12.13',
'gateway': '10.11.12.1'}
network_info = {"segmentId": seg_id,
"vlanId": "0",
"mobilityDomainId": "None",
"profileName": config_profile,
"networkName": network_name,
"configArg": cfg_args,
"organizationName": tenant_name,
"partitionName": tenant_name,
"description": network_name,
"dhcpScope": dhcp_scopes}
self.dcnm_client._create_network(network_info)
self.dcnm_client._send_request.assert_called_with('POST', url,
network_info,
'network')
def test_delete_network(self):
"""Test delete network."""
seg_id = self.testnetwork.provider__segmentation_id
tenant_name = 'cisco'
url = del_net_url % (cfg.CONF.ml2_cisco_dfa.dcnm_ip,
tenant_name, tenant_name, seg_id)
self.dcnm_client.delete_network(tenant_name, self.testnetwork)
self.dcnm_client._send_request.assert_called_with('DELETE', url,
'', 'network')
def test_delete_tenant(self):
"""Test delete tenant."""
tenant_name = 'cisco'
self.dcnm_client.delete_tenant(tenant_name)
call_cnt = self.dcnm_client._send_request.call_count
self.assertEqual(2, call_cnt)

View File

@ -0,0 +1,272 @@
# Copyright 2014 Cisco Systems, Inc.
# 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 mock
from oslo.config import cfg
import testtools
from neutron.common import exceptions as n_exc
from neutron.plugins.ml2.drivers.cisco.dfa import cisco_dfa_rest
from neutron.plugins.ml2.drivers.cisco.dfa import config
from neutron.plugins.ml2.drivers.cisco.dfa import dfa_exceptions as dexc
from neutron.plugins.ml2.drivers.cisco.dfa import dfa_instance_api
from neutron.plugins.ml2.drivers.cisco.dfa import mech_cisco_dfa
from neutron.plugins.ml2.drivers.cisco.dfa import project_events
from neutron.plugins.ml2.drivers.cisco.dfa import projects_cache_db_v2
from neutron.tests import base
FAKE_NETWORK_NAME = 'test_dfa_network'
FAKE_NETWORK_ID = '949fdd05-a26a-4819-a829-9fc2285de6ff'
FAKE_CFG_PROF_ID = '8c30f360ffe948109c28ab56f69a82e1'
FAKE_SEG_ID = 12345
FAKE_PROJECT_NAME = 'test_dfa_project'
FAKE_PROJECT_ID = 'aee5da7e699444889c662cf7ec1c8de7'
FAKE_CFG_PROFILE_NAME = 'defaultNetworkL2Profile'
FAKE_INSTANCE_NAME = 'test_dfa_instance'
FAKE_SUBNET_ID = '1a3c5ee1-cb92-4fd8-bff1-8312ac295d64'
FAKE_PORT_ID = 'ea0d92cf-d0cb-4ed2-bbcf-ed7c6aaea4cb'
FAKE_DEVICE_ID = '20305657-78b7-48f4-a7cd-1edf3edbfcad'
FAKE_SECURITY_GRP_ID = '4b5b387d-cf21-4594-b926-f5a5c602295f'
FAKE_MAC_ADDR = 'fa:16:3e:70:15:c4'
FAKE_IP_ADDR = '23.24.25.4'
FAKE_GW_ADDR = '23.24.25.1'
FAKE_DHCP_IP_RANGE_START = '23.24.25.2'
FAKE_DHCP_IP_RANGE_END = '23.24.25.254'
FAKE_HOST_ID = 'test_dfa_host'
FAKE_FWD_MODE = 'proxy-gateway'
FAKE_DCNM_USER = 'cisco'
FAKE_DCNM_PASS = 'password'
FAKE_DCNM_IP = '1.1.2.2'
class FakeNetworkContext(object):
"""Network context for testing purposes only."""
def __init__(self, network):
self._network = network
self._session = None
@property
def current(self):
return self._network
@property
def original(self):
return self._network
class FakePortContext(object):
"""Port context for testing purposes only."""
def __init__(self, plugin_context, port):
self._port = port
self._plugin_context = plugin_context
self._session = None
@property
def current(self):
return self._port
class FakeSubnetContext(object):
"""Subnet context for testing purposes only."""
def __init__(self, subnet):
self._subnet = subnet
@property
def current(self):
return self._subnet
class TestCiscoDFAMechDriver(base.BaseTestCase):
"""Test cases for cisco DFA mechanism driver."""
def setUp(self):
super(TestCiscoDFAMechDriver, self).setUp()
dcnmpatcher = mock.patch(cisco_dfa_rest.__name__ + '.DFARESTClient')
self.mdcnm = dcnmpatcher.start()
# Define retrun values for keystone project.
keys_patcher = mock.patch(project_events.__name__ + '.EventsHandler')
self.mkeys = keys_patcher.start()
inst_api_patcher = mock.patch(dfa_instance_api.__name__ +
'.DFAInstanceAPI')
self.m_inst_api = inst_api_patcher.start()
proj_patcher = mock.patch(projects_cache_db_v2.__name__ +
'.ProjectsInfoCache')
self.mock_proj = proj_patcher.start()
dfa_cfg_patcher = mock.patch(config.__name__ + '.CiscoDFAConfig')
self.m_dfa_cfg = dfa_cfg_patcher.start()
ml2_cisco_dfa_opts = {'dcnm_password': FAKE_DCNM_PASS,
'dcnm_user': FAKE_DCNM_USER,
'dcnm_ip': FAKE_DCNM_IP}
for opt, val in ml2_cisco_dfa_opts.items():
cfg.CONF.set_override(opt, val, 'ml2_cisco_dfa')
self.dfa_mech_drvr = mech_cisco_dfa.CiscoDfaMechanismDriver()
self.dfa_mech_drvr.initialize()
self.dfa_mech_drvr._keys.is_valid_project.return_value = True
self.net_context = self._create_network_context()
self.proj_info = projects_cache_db_v2.ProjectsInfoCache()
def _create_network_context(self):
net_info = {'name': FAKE_NETWORK_NAME,
'tenant_id': FAKE_PROJECT_ID,
'dfa:cfg_profile_id': FAKE_CFG_PROF_ID,
'provider:segmentation_id': FAKE_SEG_ID,
'id': FAKE_NETWORK_ID}
net_context = FakeNetworkContext(net_info)
net_context._plugin_context = mock.MagicMock()
net_context._session = net_context._plugin_context.session
return net_context
def _create_subnet_context(self):
subnet_info = {
'ipv6_ra_mode': None,
'allocation_pools': [{'start': FAKE_DHCP_IP_RANGE_START,
'end': FAKE_DHCP_IP_RANGE_END}],
'host_routes': [],
'ipv6_address_mode': None,
'cidr': '23.24.25.0/24',
'id': FAKE_SUBNET_ID,
'name': u'',
'enable_dhcp': True,
'network_id': FAKE_NETWORK_ID,
'tenant_id': FAKE_PROJECT_ID,
'dns_nameservers': [],
'gateway_ip': FAKE_GW_ADDR,
'ip_version': 4,
'shared': False}
subnet_context = FakeSubnetContext(subnet_info)
subnet_context._plugin_context = mock.MagicMock()
return subnet_context
def _create_port_context(self):
port_info = {
'status': 'ACTIVE',
'binding:host_id': FAKE_HOST_ID,
'allowed_address_pairs': [],
'extra_dhcp_opts': [],
'device_owner': u'compute:nova',
'binding:profile': {},
'fixed_ips': [{'subnet_id': FAKE_SUBNET_ID,
'ip_address': FAKE_IP_ADDR}],
'id': FAKE_PORT_ID,
'security_groups': [FAKE_SECURITY_GRP_ID],
'device_id': FAKE_DEVICE_ID,
'name': u'',
'admin_state_up': True,
'network_id': FAKE_NETWORK_ID,
'tenant_id': FAKE_PROJECT_ID,
'binding:vif_details': {u'port_filter': True,
u'ovs_hybrid_plug': True},
'binding:vnic_type': u'normal',
'binding:vif_type': u'ovs',
'mac_address': FAKE_MAC_ADDR}
port_context = FakePortContext(mock.MagicMock(), port_info)
port_context._plugin_context = mock.MagicMock()
port_context._session = port_context._plugin_context.session
return port_context
def test_create_network_postcommit_no_profile(self):
query = self.net_context._session.query.return_value
query.filter_by.return_value.one.return_value = None
# Profile does not exist, catch the exception.
with testtools.ExpectedException(n_exc.BadRequest):
self.dfa_mech_drvr.create_network_postcommit(self.net_context)
def test_create_network_postcommit_no_project(self):
self.proj_info.get_project_name.side_effect = (
dexc.ProjectIdNotFound(project_id=FAKE_PROJECT_ID))
# Project does not exist, catch the exception.
with testtools.ExpectedException(dexc.ProjectIdNotFound):
self.dfa_mech_drvr.create_network_postcommit(self.net_context)
def test_delete_network_postcommit(self):
self.dfa_mech_drvr.delete_network_postcommit(self.net_context)
self.mdcnm.delete_network.return_value = None
self.assertTrue(self.dfa_mech_drvr._dcnm_client.delete_network.called)
def test_create_subnet_postcommit(self):
subnet_ctxt = self._create_subnet_context()
proj_obj = self.dfa_mech_drvr.projects_cache_db_v2
cfgp_mock = mock.MagicMock(return_value=FAKE_CFG_PROFILE_NAME)
self.dfa_mech_drvr.get_config_profile_name = cfgp_mock
mechdrvr_mock = mock.MagicMock(return_value=self.net_context.current)
self.dfa_mech_drvr.get_network_entry = mechdrvr_mock
proj_obj.get_network_segid.return_value = FAKE_SEG_ID
proj_obj.get_project_name.return_value = FAKE_PROJECT_NAME
self.dfa_mech_drvr.create_subnet_postcommit(subnet_ctxt)
self.assertTrue(self.dfa_mech_drvr._dcnm_client.create_network.called)
def test_update_port_postcommit(self):
port_ctxt = self._create_port_context()
query = port_ctxt._session.query.return_value
query.filter_by.return_value.one.return_value.forwarding_mode = (
FAKE_FWD_MODE)
vm_info = {
'status': 'up',
'ip': port_ctxt.current.get('fixed_ips')[0]['ip_address'],
'mac': port_ctxt.current.get('mac_address'),
'segid': FAKE_SEG_ID,
'inst_name': FAKE_INSTANCE_NAME,
'inst_uuid': port_ctxt.current.get('device_id').replace('-', ''),
'host': FAKE_HOST_ID,
'port_id': port_ctxt.current.get('id'),
'network_id': port_ctxt.current.get('network_id'),
'oui_type': 'cisco',
}
self.proj_info.get_network_segid.return_value = FAKE_SEG_ID
mechdrvr_mock = self.dfa_mech_drvr._inst_api.get_instance_for_uuid
mechdrvr_mock.return_value = FAKE_INSTANCE_NAME
self.dfa_mech_drvr.dfa_notifier = mock.MagicMock()
self.dfa_mech_drvr.update_port_postcommit(port_ctxt)
self.assertTrue(self.dfa_mech_drvr.dfa_notifier.send_vm_info.called)
self.dfa_mech_drvr.dfa_notifier.send_vm_info.assert_called_with(
port_ctxt._plugin_context, vm_info)
def test_delete_port_postcommit(self):
port_ctxt = self._create_port_context()
query = port_ctxt._session.query.return_value
query.filter_by.return_value.one.return_value.forwarding_mode = (
FAKE_FWD_MODE)
vm_info = {
'status': 'down',
'ip': port_ctxt.current.get('fixed_ips')[0]['ip_address'],
'mac': port_ctxt.current.get('mac_address'),
'segid': FAKE_SEG_ID,
'inst_name': FAKE_INSTANCE_NAME,
'inst_uuid': port_ctxt.current.get('device_id').replace('-', ''),
'host': FAKE_HOST_ID,
'port_id': port_ctxt.current.get('id'),
'network_id': port_ctxt.current.get('network_id'),
'oui_type': 'cisco',
}
self.proj_info.get_network_segid.return_value = FAKE_SEG_ID
instapi_mock = self.dfa_mech_drvr._inst_api.get_instance_for_uuid
instapi_mock.return_value = FAKE_INSTANCE_NAME
self.dfa_mech_drvr.dfa_notifier = mock.MagicMock()
self.dfa_mech_drvr.delete_port_postcommit(port_ctxt)
self.assertTrue(self.dfa_mech_drvr.dfa_notifier.send_vm_info.called)
self.dfa_mech_drvr.dfa_notifier.send_vm_info.assert_called_with(
port_ctxt._plugin_context, vm_info)

View File

@ -168,6 +168,7 @@ neutron.ml2.mechanism_drivers =
arista = neutron.plugins.ml2.drivers.arista.mechanism_arista:AristaDriver
cisco_nexus = neutron.plugins.ml2.drivers.cisco.nexus.mech_cisco_nexus:CiscoNexusMechanismDriver
cisco_apic = neutron.plugins.ml2.drivers.cisco.apic.mechanism_apic:APICMechanismDriver
cisco_dfa = neutron.plugins.ml2.drivers.cisco.dfa.mech_cisco_dfa:CiscoDfaMechanismDriver
l2population = neutron.plugins.ml2.drivers.l2pop.mech_driver:L2populationMechanismDriver
bigswitch = neutron.plugins.ml2.drivers.mech_bigswitch.driver:BigSwitchMechanismDriver
ofagent = neutron.plugins.ml2.drivers.mech_ofagent:OfagentMechanismDriver