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