From c3b2c2a9a22bac5cf17813c589444d3abebaa23b Mon Sep 17 00:00:00 2001 From: Wojciech Dec Date: Tue, 16 Aug 2016 19:27:01 +0200 Subject: Adding Mitaka networking-old module with the ODL topology based port binding resolution mechanism from https://review.openstack.org/333186 Change-Id: I10d400aac9bb639c146527f0f93e6925cb74d9de Signed-off-by: Wojciech Dec --- networking-odl/networking_odl/ml2/README.odl | 41 ++ networking-odl/networking_odl/ml2/__init__.py | 0 .../networking_odl/ml2/legacy_port_binding.py | 84 ++++ networking-odl/networking_odl/ml2/mech_driver.py | 458 +++++++++++++++++++++ .../networking_odl/ml2/mech_driver_v2.py | 146 +++++++ .../networking_odl/ml2/network_topology.py | 313 ++++++++++++++ .../networking_odl/ml2/ovsdb_topology.py | 218 ++++++++++ networking-odl/networking_odl/ml2/port_binding.py | 121 ++++++ .../networking_odl/ml2/pseudo_agentdb_binding.py | 263 ++++++++++++ 9 files changed, 1644 insertions(+) create mode 100644 networking-odl/networking_odl/ml2/README.odl create mode 100644 networking-odl/networking_odl/ml2/__init__.py create mode 100644 networking-odl/networking_odl/ml2/legacy_port_binding.py create mode 100644 networking-odl/networking_odl/ml2/mech_driver.py create mode 100644 networking-odl/networking_odl/ml2/mech_driver_v2.py create mode 100644 networking-odl/networking_odl/ml2/network_topology.py create mode 100644 networking-odl/networking_odl/ml2/ovsdb_topology.py create mode 100644 networking-odl/networking_odl/ml2/port_binding.py create mode 100644 networking-odl/networking_odl/ml2/pseudo_agentdb_binding.py (limited to 'networking-odl/networking_odl/ml2') diff --git a/networking-odl/networking_odl/ml2/README.odl b/networking-odl/networking_odl/ml2/README.odl new file mode 100644 index 0000000..eef8d44 --- /dev/null +++ b/networking-odl/networking_odl/ml2/README.odl @@ -0,0 +1,41 @@ +OpenDaylight ML2 MechanismDriver +================================ +OpenDaylight is an Open Source SDN Controller developed by a plethora of +companies and hosted by the Linux Foundation. The OpenDaylight website +contains more information on the capabilities OpenDaylight provides: + + http://www.opendaylight.org + +Theory of operation +=================== +The OpenStack Neutron integration with OpenDaylight consists of the ML2 +MechanismDriver which acts as a REST proxy and passess all Neutron API +calls into OpenDaylight. OpenDaylight contains a NB REST service (called +the NeutronAPIService) which caches data from these proxied API calls and +makes it available to other services inside of OpenDaylight. One current +user of the SB side of the NeutronAPIService is the OVSDB code in +OpenDaylight. OVSDB uses the neutron information to isolate tenant networks +using GRE or VXLAN tunnels. + +How to use the OpenDaylight ML2 MechanismDriver +=============================================== +To use the ML2 MechanismDriver, you need to ensure you have it configured +as one of the "mechanism_drivers" in ML2: + + mechanism_drivers=opendaylight + +The next step is to setup the "[ml2_odl]" section in either the ml2_conf.ini +file or in a separate ml2_conf_odl.ini file. An example is shown below: + + [ml2_odl] + password = admin + username = admin + url = http://192.168.100.1:8080/controller/nb/v2/neutron + +When starting OpenDaylight, ensure you have the SimpleForwarding application +disabled or remove the .jar file from the plugins directory. Also ensure you +start OpenDaylight before you start OpenStack Neutron. + +There is devstack support for this which will automatically pull down OpenDaylight +and start it as part of devstack as well. The patch for this will likely merge +around the same time as this patch merges. diff --git a/networking-odl/networking_odl/ml2/__init__.py b/networking-odl/networking_odl/ml2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/networking-odl/networking_odl/ml2/legacy_port_binding.py b/networking-odl/networking_odl/ml2/legacy_port_binding.py new file mode 100644 index 0000000..7b9b918 --- /dev/null +++ b/networking-odl/networking_odl/ml2/legacy_port_binding.py @@ -0,0 +1,84 @@ +# Copyright (c) 2016 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from oslo_log import log + +from neutron.extensions import portbindings +from neutron.plugins.common import constants +from neutron.plugins.ml2 import driver_api +from neutron_lib import constants as n_const + +from networking_odl.ml2 import port_binding + + +LOG = log.getLogger(__name__) + + +class LegacyPortBindingManager(port_binding.PortBindingController): + + def __init__(self): + self.vif_details = {portbindings.CAP_PORT_FILTER: True} + self.supported_vnic_types = [portbindings.VNIC_NORMAL] + + def bind_port(self, port_context): + """Set binding for all valid segments + + """ + vnic_type = port_context.current.get(portbindings.VNIC_TYPE, + portbindings.VNIC_NORMAL) + if vnic_type not in self.supported_vnic_types: + LOG.debug("Refusing to bind due to unsupported vnic_type: %s", + vnic_type) + return + + valid_segment = None + for segment in port_context.segments_to_bind: + if self._check_segment(segment): + valid_segment = segment + break + + if valid_segment: + vif_type = self._get_vif_type(port_context) + LOG.debug("Bind port %(port)s on network %(network)s with valid " + "segment %(segment)s and VIF type %(vif_type)r.", + {'port': port_context.current['id'], + 'network': port_context.network.current['id'], + 'segment': valid_segment, 'vif_type': vif_type}) + + port_context.set_binding( + segment[driver_api.ID], vif_type, + self.vif_details, + status=n_const.PORT_STATUS_ACTIVE) + + def _check_segment(self, segment): + """Verify a segment is valid for the OpenDaylight MechanismDriver. + + Verify the requested segment is supported by ODL and return True or + False to indicate this to callers. + """ + + network_type = segment[driver_api.NETWORK_TYPE] + return network_type in [constants.TYPE_LOCAL, constants.TYPE_GRE, + constants.TYPE_VXLAN, constants.TYPE_VLAN] + + def _get_vif_type(self, port_context): + """Get VIF type string for given PortContext + + Dummy implementation: it always returns following constant. + neutron.extensions.portbindings.VIF_TYPE_OVS + """ + + return portbindings.VIF_TYPE_OVS diff --git a/networking-odl/networking_odl/ml2/mech_driver.py b/networking-odl/networking_odl/ml2/mech_driver.py new file mode 100644 index 0000000..adde8d9 --- /dev/null +++ b/networking-odl/networking_odl/ml2/mech_driver.py @@ -0,0 +1,458 @@ +# Copyright (c) 2013-2014 OpenStack Foundation +# 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 abc +import copy +import six + +import netaddr +from oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import excutils +import requests + +from neutron.common import utils +from neutron import context as neutron_context +from neutron.extensions import allowedaddresspairs as addr_pair +from neutron.extensions import securitygroup as sg +from neutron.plugins.ml2 import driver_api +from neutron.plugins.ml2 import driver_context +from neutron_lib import exceptions as n_exc + +from networking_odl._i18n import _LE +from networking_odl.common import callback as odl_call +from networking_odl.common import client as odl_client +from networking_odl.common import constants as odl_const +from networking_odl.common import utils as odl_utils +from networking_odl.ml2 import port_binding + + +cfg.CONF.import_group('ml2_odl', 'networking_odl.common.config') +LOG = logging.getLogger(__name__) + +not_found_exception_map = {odl_const.ODL_NETWORKS: n_exc.NetworkNotFound, + odl_const.ODL_SUBNETS: n_exc.SubnetNotFound, + odl_const.ODL_PORTS: n_exc.PortNotFound, + odl_const.ODL_SGS: sg.SecurityGroupNotFound, + odl_const.ODL_SG_RULES: + sg.SecurityGroupRuleNotFound} + + +@six.add_metaclass(abc.ABCMeta) +class ResourceFilterBase(object): + @staticmethod + @abc.abstractmethod + def filter_create_attributes(resource, context): + pass + + @staticmethod + @abc.abstractmethod + def filter_update_attributes(resource, context): + pass + + @staticmethod + @abc.abstractmethod + def filter_create_attributes_with_plugin(resource, plugin, dbcontext): + pass + + @staticmethod + def _filter_unmapped_null(resource_dict, unmapped_keys): + # NOTE(yamahata): bug work around + # https://bugs.eclipse.org/bugs/show_bug.cgi?id=475475 + # Null-value for an unmapped element causes next mapped + # collection to contain a null value + # JSON: { "unmappedField": null, "mappedCollection": [ "a" ] } + # + # Java Object: + # class Root { + # Collection mappedCollection = new ArrayList; + # } + # + # Result: + # Field B contains one element; null + # + # TODO(yamahata): update along side with neutron and ODL + # add when neutron adds more extensions + # delete when ODL neutron northbound supports it + # TODO(yamahata): do same thing for other resources + keys_to_del = [key for key in unmapped_keys + if resource_dict.get(key) is None] + if keys_to_del: + odl_utils.try_del(resource_dict, keys_to_del) + + +class NetworkFilter(ResourceFilterBase): + _UNMAPPED_KEYS = ['qos_policy_id'] + + @classmethod + def filter_create_attributes(cls, network, context): + """Filter out network attributes not required for a create.""" + odl_utils.try_del(network, ['status', 'subnets']) + cls._filter_unmapped_null(network, cls._UNMAPPED_KEYS) + + @classmethod + def filter_update_attributes(cls, network, context): + """Filter out network attributes for an update operation.""" + odl_utils.try_del(network, ['id', 'status', 'subnets', 'tenant_id']) + cls._filter_unmapped_null(network, cls._UNMAPPED_KEYS) + + @classmethod + def filter_create_attributes_with_plugin(cls, network, plugin, dbcontext): + context = driver_context.NetworkContext(plugin, dbcontext, network) + cls.filter_create_attributes(network, context) + + +class SubnetFilter(ResourceFilterBase): + @staticmethod + def filter_create_attributes(subnet, context): + """Filter out subnet attributes not required for a create.""" + pass + + @staticmethod + def filter_update_attributes(subnet, context): + """Filter out subnet attributes for an update operation.""" + odl_utils.try_del(subnet, ['id', 'network_id', 'ip_version', 'cidr', + 'allocation_pools', 'tenant_id']) + + @classmethod + def filter_create_attributes_with_plugin(cls, subnet, plugin, dbcontext): + network = plugin.get_network(dbcontext, subnet['network_id']) + context = driver_context.SubnetContext(plugin, dbcontext, subnet, + network) + cls.filter_create_attributes(subnet, context) + + +class PortFilter(ResourceFilterBase): + _UNMAPPED_KEYS = ['binding:profile', 'dns_name', + 'port_security_enabled', 'qos_policy_id'] + + @staticmethod + def _add_security_groups(port, context): + """Populate the 'security_groups' field with entire records.""" + dbcontext = context._plugin_context + groups = [context._plugin.get_security_group(dbcontext, sg) + for sg in port['security_groups']] + port['security_groups'] = groups + + @classmethod + def _fixup_allowed_ipaddress_pairs(cls, allowed_address_pairs): + """unify (ip address or network address) into network address""" + for address_pair in allowed_address_pairs: + ip_address = address_pair['ip_address'] + network_address = str(netaddr.IPNetwork(ip_address)) + address_pair['ip_address'] = network_address + + @classmethod + def filter_create_attributes(cls, port, context): + """Filter out port attributes not required for a create.""" + cls._add_security_groups(port, context) + cls._fixup_allowed_ipaddress_pairs(port[addr_pair.ADDRESS_PAIRS]) + cls._filter_unmapped_null(port, cls._UNMAPPED_KEYS) + odl_utils.try_del(port, ['status']) + + # NOTE(yamahata): work around for port creation for router + # tenant_id=''(empty string) is passed when port is created + # by l3 plugin internally for router. + # On the other hand, ODL doesn't accept empty string for tenant_id. + # In that case, deduce tenant_id from network_id for now. + # Right fix: modify Neutron so that don't allow empty string + # for tenant_id even for port for internal use. + # TODO(yamahata): eliminate this work around when neutron side + # is fixed + # assert port['tenant_id'] != '' + if port['tenant_id'] == '': + LOG.debug('empty string was passed for tenant_id: %s(port)', port) + port['tenant_id'] = context._network_context._network['tenant_id'] + + @classmethod + def filter_update_attributes(cls, port, context): + """Filter out port attributes for an update operation.""" + cls._add_security_groups(port, context) + cls._fixup_allowed_ipaddress_pairs(port[addr_pair.ADDRESS_PAIRS]) + cls._filter_unmapped_null(port, cls._UNMAPPED_KEYS) + odl_utils.try_del(port, ['network_id', 'id', 'status', 'tenant_id']) + + @classmethod + def filter_create_attributes_with_plugin(cls, port, plugin, dbcontext): + network = plugin.get_network(dbcontext, port['network_id']) + # TODO(yamahata): port binding + binding = {} + context = driver_context.PortContext( + plugin, dbcontext, port, network, binding, None) + cls.filter_create_attributes(port, context) + + +class SecurityGroupFilter(ResourceFilterBase): + @staticmethod + def filter_create_attributes(sg, context): + """Filter out security-group attributes not required for a create.""" + pass + + @staticmethod + def filter_update_attributes(sg, context): + """Filter out security-group attributes for an update operation.""" + pass + + @staticmethod + def filter_create_attributes_with_plugin(sg, plugin, dbcontext): + pass + + +class SecurityGroupRuleFilter(ResourceFilterBase): + @staticmethod + def filter_create_attributes(sg_rule, context): + """Filter out sg-rule attributes not required for a create.""" + pass + + @staticmethod + def filter_update_attributes(sg_rule, context): + """Filter out sg-rule attributes for an update operation.""" + pass + + @staticmethod + def filter_create_attributes_with_plugin(sg_rule, plugin, dbcontext): + pass + + +class OpenDaylightDriver(object): + + """OpenDaylight Python Driver for Neutron. + + This code is the backend implementation for the OpenDaylight ML2 + MechanismDriver for OpenStack Neutron. + """ + FILTER_MAP = { + odl_const.ODL_NETWORKS: NetworkFilter, + odl_const.ODL_SUBNETS: SubnetFilter, + odl_const.ODL_PORTS: PortFilter, + odl_const.ODL_SGS: SecurityGroupFilter, + odl_const.ODL_SG_RULES: SecurityGroupRuleFilter, + } + out_of_sync = True + + def __init__(self): + LOG.debug("Initializing OpenDaylight ML2 driver") + self.client = odl_client.OpenDaylightRestClient.create_client() + self.sec_handler = odl_call.OdlSecurityGroupsHandler(self) + self.port_binding_controller = port_binding.PortBindingManager.create() + # TODO(rzang): Each port binding controller should have any necessary + # parameter passed in from configuration files. + # BTW, CAP_PORT_FILTER seems being obsoleted. + # Leave the code commmeted out for now for future reference. + # + # self.vif_details = {portbindings.CAP_PORT_FILTER: True} + # self._network_topology = network_topology.NetworkTopologyManager( + # vif_details=self.vif_details) + + def synchronize(self, operation, object_type, context): + """Synchronize ODL with Neutron following a configuration change.""" + if self.out_of_sync: + self.sync_full(context._plugin) + else: + self.sync_single_resource(operation, object_type, context) + + def sync_resources(self, plugin, dbcontext, collection_name): + """Sync objects from Neutron over to OpenDaylight. + + This will handle syncing networks, subnets, and ports from Neutron to + OpenDaylight. It also filters out the requisite items which are not + valid for create API operations. + """ + filter_cls = self.FILTER_MAP[collection_name] + to_be_synced = [] + obj_getter = getattr(plugin, 'get_%s' % collection_name) + if collection_name == odl_const.ODL_SGS: + resources = obj_getter(dbcontext, default_sg=True) + else: + resources = obj_getter(dbcontext) + for resource in resources: + try: + # Convert underscores to dashes in the URL for ODL + collection_name_url = collection_name.replace('_', '-') + urlpath = collection_name_url + '/' + resource['id'] + self.client.sendjson('get', urlpath, None) + except requests.exceptions.HTTPError as e: + with excutils.save_and_reraise_exception() as ctx: + if e.response.status_code == requests.codes.not_found: + filter_cls.filter_create_attributes_with_plugin( + resource, plugin, dbcontext) + to_be_synced.append(resource) + ctx.reraise = False + else: + # TODO(yamahata): compare result with resource. + # If they don't match, update it below + pass + + if to_be_synced: + key = collection_name[:-1] if len(to_be_synced) == 1 else ( + collection_name) + # Convert underscores to dashes in the URL for ODL + collection_name_url = collection_name.replace('_', '-') + self.client.sendjson('post', collection_name_url, + {key: to_be_synced}) + + # https://bugs.launchpad.net/networking-odl/+bug/1371115 + # TODO(yamahata): update resources with unsyned attributes + # TODO(yamahata): find dangling ODL resouce that was deleted in + # neutron db + + @utils.synchronized('odl-sync-full') + def sync_full(self, plugin): + """Resync the entire database to ODL. + + Transition to the in-sync state on success. + Note: we only allow a single thread in here at a time. + """ + if not self.out_of_sync: + return + dbcontext = neutron_context.get_admin_context() + for collection_name in [odl_const.ODL_NETWORKS, + odl_const.ODL_SUBNETS, + odl_const.ODL_PORTS, + odl_const.ODL_SGS, + odl_const.ODL_SG_RULES]: + self.sync_resources(plugin, dbcontext, collection_name) + self.out_of_sync = False + + def sync_single_resource(self, operation, object_type, context): + """Sync over a single resource from Neutron to OpenDaylight. + + Handle syncing a single operation over to OpenDaylight, and correctly + filter attributes out which are not required for the requisite + operation (create or update) being handled. + """ + # Convert underscores to dashes in the URL for ODL + object_type_url = object_type.replace('_', '-') + try: + obj_id = context.current['id'] + if operation == odl_const.ODL_DELETE: + self.out_of_sync |= not self.client.try_delete( + object_type_url + '/' + obj_id) + else: + filter_cls = self.FILTER_MAP[object_type] + if operation == odl_const.ODL_CREATE: + urlpath = object_type_url + method = 'post' + attr_filter = filter_cls.filter_create_attributes + elif operation == odl_const.ODL_UPDATE: + urlpath = object_type_url + '/' + obj_id + method = 'put' + attr_filter = filter_cls.filter_update_attributes + resource = copy.deepcopy(context.current) + attr_filter(resource, context) + self.client.sendjson(method, urlpath, + {object_type_url[:-1]: resource}) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error(_LE("Unable to perform %(operation)s on " + "%(object_type)s %(object_id)s"), + {'operation': operation, + 'object_type': object_type, + 'object_id': obj_id}) + self.out_of_sync = True + + def sync_from_callback(self, operation, res_type, res_id, resource_dict): + object_type = res_type.plural.replace('_', '-') + try: + if operation == odl_const.ODL_DELETE: + self.out_of_sync |= not self.client.try_delete( + object_type + '/' + res_id) + else: + if operation == odl_const.ODL_CREATE: + urlpath = object_type + method = 'post' + elif operation == odl_const.ODL_UPDATE: + urlpath = object_type + '/' + res_id + method = 'put' + self.client.sendjson(method, urlpath, resource_dict) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error(_LE("Unable to perform %(operation)s on " + "%(object_type)s %(res_id)s " + "%(resource_dict)s"), + {'operation': operation, + 'object_type': object_type, + 'res_id': res_id, + 'resource_dict': resource_dict}) + self.out_of_sync = True + + def bind_port(self, port_context): + """Set binding for a valid segments + + """ + self.port_binding_controller.bind_port(port_context) + + +class OpenDaylightMechanismDriver(driver_api.MechanismDriver): + + """Mechanism Driver for OpenDaylight. + + This driver was a port from the NCS MechanismDriver. The API + exposed by ODL is slightly different from the API exposed by NCS, + but the general concepts are the same. + """ + + def initialize(self): + self.url = cfg.CONF.ml2_odl.url + self.timeout = cfg.CONF.ml2_odl.timeout + self.username = cfg.CONF.ml2_odl.username + self.password = cfg.CONF.ml2_odl.password + required_opts = ('url', 'username', 'password') + for opt in required_opts: + if not getattr(self, opt): + raise cfg.RequiredOptError(opt, 'ml2_odl') + + self.odl_drv = OpenDaylightDriver() + + # Postcommit hooks are used to trigger synchronization. + + def create_network_postcommit(self, context): + self.odl_drv.synchronize(odl_const.ODL_CREATE, odl_const.ODL_NETWORKS, + context) + + def update_network_postcommit(self, context): + self.odl_drv.synchronize(odl_const.ODL_UPDATE, odl_const.ODL_NETWORKS, + context) + + def delete_network_postcommit(self, context): + self.odl_drv.synchronize(odl_const.ODL_DELETE, odl_const.ODL_NETWORKS, + context) + + def create_subnet_postcommit(self, context): + self.odl_drv.synchronize(odl_const.ODL_CREATE, odl_const.ODL_SUBNETS, + context) + + def update_subnet_postcommit(self, context): + self.odl_drv.synchronize(odl_const.ODL_UPDATE, odl_const.ODL_SUBNETS, + context) + + def delete_subnet_postcommit(self, context): + self.odl_drv.synchronize(odl_const.ODL_DELETE, odl_const.ODL_SUBNETS, + context) + + def create_port_postcommit(self, context): + self.odl_drv.synchronize(odl_const.ODL_CREATE, odl_const.ODL_PORTS, + context) + + def update_port_postcommit(self, context): + self.odl_drv.synchronize(odl_const.ODL_UPDATE, odl_const.ODL_PORTS, + context) + + def delete_port_postcommit(self, context): + self.odl_drv.synchronize(odl_const.ODL_DELETE, odl_const.ODL_PORTS, + context) + + def bind_port(self, context): + self.odl_drv.bind_port(context) diff --git a/networking-odl/networking_odl/ml2/mech_driver_v2.py b/networking-odl/networking_odl/ml2/mech_driver_v2.py new file mode 100644 index 0000000..dfc8df1 --- /dev/null +++ b/networking-odl/networking_odl/ml2/mech_driver_v2.py @@ -0,0 +1,146 @@ +# Copyright (c) 2013-2014 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_config import cfg +from oslo_log import log as logging + +from neutron.db import api as db_api +from neutron.plugins.ml2 import driver_api as api + +from networking_odl.common import callback +from networking_odl.common import config as odl_conf +from networking_odl.common import constants as odl_const +from networking_odl.journal import cleanup +from networking_odl.journal import full_sync +from networking_odl.journal import journal +from networking_odl.journal import maintenance +from networking_odl.ml2 import port_binding + +LOG = logging.getLogger(__name__) + + +class OpenDaylightMechanismDriver(api.MechanismDriver): + """OpenDaylight Python Driver for Neutron. + + This code is the backend implementation for the OpenDaylight ML2 + MechanismDriver for OpenStack Neutron. + """ + + def initialize(self): + LOG.debug("Initializing OpenDaylight ML2 driver") + cfg.CONF.register_opts(odl_conf.odl_opts, "ml2_odl") + self.sg_handler = callback.OdlSecurityGroupsHandler(self) + self.journal = journal.OpendaylightJournalThread() + self.port_binding_controller = port_binding.PortBindingManager.create() + self._start_maintenance_thread() + + def _start_maintenance_thread(self): + # start the maintenance thread and register all the maintenance + # operations : + # (1) JournalCleanup - Delete completed rows from journal + # (2) CleanupProcessing - Mark orphaned processing rows to pending + # (3) Full sync - Re-sync when detecting an ODL "cold reboot" + cleanup_obj = cleanup.JournalCleanup() + self._maintenance_thread = maintenance.MaintenanceThread() + self._maintenance_thread.register_operation( + cleanup_obj.delete_completed_rows) + self._maintenance_thread.register_operation( + cleanup_obj.cleanup_processing_rows) + self._maintenance_thread.register_operation(full_sync.full_sync) + self._maintenance_thread.start() + + @staticmethod + def _record_in_journal(context, object_type, operation, data=None): + if data is None: + data = context.current + journal.record(context._plugin_context.session, object_type, + context.current['id'], operation, data) + + def create_network_precommit(self, context): + OpenDaylightMechanismDriver._record_in_journal( + context, odl_const.ODL_NETWORK, odl_const.ODL_CREATE) + + def create_subnet_precommit(self, context): + OpenDaylightMechanismDriver._record_in_journal( + context, odl_const.ODL_SUBNET, odl_const.ODL_CREATE) + + def create_port_precommit(self, context): + OpenDaylightMechanismDriver._record_in_journal( + context, odl_const.ODL_PORT, odl_const.ODL_CREATE) + + def update_network_precommit(self, context): + OpenDaylightMechanismDriver._record_in_journal( + context, odl_const.ODL_NETWORK, odl_const.ODL_UPDATE) + + def update_subnet_precommit(self, context): + OpenDaylightMechanismDriver._record_in_journal( + context, odl_const.ODL_SUBNET, odl_const.ODL_UPDATE) + + def update_port_precommit(self, context): + OpenDaylightMechanismDriver._record_in_journal( + context, odl_const.ODL_PORT, odl_const.ODL_UPDATE) + + def delete_network_precommit(self, context): + OpenDaylightMechanismDriver._record_in_journal( + context, odl_const.ODL_NETWORK, odl_const.ODL_DELETE, data=[]) + + def delete_subnet_precommit(self, context): + # Use the journal row's data field to store parent object + # uuids. This information is required for validation checking + # when deleting parent objects. + new_context = [context.current['network_id']] + OpenDaylightMechanismDriver._record_in_journal( + context, odl_const.ODL_SUBNET, odl_const.ODL_DELETE, + data=new_context) + + def delete_port_precommit(self, context): + # Use the journal row's data field to store parent object + # uuids. This information is required for validation checking + # when deleting parent objects. + new_context = [context.current['network_id']] + for subnet in context.current['fixed_ips']: + new_context.append(subnet['subnet_id']) + OpenDaylightMechanismDriver._record_in_journal( + context, odl_const.ODL_PORT, odl_const.ODL_DELETE, + data=new_context) + + @journal.call_thread_on_end + def sync_from_callback(self, operation, res_type, res_id, resource_dict): + object_type = res_type.singular + object_uuid = (resource_dict[object_type]['id'] + if operation == 'create' else res_id) + if resource_dict is not None: + resource_dict = resource_dict[object_type] + journal.record(db_api.get_session(), object_type, object_uuid, + operation, resource_dict) + + def _postcommit(self, context): + self.journal.set_sync_event() + + create_network_postcommit = _postcommit + create_subnet_postcommit = _postcommit + create_port_postcommit = _postcommit + update_network_postcommit = _postcommit + update_subnet_postcommit = _postcommit + update_port_postcommit = _postcommit + delete_network_postcommit = _postcommit + delete_subnet_postcommit = _postcommit + delete_port_postcommit = _postcommit + + def bind_port(self, port_context): + """Set binding for a valid segments + + """ + return self.port_binding_controller.bind_port(port_context) diff --git a/networking-odl/networking_odl/ml2/network_topology.py b/networking-odl/networking_odl/ml2/network_topology.py new file mode 100644 index 0000000..b0bfae1 --- /dev/null +++ b/networking-odl/networking_odl/ml2/network_topology.py @@ -0,0 +1,313 @@ +# Copyright (c) 2015-2016 OpenStack Foundation +# 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 abc +import importlib +import logging + +import six +from six.moves.urllib import parse + +from neutron.extensions import portbindings +from oslo_log import log +from oslo_serialization import jsonutils + +from networking_odl.common import cache +from networking_odl.common import client +from networking_odl.common import utils +from networking_odl._i18n import _, _LI, _LW, _LE +from networking_odl.ml2 import port_binding + + +LOG = log.getLogger(__name__) + + +class NetworkTopologyManager(port_binding.PortBindingController): + + # the first valid vif type will be chosed following the order + # on this list. This list can be modified to adapt to user preferences. + valid_vif_types = [ + portbindings.VIF_TYPE_VHOST_USER, portbindings.VIF_TYPE_OVS] + + # List of class names of registered implementations of interface + # NetworkTopologyParser + network_topology_parsers = [ + 'networking_odl.ml2.ovsdb_topology.OvsdbNetworkTopologyParser'] + + def __init__(self, vif_details=None, client=None): + # Details for binding port + self._vif_details = vif_details or {portbindings.CAP_PORT_FILTER: True} + + # Rest client used for getting network topology from ODL + self._client = client or NetworkTopologyClient.create_client() + + # Table of NetworkTopologyElement + self._elements_by_ip = cache.Cache( + self._fetch_and_parse_network_topology) + + # Parsers used for processing network topology + self._parsers = list(self._create_parsers()) + + def bind_port(self, port_context): + """Set binding for a valid segment + + """ + host_name = port_context.host + elements = list() + try: + # Append to empty list to add as much elements as possible + # in the case it raises an exception + elements.extend(self._fetch_elements_by_host(host_name)) + except Exception: + LOG.exception( + _LE('Error fetching elements for host %(host_name)r.'), + {'host_name': host_name}, exc_info=1) + + if not elements: + # In case it wasn't able to find any network topology element + # for given host then it uses the legacy OVS one keeping the old + # behaviour + LOG.warning( + _LW('Using legacy OVS network topology element for port ' + 'binding for host: %(host_name)r.'), + {'host_name': host_name}) + + # Imported here to avoid cyclic module dependencies + from networking_odl.ml2 import ovsdb_topology + elements = [ovsdb_topology.OvsdbNetworkTopologyElement()] + + # TODO(Federico Ressi): in the case there are more candidate virtual + # switches instances for the same host it choses one for binding + # port. As there isn't any know way to perform this selection it + # selects a VIF type that is valid for all switches that have + # been found and a VIF type valid for all them. This has to be improved + for vif_type in self.valid_vif_types: + vif_type_is_valid_for_all = True + for element in elements: + if vif_type not in element.valid_vif_types: + # it is invalid for at least one element: discard it + vif_type_is_valid_for_all = False + break + + if vif_type_is_valid_for_all: + # This is the best VIF type valid for all elements + LOG.debug( + "Found VIF type %(vif_type)r valid for all network " + "topology elements for host %(host_name)r.", + {'vif_type': vif_type, 'host_name': host_name}) + + for element in elements: + # It assumes that any element could be good for given host + # In most of the cases I expect exactely one element for + # every compute host + try: + return element.bind_port( + port_context, vif_type, self._vif_details) + + except Exception: + LOG.exception( + _LE('Network topology element has failed binding ' + 'port:\n%(element)s'), + {'element': element.to_json()}) + + LOG.error( + _LE('Unable to bind port element for given host and valid VIF ' + 'types:\n' + '\thostname: %(host_name)s\n' + '\tvalid VIF types: %(valid_vif_types)s'), + {'host_name': host_name, + 'valid_vif_types': ', '.join(self.valid_vif_types)}) + # TDOO(Federico Ressi): should I raise an exception here? + + def _create_parsers(self): + for parser_name in self.network_topology_parsers: + try: + yield NetworkTopologyParser.create_parser(parser_name) + + except Exception: + LOG.exception( + _LE('Error initializing topology parser: %(parser_name)r'), + {'parser_name': parser_name}) + + def _fetch_elements_by_host(self, host_name, cache_timeout=60.0): + '''Yields all network topology elements referring to given host name + + ''' + + host_addresses = [host_name] + try: + # It uses both compute host name and known IP addresses to + # recognize topology elements valid for given computed host + ip_addresses = utils.get_addresses_by_name(host_name) + except Exception: + ip_addresses = [] + LOG.exception( + _LE('Unable to resolve IP addresses for host %(host_name)r'), + {'host_name': host_name}) + else: + host_addresses.extend(ip_addresses) + + yield_elements = set() + try: + for __, element in self._elements_by_ip.fetch_all( + host_addresses, cache_timeout): + # yields every element only once + if element not in yield_elements: + yield_elements.add(element) + yield element + + except cache.CacheFetchError as error: + # This error is expected on most of the cases because typically not + # all host_addresses maps to a network topology element. + if yield_elements: + # As we need only one element for every host we ignore the + # case in which others host addresseses didn't map to any host + LOG.debug( + 'Host addresses not found in networking topology: %s', + ', '.join(error.missing_keys)) + else: + LOG.exception( + _LE('No such network topology elements for given host ' + '%(host_name)r and given IPs: %(ip_addresses)s.'), + {'host_name': host_name, + 'ip_addresses': ", ".join(ip_addresses)}) + error.reraise_cause() + + def _fetch_and_parse_network_topology(self, addresses): + # The cache calls this method to fecth new elements when at least one + # of the addresses is not in the cache or it has expired. + + # pylint: disable=unused-argument + LOG.info(_LI('Fetch network topology from ODL.')) + response = self._client.get() + response.raise_for_status() + + network_topology = response.json() + if LOG.isEnabledFor(logging.DEBUG): + topology_str = jsonutils.dumps( + network_topology, sort_keys=True, indent=4, + separators=(',', ': ')) + LOG.debug("Got network topology:\n%s", topology_str) + + at_least_one_element_for_asked_addresses = False + for parser in self._parsers: + try: + for element in parser.parse_network_topology(network_topology): + if not isinstance(element, NetworkTopologyElement): + raise TypeError(_( + "Yield element doesn't implement interface " + "'NetworkTopologyElement': {!r}").format(element)) + # the same element can be known by more host addresses + for host_address in element.host_addresses: + if host_address in addresses: + at_least_one_element_for_asked_addresses = True + yield host_address, element + except Exception: + LOG.exception( + _LE("Parser %(parser)r failed to parse network topology."), + {'parser': parser}) + + if not at_least_one_element_for_asked_addresses: + # this will mark entries for given addresses as failed to allow + # calling this method again as soon it is requested and avoid + # waiting for cache expiration + raise ValueError( + _('No such topology element for given host addresses: {}') + .format(', '.join(addresses))) + + +@six.add_metaclass(abc.ABCMeta) +class NetworkTopologyParser(object): + + @classmethod + def create_parser(cls, parser_class_name): + '''Creates a 'NetworkTopologyParser' of given class name. + + ''' + module_name, class_name = parser_class_name.rsplit('.', 1) + module = importlib.import_module(module_name) + clss = getattr(module, class_name) + if not issubclass(clss, cls): + raise TypeError(_( + "Class {class_name!r} of module {module_name!r} doesn't " + "implement 'NetworkTopologyParser' interface.").format( + class_name=class_name, module_name=module_name)) + return clss() + + @abc.abstractmethod + def parse_network_topology(self, network_topology): + '''Parses OpenDaylight network topology + + Yields all network topology elements implementing + 'NetworkTopologyElement' interface found in given network topology. + ''' + + +@six.add_metaclass(abc.ABCMeta) +class NetworkTopologyElement(object): + + @abc.abstractproperty + def host_addresses(self): + '''List of known host addresses of a single compute host + + Either host names and ip addresses are valid. + Neutron host controller must know at least one of these compute host + names or ip addresses to find this element. + ''' + + @abc.abstractproperty + def valid_vif_types(self): + '''Returns a tuple listing VIF types supported by the compute node + + ''' + + @abc.abstractmethod + def bind_port(self, port_context, vif_type, vif_details): + '''Bind port context using given vif type and vif details + + This method is expected to search for a valid segment and then + call port_context.set_binding() + ''' + + def to_dict(self): + cls = type(self) + return { + 'class': cls.__module__ + '.' + cls.__name__, + 'host_addresses': list(self.host_addresses), + 'valid_vif_types': list(self.valid_vif_types)} + + def to_json(self): + return jsonutils.dumps( + self.to_dict(), sort_keys=True, indent=4, separators=(',', ': ')) + + +class NetworkTopologyClient(client.OpenDaylightRestClient): + + _GET_ODL_NETWORK_TOPOLOGY_URL =\ + 'restconf/operational/network-topology:network-topology' + + def __init__(self, url, username, password, timeout): + if url: + url = parse.urlparse(url) + port = '' + if url.port: + port = ':' + str(url.port) + topology_url = '{}://{}{}/{}'.format( + url.scheme, url.hostname, port, + self._GET_ODL_NETWORK_TOPOLOGY_URL) + else: + topology_url = None + super(NetworkTopologyClient, self).__init__( + topology_url, username, password, timeout) diff --git a/networking-odl/networking_odl/ml2/ovsdb_topology.py b/networking-odl/networking_odl/ml2/ovsdb_topology.py new file mode 100644 index 0000000..f2c8ad8 --- /dev/null +++ b/networking-odl/networking_odl/ml2/ovsdb_topology.py @@ -0,0 +1,218 @@ +# Copyright (c) 2016 OpenStack Foundation +# 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 collections +import os + +from oslo_log import log +import six +from six.moves.urllib import parse + +from neutron.extensions import portbindings +from neutron.plugins.common import constants +from neutron.plugins.ml2 import driver_api +from neutron_lib import constants as n_const + +from networking_odl._i18n import _ +from networking_odl.ml2 import network_topology + + +LOG = log.getLogger(__name__) + + +class OvsdbNetworkTopologyParser(network_topology.NetworkTopologyParser): + + def new_element(self, uuid): + return OvsdbNetworkTopologyElement(uuid=uuid) + + def parse_network_topology(self, network_topologies): + elements_by_uuid = collections.OrderedDict() + for topology in network_topologies[ + 'network-topology']['topology']: + if topology['topology-id'].startswith('ovsdb:'): + for node in topology['node']: + # expected url format: ovsdb://uuid/[/]] + node_url = parse.urlparse(node['node-id']) + if node_url.scheme == 'ovsdb'\ + and node_url.netloc == 'uuid': + # split_res = ['', '', ''] + split_res = node_url.path.split('/', 2) + + # uuid is used to identify nodes referring to the same + # element + uuid = split_res[1] + element = elements_by_uuid.get(uuid) + if element is None: + elements_by_uuid[uuid] = element =\ + self.new_element(uuid) + + # inner_path can be [] or [] + inner_path = split_res[2:] + self._update_element_from_json_ovsdb_topology_node( + node, element, uuid, *inner_path) + + # There can be more OVS instances connected beside the same IP address + # Cache will yield more instaces for the same key + for __, element in six.iteritems(elements_by_uuid): + yield element + + def _update_element_from_json_ovsdb_topology_node( + self, node, element, uuid, path=None): + + if not path: + # global element section (root path) + + # fetch remote IP address + element.remote_ip = node["ovsdb:connection-info"]["remote-ip"] + + for vif_type_entry in node.get( + "ovsdb:interface-type-entry", []): + # Is this a good place to add others OVS VIF types? + if vif_type_entry.get("interface-type") ==\ + "ovsdb:interface-type-dpdkvhostuser": + element.support_vhost_user = True + break + else: + LOG.debug( + 'Interface type not found in network topology node %r.', + uuid) + + LOG.debug( + 'Topology element updated:\n' + ' - uuid: %(uuid)r\n' + ' - remote_ip: %(remote_ip)r\n' + ' - support_vhost_user: %(support_vhost_user)r', + {'uuid': uuid, + 'remote_ip': element.remote_ip, + 'support_vhost_user': element.support_vhost_user}) + elif path == 'bridge/br-int': + datapath_type = node.get("ovsdb:datapath-type") + if datapath_type == "ovsdb:datapath-type-netdev": + element.has_datapath_type_netdev = True + LOG.debug( + 'Topology element updated:\n' + ' - uuid: %(uuid)r\n' + ' - has_datapath_type_netdev: %(' + 'has_datapath_type_netdev)r', + {'uuid': uuid, + 'has_datapath_type_netdev': + element.has_datapath_type_netdev}) + + +class OvsdbNetworkTopologyElement(network_topology.NetworkTopologyElement): + + uuid = None + remote_ip = None # it can be None or a string + has_datapath_type_netdev = False # it can be False or True + support_vhost_user = False # it can be False or True + + # location for vhostuser sockets + vhostuser_socket_dir = '/var/run/openvswitch' + + # prefix for ovs port + port_prefix = 'vhu' + + def __init__(self, **kwargs): + for name, value in six.iteritems(kwargs): + setattr(self, name, value) + + @property + def host_addresses(self): + # For now it support only the remote IP found in connection info + return self.remote_ip, + + @property + def valid_vif_types(self): + if self.has_datapath_type_netdev and self.support_vhost_user: + return [ + portbindings.VIF_TYPE_VHOST_USER, + portbindings.VIF_TYPE_OVS] + else: + return [portbindings.VIF_TYPE_OVS] + + def bind_port(self, port_context, vif_type, vif_details): + + port_context_id = port_context.current['id'] + network_context_id = port_context.network.current['id'] + + # Bind port to the first valid segment + for segment in port_context.segments_to_bind: + if self._is_valid_segment(segment): + # Guest best VIF type for given host + vif_details = self._get_vif_details( + vif_details=vif_details, port_context_id=port_context_id, + vif_type=vif_type) + LOG.debug( + 'Bind port with valid segment:\n' + '\tport: %(port)r\n' + '\tnetwork: %(network)r\n' + '\tsegment: %(segment)r\n' + '\tVIF type: %(vif_type)r\n' + '\tVIF details: %(vif_details)r', + {'port': port_context_id, + 'network': network_context_id, + 'segment': segment, 'vif_type': vif_type, + 'vif_details': vif_details}) + port_context.set_binding( + segment[driver_api.ID], vif_type, vif_details, + status=n_const.PORT_STATUS_ACTIVE) + return + + raise ValueError( + _('Unable to find any valid segment in given context.')) + + def to_dict(self): + data = super(OvsdbNetworkTopologyElement, self).to_dict() + data.update( + {'uuid': self.uuid, + 'has_datapath_type_netdev': self.has_datapath_type_netdev, + 'support_vhost_user': self.support_vhost_user, + 'valid_vif_types': self.valid_vif_types}) + if portbindings.VIF_TYPE_VHOST_USER in self.valid_vif_types: + data.update({'port_prefix': self.port_prefix, + 'vhostuser_socket_dir': self.vhostuser_socket_dir}) + return data + + def _is_valid_segment(self, segment): + """Verify a segment is valid for the OpenDaylight MechanismDriver. + + Verify the requested segment is supported by ODL and return True or + False to indicate this to callers. + """ + + network_type = segment[driver_api.NETWORK_TYPE] + return network_type in [constants.TYPE_LOCAL, constants.TYPE_GRE, + constants.TYPE_VXLAN, constants.TYPE_VLAN] + + def _get_vif_details(self, vif_details, port_context_id, vif_type): + vif_details = dict(vif_details) + if vif_type == portbindings.VIF_TYPE_VHOST_USER: + socket_path = os.path.join( + self.vhostuser_socket_dir, + (self.port_prefix + port_context_id)[:14]) + + vif_details.update({ + portbindings.VHOST_USER_MODE: + portbindings.VHOST_USER_MODE_CLIENT, + portbindings.VHOST_USER_OVS_PLUG: True, + portbindings.VHOST_USER_SOCKET: socket_path + }) + return vif_details + + def __setattr__(self, name, value): + # raises Attribute error if the class hasn't this attribute + getattr(type(self), name) + super(OvsdbNetworkTopologyElement, self).__setattr__(name, value) diff --git a/networking-odl/networking_odl/ml2/port_binding.py b/networking-odl/networking_odl/ml2/port_binding.py new file mode 100644 index 0000000..d34dc01 --- /dev/null +++ b/networking-odl/networking_odl/ml2/port_binding.py @@ -0,0 +1,121 @@ +# Copyright (c) 2016 OpenStack Foundation +# 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 abc +import six +import stevedore + +from oslo_config import cfg +from oslo_log import log +from oslo_utils import excutils + +from networking_odl._i18n import _LI, _LE + + +LOG = log.getLogger(__name__) + + +@six.add_metaclass(abc.ABCMeta) +class PortBindingController(object): + + @abc.abstractmethod + def bind_port(self, port_context): + """Attempt to bind a port. + + :param context: PortContext instance describing the port + + This method is called outside any transaction to attempt to + establish a port binding using calling mechanism driver. Bindings + may be created at each of multiple levels of a hierarchical + network, and are established from the top level downward. At + each level, the mechanism driver determines whether it can + bind to any of the network segments in the + context.segments_to_bind property, based on the value of the + context.host property, any relevant port or network + attributes, and its own knowledge of the network topology. At + the top level, context.segments_to_bind contains the static + segments of the port's network. At each lower level of + binding, it contains static or dynamic segments supplied by + the driver that bound at the level above. If the driver is + able to complete the binding of the port to any segment in + context.segments_to_bind, it must call context.set_binding + with the binding details. If it can partially bind the port, + it must call context.continue_binding with the network + segments to be used to bind at the next lower level. + If the binding results are committed after bind_port returns, + they will be seen by all mechanism drivers as + update_port_precommit and update_port_postcommit calls. But if + some other thread or process concurrently binds or updates the + port, these binding results will not be committed, and + update_port_precommit and update_port_postcommit will not be + called on the mechanism drivers with these results. Because + binding results can be discarded rather than committed, + drivers should avoid making persistent state changes in + bind_port, or else must ensure that such state changes are + eventually cleaned up. + Implementing this method explicitly declares the mechanism + driver as having the intention to bind ports. This is inspected + by the QoS service to identify the available QoS rules you + can use with ports. + """ + + +class PortBindingManager(PortBindingController): + # At this point, there is no requirement to have multiple + # port binding controllers at the same time. + # Stay with single controller until there is a real requirement + + def __init__(self, name, controller): + self.name = name + self.controller = controller + + @classmethod + def create( + cls, namespace='networking_odl.ml2.port_binding_controllers', + name=cfg.CONF.ml2_odl.port_binding_controller): + + ext_mgr = stevedore.named.NamedExtensionManager( + namespace, [name], invoke_on_load=True) + + assert len(ext_mgr.extensions) == 1, ( + "Wrong port binding controller is specified") + + extension = ext_mgr.extensions[0] + if isinstance(extension.obj, PortBindingController): + return cls(extension.name, extension.obj) + else: + raise ValueError( + ("Port binding controller '%(name)s (%(controller)r)' " + "doesn't implement PortBindingController interface."), + {'name': extension.name, 'controller': extension.obj}) + + def bind_port(self, port_context): + controller_details = {'name': self.name, 'controller': self.controller} + try: + self.controller.bind_port(port_context) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.exception( + _LE("Controller '%(name)s (%(controller)r)' had an error " + "when binding port."), controller_details) + else: + if port_context._new_bound_segment: + LOG.info( + _LI("Controller '%(name)s (%(controller)r)' has bound " + "port."), controller_details) + else: + LOG.debug( + "Controller %(name)s (%(controller)r) hasn't bound " + "port.", controller_details) diff --git a/networking-odl/networking_odl/ml2/pseudo_agentdb_binding.py b/networking-odl/networking_odl/ml2/pseudo_agentdb_binding.py new file mode 100644 index 0000000..d24bd55 --- /dev/null +++ b/networking-odl/networking_odl/ml2/pseudo_agentdb_binding.py @@ -0,0 +1,263 @@ +# Copyright (c) 2016 OpenStack Foundation +# 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 logging +from neutron_lib import constants as nl_const +from requests import exceptions +import six.moves.urllib.parse as urlparse +from string import Template + +from oslo_config import cfg +from oslo_log import log +from oslo_serialization import jsonutils + +from neutron import context +from neutron.extensions import portbindings +from neutron import manager +from neutron.plugins.ml2 import driver_api + +from networking_odl._i18n import _LE, _LI, _LW +from networking_odl.common import client as odl_client +from networking_odl.journal import maintenance as mt +from networking_odl.ml2 import port_binding + +cfg.CONF.import_group('ml2_odl', 'networking_odl.common.config') +LOG = log.getLogger(__name__) + + +class PseudoAgentDBBindingController(port_binding.PortBindingController): + """Switch agnostic Port binding controller for OpenDayLight.""" + + AGENTDB_BINARY = 'neutron-odlagent-portbinding' + L2_TYPE = "ODL L2" + + # TODO(mzmalick): binary, topic and resource_versions to be provided + # by ODL, Pending ODL NB patches. + agentdb_row = { + 'binary': AGENTDB_BINARY, + 'host': '', + 'topic': nl_const.L2_AGENT_TOPIC, + 'configurations': {}, + 'resource_versions': '', + 'agent_type': L2_TYPE, + 'start_flag': True} + # We are not running host agents, so above start_flag is redundant + + def __init__(self, hostconf_uri=None, db_plugin=None): + """Initialization.""" + LOG.debug("Initializing ODL Port Binding Controller") + + if not hostconf_uri: + # extract host/port from ODL URL and append hostconf_uri path + hostconf_uri = self._make_hostconf_uri( + cfg.CONF.ml2_odl.url, cfg.CONF.ml2_odl.odl_hostconf_uri) + + LOG.debug("ODLPORTBINDING hostconfigs URI: %s", hostconf_uri) + + # TODO(mzmalick): disable port-binding for ODL lightweight testing + self.odl_rest_client = odl_client.OpenDaylightRestClient.create_client( + url=hostconf_uri) + + # Neutron DB plugin instance + self.agents_db = db_plugin + + # Start polling ODL restconf using maintenance thread. + # default: 30s (should be <= agent keep-alive poll interval) + self._start_maintenance_thread(cfg.CONF.ml2_odl.restconf_poll_interval) + + def _make_hostconf_uri(self, odl_url=None, path=''): + """Make ODL hostconfigs URI with host/port extraced from ODL_URL.""" + # NOTE(yamahata): for unit test. + odl_url = odl_url or 'http://localhost:8080/' + + # extract ODL_IP and ODL_PORT from ODL_ENDPOINT and append path + # urlsplit and urlunparse don't throw exceptions + purl = urlparse.urlsplit(odl_url) + return urlparse.urlunparse((purl.scheme, purl.netloc, + path, '', '', '')) + # + # TODO(mzmalick): + # 1. implement websockets for ODL hostconfig events + # + + def _start_maintenance_thread(self, poll_interval): + self._mainth = mt.MaintenanceThread() + self._mainth.maintenance_interval = poll_interval + self._mainth.register_operation(self._get_and_update_hostconfigs) + self._mainth.start() + + def _rest_get_hostconfigs(self): + try: + response = self.odl_rest_client.get() + response.raise_for_status() + hostconfigs = response.json()['hostconfigs']['hostconfig'] + except exceptions.ConnectionError: + LOG.error(_LE("Cannot connect to the Opendaylight Controller"), + exc_info=True) + return None + except KeyError: + LOG.error(_LE("got invalid hostconfigs"), + exc_info=True) + return None + except Exception: + LOG.warning(_LW("REST/GET odl hostconfig failed, "), + exc_info=True) + return None + else: + if LOG.isEnabledFor(logging.DEBUG): + _hconfig_str = jsonutils.dumps( + response, sort_keys=True, indent=4, separators=(',', ': ')) + LOG.debug("ODLPORTBINDING hostconfigs:\n%s", _hconfig_str) + + return hostconfigs + + def _get_and_update_hostconfigs(self, session=None): + LOG.info(_LI("REST/GET hostconfigs from ODL")) + + hostconfigs = self._rest_get_hostconfigs() + + if not hostconfigs: + LOG.warning(_LW("ODL hostconfigs REST/GET failed, " + "will retry on next poll")) + return # retry on next poll + + self._update_agents_db(hostconfigs=hostconfigs) + + def _get_neutron_db_plugin(self): + if (not self.agents_db) and manager.NeutronManager.has_instance(): + self.agents_db = manager.NeutronManager.get_plugin() + return self.agents_db + + def _update_agents_db(self, hostconfigs): + LOG.debug("ODLPORTBINDING Updating agents DB with ODL hostconfigs") + + agents_db = self._get_neutron_db_plugin() + + if not agents_db: # if ML2 is still initializing + LOG.warning(_LW("ML2 still initializing, Will retry agentdb" + " update on next poll")) + return # Retry on next poll + + for host_config in hostconfigs: + try: + self.agentdb_row['host'] = host_config['host-id'] + self.agentdb_row['agent_type'] = host_config['host-type'] + self.agentdb_row['configurations'] = host_config['config'] + + agents_db.create_or_update_agent( + context.get_admin_context(), self.agentdb_row) + except Exception: + LOG.exception(_LE("Unable to update agentdb.")) + continue # try next hostcofig + + def _substitute_hconfig_tmpl(self, port_context, hconfig): + # TODO(mzmalick): Explore options for inlines string splicing of + # port-id to 14 bytes as required by vhostuser types + subs_ids = { + # $IDENTIFER string substitution in hostconfigs JSON string + 'PORT_ID': port_context.current['id'][:14] + } + + # Substitute identifiers and Convert JSON string to dict + hconfig_conf_json = Template(hconfig['configurations']) + substituted_str = hconfig_conf_json.safe_substitute(subs_ids) + hconfig['configurations'] = jsonutils.loads(substituted_str) + + return hconfig + + def bind_port(self, port_context): + """bind port using ODL host configuration.""" + # Get all ODL hostconfigs for this host and type + agentdb = port_context.host_agents(self.L2_TYPE) + + if not agentdb: + LOG.warning(_LW("No valid hostconfigs in agentsdb for host %s"), + port_context.host) + return + + for raw_hconfig in agentdb: + # do any $identifier substitution + hconfig = self._substitute_hconfig_tmpl(port_context, raw_hconfig) + + # Found ODL hostconfig for this host in agentdb + LOG.debug("ODLPORTBINDING bind port with hostconfig: %s", hconfig) + + if self._hconfig_bind_port(port_context, hconfig): + break # Port binding suceeded! + else: # Port binding failed! + LOG.warning(_LW("Failed to bind Port %(pid)s for host " + "%(host)s on network %(network)s."), { + 'pid': port_context.current['id'], + 'host': port_context.host, + 'network': port_context.network.current['id']}) + else: # No hostconfig found for host in agentdb. + LOG.warning(_LW("No ODL hostconfigs for host %s found in agentdb"), + port_context.host) + + def _hconfig_bind_port(self, port_context, hconfig): + """bind port after validating odl host configuration.""" + valid_segment = None + + for segment in port_context.segments_to_bind: + if self._is_valid_segment(segment, hconfig['configurations']): + valid_segment = segment + break + else: + LOG.debug("No valid segments found!") + return False + + confs = hconfig['configurations']['supported_vnic_types'] + + # nova provides vnic_type in port_context to neutron. + # neutron provides supported vif_type for binding based on vnic_type + # in this case ODL hostconfigs has the vif_type to bind for vnic_type + vnic_type = port_context.current.get(portbindings.VNIC_TYPE) + + if vnic_type != portbindings.VNIC_NORMAL: + LOG.error(_LE("Binding failed: unsupported VNIC %s"), vnic_type) + return False + + for conf in confs: + if conf["vnic_type"] == vnic_type: + vif_type = conf.get('vif_type', portbindings.VIF_TYPE_OVS) + LOG.debug("Binding vnic:'%s' to vif:'%s'", vnic_type, vif_type) + break + else: + vif_type = portbindings.VIF_TYPE_OVS # default: OVS + LOG.warning(_LW("No supported vif type found for host %s!, " + "defaulting to OVS"), port_context.host) + + vif_details = conf.get('vif_details', {}) + + if not vif_details: # empty vif_details could be trouble, warn. + LOG.warning(_LW("hostconfig:vif_details was empty!")) + + LOG.debug("Bind port %(port)s on network %(network)s with valid " + "segment %(segment)s and VIF type %(vif_type)r " + "VIF details %(vif_details)r.", + {'port': port_context.current['id'], + 'network': port_context.network.current['id'], + 'segment': valid_segment, 'vif_type': vif_type, + 'vif_details': vif_details}) + + port_context.set_binding(valid_segment[driver_api.ID], vif_type, + vif_details, + status=nl_const.PORT_STATUS_ACTIVE) + return True + + def _is_valid_segment(self, segment, conf): + """Verify a segment is supported by ODL.""" + network_type = segment[driver_api.NETWORK_TYPE] + return network_type in conf['allowed_network_types'] -- cgit 1.2.3-korg