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:
parent
2ae77d0329
commit
3680fd61c9
@ -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
|
||||
|
@ -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
|
||||
|
@ -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')
|
@ -1 +1 @@
|
||||
32f3915891fd
|
||||
469426cd2173
|
||||
|
@ -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
|
||||
|
0
neutron/plugins/ml2/drivers/cisco/dfa/__init__.py
Normal file
0
neutron/plugins/ml2/drivers/cisco/dfa/__init__.py
Normal file
109
neutron/plugins/ml2/drivers/cisco/dfa/cfg_profile_db_v2.py
Normal file
109
neutron/plugins/ml2/drivers/cisco/dfa/cfg_profile_db_v2.py
Normal 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)
|
303
neutron/plugins/ml2/drivers/cisco/dfa/cisco_dfa_rest.py
Normal file
303
neutron/plugins/ml2/drivers/cisco/dfa/cisco_dfa_rest.py
Normal 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)
|
53
neutron/plugins/ml2/drivers/cisco/dfa/config.py
Normal file
53
neutron/plugins/ml2/drivers/cisco/dfa/config.py
Normal 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
|
22
neutron/plugins/ml2/drivers/cisco/dfa/constants.py
Normal file
22
neutron/plugins/ml2/drivers/cisco/dfa/constants.py
Normal 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'
|
63
neutron/plugins/ml2/drivers/cisco/dfa/dfa_exceptions.py
Normal file
63
neutron/plugins/ml2/drivers/cisco/dfa/dfa_exceptions.py
Normal 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.")
|
140
neutron/plugins/ml2/drivers/cisco/dfa/dfa_instance_api.py
Normal file
140
neutron/plugins/ml2/drivers/cisco/dfa/dfa_instance_api.py
Normal 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
|
49
neutron/plugins/ml2/drivers/cisco/dfa/dfa_mech_driver_rpc.py
Normal file
49
neutron/plugins/ml2/drivers/cisco/dfa/dfa_mech_driver_rpc.py
Normal 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)
|
59
neutron/plugins/ml2/drivers/cisco/dfa/dfa_models_v2.py
Normal file
59
neutron/plugins/ml2/drivers/cisco/dfa/dfa_models_v2.py
Normal 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))
|
277
neutron/plugins/ml2/drivers/cisco/dfa/mech_cisco_dfa.py
Normal file
277
neutron/plugins/ml2/drivers/cisco/dfa/mech_cisco_dfa.py
Normal 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)
|
184
neutron/plugins/ml2/drivers/cisco/dfa/project_events.py
Normal file
184
neutron/plugins/ml2/drivers/cisco/dfa/project_events.py
Normal 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
|
@ -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']
|
153
neutron/tests/unit/ml2/drivers/cisco/dfa/test_cisco_dfa_rest.py
Normal file
153
neutron/tests/unit/ml2/drivers/cisco/dfa/test_cisco_dfa_rest.py
Normal 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)
|
272
neutron/tests/unit/ml2/drivers/cisco/dfa/test_mech_cisco_dfa.py
Normal file
272
neutron/tests/unit/ml2/drivers/cisco/dfa/test_mech_cisco_dfa.py
Normal 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)
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user