summaryrefslogtreecommitdiffstats
path: root/networking-odl/networking_odl/ml2/pseudo_agentdb_binding.py
diff options
context:
space:
mode:
Diffstat (limited to 'networking-odl/networking_odl/ml2/pseudo_agentdb_binding.py')
-rw-r--r--networking-odl/networking_odl/ml2/pseudo_agentdb_binding.py263
1 files changed, 263 insertions, 0 deletions
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']