From 6e307e0a388cc878b95c6e40f92faadf2ea074b8 Mon Sep 17 00:00:00 2001 From: Robert Kukura Date: Wed, 23 Mar 2016 13:21:49 -0400 Subject: [PATCH] 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 --- devstack/lib/apic_aim | 49 ++ devstack/lib/gbp | 13 +- devstack/override-defaults | 12 + devstack/plugin.sh | 9 +- devstack/settings | 13 +- doc/source/devref/apic-aim-ml2-driver.rst | 91 +++ doc/source/devref/index.rst | 1 + etc/policy.json | 2 + .../plugins/ml2plus/drivers/__init__.py | 0 .../ml2plus/drivers/apic_aim/__init__.py | 0 .../ml2plus/drivers/apic_aim/apic_mapper.py | 133 ++++ .../plugins/ml2plus/drivers/apic_aim/cache.py | 82 +++ .../drivers/apic_aim/extension_driver.py | 57 ++ .../drivers/apic_aim/extensions/__init__.py | 0 .../drivers/apic_aim/extensions/cisco_apic.py | 69 ++ .../drivers/apic_aim/mechanism_driver.py | 589 ++++++++++++++++++ .../plugins/ml2plus/drivers/apic_aim/model.py | 63 ++ .../unit/plugins/ml2plus/test_apic_aim.py | 294 +++++++++ .../services/grouppolicy/test_apic_mapping.py | 8 +- setup.cfg | 3 + test-requirements.txt | 2 + 21 files changed, 1471 insertions(+), 19 deletions(-) create mode 100644 devstack/lib/apic_aim create mode 100644 doc/source/devref/apic-aim-ml2-driver.rst create mode 100644 gbpservice/neutron/plugins/ml2plus/drivers/__init__.py create mode 100644 gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/__init__.py create mode 100644 gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/apic_mapper.py create mode 100644 gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/cache.py create mode 100644 gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/extension_driver.py create mode 100644 gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/extensions/__init__.py create mode 100644 gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/extensions/cisco_apic.py create mode 100644 gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/mechanism_driver.py create mode 100644 gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/model.py create mode 100644 gbpservice/neutron/tests/unit/plugins/ml2plus/test_apic_aim.py diff --git a/devstack/lib/apic_aim b/devstack/lib/apic_aim new file mode 100644 index 000000000..8c02b0690 --- /dev/null +++ b/devstack/lib/apic_aim @@ -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: diff --git a/devstack/lib/gbp b/devstack/lib/gbp index 1d3d41b5e..cb93d0735 100755 --- a/devstack/lib/gbp +++ b/devstack/lib/gbp @@ -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 diff --git a/devstack/override-defaults b/devstack/override-defaults index 09ac66b05..91a11bb8c 100755 --- a/devstack/override-defaults +++ b/devstack/override-defaults @@ -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 diff --git a/devstack/plugin.sh b/devstack/plugin.sh index b5471646f..e95b18158 100755 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -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,8 +70,9 @@ 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 + start_apache_server elif [[ "$1" == "stack" && "$2" == "extra" ]]; then echo_summary "Initializing $GBP" if [[ $ENABLE_NFP = True ]]; then diff --git a/devstack/settings b/devstack/settings index f1a42f19b..22296453e 100755 --- a/devstack/settings +++ b/devstack/settings @@ -1,5 +1,8 @@ # Make sure the plugin name in local.conf is "gbp", as in: enable_plugin gbp 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 diff --git a/doc/source/devref/apic-aim-ml2-driver.rst b/doc/source/devref/apic-aim-ml2-driver.rst new file mode 100644 index 000000000..f9c45c093 --- /dev/null +++ b/doc/source/devref/apic-aim-ml2-driver.rst @@ -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. diff --git a/doc/source/devref/index.rst b/doc/source/devref/index.rst index 379aafc91..7d16f0d86 100644 --- a/doc/source/devref/index.rst +++ b/doc/source/devref/index.rst @@ -46,6 +46,7 @@ GBP Design shared-resources traffic-stitching-plumber-model traffic-stitching-plumber-placement-type + apic-aim-ml2-driver Module Reference ---------------- diff --git a/etc/policy.json b/etc/policy.json index 490f898f3..9c51ce854 100644 --- a/etc/policy.json +++ b/etc/policy.json @@ -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", diff --git a/gbpservice/neutron/plugins/ml2plus/drivers/__init__.py b/gbpservice/neutron/plugins/ml2plus/drivers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/__init__.py b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/apic_mapper.py b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/apic_mapper.py new file mode 100644 index 000000000..b22f2461a --- /dev/null +++ b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/apic_mapper.py @@ -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) diff --git a/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/cache.py b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/cache.py new file mode 100644 index 000000000..424b02116 --- /dev/null +++ b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/cache.py @@ -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) diff --git a/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/extension_driver.py b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/extension_driver.py new file mode 100644 index 000000000..872ba8be0 --- /dev/null +++ b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/extension_driver.py @@ -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) diff --git a/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/extensions/__init__.py b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/extensions/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/extensions/cisco_apic.py b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/extensions/cisco_apic.py new file mode 100644 index 000000000..e8dbdab58 --- /dev/null +++ b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/extensions/cisco_apic.py @@ -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 {} diff --git a/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/mechanism_driver.py b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/mechanism_driver.py new file mode 100644 index 000000000..c7eddd9b9 --- /dev/null +++ b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/mechanism_driver.py @@ -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 diff --git a/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/model.py b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/model.py new file mode 100644 index 000000000..523eea9f3 --- /dev/null +++ b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/model.py @@ -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() diff --git a/gbpservice/neutron/tests/unit/plugins/ml2plus/test_apic_aim.py b/gbpservice/neutron/tests/unit/plugins/ml2plus/test_apic_aim.py new file mode 100644 index 000000000..09ecd8cb5 --- /dev/null +++ b/gbpservice/neutron/tests/unit/plugins/ml2plus/test_apic_aim.py @@ -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 diff --git a/gbpservice/neutron/tests/unit/services/grouppolicy/test_apic_mapping.py b/gbpservice/neutron/tests/unit/services/grouppolicy/test_apic_mapping.py index e8b6cd308..6f9cf2d8c 100644 --- a/gbpservice/neutron/tests/unit/services/grouppolicy/test_apic_mapping.py +++ b/gbpservice/neutron/tests/unit/services/grouppolicy/test_apic_mapping.py @@ -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' diff --git a/setup.cfg b/setup.cfg index e6ffb5cb7..91211f4ba 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 diff --git a/test-requirements.txt b/test-requirements.txt index 6ba5c7cfd..34dfc027e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -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