summaryrefslogtreecommitdiffstats
path: root/networking-odl/networking_odl/ml2
diff options
context:
space:
mode:
Diffstat (limited to 'networking-odl/networking_odl/ml2')
-rw-r--r--networking-odl/networking_odl/ml2/README.odl41
-rw-r--r--networking-odl/networking_odl/ml2/__init__.py0
-rw-r--r--networking-odl/networking_odl/ml2/legacy_port_binding.py84
-rw-r--r--networking-odl/networking_odl/ml2/mech_driver.py458
-rw-r--r--networking-odl/networking_odl/ml2/mech_driver_v2.py146
-rw-r--r--networking-odl/networking_odl/ml2/network_topology.py313
-rw-r--r--networking-odl/networking_odl/ml2/ovsdb_topology.py218
-rw-r--r--networking-odl/networking_odl/ml2/port_binding.py121
-rw-r--r--networking-odl/networking_odl/ml2/pseudo_agentdb_binding.py263
9 files changed, 1644 insertions, 0 deletions
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
--- /dev/null
+++ b/networking-odl/networking_odl/ml2/__init__.py
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<String> mappedCollection = new ArrayList<String>;
+ # }
+ #
+ # 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/<uuid>[/<path>]]
+ node_url = parse.urlparse(node['node-id'])
+ if node_url.scheme == 'ovsdb'\
+ and node_url.netloc == 'uuid':
+ # split_res = ['', '<uuid>', '<path>']
+ 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 [<path>]
+ 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']