inject_user_key routine fixes
[osm/RO.git] / osm_ro / wim / wan_link_actions.py
index 61c6dd9..034e415 100644 (file)
 ##
 # pylint: disable=E1101,E0203,W0201
 import json
+from pprint import pformat
+from sys import exc_info
 from time import time
 
+from six import reraise
+
 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,
+    NoExternalPortFound
 )
 from wimconn import WimConnectorError
 
@@ -157,60 +161,164 @@ class WanLinkCreate(RefreshMixin, CreateAction):
         Returns:
             dict: Record representing the wan_port_mapping associated to the
                   given instance_net. The expected fields are:
-                  **wim_id**, **datacenter_id**, **pop_switch_id** (the local
+                  **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**.
         """
-        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_connection_point_info_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'])
+        # 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:
+            ex = InconsistentState('No WIM port mapping found:'
+                                   'wim_account: {}\ncriteria:\n{}'.format(
+                                       self.wim_account_id, pformat(criteria)))
+            reraise(ex.__class__, ex, exc_info()[2])
+
+        # 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_connection_point_info_sdn(self, persistence, ovim,
-                                       instance_net, wim_id):
+    def _get_port_sdn(self, ovim, instance_net):
         criteria = {'net_id': instance_net['sdn_net_id']}
-        local_port_mapping = ovim.get_ports(filter=criteria)
+        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",
+              }
+            }
 
-        if len(local_port_mapping) > 1:
-            raise MultipleRecordsFound(criteria, 'ovim.ports')
-        local_port_mapping = local_port_mapping[0]
+        Then, ``switchA`` and ``portB`` will be used to stablish the WAN
+        connection.
 
-        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']}
+        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
 
-        wan_port_mapping = persistence.query_one(
-            FROM='wim_port_mappings',
-            WHERE=criteria)
+        return (rule['vim_external_port']['switch'],
+                rule['vim_external_port']['port'])
 
-        if local_port_mapping.get('vlan'):
-            wan_port_mapping['wan_service_mapping_info']['vlan'] = (
-                local_port_mapping['vlan'])
+    @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.
 
-        return wan_port_mapping
+        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 'vlan' in details:
+        if details.get('encapsulation_type') == 'vlan':
             point['service_endpoint_encapsulation_type'] = 'dot1q'
             point['service_endpoint_encapsulation_info'] = {
-                'vlan': details['vlan']
+                'vlan': details['encapsulation_id']
             }
         else:
             point['service_endpoint_encapsulation_type'] = 'none'
@@ -247,7 +355,8 @@ class WanLinkCreate(RefreshMixin, CreateAction):
                 connection_points
                 # TODO: other properties, e.g. bandwidth
             )
-        except (WimConnectorError, InconsistentState) as ex:
+        except (WimConnectorError, InconsistentState,
+                NoExternalPortFound) as ex:
             self.logger.exception(ex)
             return self.fail(
                 persistence,