diff options
Diffstat (limited to 'networking-odl/networking_odl/ml2/network_topology.py')
-rw-r--r-- | networking-odl/networking_odl/ml2/network_topology.py | 313 |
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) |