From 3680fd61c9a1cceaa011b4d0d37d235abf490c50 Mon Sep 17 00:00:00 2001 From: Nader Lahouti Date: Fri, 27 Jun 2014 18:09:19 -0700 Subject: [PATCH] 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 --- etc/neutron/plugins/ml2/ml2_conf.ini | 1 + etc/neutron/plugins/ml2/ml2_conf_cisco.ini | 14 + .../469426cd2173_cisco_dfa_mech_driver.py | 58 ++++ .../alembic_migrations/versions/HEAD | 2 +- neutron/db/migration/models/head.py | 1 + .../plugins/ml2/drivers/cisco/dfa/__init__.py | 0 .../drivers/cisco/dfa/cfg_profile_db_v2.py | 109 +++++++ .../ml2/drivers/cisco/dfa/cisco_dfa_rest.py | 303 ++++++++++++++++++ .../plugins/ml2/drivers/cisco/dfa/config.py | 53 +++ .../ml2/drivers/cisco/dfa/constants.py | 22 ++ .../ml2/drivers/cisco/dfa/dfa_exceptions.py | 63 ++++ .../ml2/drivers/cisco/dfa/dfa_instance_api.py | 140 ++++++++ .../drivers/cisco/dfa/dfa_mech_driver_rpc.py | 49 +++ .../ml2/drivers/cisco/dfa/dfa_models_v2.py | 59 ++++ .../ml2/drivers/cisco/dfa/mech_cisco_dfa.py | 277 ++++++++++++++++ .../ml2/drivers/cisco/dfa/project_events.py | 184 +++++++++++ .../drivers/cisco/dfa/projects_cache_db_v2.py | 95 ++++++ .../unit/ml2/drivers/cisco/dfa/__init__.py | 0 .../drivers/cisco/dfa/test_cisco_dfa_rest.py | 153 +++++++++ .../drivers/cisco/dfa/test_mech_cisco_dfa.py | 272 ++++++++++++++++ setup.cfg | 1 + 21 files changed, 1855 insertions(+), 1 deletion(-) create mode 100644 neutron/db/migration/alembic_migrations/versions/469426cd2173_cisco_dfa_mech_driver.py create mode 100644 neutron/plugins/ml2/drivers/cisco/dfa/__init__.py create mode 100644 neutron/plugins/ml2/drivers/cisco/dfa/cfg_profile_db_v2.py create mode 100644 neutron/plugins/ml2/drivers/cisco/dfa/cisco_dfa_rest.py create mode 100644 neutron/plugins/ml2/drivers/cisco/dfa/config.py create mode 100644 neutron/plugins/ml2/drivers/cisco/dfa/constants.py create mode 100644 neutron/plugins/ml2/drivers/cisco/dfa/dfa_exceptions.py create mode 100644 neutron/plugins/ml2/drivers/cisco/dfa/dfa_instance_api.py create mode 100644 neutron/plugins/ml2/drivers/cisco/dfa/dfa_mech_driver_rpc.py create mode 100644 neutron/plugins/ml2/drivers/cisco/dfa/dfa_models_v2.py create mode 100644 neutron/plugins/ml2/drivers/cisco/dfa/mech_cisco_dfa.py create mode 100644 neutron/plugins/ml2/drivers/cisco/dfa/project_events.py create mode 100644 neutron/plugins/ml2/drivers/cisco/dfa/projects_cache_db_v2.py create mode 100644 neutron/tests/unit/ml2/drivers/cisco/dfa/__init__.py create mode 100644 neutron/tests/unit/ml2/drivers/cisco/dfa/test_cisco_dfa_rest.py create mode 100644 neutron/tests/unit/ml2/drivers/cisco/dfa/test_mech_cisco_dfa.py diff --git a/etc/neutron/plugins/ml2/ml2_conf.ini b/etc/neutron/plugins/ml2/ml2_conf.ini index 54722df91..15281f152 100644 --- a/etc/neutron/plugins/ml2/ml2_conf.ini +++ b/etc/neutron/plugins/ml2/ml2_conf.ini @@ -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 diff --git a/etc/neutron/plugins/ml2/ml2_conf_cisco.ini b/etc/neutron/plugins/ml2/ml2_conf_cisco.ini index ea361d046..16ab0251f 100644 --- a/etc/neutron/plugins/ml2/ml2_conf_cisco.ini +++ b/etc/neutron/plugins/ml2/ml2_conf_cisco.ini @@ -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 diff --git a/neutron/db/migration/alembic_migrations/versions/469426cd2173_cisco_dfa_mech_driver.py b/neutron/db/migration/alembic_migrations/versions/469426cd2173_cisco_dfa_mech_driver.py new file mode 100644 index 000000000..7d10ee73c --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/469426cd2173_cisco_dfa_mech_driver.py @@ -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') diff --git a/neutron/db/migration/alembic_migrations/versions/HEAD b/neutron/db/migration/alembic_migrations/versions/HEAD index b3519602a..0868bebd2 100644 --- a/neutron/db/migration/alembic_migrations/versions/HEAD +++ b/neutron/db/migration/alembic_migrations/versions/HEAD @@ -1 +1 @@ -32f3915891fd +469426cd2173 diff --git a/neutron/db/migration/models/head.py b/neutron/db/migration/models/head.py index 1d82bca79..6ed1bf1df 100644 --- a/neutron/db/migration/models/head.py +++ b/neutron/db/migration/models/head.py @@ -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 diff --git a/neutron/plugins/ml2/drivers/cisco/dfa/__init__.py b/neutron/plugins/ml2/drivers/cisco/dfa/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron/plugins/ml2/drivers/cisco/dfa/cfg_profile_db_v2.py b/neutron/plugins/ml2/drivers/cisco/dfa/cfg_profile_db_v2.py new file mode 100644 index 000000000..430e54cbc --- /dev/null +++ b/neutron/plugins/ml2/drivers/cisco/dfa/cfg_profile_db_v2.py @@ -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) diff --git a/neutron/plugins/ml2/drivers/cisco/dfa/cisco_dfa_rest.py b/neutron/plugins/ml2/drivers/cisco/dfa/cisco_dfa_rest.py new file mode 100644 index 000000000..1572be754 --- /dev/null +++ b/neutron/plugins/ml2/drivers/cisco/dfa/cisco_dfa_rest.py @@ -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) diff --git a/neutron/plugins/ml2/drivers/cisco/dfa/config.py b/neutron/plugins/ml2/drivers/cisco/dfa/config.py new file mode 100644 index 000000000..82096ff33 --- /dev/null +++ b/neutron/plugins/ml2/drivers/cisco/dfa/config.py @@ -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 diff --git a/neutron/plugins/ml2/drivers/cisco/dfa/constants.py b/neutron/plugins/ml2/drivers/cisco/dfa/constants.py new file mode 100644 index 000000000..c051b61c2 --- /dev/null +++ b/neutron/plugins/ml2/drivers/cisco/dfa/constants.py @@ -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' diff --git a/neutron/plugins/ml2/drivers/cisco/dfa/dfa_exceptions.py b/neutron/plugins/ml2/drivers/cisco/dfa/dfa_exceptions.py new file mode 100644 index 000000000..bef4e16d1 --- /dev/null +++ b/neutron/plugins/ml2/drivers/cisco/dfa/dfa_exceptions.py @@ -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.") diff --git a/neutron/plugins/ml2/drivers/cisco/dfa/dfa_instance_api.py b/neutron/plugins/ml2/drivers/cisco/dfa/dfa_instance_api.py new file mode 100644 index 000000000..a8f320baf --- /dev/null +++ b/neutron/plugins/ml2/drivers/cisco/dfa/dfa_instance_api.py @@ -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 diff --git a/neutron/plugins/ml2/drivers/cisco/dfa/dfa_mech_driver_rpc.py b/neutron/plugins/ml2/drivers/cisco/dfa/dfa_mech_driver_rpc.py new file mode 100644 index 000000000..c5a99210e --- /dev/null +++ b/neutron/plugins/ml2/drivers/cisco/dfa/dfa_mech_driver_rpc.py @@ -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) diff --git a/neutron/plugins/ml2/drivers/cisco/dfa/dfa_models_v2.py b/neutron/plugins/ml2/drivers/cisco/dfa/dfa_models_v2.py new file mode 100644 index 000000000..adecd8f13 --- /dev/null +++ b/neutron/plugins/ml2/drivers/cisco/dfa/dfa_models_v2.py @@ -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)) diff --git a/neutron/plugins/ml2/drivers/cisco/dfa/mech_cisco_dfa.py b/neutron/plugins/ml2/drivers/cisco/dfa/mech_cisco_dfa.py new file mode 100644 index 000000000..909ad7e74 --- /dev/null +++ b/neutron/plugins/ml2/drivers/cisco/dfa/mech_cisco_dfa.py @@ -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) diff --git a/neutron/plugins/ml2/drivers/cisco/dfa/project_events.py b/neutron/plugins/ml2/drivers/cisco/dfa/project_events.py new file mode 100644 index 000000000..7ea1c10db --- /dev/null +++ b/neutron/plugins/ml2/drivers/cisco/dfa/project_events.py @@ -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 diff --git a/neutron/plugins/ml2/drivers/cisco/dfa/projects_cache_db_v2.py b/neutron/plugins/ml2/drivers/cisco/dfa/projects_cache_db_v2.py new file mode 100644 index 000000000..0ab597f4f --- /dev/null +++ b/neutron/plugins/ml2/drivers/cisco/dfa/projects_cache_db_v2.py @@ -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'] diff --git a/neutron/tests/unit/ml2/drivers/cisco/dfa/__init__.py b/neutron/tests/unit/ml2/drivers/cisco/dfa/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron/tests/unit/ml2/drivers/cisco/dfa/test_cisco_dfa_rest.py b/neutron/tests/unit/ml2/drivers/cisco/dfa/test_cisco_dfa_rest.py new file mode 100644 index 000000000..7e183cdd6 --- /dev/null +++ b/neutron/tests/unit/ml2/drivers/cisco/dfa/test_cisco_dfa_rest.py @@ -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) diff --git a/neutron/tests/unit/ml2/drivers/cisco/dfa/test_mech_cisco_dfa.py b/neutron/tests/unit/ml2/drivers/cisco/dfa/test_mech_cisco_dfa.py new file mode 100644 index 000000000..f23bc0e6f --- /dev/null +++ b/neutron/tests/unit/ml2/drivers/cisco/dfa/test_mech_cisco_dfa.py @@ -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) diff --git a/setup.cfg b/setup.cfg index d006b09a6..306b69802 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 -- 2.45.2