]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Cisco DFA ML2 Mechanism Driver
authorNader Lahouti <nlahouti@cisco.com>
Sat, 28 Jun 2014 01:09:19 +0000 (18:09 -0700)
committerNader Lahouti <nlahouti@cisco.com>
Sun, 31 Aug 2014 12:28:00 +0000 (05:28 -0700)
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

21 files changed:
etc/neutron/plugins/ml2/ml2_conf.ini
etc/neutron/plugins/ml2/ml2_conf_cisco.ini
neutron/db/migration/alembic_migrations/versions/469426cd2173_cisco_dfa_mech_driver.py [new file with mode: 0644]
neutron/db/migration/alembic_migrations/versions/HEAD
neutron/db/migration/models/head.py
neutron/plugins/ml2/drivers/cisco/dfa/__init__.py [new file with mode: 0644]
neutron/plugins/ml2/drivers/cisco/dfa/cfg_profile_db_v2.py [new file with mode: 0644]
neutron/plugins/ml2/drivers/cisco/dfa/cisco_dfa_rest.py [new file with mode: 0644]
neutron/plugins/ml2/drivers/cisco/dfa/config.py [new file with mode: 0644]
neutron/plugins/ml2/drivers/cisco/dfa/constants.py [new file with mode: 0644]
neutron/plugins/ml2/drivers/cisco/dfa/dfa_exceptions.py [new file with mode: 0644]
neutron/plugins/ml2/drivers/cisco/dfa/dfa_instance_api.py [new file with mode: 0644]
neutron/plugins/ml2/drivers/cisco/dfa/dfa_mech_driver_rpc.py [new file with mode: 0644]
neutron/plugins/ml2/drivers/cisco/dfa/dfa_models_v2.py [new file with mode: 0644]
neutron/plugins/ml2/drivers/cisco/dfa/mech_cisco_dfa.py [new file with mode: 0644]
neutron/plugins/ml2/drivers/cisco/dfa/project_events.py [new file with mode: 0644]
neutron/plugins/ml2/drivers/cisco/dfa/projects_cache_db_v2.py [new file with mode: 0644]
neutron/tests/unit/ml2/drivers/cisco/dfa/__init__.py [new file with mode: 0644]
neutron/tests/unit/ml2/drivers/cisco/dfa/test_cisco_dfa_rest.py [new file with mode: 0644]
neutron/tests/unit/ml2/drivers/cisco/dfa/test_mech_cisco_dfa.py [new file with mode: 0644]
setup.cfg

index 54722df91d84c78859eb8784abe36ff3cab3c44f..15281f15201726bc74e6110d8a53a50dfeeadca0 100644 (file)
@@ -20,6 +20,7 @@
 # Example: mechanism_drivers = cisco,logger
 # Example: mechanism_drivers = openvswitch,brocade
 # Example: mechanism_drivers = linuxbridge,brocade
+# Example: mechanism_drivers = openvswitch,cisco_dfa
 
 [ml2_type_flat]
 # (ListOpt) List of physical_network names with which flat networks
index ea361d046f3a2323b802a0b3dc31525759944b53..16ab0251f78eb4b3c2995166997c1cad56996fb8 100644 (file)
 # 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 (file)
index 0000000..7d10ee7
--- /dev/null
@@ -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')
index b3519602ad1bcc24a021f6f85391fdfe5b3978d7..0868bebd206c9ea4ccba5a7eb6729cd2e4c9fce9 100644 (file)
@@ -1 +1 @@
-32f3915891fd
+469426cd2173
index 1d82bca79b7a0d75013fe0e532b7f13708b7a95e..6ed1bf1df255d894b436769fadee400f38383459 100644 (file)
@@ -59,6 +59,7 @@ from neutron.plugins.ml2.drivers.arista import db  # noqa
 from neutron.plugins.ml2.drivers.brocade.db import (  # noqa
     models as ml2_brocade_models)
 from neutron.plugins.ml2.drivers.cisco.apic import apic_model  # noqa
+from neutron.plugins.ml2.drivers.cisco.dfa import dfa_models_v2  # noqa
 from neutron.plugins.ml2.drivers.cisco.nexus import (  # noqa
     nexus_models_v2 as ml2_nexus_models_v2)
 from neutron.plugins.ml2.drivers import type_flat  # noqa
diff --git a/neutron/plugins/ml2/drivers/cisco/dfa/__init__.py b/neutron/plugins/ml2/drivers/cisco/dfa/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
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 (file)
index 0000000..430e54c
--- /dev/null
@@ -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 (file)
index 0000000..1572be7
--- /dev/null
@@ -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 (file)
index 0000000..82096ff
--- /dev/null
@@ -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 (file)
index 0000000..c051b61
--- /dev/null
@@ -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 (file)
index 0000000..bef4e16
--- /dev/null
@@ -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 (file)
index 0000000..a8f320b
--- /dev/null
@@ -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 (file)
index 0000000..c5a9921
--- /dev/null
@@ -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 (file)
index 0000000..adecd8f
--- /dev/null
@@ -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 (file)
index 0000000..909ad7e
--- /dev/null
@@ -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 (file)
index 0000000..7ea1c10
--- /dev/null
@@ -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 (file)
index 0000000..0ab597f
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
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 (file)
index 0000000..7e183cd
--- /dev/null
@@ -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 (file)
index 0000000..f23bc0e
--- /dev/null
@@ -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)
index d006b09a6f6024e5fb58e902d36ca3a51ce5efa2..306b698024b27f46c55b70467c9a0e2f20254a3c 100644 (file)
--- 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