New APIC mechanism and extension drivers
This is a very preliminary version of a new APIC mechanism driver utilizing the ACI Integration Module (AIM) library concurrently being developed. A corresponding extension driver exposes details regarding the mapping of the Neutron resources to APIC. These drivers require the Ml2Plus extended driver APIs. See the apic-aim-ml2-driver devref for implementation details and for devstack configuration instructions. Change-Id: I82df32f0880d6a0d53b305f6c6391fcbea049d1b
This commit is contained in:
parent
696005faae
commit
6e307e0a38
49
devstack/lib/apic_aim
Normal file
49
devstack/lib/apic_aim
Normal file
@ -0,0 +1,49 @@
|
||||
function install_apic_aim {
|
||||
echo_summary "Installing apic_aim"
|
||||
|
||||
install_apic_ml2
|
||||
install_aim
|
||||
install_opflex
|
||||
}
|
||||
|
||||
function configure_apic_aim {
|
||||
echo_summary "Configuring apic_aim"
|
||||
|
||||
# devstack/lib/neutron_plugins/ml2 does not allow overriding
|
||||
# Q_PLUGIN_CLASS in override_defaults, so do it here instread
|
||||
iniset $NEUTRON_CONF DEFAULT core_plugin ml2plus
|
||||
|
||||
iniset /$Q_PLUGIN_CONF_FILE apic_aim_auth auth_plugin v3password
|
||||
iniset /$Q_PLUGIN_CONF_FILE apic_aim_auth auth_url $KEYSTONE_SERVICE_URI_V3
|
||||
iniset /$Q_PLUGIN_CONF_FILE apic_aim_auth username admin
|
||||
iniset /$Q_PLUGIN_CONF_FILE apic_aim_auth password $ADMIN_PASSWORD
|
||||
iniset /$Q_PLUGIN_CONF_FILE apic_aim_auth user_domain_name default
|
||||
iniset /$Q_PLUGIN_CONF_FILE apic_aim_auth project_domain_name default
|
||||
iniset /$Q_PLUGIN_CONF_FILE apic_aim_auth project_name admin
|
||||
|
||||
init_aim
|
||||
}
|
||||
|
||||
function install_aim {
|
||||
git_clone $AIM_REPO $AIM_DIR $AIM_BRANCH
|
||||
mv $AIM_DIR/test-requirements.txt $AIM_DIR/_test-requirements.txt
|
||||
setup_develop $AIM_DIR
|
||||
mv $AIM_DIR/_test-requirements.txt $AIM_DIR/test-requirements.txt
|
||||
}
|
||||
|
||||
function init_aim {
|
||||
aim -c $NEUTRON_CONF db-migration upgrade
|
||||
}
|
||||
|
||||
function install_opflex {
|
||||
git_clone $OPFLEX_REPO $OPFLEX_DIR $OPFLEX_BRANCH
|
||||
mv $OPFLEX_DIR/test-requirements.txt $OPFLEX_DIR/_test-requirements.txt
|
||||
touch $OPFLEX_DIR/setup.cfg
|
||||
setup_develop $OPFLEX_DIR
|
||||
mv $OPFLEX_DIR/_test-requirements.txt $OPFLEX_DIR/test-requirements.txt
|
||||
}
|
||||
|
||||
# Tell emacs to use shell-script-mode
|
||||
## Local variables:
|
||||
## mode: shell-script
|
||||
## End:
|
@ -26,6 +26,8 @@ AIM_REPO=http://github.com/noironetworks/aci-integration-module.git
|
||||
AIM_DIR=$DEST/aim
|
||||
APICML2_REPO=http://github.com/noironetworks/apic-ml2-driver.git
|
||||
APICML2_DIR=$DEST/apic_ml2
|
||||
OPFLEX_REPO=http://github.com/noironetworks/python-opflex-agent.git
|
||||
OPFLEX_DIR=$DEST/opflexagent
|
||||
|
||||
# Save trace setting
|
||||
XTRACE=$(set +o | grep xtrace)
|
||||
@ -81,17 +83,6 @@ function install_gbpui {
|
||||
mv $GBPUI_DIR/_test-requirements.txt $GBPUI_DIR/test-requirements.txt
|
||||
}
|
||||
|
||||
function install_aim {
|
||||
git_clone $AIM_REPO $AIM_DIR $AIM_BRANCH
|
||||
mv $AIM_DIR/test-requirements.txt $AIM_DIR/_test-requirements.txt
|
||||
setup_develop $AIM_DIR
|
||||
mv $AIM_DIR/_test-requirements.txt $AIM_DIR/test-requirements.txt
|
||||
}
|
||||
|
||||
function init_aim {
|
||||
aim -c $NEUTRON_CONF db-migration upgrade
|
||||
}
|
||||
|
||||
function install_apic_ml2 {
|
||||
git_clone $APICML2_REPO $APICML2_DIR $APICML2_BRANCH
|
||||
mv $APICML2_DIR/test-requirements.txt $APICML2_DIR/_test-requirements.txt
|
||||
|
@ -1 +1,13 @@
|
||||
NEUTRON_CREATE_INITIAL_NETWORKS="False"
|
||||
|
||||
ENABLE_APIC_AIM=${ENABLE_APIC_AIM:-False}
|
||||
|
||||
if [[ $ENABLE_APIC_AIM = True ]]; then
|
||||
echo_summary "Overriding defaults for apic_aim"
|
||||
|
||||
Q_PLUGIN=${Q_PLUGIN:-ml2}
|
||||
Q_ML2_TENANT_NETWORK_TYPE=${Q_ML2_TENANT_NETWORK_TYPE:-opflex}
|
||||
Q_ML2_PLUGIN_TYPE_DRIVERS=${Q_ML2_PLUGIN_TYPE_DRIVERS:-local,vlan,opflex}
|
||||
Q_ML2_PLUGIN_MECHANISM_DRIVERS=${Q_ML2_PLUGIN_MECHANISM_DRIVERS:-apic_aim}
|
||||
Q_ML2_PLUGIN_EXT_DRIVERS=${Q_ML2_PLUGIN_EXT_DRIVERS-apic_aim,port_security}
|
||||
fi
|
||||
|
@ -49,6 +49,7 @@ if is_service_enabled group-policy; then
|
||||
echo_summary "Preparing $GBP"
|
||||
elif [[ "$1" == "stack" && "$2" == "install" ]]; then
|
||||
echo_summary "Installing $GBP"
|
||||
[[ $ENABLE_APIC_AIM = True ]] && install_apic_aim
|
||||
if [[ $ENABLE_NFP = True ]]; then
|
||||
echo_summary "Installing $NFP"
|
||||
[[ $DISABLE_BUILD_IMAGE = False ]] && prepare_nfp_image_builder
|
||||
@ -60,9 +61,8 @@ if is_service_enabled group-policy; then
|
||||
gbp_configure_neutron
|
||||
[[ $ENABLE_NFP = True ]] && echo_summary "Configuring $NFP"
|
||||
[[ $ENABLE_NFP = True ]] && nfp_configure_neutron
|
||||
# install_apic_ml2
|
||||
# install_aim
|
||||
# init_aim
|
||||
# REVISIT move installs to install phase?
|
||||
# install_apic_ml2
|
||||
install_gbpclient
|
||||
install_gbpservice
|
||||
[[ $ENABLE_NFP = True ]] && install_nfpgbpservice
|
||||
@ -70,6 +70,7 @@ if is_service_enabled group-policy; then
|
||||
[[ $ENABLE_NFP = True ]] && init_nfpgbpservice
|
||||
install_gbpheat
|
||||
install_gbpui
|
||||
[[ $ENABLE_APIC_AIM = True ]] && configure_apic_aim
|
||||
stop_apache_server
|
||||
start_apache_server
|
||||
elif [[ "$1" == "stack" && "$2" == "extra" ]]; then
|
||||
|
@ -1,5 +1,8 @@
|
||||
# Make sure the plugin name in local.conf is "gbp", as in: enable_plugin gbp <remote> <branch>
|
||||
source $DEST/gbp/devstack/lib/gbp
|
||||
|
||||
[[ $ENABLE_APIC_AIM = True ]] && source $DEST/gbp/devstack/lib/apic_aim
|
||||
|
||||
ENABLE_NFP=${ENABLE_NFP:-False}
|
||||
[[ $ENABLE_NFP = True ]] && source $DEST/gbp/devstack/lib/nfp
|
||||
[[ $ENABLE_NFP = True ]] && DISABLE_BUILD_IMAGE=${DISABLE_BUILD_IMAGE:-False}
|
||||
@ -21,14 +24,20 @@ GBPHEAT_REPO=${GBPHEAT_REPO:-${GIT_BASE}/openstack/group-based-policy-automation
|
||||
GBPHEAT_BRANCH=${GBPHEAT_BRANCH:-master}
|
||||
AIM_BRANCH=${AIM_BRANCH:-master}
|
||||
APICML2_BRANCH=${APICML2_BRANCH:-master}
|
||||
OPFLEX_BRANCH=${OPFLEX_BRANCH:-master}
|
||||
|
||||
# Enable necessary services, including group-policy (and disable others)
|
||||
disable_service n-net
|
||||
enable_service n-novnc
|
||||
enable_service q-svc
|
||||
enable_service q-agt
|
||||
if [[ $ENABLE_APIC_AIM = True ]]; then
|
||||
disable_service q-agt
|
||||
disable_service q-l3
|
||||
else
|
||||
enable_service q-agt
|
||||
enable_service q-l3
|
||||
fi
|
||||
enable_service q-dhcp
|
||||
enable_service q-l3
|
||||
enable_service q-fwaas
|
||||
enable_service q-lbaas
|
||||
enable_service q-meta
|
||||
|
91
doc/source/devref/apic-aim-ml2-driver.rst
Normal file
91
doc/source/devref/apic-aim-ml2-driver.rst
Normal file
@ -0,0 +1,91 @@
|
||||
..
|
||||
This work is licensed under a Creative Commons Attribution 3.0 Unported
|
||||
License.
|
||||
|
||||
http://creativecommons.org/licenses/by/3.0/legalcode
|
||||
|
||||
APIC-AIM ML2 Driver
|
||||
===================
|
||||
|
||||
The APIC-AIM ML2 mechanism driver and associated extension driver
|
||||
utilize the ACI Integration Module (AIM) library to provide improved
|
||||
integration between Neutron and the Cisco APIC. The most significant
|
||||
advantage of the APIC-AIM ML2 drivers over the previous APIC ML2
|
||||
drivers is that they are intended to coexist with the AIM GBP policy
|
||||
driver, providing full simultaneous support for both Neutron and GBP
|
||||
APIs within the same OpenStack deployment, including sharing of
|
||||
resources between Neutron and GBP workflows where appropriate.
|
||||
|
||||
Additionally, the AIM-based mechanism and policy driver architecture
|
||||
is completely transactional, and thus provides improved robustness,
|
||||
performance, and scalability. A set of replicated AIM daemons is
|
||||
responsible for continually maintaining consistency between the AIM DB
|
||||
state specified by the drivers and the APIC state.
|
||||
|
||||
ML2Plus Plugin
|
||||
--------------
|
||||
|
||||
The ML2Plus core plugin extends the ML2 plugin with several driver API
|
||||
features that are needed for APIC AIM support. An extended
|
||||
MechanismDriver abstract base class adds an ensure_tenant() method
|
||||
that is called before any transaction creating a new resource, and
|
||||
(soon) adds precommit and postcommit calls for operations on
|
||||
additional resources such as address scope. An extended
|
||||
ExtensionDriver base class will support extending those additional
|
||||
resources.
|
||||
|
||||
ML2 configuration is unchanged, and compatibility is maintained with
|
||||
all existing ML2 drivers.
|
||||
|
||||
APIC-AIM Mechanism Driver
|
||||
-------------------------
|
||||
|
||||
The apic-aim ML2 mechanism driver maps Neutron resources to the APIC
|
||||
resource configurations that provide the required Neutron networking
|
||||
semantics. Currently, the following Neutron -> AIM mappings are
|
||||
implemented:
|
||||
|
||||
tenant -> Tenant, ApplicationProfile
|
||||
network -> BridgeDomain, default EndpointGroup
|
||||
subnet -> Subnet
|
||||
|
||||
Neutron ports are realized as Endpoints within an APIC
|
||||
EndpointGroup. A port created using Neutron APIs belongs to the
|
||||
network's default EndpointGroup. A port created as a GBP PolicyTarget
|
||||
does not use its PolicyTargetGroup's L2Policy's network's default
|
||||
EndpointGroup, but instead belongs to an APIC EndpointGroup mapped
|
||||
from its PolicyTargetGroup.
|
||||
|
||||
Additional mappings that are under development include:
|
||||
|
||||
address scope -> VRF
|
||||
router -> contract rules
|
||||
|
||||
Port binding for the OpFlex L2 agent and support for the
|
||||
get_gbp_details RPC are implemented. DVS port binding and other RPCs
|
||||
remain to be implemented.
|
||||
|
||||
APIC-AIM Extension Driver
|
||||
-------------------------
|
||||
|
||||
The apic-aim ML2 extension driver provides administrators with read
|
||||
access to the distinguished names of the set of APIC resources to
|
||||
which each Neutron resource is mapped, as well as to APIC-specific
|
||||
status and AIM daemon synchronization status for those resources.
|
||||
|
||||
The extension driver may eventually also allow DNs of existing APIC
|
||||
resources to be specified when creating Neutron resources.
|
||||
|
||||
DevStack Support
|
||||
----------------
|
||||
|
||||
The ML2Plus core plugin and APIC-AIM mechanism and extension drivers
|
||||
can be configured by including the following in local.conf when
|
||||
running devstack::
|
||||
|
||||
enable_plugin gbp https://git.openstack.org/openstack/group-based-policy master
|
||||
|
||||
ENABLE_APIC_AIM=True
|
||||
|
||||
Note that the GBP devstack plugin installs the python-opflex-agent
|
||||
repo, but does not yet configure or run the OpFlex L2 agent.
|
@ -46,6 +46,7 @@ GBP Design
|
||||
shared-resources
|
||||
traffic-stitching-plumber-model
|
||||
traffic-stitching-plumber-placement-type
|
||||
apic-aim-ml2-driver
|
||||
|
||||
Module Reference
|
||||
----------------
|
||||
|
@ -41,6 +41,8 @@
|
||||
"get_network:provider:physical_network": "rule:admin_only",
|
||||
"get_network:provider:segmentation_id": "rule:admin_only",
|
||||
"get_network:queue_id": "rule:admin_only",
|
||||
"get_network:apic:distinguished_names": "rule:admin_only",
|
||||
"get_network:apic:synchronization_state": "rule:admin_only",
|
||||
"create_network:shared": "rule:admin_only",
|
||||
"create_network:router:external": "rule:admin_only",
|
||||
"create_network:segments": "rule:admin_only",
|
||||
|
@ -0,0 +1,133 @@
|
||||
# Copyright (c) 2016 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 re
|
||||
|
||||
from neutron._i18n import _LI
|
||||
|
||||
LOG = None
|
||||
|
||||
|
||||
NAME_TYPE_TENANT = 'tenant'
|
||||
NAME_TYPE_NETWORK = 'network'
|
||||
NAME_TYPE_POLICY_TARGET_GROUP = 'policy_target_group'
|
||||
|
||||
MAX_APIC_NAME_LENGTH = 46
|
||||
|
||||
|
||||
# TODO(rkukura): This is name mapper is copied from the apicapi repo,
|
||||
# and modified to pass in resource names rather than calling the core
|
||||
# plugin to get them, and to use the existing DB session. We need
|
||||
# decide whether to make these changes in apicapi (maybe on a branch),
|
||||
# move this some other repo, or keep it here. The changes are not
|
||||
# backwards compatible. The implementation should also be cleaned up
|
||||
# and simplified. For example, sessions should be passed in place of
|
||||
# contexts, and the core plugin calls eliminated.
|
||||
|
||||
|
||||
def truncate(string, max_length):
|
||||
if max_length < 0:
|
||||
return ''
|
||||
return string[:max_length] if len(string) > max_length else string
|
||||
|
||||
|
||||
class APICNameMapper(object):
|
||||
def __init__(self, db, log):
|
||||
self.db = db
|
||||
self.min_suffix = 5
|
||||
global LOG
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
def mapper(name_type):
|
||||
"""Wrapper to land all the common operations between mappers."""
|
||||
def wrap(func):
|
||||
def inner(inst, session, resource_id, resource_name=None):
|
||||
saved_name = inst.db.get_apic_name(session,
|
||||
resource_id,
|
||||
name_type)
|
||||
if saved_name:
|
||||
result = saved_name[0]
|
||||
return result
|
||||
name = ''
|
||||
try:
|
||||
name = func(inst, session, resource_id, resource_name)
|
||||
except Exception as e:
|
||||
LOG.warn(("Exception in looking up name %s"), name_type)
|
||||
LOG.error(e.message)
|
||||
|
||||
purged_id = re.sub(r"-+", "-", resource_id)
|
||||
result = purged_id[:inst.min_suffix]
|
||||
if name:
|
||||
name = re.sub(r"-+", "-", name)
|
||||
# Keep as many uuid chars as possible
|
||||
id_suffix = "_" + result
|
||||
max_name_length = MAX_APIC_NAME_LENGTH - len(id_suffix)
|
||||
result = truncate(name, max_name_length) + id_suffix
|
||||
|
||||
result = truncate(result, MAX_APIC_NAME_LENGTH)
|
||||
# Remove forbidden whitespaces
|
||||
result = result.replace(' ', '')
|
||||
result = inst._grow_id_if_needed(
|
||||
session, purged_id, name_type, result,
|
||||
start=inst.min_suffix)
|
||||
else:
|
||||
result = purged_id
|
||||
|
||||
inst.db.add_apic_name(session, resource_id,
|
||||
name_type, result)
|
||||
return result
|
||||
return inner
|
||||
return wrap
|
||||
|
||||
def _grow_id_if_needed(self, session, resource_id, name_type,
|
||||
current_result, start=0):
|
||||
result = current_result
|
||||
if result.endswith('_'):
|
||||
result = result[:-1]
|
||||
try:
|
||||
x = 0
|
||||
while True:
|
||||
if self.db.get_filtered_apic_names(session,
|
||||
neutron_type=name_type,
|
||||
apic_name=result):
|
||||
if x == 0 and start == 0:
|
||||
result += '_'
|
||||
# This name overlaps, add more ID characters
|
||||
result += resource_id[start + x]
|
||||
x += 1
|
||||
else:
|
||||
break
|
||||
except AttributeError:
|
||||
LOG.info(_LI("Current DB API doesn't support "
|
||||
"get_filtered_apic_names."))
|
||||
except IndexError:
|
||||
LOG.debug("Ran out of ID characters.")
|
||||
return result
|
||||
|
||||
@mapper(NAME_TYPE_TENANT)
|
||||
def tenant(self, session, tenant_id, tenant_name=None):
|
||||
return tenant_name
|
||||
|
||||
@mapper(NAME_TYPE_NETWORK)
|
||||
def network(self, session, network_id, network_name=None):
|
||||
return network_name
|
||||
|
||||
@mapper(NAME_TYPE_POLICY_TARGET_GROUP)
|
||||
def policy_target_group(self, session, policy_target_group_id,
|
||||
policy_target_group_name=None):
|
||||
return policy_target_group_name
|
||||
|
||||
def delete_apic_name(self, session, object_id):
|
||||
self.db.delete_apic_name(session, object_id)
|
82
gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/cache.py
Normal file
82
gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/cache.py
Normal file
@ -0,0 +1,82 @@
|
||||
# Copyright (c) 2016 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 import auth as ksc_auth
|
||||
from keystoneclient import session as ksc_session
|
||||
from keystoneclient.v3 import client as ksc_client
|
||||
from neutron._i18n import _LW
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
# REVISIT(rkukura): We use keystone to get the name of the keystone
|
||||
# project owning each neutron resource, which by default, requires
|
||||
# admin. If we keep this, we should probably move it to a separate
|
||||
# config module. But we should also investigate whether admin is even
|
||||
# needed, or if neutron's credentials could somehow be used.
|
||||
AUTH_GROUP = 'apic_aim_auth'
|
||||
ksc_session.Session.register_conf_options(cfg.CONF, AUTH_GROUP)
|
||||
ksc_auth.register_conf_options(cfg.CONF, AUTH_GROUP)
|
||||
|
||||
|
||||
class ProjectNameCache(object):
|
||||
"""Cache of Keystone project ID to project name mappings."""
|
||||
|
||||
def __init__(self):
|
||||
self.project_names = {}
|
||||
self.keystone = None
|
||||
|
||||
def ensure_project(self, project_id):
|
||||
"""Ensure cache contains mapping for project.
|
||||
|
||||
:param project_id: ID of the project
|
||||
|
||||
Ensure that the cache contains a mapping for the project
|
||||
identified by project_id. If it is not, Keystone will be
|
||||
queried for the current list of projects, and any new mappings
|
||||
will be added to the cache. This method should never be called
|
||||
inside a transaction with a project_id not already in the
|
||||
cache.
|
||||
"""
|
||||
if project_id not in self.project_names:
|
||||
if self.keystone is None:
|
||||
LOG.debug("Getting keystone client")
|
||||
auth = ksc_auth.load_from_conf_options(cfg.CONF, AUTH_GROUP)
|
||||
LOG.debug("Got auth: %s" % auth)
|
||||
if not auth:
|
||||
LOG.warning(_LW('No auth_plugin configured in %s'),
|
||||
AUTH_GROUP)
|
||||
session = ksc_session.Session.load_from_conf_options(
|
||||
cfg.CONF, AUTH_GROUP, auth=auth)
|
||||
LOG.debug("Got session: %s" % session)
|
||||
self.keystone = ksc_client.Client(session=session)
|
||||
LOG.debug("Got client: %s" % self.keystone)
|
||||
LOG.debug("Calling project API")
|
||||
projects = self.keystone.projects.list()
|
||||
LOG.debug("Received projects: %s" % projects)
|
||||
for project in projects:
|
||||
self.project_names[project.id] = project.name
|
||||
|
||||
def get_project_name(self, project_id):
|
||||
"""Get name of project from cache.
|
||||
|
||||
:param project_id: ID of the project
|
||||
|
||||
Get the name of the project identified by project_id from the
|
||||
cache. If the cache contains project_id, the project's name is
|
||||
returned. If not, None is returned.
|
||||
"""
|
||||
return self.project_names.get(project_id)
|
@ -0,0 +1,57 @@
|
||||
# Copyright (c) 2016 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._i18n import _LI
|
||||
from neutron.api import extensions
|
||||
from neutron import manager as n_manager
|
||||
from neutron.plugins.ml2 import driver_api
|
||||
from oslo_log import log
|
||||
|
||||
from gbpservice.neutron.plugins.ml2plus.drivers.apic_aim import (
|
||||
extensions as extensions_pkg)
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class ApicExtensionDriver(driver_api.ExtensionDriver):
|
||||
|
||||
def __init__(self):
|
||||
LOG.info(_LI("APIC AIM ED __init__"))
|
||||
self._mechanism_driver = None
|
||||
|
||||
def initialize(self):
|
||||
LOG.info(_LI("APIC AIM ED initializing"))
|
||||
extensions.append_api_extensions_path(extensions_pkg.__path__)
|
||||
|
||||
@property
|
||||
def _md(self):
|
||||
if not self._mechanism_driver:
|
||||
# REVISIT(rkukura): It might be safer to search the MDs by
|
||||
# class rather than index by name, or to use a class
|
||||
# variable to find the instance.
|
||||
plugin = n_manager.NeutronManager.get_plugin()
|
||||
mech_mgr = plugin.mechanism_manager
|
||||
self._mechanism_driver = mech_mgr.mech_drivers['apic_aim'].obj
|
||||
return self._mechanism_driver
|
||||
|
||||
@property
|
||||
def extension_alias(self):
|
||||
return "cisco-apic"
|
||||
|
||||
def extend_network_dict(self, session, base_model, result):
|
||||
self._md.extend_network_dict(session, base_model, result)
|
||||
|
||||
def extend_subnet_dict(self, session, base_model, result):
|
||||
self._md.extend_subnet_dict(session, base_model, result)
|
@ -0,0 +1,69 @@
|
||||
# Copyright (c) 2016 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.api import extensions
|
||||
from neutron.api.v2 import attributes
|
||||
from neutron.extensions import address_scope
|
||||
|
||||
ALIAS = 'cisco-apic'
|
||||
|
||||
DIST_NAMES = 'apic:distinguished_names'
|
||||
SYNC_STATE = 'apic:synchronization_state'
|
||||
|
||||
BD = 'BridgeDomain'
|
||||
CTX = 'Context'
|
||||
EPG = 'EndpointGroup'
|
||||
SUBNET = 'Subnet'
|
||||
|
||||
SYNC_SYNCED = 'synced'
|
||||
SYNC_PENDING = 'pending'
|
||||
SYNC_FAILED = 'failed'
|
||||
|
||||
APIC_ATTRIBUTES = {
|
||||
DIST_NAMES: {'allow_post': False, 'allow_put': False, 'is_visible': True},
|
||||
SYNC_STATE: {'allow_post': False, 'allow_put': False, 'is_visible': True}
|
||||
}
|
||||
|
||||
EXTENDED_ATTRIBUTES_2_0 = {
|
||||
attributes.NETWORKS: APIC_ATTRIBUTES,
|
||||
attributes.SUBNETS: APIC_ATTRIBUTES,
|
||||
address_scope.ADDRESS_SCOPES: APIC_ATTRIBUTES
|
||||
}
|
||||
|
||||
|
||||
class Cisco_apic(extensions.ExtensionDescriptor):
|
||||
|
||||
@classmethod
|
||||
def get_name(cls):
|
||||
return "Cisco APIC"
|
||||
|
||||
@classmethod
|
||||
def get_alias(cls):
|
||||
return ALIAS
|
||||
|
||||
@classmethod
|
||||
def get_description(cls):
|
||||
return ("Extension exposing mapping of Neutron resources to Cisco "
|
||||
"APIC constructs")
|
||||
|
||||
@classmethod
|
||||
def get_updated(cls):
|
||||
return "2016-03-31T12:00:00-00:00"
|
||||
|
||||
def get_extended_resources(self, version):
|
||||
if version == "2.0":
|
||||
return EXTENDED_ATTRIBUTES_2_0
|
||||
else:
|
||||
return {}
|
@ -0,0 +1,589 @@
|
||||
# Copyright (c) 2016 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 aim import aim_manager
|
||||
from aim.api import resource as aim_resource
|
||||
from aim import context as aim_context
|
||||
from neutron._i18n import _LE
|
||||
from neutron._i18n import _LI
|
||||
from neutron._i18n import _LW
|
||||
from neutron.agent.linux import dhcp
|
||||
from neutron.common import constants as n_constants
|
||||
from neutron.common import rpc as n_rpc
|
||||
from neutron.db import models_v2
|
||||
from neutron.extensions import portbindings
|
||||
from neutron import manager
|
||||
from neutron.plugins.ml2 import driver_api as api
|
||||
from neutron.plugins.ml2 import rpc as ml2_rpc
|
||||
from opflexagent import constants as ofcst
|
||||
from opflexagent import rpc as o_rpc
|
||||
from oslo_log import log
|
||||
|
||||
from gbpservice.neutron.plugins.ml2plus import driver_api as api_plus
|
||||
from gbpservice.neutron.plugins.ml2plus.drivers.apic_aim import apic_mapper
|
||||
from gbpservice.neutron.plugins.ml2plus.drivers.apic_aim import cache
|
||||
from gbpservice.neutron.plugins.ml2plus.drivers.apic_aim.extensions import (
|
||||
cisco_apic)
|
||||
from gbpservice.neutron.plugins.ml2plus.drivers.apic_aim import model
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
AP_NAME = 'NeutronAP'
|
||||
AGENT_TYPE_DVS = 'DVS agent'
|
||||
VIF_TYPE_DVS = 'dvs'
|
||||
PROMISCUOUS_TYPES = [n_constants.DEVICE_OWNER_DHCP,
|
||||
n_constants.DEVICE_OWNER_LOADBALANCER]
|
||||
|
||||
|
||||
class ApicMechanismDriver(api_plus.MechanismDriver):
|
||||
|
||||
def __init__(self):
|
||||
LOG.info(_LI("APIC AIM MD __init__"))
|
||||
|
||||
def initialize(self):
|
||||
LOG.info(_LI("APIC AIM MD initializing"))
|
||||
self.project_name_cache = cache.ProjectNameCache()
|
||||
self.db = model.DbModel()
|
||||
self.name_mapper = apic_mapper.APICNameMapper(self.db, log)
|
||||
self.aim = aim_manager.AimManager()
|
||||
|
||||
# REVISIT(rkukura): Read from config or possibly from AIM?
|
||||
self.enable_dhcp_opt = True
|
||||
self.enable_metadata_opt = True
|
||||
|
||||
self._setup_opflex_rpc_listeners()
|
||||
|
||||
def _setup_opflex_rpc_listeners(self):
|
||||
self.opflex_endpoints = [o_rpc.GBPServerRpcCallback(self)]
|
||||
self.opflex_topic = o_rpc.TOPIC_OPFLEX
|
||||
self.opflex_conn = n_rpc.create_connection(new=True)
|
||||
self.opflex_conn.create_consumer(
|
||||
self.opflex_topic, self.opflex_endpoints, fanout=False)
|
||||
self.opflex_conn.consume_in_threads()
|
||||
|
||||
def ensure_tenant(self, plugin_context, tenant_id):
|
||||
LOG.info(_LI("APIC AIM MD ensuring tenant_id: %s"), tenant_id)
|
||||
|
||||
self.project_name_cache.ensure_project(tenant_id)
|
||||
|
||||
# TODO(rkukura): Move the following to precommit methods so
|
||||
# AIM tenants and application profiles are created whenever
|
||||
# needed.
|
||||
session = plugin_context.session
|
||||
with session.begin(subtransactions=True):
|
||||
project_name = self.project_name_cache.get_project_name(tenant_id)
|
||||
tenant_name = self.name_mapper.tenant(session, tenant_id,
|
||||
project_name)
|
||||
LOG.info(_LI("Mapped tenant_id %(id)s to %(apic_name)s"),
|
||||
{'id': tenant_id, 'apic_name': tenant_name})
|
||||
|
||||
aim_ctx = aim_context.AimContext(session)
|
||||
|
||||
tenant = aim_resource.Tenant(name=tenant_name)
|
||||
if not self.aim.get(aim_ctx, tenant):
|
||||
self.aim.create(aim_ctx, tenant)
|
||||
|
||||
ap = aim_resource.ApplicationProfile(tenant_name=tenant_name,
|
||||
name=AP_NAME)
|
||||
if not self.aim.get(aim_ctx, ap):
|
||||
self.aim.create(aim_ctx, ap)
|
||||
|
||||
def create_network_precommit(self, context):
|
||||
LOG.info(_LI("APIC AIM MD creating network: %s"), context.current)
|
||||
|
||||
session = context._plugin_context.session
|
||||
|
||||
tenant_id = context.current['tenant_id']
|
||||
tenant_name = self.name_mapper.tenant(session, tenant_id)
|
||||
LOG.info(_LI("Mapped tenant_id %(id)s to %(apic_name)s"),
|
||||
{'id': tenant_id, 'apic_name': tenant_name})
|
||||
|
||||
id = context.current['id']
|
||||
name = context.current['name']
|
||||
bd_name = self.name_mapper.network(session, id, name)
|
||||
LOG.info(_LI("Mapped network_id %(id)s with name %(name)s to "
|
||||
"%(apic_name)s"),
|
||||
{'id': id, 'name': name, 'apic_name': bd_name})
|
||||
|
||||
aim_ctx = aim_context.AimContext(session)
|
||||
|
||||
bd = aim_resource.BridgeDomain(tenant_name=tenant_name,
|
||||
name=bd_name)
|
||||
self.aim.create(aim_ctx, bd)
|
||||
|
||||
epg = aim_resource.EndpointGroup(tenant_name=tenant_name,
|
||||
app_profile_name=AP_NAME,
|
||||
name=bd_name)
|
||||
self.aim.create(aim_ctx, epg)
|
||||
|
||||
def delete_network_precommit(self, context):
|
||||
LOG.info(_LI("APIC AIM MD deleting network: %s"), context.current)
|
||||
|
||||
session = context._plugin_context.session
|
||||
|
||||
tenant_id = context.current['tenant_id']
|
||||
tenant_name = self.name_mapper.tenant(session, tenant_id)
|
||||
LOG.info(_LI("Mapped tenant_id %(id)s to %(apic_name)s"),
|
||||
{'id': tenant_id, 'apic_name': tenant_name})
|
||||
|
||||
id = context.current['id']
|
||||
bd_name = self.name_mapper.network(session, id)
|
||||
LOG.info(_LI("Mapped network_id %(id)s to %(apic_name)s"),
|
||||
{'id': id, 'apic_name': bd_name})
|
||||
|
||||
aim_ctx = aim_context.AimContext(session)
|
||||
|
||||
epg = aim_resource.EndpointGroup(tenant_name=tenant_name,
|
||||
app_profile_name=AP_NAME,
|
||||
name=bd_name)
|
||||
self.aim.delete(aim_ctx, epg)
|
||||
|
||||
bd = aim_resource.BridgeDomain(tenant_name=tenant_name,
|
||||
name=bd_name)
|
||||
self.aim.delete(aim_ctx, bd)
|
||||
|
||||
self.name_mapper.delete_apic_name(session, id)
|
||||
|
||||
def extend_network_dict(self, session, base_model, result):
|
||||
LOG.info(_LI("APIC AIM MD extending dict for network: %s"), result)
|
||||
|
||||
sync_state = cisco_apic.SYNC_SYNCED
|
||||
|
||||
tenant_id = result['tenant_id']
|
||||
tenant_name = self.name_mapper.tenant(session, tenant_id)
|
||||
LOG.info(_LI("Mapped tenant_id %(id)s to %(apic_name)s"),
|
||||
{'id': tenant_id, 'apic_name': tenant_name})
|
||||
|
||||
id = result['id']
|
||||
name = result['name']
|
||||
bd_name = self.name_mapper.network(session, id, name)
|
||||
LOG.info(_LI("Mapped network_id %(id)s with name %(name)s to "
|
||||
"%(apic_name)s"),
|
||||
{'id': id, 'name': name, 'apic_name': bd_name})
|
||||
|
||||
aim_ctx = aim_context.AimContext(session)
|
||||
|
||||
bd = aim_resource.BridgeDomain(tenant_name=tenant_name,
|
||||
name=bd_name)
|
||||
bd = self.aim.get(aim_ctx, bd)
|
||||
LOG.debug("got BD with DN: %s", bd.dn)
|
||||
|
||||
epg = aim_resource.EndpointGroup(tenant_name=tenant_name,
|
||||
app_profile_name=AP_NAME,
|
||||
name=bd_name)
|
||||
epg = self.aim.get(aim_ctx, epg)
|
||||
LOG.debug("got EPG with DN: %s", epg.dn)
|
||||
|
||||
result[cisco_apic.DIST_NAMES] = {cisco_apic.BD: bd.dn,
|
||||
cisco_apic.EPG: epg.dn}
|
||||
|
||||
bd_status = self.aim.get_status(aim_ctx, bd)
|
||||
self._merge_status(sync_state, bd_status)
|
||||
epg_status = self.aim.get_status(aim_ctx, epg)
|
||||
self._merge_status(sync_state, epg_status)
|
||||
result[cisco_apic.SYNC_STATE] = sync_state
|
||||
|
||||
def create_subnet_precommit(self, context):
|
||||
LOG.info(_LI("APIC AIM MD creating subnet: %s"), context.current)
|
||||
|
||||
# REVISIT(rkukura): Do we need to do any of the
|
||||
# constraints/scope stuff?
|
||||
|
||||
gateway_ip_mask = self._gateway_ip_mask(context.current)
|
||||
if gateway_ip_mask:
|
||||
session = context._plugin_context.session
|
||||
|
||||
network_id = context.current['network_id']
|
||||
# REVISIT(rkukura): Should Ml2Plus extend SubnetContext
|
||||
# with network?
|
||||
network = (session.query(models_v2.Network).
|
||||
filter_by(id=network_id).
|
||||
one())
|
||||
|
||||
tenant_id = network.tenant_id
|
||||
tenant_name = self.name_mapper.tenant(session, tenant_id)
|
||||
LOG.info(_LI("Mapped tenant_id %(id)s to %(apic_name)s"),
|
||||
{'id': tenant_id, 'apic_name': tenant_name})
|
||||
|
||||
network_name = network.name
|
||||
bd_name = self.name_mapper.network(session, network_id,
|
||||
network_name)
|
||||
LOG.info(_LI("Mapped network_id %(id)s with name %(name)s to "
|
||||
"%(apic_name)s"),
|
||||
{'id': network_id, 'name': network_name,
|
||||
'apic_name': bd_name})
|
||||
|
||||
aim_ctx = aim_context.AimContext(session)
|
||||
|
||||
subnet = aim_resource.Subnet(tenant_name=tenant_name,
|
||||
bd_name=bd_name,
|
||||
gw_ip_mask=gateway_ip_mask)
|
||||
subnet = self.aim.create(aim_ctx, subnet)
|
||||
subnet_dn = subnet.dn
|
||||
subnet_status = self.aim.get_status(aim_ctx, subnet)
|
||||
sync_state = cisco_apic.SYNC_SYNCED
|
||||
self._merge_status(sync_state, subnet_status)
|
||||
|
||||
# ML2 does not extend subnet dict after precommit.
|
||||
context.current[cisco_apic.DIST_NAMES] = {cisco_apic.SUBNET:
|
||||
subnet_dn}
|
||||
context.current[cisco_apic.SYNC_STATE] = sync_state
|
||||
|
||||
def update_subnet_precommit(self, context):
|
||||
LOG.info(_LI("APIC AIM MD updating subnet: %s"), context.current)
|
||||
|
||||
if context.current['gateway_ip'] != context.original['gateway_ip']:
|
||||
session = context._plugin_context.session
|
||||
|
||||
network_id = context.current['network_id']
|
||||
# REVISIT(rkukura): Should Ml2Plus extend SubnetContext
|
||||
# with network?
|
||||
network = (session.query(models_v2.Network).
|
||||
filter_by(id=network_id).
|
||||
one())
|
||||
|
||||
tenant_id = network.tenant_id
|
||||
tenant_name = self.name_mapper.tenant(session, tenant_id)
|
||||
LOG.info(_LI("Mapped tenant_id %(id)s to %(apic_name)s"),
|
||||
{'id': tenant_id, 'apic_name': tenant_name})
|
||||
|
||||
network_name = network.name
|
||||
bd_name = self.name_mapper.network(session, network_id,
|
||||
network_name)
|
||||
LOG.info(_LI("Mapped network_id %(id)s with name %(name)s to "
|
||||
"%(apic_name)s"),
|
||||
{'id': network_id, 'name': network_name,
|
||||
'apic_name': bd_name})
|
||||
|
||||
aim_ctx = aim_context.AimContext(session)
|
||||
|
||||
gateway_ip_mask = self._gateway_ip_mask(context.original)
|
||||
if gateway_ip_mask:
|
||||
subnet = aim_resource.Subnet(tenant_name=tenant_name,
|
||||
bd_name=bd_name,
|
||||
gw_ip_mask=gateway_ip_mask)
|
||||
self.aim.delete(aim_ctx, subnet)
|
||||
|
||||
gateway_ip_mask = self._gateway_ip_mask(context.current)
|
||||
if gateway_ip_mask:
|
||||
subnet = aim_resource.Subnet(tenant_name=tenant_name,
|
||||
bd_name=bd_name,
|
||||
gw_ip_mask=gateway_ip_mask)
|
||||
subnet = self.aim.create(aim_ctx, subnet)
|
||||
subnet_dn = subnet.dn
|
||||
subnet_status = self.aim.get_status(aim_ctx, subnet)
|
||||
sync_state = cisco_apic.SYNC_SYNCED
|
||||
self._merge_status(sync_state, subnet_status)
|
||||
|
||||
# ML2 does not extend subnet dict after precommit.
|
||||
context.current[cisco_apic.DIST_NAMES] = {cisco_apic.SUBNET:
|
||||
subnet_dn}
|
||||
context.current[cisco_apic.SYNC_STATE] = sync_state
|
||||
|
||||
def delete_subnet_precommit(self, context):
|
||||
LOG.info(_LI("APIC AIM MD deleting subnet: %s"), context.current)
|
||||
|
||||
gateway_ip_mask = self._gateway_ip_mask(context.current)
|
||||
if gateway_ip_mask:
|
||||
session = context._plugin_context.session
|
||||
|
||||
network_id = context.current['network_id']
|
||||
# REVISIT(rkukura): Should Ml2Plus extend SubnetContext
|
||||
# with network?
|
||||
network = (session.query(models_v2.Network).
|
||||
filter_by(id=network_id).
|
||||
one())
|
||||
|
||||
tenant_id = network.tenant_id
|
||||
tenant_name = self.name_mapper.tenant(session, tenant_id)
|
||||
LOG.info(_LI("Mapped tenant_id %(id)s to %(apic_name)s"),
|
||||
{'id': tenant_id, 'apic_name': tenant_name})
|
||||
|
||||
network_name = network.name
|
||||
bd_name = self.name_mapper.network(session, network_id,
|
||||
network_name)
|
||||
LOG.info(_LI("Mapped network_id %(id)s with name %(name)s to "
|
||||
"%(apic_name)s"),
|
||||
{'id': network_id, 'name': network_name,
|
||||
'apic_name': bd_name})
|
||||
|
||||
aim_ctx = aim_context.AimContext(session)
|
||||
|
||||
subnet = aim_resource.Subnet(tenant_name=tenant_name,
|
||||
bd_name=bd_name,
|
||||
gw_ip_mask=gateway_ip_mask)
|
||||
self.aim.delete(aim_ctx, subnet)
|
||||
|
||||
def extend_subnet_dict(self, session, base_model, result):
|
||||
LOG.info(_LI("APIC AIM MD extending dict for subnet: %s"), result)
|
||||
|
||||
subnet_dn = None
|
||||
sync_state = cisco_apic.SYNC_SYNCED
|
||||
|
||||
gateway_ip_mask = self._gateway_ip_mask(result)
|
||||
if gateway_ip_mask:
|
||||
network_id = result['network_id']
|
||||
network = (session.query(models_v2.Network).
|
||||
filter_by(id=network_id).
|
||||
one())
|
||||
|
||||
tenant_id = network.tenant_id
|
||||
tenant_name = self.name_mapper.tenant(session, tenant_id)
|
||||
LOG.info(_LI("Mapped tenant_id %(id)s to %(apic_name)s"),
|
||||
{'id': tenant_id, 'apic_name': tenant_name})
|
||||
|
||||
network_name = network.name
|
||||
bd_name = self.name_mapper.network(session, network_id,
|
||||
network_name)
|
||||
LOG.info(_LI("Mapped network_id %(id)s with name %(name)s to "
|
||||
"%(apic_name)s"),
|
||||
{'id': network_id, 'name': network_name,
|
||||
'apic_name': bd_name})
|
||||
|
||||
aim_ctx = aim_context.AimContext(session)
|
||||
|
||||
subnet = aim_resource.Subnet(tenant_name=tenant_name,
|
||||
bd_name=bd_name,
|
||||
gw_ip_mask=gateway_ip_mask)
|
||||
subnet = self.aim.get(aim_ctx, subnet)
|
||||
if subnet:
|
||||
LOG.debug("got Subnet with DN: %s", subnet.dn)
|
||||
subnet_dn = subnet.dn
|
||||
subnet_status = self.aim.get_status(aim_ctx, subnet)
|
||||
self._merge_status(sync_state, subnet_status)
|
||||
else:
|
||||
# This should always get replaced with the real DN
|
||||
# during precommit.
|
||||
subnet_dn = "AIM Subnet not yet created"
|
||||
|
||||
result[cisco_apic.DIST_NAMES] = {cisco_apic.SUBNET: subnet_dn}
|
||||
result[cisco_apic.SYNC_STATE] = sync_state
|
||||
|
||||
def bind_port(self, context):
|
||||
LOG.debug("Attempting to bind port %(port)s on network %(net)s",
|
||||
{'port': context.current['id'],
|
||||
'net': context.network.current['id']})
|
||||
|
||||
# TODO(rkukura): Add support for baremetal hosts, SR-IOV and
|
||||
# other situations requiring dynamic segments.
|
||||
|
||||
# Check the VNIC type.
|
||||
vnic_type = context.current.get(portbindings.VNIC_TYPE,
|
||||
portbindings.VNIC_NORMAL)
|
||||
if vnic_type not in [portbindings.VNIC_NORMAL]:
|
||||
LOG.debug("Refusing to bind due to unsupported vnic_type: %s",
|
||||
vnic_type)
|
||||
return
|
||||
|
||||
# For compute ports, try to bind DVS agent first.
|
||||
if context.current['device_owner'].startswith('compute:'):
|
||||
if self._agent_bind_port(context, AGENT_TYPE_DVS,
|
||||
self._dvs_bind_port):
|
||||
return
|
||||
|
||||
# Try to bind OpFlex agent.
|
||||
self._agent_bind_port(context, ofcst.AGENT_TYPE_OPFLEX_OVS,
|
||||
self._opflex_bind_port)
|
||||
|
||||
def _agent_bind_port(self, context, agent_type, bind_strategy):
|
||||
for agent in context.host_agents(agent_type):
|
||||
LOG.debug("Checking agent: %s", agent)
|
||||
if agent['alive']:
|
||||
for segment in context.segments_to_bind:
|
||||
if bind_strategy(context, segment, agent):
|
||||
LOG.debug("Bound using segment: %s", segment)
|
||||
else:
|
||||
LOG.warning(_LW("Refusing to bind port %(port)s to dead "
|
||||
"agent: %(agent)s"),
|
||||
{'port': context.current['id'], 'agent': agent})
|
||||
|
||||
def _opflex_bind_port(self, context, segment, agent):
|
||||
network_type = segment[api.NETWORK_TYPE]
|
||||
if network_type == ofcst.TYPE_OPFLEX:
|
||||
opflex_mappings = agent['configurations'].get('opflex_networks')
|
||||
LOG.debug("Checking segment: %(segment)s "
|
||||
"for physical network: %(mappings)s ",
|
||||
{'segment': segment, 'mappings': opflex_mappings})
|
||||
if (opflex_mappings is not None and
|
||||
segment[api.PHYSICAL_NETWORK] not in opflex_mappings):
|
||||
return False
|
||||
elif network_type != 'local':
|
||||
return False
|
||||
|
||||
context.set_binding(segment[api.ID],
|
||||
portbindings.VIF_TYPE_OVS,
|
||||
{portbindings.CAP_PORT_FILTER: False,
|
||||
portbindings.OVS_HYBRID_PLUG: False})
|
||||
|
||||
def _dvs_bind_port(self, context, segment, agent):
|
||||
# TODO(rkukura): Implement DVS port binding
|
||||
return False
|
||||
|
||||
# RPC Method
|
||||
def get_gbp_details(self, context, **kwargs):
|
||||
LOG.debug("APIC AIM MD handling get_gbp_details for: %s", kwargs)
|
||||
try:
|
||||
return self._get_gbp_details(context, kwargs)
|
||||
except Exception as e:
|
||||
device = kwargs.get('device')
|
||||
LOG.error(_LE("An exception has occurred while retrieving device "
|
||||
"gbp details for %s"), device)
|
||||
LOG.exception(e)
|
||||
return {'device': device}
|
||||
|
||||
def request_endpoint_details(self, context, **kwargs):
|
||||
LOG.debug("APIC AIM MD handling get_endpoint_details for: %s", kwargs)
|
||||
try:
|
||||
request = kwargs.get('request')
|
||||
result = {'device': request['device'],
|
||||
'timestamp': request['timestamp'],
|
||||
'request_id': request['request_id'],
|
||||
'gbp_details': None,
|
||||
'neutron_details': None}
|
||||
result['gbp_details'] = self._get_gbp_details(context, request)
|
||||
result['neutron_details'] = ml2_rpc.RpcCallbacks(
|
||||
None, None).get_device_details(context, **request)
|
||||
return result
|
||||
except Exception as e:
|
||||
LOG.error(_LE("An exception has occurred while requesting device "
|
||||
"gbp details for %s"), request.get('device'))
|
||||
LOG.exception(e)
|
||||
return None
|
||||
|
||||
def _get_gbp_details(self, context, request):
|
||||
device = request.get('device')
|
||||
host = request.get('host')
|
||||
|
||||
core_plugin = manager.NeutronManager.get_plugin()
|
||||
port_id = core_plugin._device_to_port_id(context, device)
|
||||
port_context = core_plugin.get_bound_port_context(context, port_id,
|
||||
host)
|
||||
if not port_context:
|
||||
LOG.warning(_LW("Device %(device)s requested by agent "
|
||||
"%(agent_id)s not found in database"),
|
||||
{'device': port_id,
|
||||
'agent_id': request.get('agent_id')})
|
||||
return {'device': device}
|
||||
|
||||
port = port_context.current
|
||||
if port[portbindings.HOST_ID] != host:
|
||||
LOG.warning(_LW("Device %(device)s requested by agent "
|
||||
"%(agent_id)s not found bound for host %(host)s"),
|
||||
{'device': port_id, 'host': host,
|
||||
'agent_id': request.get('agent_id')})
|
||||
return
|
||||
|
||||
session = context.session
|
||||
with session.begin(subtransactions=True):
|
||||
# REVISIT(rkukura): Should AIM resources be
|
||||
# validated/created here if necessary? Also need to avoid
|
||||
# creating any new name mappings without first getting
|
||||
# their resource names.
|
||||
|
||||
# TODO(rkukura): For GBP, we need to use the EPG
|
||||
# associated with the port's PT's PTG. For now, we just use the
|
||||
# network's default EPG.
|
||||
|
||||
# TODO(rkukura): Use common tenant for shared networks.
|
||||
|
||||
# TODO(rkukura): Scope the tenant's AIM name.
|
||||
|
||||
network = port_context.network.current
|
||||
epg_tenant_name = self.name_mapper.tenant(session,
|
||||
network['tenant_id'])
|
||||
epg_name = self.name_mapper.network(session, network['id'], None)
|
||||
|
||||
promiscuous_mode = port['device_owner'] in PROMISCUOUS_TYPES
|
||||
|
||||
details = {'allowed_address_pairs': port['allowed_address_pairs'],
|
||||
'app_profile_name': AP_NAME,
|
||||
'device': device,
|
||||
'enable_dhcp_optimization': self.enable_dhcp_opt,
|
||||
'enable_metadata_optimization': self.enable_metadata_opt,
|
||||
'endpoint_group_name': epg_name,
|
||||
'host': host,
|
||||
'l3_policy_id': network['tenant_id'], # TODO(rkukura)
|
||||
'mac_address': port['mac_address'],
|
||||
'port_id': port_id,
|
||||
'promiscuous_mode': promiscuous_mode,
|
||||
'ptg_tenant': epg_tenant_name,
|
||||
'subnets': self._get_subnet_details(core_plugin, context,
|
||||
port)}
|
||||
|
||||
if port['device_owner'].startswith('compute:') and port['device_id']:
|
||||
# REVISIT(rkukura): Do we need to map to name using nova client?
|
||||
details['vm-name'] = port['device_id']
|
||||
|
||||
# TODO(rkukura): Mark active allowed_address_pairs
|
||||
|
||||
# TODO(rkukura): Add the following details common to the old
|
||||
# GBP and ML2 drivers: floating_ip, host_snat_ips, ip_mapping,
|
||||
# vrf_name, vrf_subnets, vrf_tenant.
|
||||
|
||||
# TODO(rkukura): Add the following details unique to the old
|
||||
# ML2 driver: attestation, interface_mtu.
|
||||
|
||||
# TODO(rkukura): Add the following details unique to the old
|
||||
# GBP driver: extra_details, extra_ips, fixed_ips,
|
||||
# l2_policy_id.
|
||||
|
||||
return details
|
||||
|
||||
def _get_subnet_details(self, core_plugin, context, port):
|
||||
subnets = core_plugin.get_subnets(
|
||||
context,
|
||||
filters={'id': [ip['subnet_id'] for ip in port['fixed_ips']]})
|
||||
for subnet in subnets:
|
||||
dhcp_ips = set()
|
||||
for port in core_plugin.get_ports(
|
||||
context, filters={
|
||||
'network_id': [subnet['network_id']],
|
||||
'device_owner': [n_constants.DEVICE_OWNER_DHCP]}):
|
||||
dhcp_ips |= set([x['ip_address'] for x in port['fixed_ips']
|
||||
if x['subnet_id'] == subnet['id']])
|
||||
dhcp_ips = list(dhcp_ips)
|
||||
if not subnet['dns_nameservers']:
|
||||
# Use DHCP namespace port IP
|
||||
subnet['dns_nameservers'] = dhcp_ips
|
||||
# Ser Default route if needed
|
||||
metadata = default = False
|
||||
if subnet['ip_version'] == 4:
|
||||
for route in subnet['host_routes']:
|
||||
if route['destination'] == '0.0.0.0/0':
|
||||
default = True
|
||||
if route['destination'] == dhcp.METADATA_DEFAULT_CIDR:
|
||||
metadata = True
|
||||
# Set missing routes
|
||||
if not default:
|
||||
subnet['host_routes'].append(
|
||||
{'destination': '0.0.0.0/0',
|
||||
'nexthop': subnet['gateway_ip']})
|
||||
if not metadata and dhcp_ips and not self.enable_metadata_opt:
|
||||
subnet['host_routes'].append(
|
||||
{'destination': dhcp.METADATA_DEFAULT_CIDR,
|
||||
'nexthop': dhcp_ips[0]})
|
||||
subnet['dhcp_server_ips'] = dhcp_ips
|
||||
return subnets
|
||||
|
||||
def _merge_status(self, sync_state, status):
|
||||
if status.is_error():
|
||||
sync_state = cisco_apic.SYNC_ERROR
|
||||
elif status.is_build() and sync_state is not cisco_apic.SYNC_ERROR:
|
||||
sync_state = cisco_apic.SYNC_BUILD
|
||||
|
||||
def _gateway_ip_mask(self, subnet):
|
||||
gateway_ip = subnet['gateway_ip']
|
||||
if gateway_ip:
|
||||
prefix_len = subnet['cidr'].split('/')[1]
|
||||
return gateway_ip + '/' + prefix_len
|
63
gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/model.py
Normal file
63
gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/model.py
Normal file
@ -0,0 +1,63 @@
|
||||
# Copyright (c) 2016 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 apic_ml2.neutron.plugins.ml2.drivers.cisco.apic import (
|
||||
apic_model as old_model)
|
||||
from neutron._i18n import _LI
|
||||
from oslo_log import log
|
||||
from sqlalchemy import orm
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
# REVISIT(rkukura): Temporarily using ApicName model defined in old
|
||||
# apic-ml2 driver with migration in neutron. We should define our
|
||||
# own, and may want to switch to per-resource name mapping tables with
|
||||
# foriegn keys.
|
||||
|
||||
class DbModel(object):
|
||||
|
||||
def __init__(self):
|
||||
LOG.info(_LI("APIC AIM DbModel __init__"))
|
||||
|
||||
def add_apic_name(self, session, neutron_id, neutron_type, apic_name):
|
||||
name = old_model.ApicName(neutron_id=neutron_id,
|
||||
neutron_type=neutron_type,
|
||||
apic_name=apic_name)
|
||||
with session.begin(subtransactions=True):
|
||||
session.add(name)
|
||||
|
||||
def get_apic_name(self, session, neutron_id, neutron_type):
|
||||
return session.query(old_model.ApicName.apic_name).filter_by(
|
||||
neutron_id=neutron_id, neutron_type=neutron_type).first()
|
||||
|
||||
def delete_apic_name(self, session, neutron_id):
|
||||
with session.begin(subtransactions=True):
|
||||
try:
|
||||
session.query(old_model.ApicName).filter_by(
|
||||
neutron_id=neutron_id).delete()
|
||||
except orm.exc.NoResultFound:
|
||||
return
|
||||
|
||||
def get_filtered_apic_names(self, session, neutron_id=None,
|
||||
neutron_type=None, apic_name=None):
|
||||
query = session.query(old_model.ApicName.apic_name)
|
||||
if neutron_id:
|
||||
query = query.filter_by(neutron_id=neutron_id)
|
||||
if neutron_type:
|
||||
query = query.filter_by(neutron_type=neutron_type)
|
||||
if apic_name:
|
||||
query = query.filter_by(apic_name=apic_name)
|
||||
return query.all()
|
294
gbpservice/neutron/tests/unit/plugins/ml2plus/test_apic_aim.py
Normal file
294
gbpservice/neutron/tests/unit/plugins/ml2plus/test_apic_aim.py
Normal file
@ -0,0 +1,294 @@
|
||||
# Copyright (c) 2016 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 aim.db import model_base as aim_model_base
|
||||
from keystoneclient.v3 import client as ksc_client
|
||||
from neutron import context
|
||||
from neutron.db import api as db_api
|
||||
from neutron import manager
|
||||
from neutron.plugins.ml2 import config
|
||||
from neutron.tests.unit.db import test_db_base_plugin_v2 as test_plugin
|
||||
from opflexagent import constants as ofcst
|
||||
|
||||
PLUGIN_NAME = 'gbpservice.neutron.plugins.ml2plus.plugin.Ml2PlusPlugin'
|
||||
|
||||
AGENT_CONF_OPFLEX = {'alive': True, 'binary': 'somebinary',
|
||||
'topic': 'sometopic',
|
||||
'agent_type': ofcst.AGENT_TYPE_OPFLEX_OVS,
|
||||
'configurations': {
|
||||
'opflex_networks': None,
|
||||
'bridge_mappings': {'physnet1': 'br-eth1'}}}
|
||||
|
||||
|
||||
# REVISIT(rkukura): Use mock for this instead?
|
||||
class FakeTenant(object):
|
||||
def __init__(self, id, name):
|
||||
self.id = id
|
||||
self.name = name
|
||||
|
||||
|
||||
class FakeProjectManager(object):
|
||||
def list(self):
|
||||
return [FakeTenant('test-tenant', 'TestTenantName'),
|
||||
FakeTenant('bad_tenant_id', 'BadTenantName')]
|
||||
|
||||
|
||||
class FakeKeystoneClient(object):
|
||||
def __init__(self, **kwargs):
|
||||
self.projects = FakeProjectManager()
|
||||
|
||||
|
||||
class ApicAimTestCase(test_plugin.NeutronDbPluginV2TestCase):
|
||||
|
||||
def setUp(self):
|
||||
# Enable the test mechanism driver to ensure that
|
||||
# we can successfully call through to all mechanism
|
||||
# driver apis.
|
||||
config.cfg.CONF.set_override('mechanism_drivers',
|
||||
['logger', 'apic_aim'],
|
||||
'ml2')
|
||||
config.cfg.CONF.set_override('extension_drivers',
|
||||
['apic_aim'],
|
||||
'ml2')
|
||||
config.cfg.CONF.set_override('type_drivers',
|
||||
['opflex', 'local', 'vlan'],
|
||||
'ml2')
|
||||
config.cfg.CONF.set_override('tenant_network_types',
|
||||
['opflex'],
|
||||
'ml2')
|
||||
config.cfg.CONF.set_override('network_vlan_ranges',
|
||||
['physnet1:1000:1099'],
|
||||
group='ml2_type_vlan')
|
||||
|
||||
super(ApicAimTestCase, self).setUp(PLUGIN_NAME)
|
||||
self.port_create_status = 'DOWN'
|
||||
|
||||
self.saved_keystone_client = ksc_client.Client
|
||||
ksc_client.Client = FakeKeystoneClient
|
||||
|
||||
engine = db_api.get_engine()
|
||||
aim_model_base.Base.metadata.create_all(engine)
|
||||
|
||||
self.plugin = manager.NeutronManager.get_plugin()
|
||||
self.plugin.start_rpc_listeners()
|
||||
self.driver = self.plugin.mechanism_manager.mech_drivers[
|
||||
'apic_aim'].obj
|
||||
|
||||
def tearDown(self):
|
||||
ksc_client.Client = self.saved_keystone_client
|
||||
super(ApicAimTestCase, self).tearDown()
|
||||
|
||||
|
||||
class TestApicExtension(ApicAimTestCase):
|
||||
def _verify_dn(self, dist_names, key, mo_types, id):
|
||||
dn = dist_names.get(key)
|
||||
self.assertIsInstance(dn, basestring)
|
||||
self.assertEqual('uni/', dn[:4])
|
||||
for mo_type in mo_types:
|
||||
self.assertIn('/' + mo_type + '-', dn)
|
||||
self.assertIn(id, dn)
|
||||
|
||||
def _verify_no_dn(self, dist_names, key):
|
||||
self.assertIn(key, dist_names)
|
||||
self.assertIsNone(dist_names.get(key))
|
||||
|
||||
def _verify_network_dist_names(self, net):
|
||||
id = net['id']
|
||||
dist_names = net.get('apic:distinguished_names')
|
||||
self.assertIsInstance(dist_names, dict)
|
||||
self._verify_dn(dist_names, 'BridgeDomain', ['tn', 'BD'], id[:5])
|
||||
self._verify_dn(dist_names, 'EndpointGroup', ['tn', 'ap', 'epg'],
|
||||
id[:5])
|
||||
|
||||
def test_network(self):
|
||||
# Test create.
|
||||
net = self._make_network(self.fmt, 'net1', True)['network']
|
||||
net_id = net['id']
|
||||
self._verify_network_dist_names(net)
|
||||
|
||||
# Test show.
|
||||
res = self._show('networks', net_id)['network']
|
||||
self._verify_network_dist_names(res)
|
||||
|
||||
# Test update.
|
||||
data = {'network': {'name': 'newnamefornet'}}
|
||||
res = self._update('networks', net_id, data)['network']
|
||||
self._verify_network_dist_names(res)
|
||||
|
||||
def _verify_subnet_dist_names(self, subnet):
|
||||
dist_names = subnet.get('apic:distinguished_names')
|
||||
self.assertIsInstance(dist_names, dict)
|
||||
if subnet['gateway_ip']:
|
||||
id = subnet['gateway_ip'] + '/' + subnet['cidr'].split('/')[1]
|
||||
self._verify_dn(dist_names, 'Subnet', ['tn', 'BD', 'subnet'], id)
|
||||
else:
|
||||
self._verify_no_dn(dist_names, 'Subnet')
|
||||
|
||||
def test_subnet_without_gw(self):
|
||||
# Test create without gateway.
|
||||
net = self._make_network(self.fmt, 'net', True)
|
||||
pools = [{'start': '10.0.0.2', 'end': '10.0.0.254'}]
|
||||
subnet = self._make_subnet(self.fmt, net, None,
|
||||
'10.0.0.0/24',
|
||||
allocation_pools=pools)['subnet']
|
||||
subnet_id = subnet['id']
|
||||
self._verify_subnet_dist_names(subnet)
|
||||
|
||||
# Test show.
|
||||
res = self._show('subnets', subnet_id)['subnet']
|
||||
self._verify_subnet_dist_names(res)
|
||||
|
||||
# Test update.
|
||||
data = {'subnet': {'name': 'newnameforsubnet'}}
|
||||
res = self._update('subnets', subnet_id, data)['subnet']
|
||||
self._verify_subnet_dist_names(res)
|
||||
|
||||
# Test update adding gateay.
|
||||
data = {'subnet': {'gateway_ip': '10.0.0.1'}}
|
||||
res = self._update('subnets', subnet_id, data)['subnet']
|
||||
self._verify_subnet_dist_names(res)
|
||||
|
||||
# Test show after adding gateway.
|
||||
res = self._show('subnets', subnet_id)['subnet']
|
||||
self._verify_subnet_dist_names(res)
|
||||
|
||||
def test_subnet_with_gw(self):
|
||||
# Test create.
|
||||
net = self._make_network(self.fmt, 'net', True)
|
||||
subnet = self._make_subnet(self.fmt, net, '10.0.1.1',
|
||||
'10.0.1.0/24')['subnet']
|
||||
subnet_id = subnet['id']
|
||||
self._verify_subnet_dist_names(subnet)
|
||||
|
||||
# Test show.
|
||||
res = self._show('subnets', subnet_id)['subnet']
|
||||
self._verify_subnet_dist_names(res)
|
||||
|
||||
# Test update.
|
||||
data = {'subnet': {'name': 'newnameforsubnet'}}
|
||||
res = self._update('subnets', subnet_id, data)['subnet']
|
||||
self._verify_subnet_dist_names(res)
|
||||
|
||||
# Test update removing gateway.
|
||||
data = {'subnet': {'gateway_ip': None}}
|
||||
res = self._update('subnets', subnet_id, data)['subnet']
|
||||
self._verify_subnet_dist_names(res)
|
||||
|
||||
# Test show after removing gateway.
|
||||
res = self._show('subnets', subnet_id)['subnet']
|
||||
self._verify_subnet_dist_names(res)
|
||||
|
||||
|
||||
class TestPortBinding(ApicAimTestCase):
|
||||
def _register_agent(self, host, agent_conf):
|
||||
agent = {'host': host}
|
||||
agent.update(agent_conf)
|
||||
self.plugin.create_or_update_agent(context.get_admin_context(), agent)
|
||||
|
||||
def _bind_port_to_host(self, port_id, host):
|
||||
data = {'port': {'binding:host_id': host,
|
||||
'device_owner': 'compute:',
|
||||
'device_id': 'someid'}}
|
||||
req = self.new_update_request('ports', data, port_id,
|
||||
self.fmt)
|
||||
return self.deserialize(self.fmt, req.get_response(self.api))
|
||||
|
||||
def test_bind_opflex_agent(self):
|
||||
self._register_agent('host1', AGENT_CONF_OPFLEX)
|
||||
net = self._make_network(self.fmt, 'net1', True)
|
||||
self._make_subnet(self.fmt, net, '10.0.1.1', '10.0.1.0/24')
|
||||
port = self._make_port(self.fmt, net['network']['id'])['port']
|
||||
port_id = port['id']
|
||||
port = self._bind_port_to_host(port_id, 'host1')['port']
|
||||
self.assertEqual('ovs', port['binding:vif_type'])
|
||||
self.assertEqual({'port_filter': False, 'ovs_hybrid_plug': False},
|
||||
port['binding:vif_details'])
|
||||
|
||||
# Verify get_gbp_details.
|
||||
device = 'tap%s' % port_id
|
||||
details = self.driver.get_gbp_details(context.get_admin_context(),
|
||||
device=device, host='host1')
|
||||
self.assertEqual(port['allowed_address_pairs'],
|
||||
details['allowed_address_pairs'])
|
||||
self.assertEqual('NeutronAP', details['app_profile_name'])
|
||||
self.assertEqual(device, details['device'])
|
||||
self.assertTrue(details['enable_dhcp_optimization'])
|
||||
self.assertTrue(details['enable_metadata_optimization'])
|
||||
self.assertIn('net1', details['endpoint_group_name'])
|
||||
self.assertEqual('host1', details['host'])
|
||||
self.assertEqual('test-tenant', details['l3_policy_id'])
|
||||
self.assertEqual(port['mac_address'], details['mac_address'])
|
||||
self.assertEqual(port_id, details['port_id'])
|
||||
self.assertFalse(details['promiscuous_mode'])
|
||||
self.assertIn('TestTenantName', details['ptg_tenant'])
|
||||
self.assertEqual(1, len(details['subnets']))
|
||||
self.assertEqual('someid', details['vm-name'])
|
||||
|
||||
# Verify request_endpoint_details.
|
||||
req_details = self.driver.request_endpoint_details(
|
||||
context.get_admin_context(),
|
||||
request={'device': 'tap%s' % port_id, 'host': 'host1',
|
||||
'timestamp': 0, 'request_id': 'a_request_id'})
|
||||
self.assertEqual(details, req_details['gbp_details'])
|
||||
self.assertEqual(port_id, req_details['neutron_details']['port_id'])
|
||||
|
||||
# TODO(rkukura): Verify subnet details. Also, test with
|
||||
# variations of DHCP IPs on subnet, dns_nameservers and
|
||||
# host_routes values, etc..
|
||||
|
||||
# TODO(rkukura): Add tests for promiscuous_mode cases.
|
||||
|
||||
def test_bind_unsupported_vnic_type(self):
|
||||
net = self._make_network(self.fmt, 'net1', True)
|
||||
self._make_subnet(self.fmt, net, '10.0.1.1', '10.0.1.0/24')
|
||||
vnic_arg = {'binding:vnic_type': 'macvtap'}
|
||||
port = self._make_port(self.fmt, net['network']['id'],
|
||||
arg_list=('binding:vnic_type',),
|
||||
**vnic_arg)['port']
|
||||
port = self._bind_port_to_host(port['id'], 'host1')['port']
|
||||
self.assertEqual('binding_failed', port['binding:vif_type'])
|
||||
|
||||
# TODO(rkukura): Add tests for opflex, local and unsupported
|
||||
# network_type values.
|
||||
|
||||
|
||||
class TestMl2BasicGet(test_plugin.TestBasicGet,
|
||||
ApicAimTestCase):
|
||||
pass
|
||||
|
||||
|
||||
class TestMl2V2HTTPResponse(test_plugin.TestV2HTTPResponse,
|
||||
ApicAimTestCase):
|
||||
pass
|
||||
|
||||
|
||||
class TestMl2PortsV2(test_plugin.TestPortsV2,
|
||||
ApicAimTestCase):
|
||||
pass
|
||||
|
||||
|
||||
class TestMl2NetworksV2(test_plugin.TestNetworksV2,
|
||||
ApicAimTestCase):
|
||||
pass
|
||||
|
||||
|
||||
class TestMl2SubnetsV2(test_plugin.TestSubnetsV2,
|
||||
ApicAimTestCase):
|
||||
pass
|
||||
|
||||
|
||||
class TestMl2SubnetPoolsV2(test_plugin.TestSubnetPoolsV2,
|
||||
ApicAimTestCase):
|
||||
pass
|
@ -35,8 +35,6 @@ from opflexagent import constants as ocst
|
||||
from oslo_config import cfg
|
||||
from oslo_serialization import jsonutils
|
||||
|
||||
sys.modules["apicapi"] = mock.Mock()
|
||||
|
||||
from gbpservice.neutron.plugins.ml2.drivers.grouppolicy.apic import driver
|
||||
from gbpservice.neutron.services.grouppolicy import (
|
||||
group_policy_context as p_context)
|
||||
@ -97,6 +95,8 @@ class ApicMappingTestCase(
|
||||
def setUp(self, sc_plugin=None, nat_enabled=True,
|
||||
pre_existing_l3out=False, default_agent_conf=True,
|
||||
ml2_options=None):
|
||||
self.saved_apicapi = sys.modules["apicapi"]
|
||||
sys.modules["apicapi"] = mock.Mock()
|
||||
if default_agent_conf:
|
||||
self.agent_conf = AGENT_CONF
|
||||
cfg.CONF.register_opts(sg_cfg.security_group_opts, 'SECURITYGROUP')
|
||||
@ -240,6 +240,10 @@ class ApicMappingTestCase(
|
||||
self.driver.apic_manager.apic.fvCtx.name = echo2
|
||||
self._db_plugin = n_db.NeutronDbPluginV2()
|
||||
|
||||
def tearDown(self):
|
||||
sys.modules["apicapi"] = self.saved_apicapi
|
||||
super(ApicMappingTestCase, self).tearDown()
|
||||
|
||||
def _build_external_dict(self, name, cidr_exposed, is_edge_nat=False):
|
||||
ext_info = {
|
||||
'enable_nat': 'True' if self.nat_enabled else 'False'
|
||||
|
@ -59,10 +59,13 @@ gbpservice.neutron.group_policy.policy_drivers =
|
||||
nuage_gbp_driver = gbpservice.neutron.services.grouppolicy.drivers.nuage.driver:NuageGBPDriver
|
||||
neutron.ml2.mechanism_drivers =
|
||||
logger_plus = gbpservice.neutron.tests.unit.plugins.ml2plus.drivers.mechanism_logger:LoggerPlusMechanismDriver
|
||||
apic_aim = gbpservice.neutron.plugins.ml2plus.drivers.apic_aim.mechanism_driver:ApicMechanismDriver
|
||||
apic_gbp = gbpservice.neutron.plugins.ml2.drivers.grouppolicy.apic.driver:APICMechanismGBPDriver
|
||||
nuage_gbp = gbpservice.neutron.plugins.ml2.drivers.grouppolicy.nuage.driver:NuageMechanismGBPDriver
|
||||
odl_gbp = gbpservice.neutron.plugins.ml2.drivers.grouppolicy.odl.driver:OdlMechanismGBPDriver
|
||||
stitching_gbp = gbpservice.neutron.plugins.ml2.drivers.grouppolicy.stitching.driver:TrafficStitchingMechanismGBPDriver
|
||||
neutron.ml2.extension_drivers =
|
||||
apic_aim = gbpservice.neutron.plugins.ml2plus.drivers.apic_aim.extension_driver:ApicExtensionDriver
|
||||
gbpservice.neutron.servicechain.servicechain_drivers =
|
||||
dummy = gbpservice.neutron.services.servicechain.plugins.msc.drivers.dummy_driver:NoopDriver
|
||||
simplechain_driver = gbpservice.neutron.services.servicechain.plugins.msc.drivers.simplechain_driver:SimpleChainDriver
|
||||
|
@ -27,6 +27,7 @@ os-testr>=0.4.1 # Apache-2.0
|
||||
ddt>=1.0.1 # MIT
|
||||
pylint==1.4.5 # GNU GPL v2
|
||||
reno>=0.1.1 # Apache2
|
||||
pyOpenSSL>=0.13.0,<=0.15.1
|
||||
|
||||
# Since version numbers for these are specified in
|
||||
# https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt,
|
||||
@ -35,3 +36,4 @@ python-heatclient
|
||||
python-keystoneclient
|
||||
|
||||
-e git+https://github.com/noironetworks/python-opflex-agent.git@master#egg=python-opflexagent-agent
|
||||
-e git+https://github.com/noironetworks/aci-integration-module.git#egg=aci-integration-module
|
||||
|
Loading…
Reference in New Issue
Block a user