+++ /dev/null
-# -*- coding: utf-8 -*-
-##
-# Copyright 2018 University of Bristol - High Performance Networks Research
-# Group
-# All Rights Reserved.
-#
-# Contributors: Anderson Bravalheri, Dimitrios Gkounis, Abubakar Siddique
-# Muqaddas, Navdeep Uniyal, Reza Nejabati and Dimitra Simeonidou
-#
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact with: <highperformance-networks@bristol.ac.uk>
-#
-# Neither the name of the University of Bristol nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
-# This work has been performed in the context of DCMS UK 5G Testbeds
-# & Trials Programme and in the framework of the Metro-Haul project -
-# funded by the European Commission under Grant number 761727 through the
-# Horizon 2020 and 5G-PPP programmes.
-##
-# pylint: disable=E1101,E0203,W0201
-import json
-from pprint import pformat
-# from sys import exc_info
-from time import time
-
-from ..utils import filter_dict_keys as filter_keys
-from ..utils import merge_dicts, remove_none_items, safe_get, truncate
-from .actions import CreateAction, DeleteAction, FindAction
-from .errors import (
- InconsistentState,
- NoRecordFound,
- NoExternalPortFound
-)
-from osm_ro_plugin.sdnconn import SdnConnectorError
-
-INSTANCE_NET_STATUS_ERROR = ('DOWN', 'ERROR', 'VIM_ERROR',
- 'DELETED', 'SCHEDULED_DELETION')
-INSTANCE_NET_STATUS_PENDING = ('BUILD', 'INACTIVE', 'SCHEDULED_CREATION')
-INSTANCE_VM_STATUS_ERROR = ('ERROR', 'VIM_ERROR',
- 'DELETED', 'SCHEDULED_DELETION')
-
-
-class RefreshMixin(object):
- def refresh(self, connector, persistence):
- """Ask the external WAN Infrastructure Manager system for updates on
- the status of the task.
-
- Arguments:
- connector: object with API for accessing the WAN
- Infrastructure Manager system
- persistence: abstraction layer for the database
- """
- fields = ('sdn_status', 'sdn_info', 'error_msg')
- result = dict.fromkeys(fields)
-
- try:
- result.update(
- connector
- .get_connectivity_service_status(self.wim_internal_id))
- except SdnConnectorError as ex:
- self.logger.exception(ex)
- result.update(sdn_status='WIM_ERROR', error_msg=truncate(ex))
-
- result = filter_keys(result, fields)
-
- action_changes = remove_none_items({
- 'extra': merge_dicts(self.extra, result),
- 'status': 'BUILD' if result['sdn_status'] == 'BUILD' else None,
- 'error_msg': result['error_msg'],
- 'modified_at': time()})
- link_changes = merge_dicts(result, status=result.pop('sdn_status'))
- # ^ Rename field: sdn_status => status
-
- persistence.update_wan_link(self.item_id,
- remove_none_items(link_changes))
-
- self.save(persistence, **action_changes)
-
- return result
-
-
-class WanLinkCreate(RefreshMixin, CreateAction):
- def fail(self, persistence, reason, status='FAILED'):
- changes = {'status': 'ERROR', 'error_msg': truncate(reason)}
- persistence.update_wan_link(self.item_id, changes)
- return super(WanLinkCreate, self).fail(persistence, reason, status)
-
- def process(self, connector, persistence, ovim):
- """Process the current task.
- First we check if all the dependencies are ready,
- then we call ``execute`` to actually execute the action.
-
- Arguments:
- connector: object with API for accessing the WAN
- Infrastructure Manager system
- persistence: abstraction layer for the database
- ovim: instance of openvim, abstraction layer that enable
- SDN-related operations
- """
- wan_link = persistence.get_by_uuid('instance_wim_nets', self.item_id)
-
- # First we check if all the dependencies are solved
- instance_nets = persistence.get_instance_nets(
- wan_link['instance_scenario_id'], wan_link['sce_net_id'])
-
- try:
- dependency_statuses = [n['status'] for n in instance_nets]
- except KeyError:
- self.logger.debug('`status` not found in\n\n%s\n\n',
- json.dumps(instance_nets, indent=4))
- errored = [instance_nets[i]
- for i, status in enumerate(dependency_statuses)
- if status in INSTANCE_NET_STATUS_ERROR]
- if errored:
- return self.fail(
- persistence,
- 'Impossible to stablish WAN connectivity due to an issue '
- 'with the local networks:\n\t' +
- '\n\t'.join('{uuid}: {status}'.format(**n) for n in errored))
-
- pending = [instance_nets[i]
- for i, status in enumerate(dependency_statuses)
- if status in INSTANCE_NET_STATUS_PENDING]
- if pending:
- return self.defer(
- persistence,
- 'Still waiting for the local networks to be active:\n\t' +
- '\n\t'.join('{uuid}: {status}'.format(**n) for n in pending))
-
- return self.execute(connector, persistence, ovim, instance_nets)
-
- def _get_connection_point_info(self, persistence, ovim, instance_net):
- """Retrieve information about the connection PoP <> WAN
-
- Arguments:
- persistence: object that encapsulates persistence logic
- (e.g. db connection)
- ovim: object that encapsulates network management logic (openvim)
- instance_net: record with the information about a local network
- (inside a VIM). This network will be connected via a WAN link
- to a different network in a distinct VIM.
- This method is used to trace what would be the way this network
- can be accessed from the outside world.
-
- Returns:
- dict: Record representing the wan_port_mapping associated to the
- given instance_net. The expected fields are:
- **wim_id**, **datacenter_id**, **device_id** (the local
- network is expected to be connected at this switch dpid),
- **device_interface_id**, **service_endpoint_id**,
- **service_mapping_info**.
- """
- # First, we need to find a route from the datacenter to the outside
- # world. For that, we can use the rules given in the datacenter
- # configuration:
- datacenter_id = instance_net['datacenter_id']
- datacenter = persistence.get_datacenter_by(datacenter_id)
- rules = safe_get(datacenter, 'config.external_connections', {}) or {}
- vim_info = instance_net.get('vim_info', {}) or {}
- # Alternatively, we can look for it, using the SDN assist
- external_port = (self._evaluate_rules(rules, vim_info) or
- self._get_port_sdn(ovim, instance_net))
-
- if not external_port:
- raise NoExternalPortFound(instance_net)
-
- # Then, we find the WAN switch that is connected to this external port
- try:
- wim_account = persistence.get_wim_account_by(
- uuid=self.wim_account_id)
-
- criteria = {
- 'wim_id': wim_account['wim_id'],
- 'device_id': external_port[0],
- 'device_interface_id': external_port[1],
- 'datacenter_id': datacenter_id}
-
- wan_port_mapping = persistence.query_one(
- FROM='wim_port_mappings',
- WHERE=criteria)
- except NoRecordFound as e:
- ex = InconsistentState('No WIM port mapping found:'
- 'wim_account: {}\ncriteria:\n{}'.format(
- self.wim_account_id, pformat(criteria)))
- raise ex from e
-
- # It is important to return encapsulation information if present
- mapping = merge_dicts(
- wan_port_mapping.get('service_mapping_info'),
- filter_keys(vim_info, ('encapsulation_type', 'encapsulation_id'))
- )
-
- return merge_dicts(wan_port_mapping, service_mapping_info=mapping)
-
- def _get_port_sdn(self, ovim, instance_net):
- try:
- local_port_mapping = ovim.get_ports(instance_net['sdn_net_id'])
-
- if local_port_mapping:
- return (local_port_mapping[0]['switch_dpid'],
- local_port_mapping[0]['switch_port'])
- except: # noqa
- self.logger.exception('Problems when calling OpenVIM')
-
- self.logger.debug("No ports found for sdn_net_id='{}'", instance_net['sdn_net_id'])
- return None
-
- def _evaluate_rules(self, rules, vim_info):
- """Given a ``vim_info`` dict from a ``instance_net`` record, evaluate
- the set of rules provided during the VIM/datacenter registration to
- determine an external port used to connect that VIM/datacenter to
- other ones where different parts of the NS will be instantiated.
-
- For example, considering a VIM/datacenter is registered like the
- following::
-
- vim_record = {
- "uuid": ...
- ... # Other properties associated with the VIM/datacenter
- "config": {
- ... # Other configuration
- "external_connections": [
- {
- "condition": {
- "provider:physical_network": "provider_net1",
- ... # This method will look up all the keys listed here
- # in the instance_nets.vim_info dict and compare the
- # values. When all the values match, the associated
- # vim_external_port will be selected.
- },
- "vim_external_port": {"switch": "switchA", "port": "portB"}
- },
- ... # The user can provide as many rules as needed, however
- # only the first one to match will be applied.
- ]
- }
- }
-
- When an ``instance_net`` record is instantiated in that datacenter with
- the following information::
-
- instance_net = {
- "uuid": ...
- ...
- "vim_info": {
- ...
- "provider_physical_network": "provider_net1",
- }
- }
-
- Then, ``switchA`` and ``portB`` will be used to stablish the WAN
- connection.
-
- Arguments:
- rules (list): Set of dicts containing the keys ``condition`` and
- ``vim_external_port``. This list should be extracted from
- ``vim['config']['external_connections']`` (as stored in the
- database).
- vim_info (dict): Information given by the VIM Connector, against
- which the rules will be evaluated.
-
- Returns:
- tuple: switch id (local datacenter switch) and port or None if
- the rule does not match.
- """
- rule = next((r for r in rules if self._evaluate_rule(r, vim_info)), {})
- if 'vim_external_port' not in rule:
- self.logger.debug('No external port found.\n'
- 'rules:\n%r\nvim_info:\n%r\n\n', rules, vim_info)
- return None
-
- return (rule['vim_external_port']['switch'],
- rule['vim_external_port']['port'])
-
- @staticmethod
- def _evaluate_rule(rule, vim_info):
- """Evaluate the conditions from a single rule to ``vim_info`` and
- determine if the rule should be applicable or not.
-
- Please check :obj:`~._evaluate_rules` for more information.
-
- Arguments:
- rule (dict): Data structure containing the keys ``condition`` and
- ``vim_external_port``. This should be one of the elements in
- ``vim['config']['external_connections']`` (as stored in the
- database).
- vim_info (dict): Information given by the VIM Connector, against
- which the rules will be evaluated.
-
- Returns:
- True or False: If all the conditions are met.
- """
- condition = rule.get('condition', {}) or {}
- return all(safe_get(vim_info, k) == v for k, v in condition.items())
-
- @staticmethod
- def _derive_connection_point(wan_info):
- point = {'service_endpoint_id': wan_info['service_endpoint_id']}
- # TODO: Cover other scenarios, e.g. VXLAN.
- details = wan_info.get('service_mapping_info', {})
- if details.get('encapsulation_type') == 'vlan':
- point['service_endpoint_encapsulation_type'] = 'dot1q'
- point['service_endpoint_encapsulation_info'] = {
- 'vlan': details['encapsulation_id'],
- 'switch_dpid': wan_info['switch_dpid'],
- 'switch_port': wan_info['switch_port']
- }
- else:
- point['service_endpoint_encapsulation_type'] = 'none'
- return point
-
- @staticmethod
- def _derive_service_type(connection_points):
- # TODO: add multipoint and L3 connectivity.
- if len(connection_points) == 2:
- return 'ELINE'
- else:
- # added to support DPB WIM connector
- return 'ELAN'
-
- def _update_persistent_data(self, persistence, service_uuid, conn_info):
- """Store plugin/connector specific information in the database"""
- persistence.update_wan_link(self.item_id, {
- 'wim_internal_id': service_uuid,
- 'sdn_info': {'conn_info': conn_info},
- 'status': 'BUILD'})
-
- def execute(self, connector, persistence, ovim, instance_nets):
- """Actually execute the action, since now we are sure all the
- dependencies are solved
- """
- try:
- wan_info = (self._get_connection_point_info(persistence, ovim, net)
- for net in instance_nets)
- connection_points = [self._derive_connection_point(w)
- for w in wan_info]
-
- uuid, info = connector.create_connectivity_service(
- self._derive_service_type(connection_points),
- connection_points
- # TODO: other properties, e.g. bandwidth
- )
- except (SdnConnectorError, InconsistentState,
- NoExternalPortFound) as ex:
- self.logger.exception(ex)
- return self.fail(
- persistence,
- 'Impossible to stablish WAN connectivity.\n\t{}'.format(ex))
-
- self.logger.debug('WAN connectivity established %s\n%s\n',
- uuid, json.dumps(info, indent=4))
- self.wim_internal_id = uuid
- self._update_persistent_data(persistence, uuid, info)
- self.succeed(persistence)
- return uuid
-
-
-class WanLinkDelete(DeleteAction):
- def succeed(self, persistence):
- try:
- persistence.update_wan_link(self.item_id, {'status': 'DELETED'})
- except NoRecordFound:
- self.logger.debug('%s(%s) record already deleted',
- self.item, self.item_id)
-
- return super(WanLinkDelete, self).succeed(persistence)
-
- def get_wan_link(self, persistence):
- """Retrieve information about the wan_link
-
- It might be cached, or arrive from the database
- """
- if self.extra.get('wan_link'):
- # First try a cached version of the data
- return self.extra['wan_link']
-
- return persistence.get_by_uuid(
- 'instance_wim_nets', self.item_id)
-
- def process(self, connector, persistence, ovim):
- """Delete a WAN link previously created"""
- wan_link = self.get_wan_link(persistence)
- if 'ERROR' in (wan_link.get('status') or ''):
- return self.fail(
- persistence,
- 'Impossible to delete WAN connectivity, '
- 'it was never successfully established:'
- '\n\t{}'.format(wan_link['error_msg']))
-
- internal_id = wan_link.get('wim_internal_id') or self.internal_id
-
- if not internal_id:
- self.logger.debug('No wim_internal_id found in\n%s\n%s\n'
- 'Assuming no network was created yet, '
- 'so no network have to be deleted.',
- json.dumps(wan_link, indent=4),
- json.dumps(self.as_dict(), indent=4))
- return self.succeed(persistence)
-
- try:
- id = self.wim_internal_id
- conn_info = safe_get(wan_link, 'sdn_info.conn_info')
- self.logger.debug('Connection Service %s (wan_link: %s):\n%s\n',
- id, wan_link['uuid'],
- json.dumps(conn_info, indent=4))
- result = connector.delete_connectivity_service(id, conn_info)
- except (SdnConnectorError, InconsistentState) as ex:
- self.logger.exception(ex)
- return self.fail(
- persistence,
- 'Impossible to delete WAN connectivity.\n\t{}'.format(ex))
-
- self.logger.debug('WAN connectivity removed %s', result)
- self.succeed(persistence)
-
- return result
-
-
-class WanLinkFind(RefreshMixin, FindAction):
- pass
-
-
-ACTIONS = {
- 'CREATE': WanLinkCreate,
- 'DELETE': WanLinkDelete,
- 'FIND': WanLinkFind,
-}