--- /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 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,
+ MultipleRecordsFound,
+ NoRecordFound,
+)
+from wimconn import WimConnectorError
+
+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 = ('wim_status', 'wim_info', 'error_msg')
+ result = dict.fromkeys(fields)
+
+ try:
+ result.update(
+ connector
+ .get_connectivity_service_status(self.wim_internal_id))
+ except WimConnectorError as ex:
+ self.logger.exception(ex)
+ result.update(wim_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['wim_status'] == 'BUILD' else None,
+ 'error_msg': result['error_msg'],
+ 'modified_at': time()})
+ link_changes = merge_dicts(result, status=result.pop('wim_status'))
+ # ^ Rename field: wim_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_endpoint(self, persistence, ovim, instance_net):
+ """Retrieve the endpoint (information about the connection PoP <> WAN
+ """
+ wim_account = persistence.get_wim_account_by(uuid=self.wim_account_id)
+
+ # TODO: make more generic to support networks that are not created with
+ # the SDN assist. This method should have a consistent way of getting
+ # the endpoint for all different types of networks used in the VIM
+ # (provider networks, SDN assist, overlay networks, ...)
+ if instance_net.get('sdn_net_id'):
+ return self.get_endpoint_sdn(
+ persistence, ovim, instance_net, wim_account['wim_id'])
+ else:
+ raise InconsistentState(
+ 'The field `instance_nets.sdn_net_id` was expected to be '
+ 'found in the database for the record %s after the network '
+ 'become active, but it is still NULL', instance_net['uuid'])
+
+ def get_endpoint_sdn(self, persistence, ovim, instance_net, wim_id):
+ criteria = {'net_id': instance_net['sdn_net_id']}
+ local_port_mapping = ovim.get_ports(filter=criteria)
+
+ if len(local_port_mapping) > 1:
+ raise MultipleRecordsFound(criteria, 'ovim.ports')
+ local_port_mapping = local_port_mapping[0]
+
+ criteria = {
+ 'wim_id': wim_id,
+ 'pop_switch_dpid': local_port_mapping['switch_dpid'],
+ 'pop_switch_port': local_port_mapping['switch_port'],
+ 'datacenter_id': instance_net['datacenter_id']}
+
+ wan_port_mapping = persistence.query_one(
+ FROM='wim_port_mappings',
+ WHERE=criteria)
+
+ if local_port_mapping.get('vlan'):
+ wan_port_mapping['wan_service_mapping_info']['vlan'] = (
+ local_port_mapping['vlan'])
+
+ return wan_port_mapping
+
+ @staticmethod
+ def _derive_connection_point(endpoint):
+ point = {'service_endpoint_id': endpoint['wan_service_endpoint_id']}
+ # TODO: Cover other scenarios, e.g. VXLAN.
+ info = endpoint.get('wan_service_mapping_info', {})
+ if 'vlan' in info:
+ point['service_endpoint_encapsulation_type'] = 'dot1q'
+ point['service_endpoint_encapsulation_info'] = {
+ 'vlan': info['vlan']
+ }
+ 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:
+ raise NotImplementedError('Multipoint connectivity is not '
+ 'supported yet.')
+
+ def _update_persistent_data(self, persistence, service_uuid,
+ endpoints, conn_info):
+ """Store plugin/connector specific information in the database"""
+ persistence.update_wan_link(self.item_id, {
+ 'wim_internal_id': service_uuid,
+ 'wim_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:
+ endpoints = [self.get_endpoint(persistence, ovim, net)
+ for net in instance_nets]
+ connection_points = [self._derive_connection_point(e)
+ for e in endpoints]
+
+ uuid, info = connector.create_connectivity_service(
+ self._derive_service_type(connection_points),
+ connection_points
+ # TODO: other properties, e.g. bandwidth
+ )
+ except (WimConnectorError, InconsistentState) 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, endpoints, 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, 'wim_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 (WimConnectorError, 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,
+}