feature 8029 change RO to python3. Using vim plugins
[osm/RO.git] / RO / osm_ro / wim / wan_link_actions.py
diff --git a/RO/osm_ro/wim/wan_link_actions.py b/RO/osm_ro/wim/wan_link_actions.py
new file mode 100644 (file)
index 0000000..0d878b2
--- /dev/null
@@ -0,0 +1,440 @@
+# -*- 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 .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_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**, **pop_switch_dpid** (the local
+                  network is expected to be connected at this switch),
+                  **pop_switch_port**, **wan_service_endpoint_id**,
+                  **wan_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'],
+                'pop_switch_dpid': external_port[0],
+                'pop_switch_port': 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('wan_service_mapping_info'),
+            filter_keys(vim_info, ('encapsulation_type', 'encapsulation_id'))
+        )
+
+        return merge_dicts(wan_port_mapping, wan_service_mapping_info=mapping)
+
+    def _get_port_sdn(self, ovim, instance_net):
+        criteria = {'net_id': instance_net['sdn_net_id']}
+        try:
+            local_port_mapping = ovim.get_ports(filter=criteria)
+
+            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 using criteria:\n%r\n.', criteria)
+        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['wan_service_endpoint_id']}
+        # TODO: Cover other scenarios, e.g. VXLAN.
+        details = wan_info.get('wan_service_mapping_info', {})
+        if details.get('encapsulation_type') == 'vlan':
+            point['service_endpoint_encapsulation_type'] = 'dot1q'
+            point['service_endpoint_encapsulation_info'] = {
+                'vlan': details['encapsulation_id']
+            }
+        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, 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:
+            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 (WimConnectorError, 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, '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,
+}