Merge branch 'WIM' 13/7113/2
authortierno <alfonso.tiernosepulveda@telefonica.com>
Mon, 21 Jan 2019 09:55:46 +0000 (09:55 +0000)
committertierno <alfonso.tiernosepulveda@telefonica.com>
Mon, 21 Jan 2019 09:55:46 +0000 (09:55 +0000)
Change-Id: Id314390ba524d22d4e1f74cfe8ef7c808bb8ac0e
Signed-off-by: tierno <alfonso.tiernosepulveda@telefonica.com>
16 files changed:
openmano
osm_ro/nfvo.py
osm_ro/vimconn_openstack.py
osm_ro/wim/engine.py
osm_ro/wim/errors.py
osm_ro/wim/http_handler.py
osm_ro/wim/persistence.py
osm_ro/wim/schemas.py
osm_ro/wim/tests/fixtures.py
osm_ro/wim/tests/test_actions.py
osm_ro/wim/tests/test_engine.py
osm_ro/wim/tests/test_http_handler.py
osm_ro/wim/tests/test_persistence.py
osm_ro/wim/wan_link_actions.py
osm_ro/wim/wim_thread.py
osm_ro/wim/wimconn_dynpac.py [new file with mode: 0644]

index 1577656..13a93da 100755 (executable)
--- a/openmano
+++ b/openmano
@@ -2254,7 +2254,7 @@ if __name__=="__main__":
     wim_create_parser.add_argument("url", action="store",
                                    help="url for the wim")
     wim_create_parser.add_argument("--type", action="store",
-                                   help="wim type: tapi, onos or odl (default)")
+                                   help="wim type: tapi, onos, dynpac or odl (default)")
     wim_create_parser.add_argument("--config", action="store",
                                    help="additional configuration in json/yaml format")
     wim_create_parser.add_argument("--description", action="store",
index 00738e9..4341422 100644 (file)
@@ -61,6 +61,7 @@ from .http_tools import errors as httperrors
 from .wim.engine import WimEngine
 from .wim.persistence import WimPersistence
 from copy import deepcopy
+from pprint import pformat
 #
 
 global global_config
@@ -3055,6 +3056,8 @@ def create_instance(mydb, tenant_id, instance_dict):
     # Auxiliary dictionaries from x to y
     sce_net2instance = {}
     net2task_id = {'scenario': {}}
+    # Mapping between local networks and WIMs
+    wim_usage = {}
 
     def ip_profile_IM2RO(ip_profile_im):
         # translate from input format to database format
@@ -3200,6 +3203,18 @@ def create_instance(mydb, tenant_id, instance_dict):
             if not involved_datacenters:
                 involved_datacenters.append(default_datacenter_id)
 
+            # --> WIM
+            # TODO: use this information during network creation
+            wim_account_id = None
+            if len(involved_datacenters) > 1 and 'uuid' in sce_net:
+                # OBS: sce_net without uuid are used internally to VNFs
+                # and the assumption is that VNFs will not be split among
+                # different datacenters
+                wim_account_id = wim_engine.find_suitable_wim_account(
+                    involved_datacenters, tenant_id)
+                wim_usage[sce_net['uuid']] = wim_account_id
+            # <-- WIM
+
             descriptor_net = {}
             if instance_dict.get("networks") and instance_dict["networks"].get(sce_net["name"]):
                 descriptor_net = instance_dict["networks"][sce_net["name"]]
@@ -3527,7 +3542,8 @@ def create_instance(mydb, tenant_id, instance_dict):
         db_instance_action["number_tasks"] = task_index
 
         # --> WIM
-        wan_links = wim_engine.derive_wan_links(db_instance_nets, tenant_id)
+        logger.debug('wim_usage:\n%s\n\n', pformat(wim_usage))
+        wan_links = wim_engine.derive_wan_links(wim_usage, db_instance_nets, tenant_id)
         wim_actions = wim_engine.create_actions(wan_links)
         wim_actions, db_instance_action = (
             wim_engine.incorporate_actions(wim_actions, db_instance_action))
index 876fa2f..0930eb0 100644 (file)
@@ -568,7 +568,9 @@ class vimconnector(vimconn.vimconnector):
             subnets.append(subnet)
         net["subnets"] = subnets
         net["encapsulation"] = net.get('provider:network_type')
+        net["encapsulation_type"] = net.get('provider:network_type')
         net["segmentation_id"] = net.get('provider:segmentation_id')
+        net["encapsulation_id"] = net.get('provider:segmentation_id')
         return net
 
     def delete_network(self, net_id):
index dde5dee..6ff2b4f 100644 (file)
@@ -48,12 +48,17 @@ import logging
 from contextlib import contextmanager
 from itertools import groupby
 from operator import itemgetter
+from sys import exc_info
 from uuid import uuid4
 
+from six import reraise
+
 from ..utils import remove_none_items
 from .actions import Action
 from .errors import (
+    DbBaseException,
     NoWimConnectedToDatacenters,
+    UnexpectedDatabaseError,
     WimAccountNotActive
 )
 from .wim_thread import WimThread
@@ -76,10 +81,29 @@ class WimEngine(object):
         Please check the wim schema to have more information about
         ``properties``.
 
+        The ``config`` property might contain a ``wim_port_mapping`` dict,
+        In this case, the method ``create_wim_port_mappings`` will be
+        automatically invoked.
+
         Returns:
             str: uuid of the newly created WIM record
         """
-        return self.persist.create_wim(properties)
+        port_mapping = ((properties.get('config', {}) or {})
+                        .pop('wim_port_mapping', {}))
+        uuid = self.persist.create_wim(properties)
+
+        if port_mapping:
+            try:
+                self.create_wim_port_mappings(uuid, port_mapping)
+            except DbBaseException:
+                # Rollback
+                self.delete_wim(uuid)
+                ex = UnexpectedDatabaseError('Failed to create port mappings'
+                                             'Rolling back wim creation')
+                self.logger.exception(str(ex))
+                reraise(ex.__class__, ex, exc_info()[2])
+
+        return uuid
 
     def get_wim(self, uuid_or_name, tenant_id=None):
         """Retrieve existing WIM record by name or id.
@@ -95,8 +119,35 @@ class WimEngine(object):
 
         ``properties`` is a dictionary with the properties being changed,
         if a property is not present, the old value will be preserved
+
+        Similarly to create_wim, the ``config`` property might contain a
+        ``wim_port_mapping`` dict, In this case, port mappings will be
+        automatically updated.
         """
-        return self.persist.update_wim(uuid_or_name, properties)
+        port_mapping = ((properties.get('config', {}) or {})
+                        .pop('wim_port_mapping', {}))
+        orig_props = self.persist.get_by_name_or_uuid('wims', uuid_or_name)
+        uuid = orig_props['uuid']
+
+        response = self.persist.update_wim(uuid, properties)
+
+        if port_mapping:
+            try:
+                # It is very complex to diff and update individually all the
+                # port mappings. Therefore a practical approach is just delete
+                # and create it again.
+                self.persist.delete_wim_port_mappings(uuid)
+                # ^  Calling from persistence avoid reloading twice the thread
+                self.create_wim_port_mappings(uuid, port_mapping)
+            except DbBaseException:
+                # Rollback
+                self.update_wim(uuid_or_name, orig_props)
+                ex = UnexpectedDatabaseError('Failed to update port mappings'
+                                             'Rolling back wim updates\n')
+                self.logger.exception(str(ex))
+                reraise(ex.__class__, ex, exc_info()[2])
+
+        return response
 
     def delete_wim(self, uuid_or_name):
         """Kill the corresponding wim threads and erase the WIM record"""
@@ -247,7 +298,7 @@ class WimEngine(object):
         """Find a single WIM that is able to connect all the datacenters
         listed
 
-        Raises
+        Raises:
             NoWimConnectedToDatacenters: if no WIM connected to all datacenters
                 at once is found
         """
@@ -261,14 +312,35 @@ class WimEngine(object):
         #       used here)
         return suitable_wim_ids[0]
 
+    def find_suitable_wim_account(self, datacenter_ids, tenant):
+        """Find a WIM account that is able to connect all the datacenters
+        listed
+
+        Arguments:
+            datacenter_ids (list): List of UUIDs of all the datacenters (vims),
+                that need to be connected.
+            tenant (str): UUID of the OSM tenant
+
+        Returns:
+            str: UUID of the WIM account that is able to connect all the
+                 datacenters.
+        """
+        wim_id = self.find_common_wim(datacenter_ids, tenant)
+        return self.persist.get_wim_account_by(wim_id, tenant)['uuid']
+
     def derive_wan_link(self,
+                        wim_usage,
                         instance_scenario_id, sce_net_id,
                         networks, tenant):
         """Create a instance_wim_nets record for the given information"""
-        datacenters = [n['datacenter_id'] for n in networks]
-        wim_id = self.find_common_wim(datacenters, tenant)
-
-        account = self.persist.get_wim_account_by(wim_id, tenant)
+        if sce_net_id in wim_usage:
+            account_id = wim_usage[sce_net_id]
+            account = self.persist.get_wim_account_by(uuid=account_id)
+            wim_id = account['wim_id']
+        else:
+            datacenters = [n['datacenter_id'] for n in networks]
+            wim_id = self.find_common_wim(datacenters, tenant)
+            account = self.persist.get_wim_account_by(wim_id, tenant)
 
         return {
             'uuid': str(uuid4()),
@@ -278,15 +350,17 @@ class WimEngine(object):
             'wim_account_id': account['uuid']
         }
 
-    def derive_wan_links(self, networks, tenant=None):
+    def derive_wan_links(self, wim_usage, networks, tenant=None):
         """Discover and return what are the wan_links that have to be created
         considering a set of networks (VLDs) required for a scenario instance
         (NSR).
 
         Arguments:
+            wim_usage(dict): Mapping between sce_net_id and wim_id
             networks(list): Dicts containing the information about the networks
                 that will be instantiated to materialize a Network Service
                 (scenario) instance.
+                Corresponding to the ``instance_net`` record.
 
         Returns:
             list: list of WAN links to be written to the database
@@ -302,7 +376,8 @@ class WimEngine(object):
                       if counter > 1]
 
         return [
-            self.derive_wan_link(key[0], key[1], grouped_networks[key], tenant)
+            self.derive_wan_link(wim_usage,
+                                 key[0], key[1], grouped_networks[key], tenant)
             for key in wan_groups
         ]
 
index 16c53b5..ca8c2b7 100644 (file)
@@ -178,3 +178,12 @@ class WimAccountNotActive(HttpMappedError, KeyError):
         message += ('\nThe thread responsible for processing the actions have '
                     'suddenly stopped, or have never being spawned')
         super(WimAccountNotActive, self).__init__(message, http_code)
+
+
+class NoExternalPortFound(HttpMappedError):
+    """No external port associated to the instance_net"""
+
+    def __init__(self, instance_net):
+        super(NoExternalPortFound, self).__init__(
+            '{} uuid({})'.format(self.__class__.__doc__, instance_net['uuid']),
+            http_code=Not_Found)
index f5eeed9..b88dab3 100644 (file)
@@ -120,6 +120,9 @@ class WimHandler(BaseHandler):
     def http_get_wim(self, tenant_id, wim_id):
         tenant_id = None if tenant_id == 'any' else tenant_id
         wim = self.engine.get_wim(wim_id, tenant_id)
+        mappings = self.engine.get_wim_port_mappings(wim_id)
+        wim['config'] = utils.merge_dicts(wim.get('config', {}) or {},
+                                          wim_port_mapping=mappings)
         return format_out({'wim': wim})
 
     @route('POST', '/wims')
index 8a74d49..b956965 100644 (file)
@@ -326,12 +326,11 @@ class WimPersistence(object):
         where = {'uuid': wim['uuid']}
 
         # unserialize config, edit and serialize it again
-        if wim_descriptor.get('config'):
-            new_config_dict = wim_descriptor["config"]
-            config_dict = remove_none_items(merge_dicts(
-                wim.get('config') or {}, new_config_dict))
-            wim_descriptor['config'] = (
-                _serialize(config_dict) if config_dict else None)
+        new_config_dict = wim_descriptor.get('config', {}) or {}
+        config_dict = remove_none_items(merge_dicts(
+            wim.get('config', {}) or {}, new_config_dict))
+        wim_descriptor['config'] = (
+            _serialize(config_dict) if config_dict else None)
 
         with self.lock:
             self.db.update_rows('wims', wim_descriptor, where)
@@ -363,10 +362,10 @@ class WimPersistence(object):
             kwargs.setdefault('WHERE', {'wim_account.uuid': uuid})
         return self.query(FROM=_WIM_ACCOUNT_JOIN, **kwargs)
 
-    def get_wim_account_by(self, wim=None, tenant=None, **kwargs):
+    def get_wim_account_by(self, wim=None, tenant=None, uuid=None, **kwargs):
         """Similar to ``get_wim_accounts_by``, but ensuring just one result"""
         kwargs.setdefault('error_if_multiple', True)
-        return self.get_wim_accounts_by(wim, tenant, **kwargs)[0]
+        return self.get_wim_accounts_by(wim, tenant, uuid, **kwargs)[0]
 
     def get_wim_accounts(self, **kwargs):
         """Retrieve all the accounts from the database"""
@@ -508,8 +507,12 @@ class WimPersistence(object):
 
         See :obj:`~.query` for additional keyword arguments.
         """
-        kwargs.update(datacenter=datacenter, tenant=tenant)
-        return self.query(_DATACENTER_JOIN, **kwargs)
+        if tenant:
+            kwargs.update(datacenter=datacenter, tenant=tenant)
+            return self.query(_DATACENTER_JOIN, **kwargs)
+        else:
+            return [self.get_by_name_or_uuid('datacenters',
+                                             datacenter, **kwargs)]
 
     def get_datacenter_by(self, datacenter=None, tenant=None, **kwargs):
         """Similar to ``get_datacenters_by``, but ensuring just one result"""
@@ -622,7 +625,7 @@ class WimPersistence(object):
         return [
             {'wim_id': key[0],
              'datacenter_id': key[1],
-             'wan_pop_port_mappings': [
+             'pop_wan_mappings': [
                  filter_out_dict_keys(mapping, (
                      'id', 'wim_id', 'datacenter_id',
                      'created_at', 'modified_at'))
index a040405..fb65fdd 100644 (file)
@@ -39,17 +39,87 @@ from ..openmano_schemas import (
 )
 
 # WIM -------------------------------------------------------------------------
-wim_types = ["tapi", "onos", "odl"]
+wim_types = ["tapi", "onos", "odl", "dynpac"]
+
+dpid_type = {
+    "type": "string",
+    "pattern":
+        "^[0-9a-zA-Z]+(:[0-9a-zA-Z]+)*$"
+}
+
+port_type = {
+    "oneOf": [
+        {"type": "string",
+         "minLength": 1,
+         "maxLength": 5},
+        {"type": "integer",
+         "minimum": 1,
+         "maximum": 65534}
+    ]
+}
+
+wim_port_mapping_desc = {
+    "type": "array",
+    "items": {
+        "type": "object",
+        "properties": {
+            "datacenter_name": nameshort_schema,
+            "pop_wan_mappings": {
+                "type": "array",
+                "items": {
+                    "type": "object",
+                    "properties": {
+                        "pop_switch_dpid": dpid_type,
+                        "pop_switch_port": port_type,
+                        "wan_service_endpoint_id": name_schema,
+                        "wan_service_mapping_info": {
+                            "type": "object",
+                            "properties": {
+                                "mapping_type": name_schema,
+                                "wan_switch_dpid": dpid_type,
+                                "wan_switch_port": port_type
+                            },
+                            "additionalProperties": True,
+                            "required": ["mapping_type"]
+                        }
+                    },
+                    "oneOf": [
+                        {
+                            "required": [
+                                "pop_switch_dpid",
+                                "pop_switch_port",
+                                "wan_service_endpoint_id"
+                            ]
+                        },
+                        {
+                            "required": [
+                                "pop_switch_dpid",
+                                "pop_switch_port",
+                                "wan_service_mapping_info"
+                            ]
+                        }
+                    ]
+                }
+            }
+        },
+        "required": ["datacenter_name", "pop_wan_mappings"]
+    }
+}
 
 wim_schema_properties = {
     "name": name_schema,
     "description": description_schema,
     "type": {
         "type": "string",
-        "enum": ["tapi", "onos", "odl"]
+        "enum": ["tapi", "onos", "odl", "dynpac"]
     },
     "wim_url": description_schema,
-    "config": {"type": "object"}
+    "config": {
+        "type": "object",
+        "properties": {
+            "wim_port_mapping": wim_port_mapping_desc
+        }
+    }
 }
 
 wim_schema = {
@@ -103,75 +173,12 @@ wim_account_schema = {
     "additionalProperties": False
 }
 
-dpid_type = {
-    "type": "string",
-    "pattern":
-        "^[0-9a-zA-Z]+(:[0-9a-zA-Z]+)*$"
-}
-
-port_type = {
-    "oneOf": [
-        {"type": "string",
-         "minLength": 1,
-         "maxLength": 5},
-        {"type": "integer",
-         "minimum": 1,
-         "maximum": 65534}
-    ]
-}
-
 wim_port_mapping_schema = {
     "$schema": "http://json-schema.org/draft-04/schema#",
     "title": "wim mapping information schema",
     "type": "object",
     "properties": {
-        "wim_port_mapping": {
-            "type": "array",
-            "items": {
-                "type": "object",
-                "properties": {
-                    "datacenter_name": nameshort_schema,
-                    "pop_wan_mappings": {
-                        "type": "array",
-                        "items": {
-                            "type": "object",
-                            "properties": {
-                                "pop_switch_dpid": dpid_type,
-                                "pop_switch_port": port_type,
-                                "wan_service_endpoint_id": name_schema,
-                                "wan_service_mapping_info": {
-                                    "type": "object",
-                                    "properties": {
-                                        "mapping_type": name_schema,
-                                        "wan_switch_dpid": dpid_type,
-                                        "wan_switch_port": port_type
-                                    },
-                                    "additionalProperties": True,
-                                    "required": ["mapping_type"]
-                                }
-                            },
-                            "oneOf": [
-                                {
-                                    "required": [
-                                        "pop_switch_dpid",
-                                        "pop_switch_port",
-                                        "wan_service_endpoint_id"
-                                    ]
-                                },
-                                {
-                                    "required": [
-                                        "pop_switch_dpid",
-                                        "pop_switch_port",
-                                        "wan_service_mapping_info"
-                                    ]
-                                }
-                            ]
-                        }
-                    }
-                },
-                "required": ["datacenter_name", "pop_wan_mappings"]
-            }
-        }
+        "wim_port_mapping": wim_port_mapping_desc
     },
     "required": ["wim_port_mapping"]
 }
index 1b52e49..cb662ab 100644 (file)
@@ -36,7 +36,9 @@
 from __future__ import unicode_literals
 
 import json
+from itertools import izip
 from time import time
+from textwrap import wrap
 
 from six.moves import range
 
@@ -86,11 +88,28 @@ def wim_set(identifier=0, tenant=0):
     ]
 
 
-def datacenter(identifier):
+def _datacenter_to_switch_port(dc_id, port=None):
+    digits = 16
+    switch = ':'.join(wrap(('%0' + str(digits) + 'x') % int(dc_id), 2))
+    return (switch, str((port or int(dc_id)) + 1))
+
+
+def datacenter(identifier, external_ports_config=False):
+    config = '' if not external_ports_config else json.dumps({
+        'external_connections': [
+            {'condition': {
+                'provider:physical_network': 'provider',
+                'encapsulation_type': 'vlan'},
+                'vim_external_port':
+                dict(izip(('switch', 'port'),
+                          _datacenter_to_switch_port(identifier)))}
+        ]})
+
     return {'uuid': uuid('dc%d' % identifier),
             'name': 'dc%d' % identifier,
             'type': 'openvim',
-            'vim_url': 'localhost'}
+            'vim_url': 'localhost',
+            'config': config}
 
 
 def datacenter_account(datacenter, tenant):
@@ -107,7 +126,7 @@ def datacenter_tenant_association(datacenter, tenant):
                 uuid('dc-account%d%d' % (tenant, datacenter))}
 
 
-def datacenter_set(identifier, tenant):
+def datacenter_set(identifier=0, tenant=0):
     """Records necessary to create a datacenter and connect it to a tenant"""
     return [
         {'datacenters': [datacenter(identifier)]},
@@ -119,17 +138,19 @@ def datacenter_set(identifier, tenant):
 
 
 def wim_port_mapping(wim, datacenter,
-                     pop_dpid='AA:AA:AA:AA:AA:AA:AA:AA', pop_port=0,
-                     wan_dpid='BB:BB:BB:BB:BB:BB:BB:BB', wan_port=0):
+                     pop_dpid='AA:AA:AA:AA:AA:AA:AA:AA', pop_port=None,
+                     wan_dpid='BB:BB:BB:BB:BB:BB:BB:BB', wan_port=None):
     mapping_info = {'mapping_type': 'dpid-port',
                     'wan_switch_dpid': wan_dpid,
-                    'wan_switch_port': wan_port + datacenter + 1}
+                    'wan_switch_port': (str(wan_port) if wan_port else
+                                        str(int(datacenter) + int(wim) + 1))}
     id_ = 'dpid-port|' + sha1(json.dumps(mapping_info, sort_keys=True))
 
     return {'wim_id': uuid('wim%d' % wim),
             'datacenter_id': uuid('dc%d' % datacenter),
             'pop_switch_dpid': pop_dpid,
-            'pop_switch_port': pop_port + wim + 1,
+            'pop_switch_port': (str(pop_port) if pop_port else
+                                str(int(datacenter) + int(wim) + 1)),
             # ^  Datacenter router have one port managed by each WIM
             'wan_service_endpoint_id': id_,
             # ^  WIM managed router have one port connected to each DC
@@ -146,7 +167,7 @@ def processed_port_mapping(wim, datacenter,
     return {
         'wim_id': uuid('wim%d' % wim),
         'datacenter_id': uuid('dc%d' % datacenter),
-        'wan_pop_port_mappings': [
+        'pop_wan_mappings': [
             {'pop_switch_dpid': pop_dpid,
              'pop_switch_port': wim + 1 + i,
              'wan_service_endpoint_id':
@@ -161,7 +182,8 @@ def processed_port_mapping(wim, datacenter,
 
 
 def consistent_set(num_wims=NUM_WIMS, num_tenants=NUM_TENANTS,
-                   num_datacenters=NUM_DATACENTERS):
+                   num_datacenters=NUM_DATACENTERS,
+                   external_ports_config=False):
     return [
         {'nfvo_tenants': [tenant(i) for i in range(num_tenants)]},
         {'wims': [wim(j) for j in range(num_wims)]},
@@ -176,7 +198,7 @@ def consistent_set(num_wims=NUM_WIMS, num_tenants=NUM_TENANTS,
             for j in range(num_wims)
         ]},
         {'datacenters': [
-            datacenter(k)
+            datacenter(k, external_ports_config)
             for k in range(num_datacenters)
         ]},
         {'datacenter_tenants': [
@@ -190,14 +212,15 @@ def consistent_set(num_wims=NUM_WIMS, num_tenants=NUM_TENANTS,
             for k in range(num_datacenters)
         ]},
         {'wim_port_mappings': [
-            wim_port_mapping(j, k)
+            (wim_port_mapping(j, k, *_datacenter_to_switch_port(k))
+             if external_ports_config else wim_port_mapping(j, k))
             for j in range(num_wims)
             for k in range(num_datacenters)
         ]},
     ]
 
 
-def instance_nets(num_datacenters=2, num_links=2):
+def instance_nets(num_datacenters=2, num_links=2, status='BUILD'):
     """Example of multi-site deploy with N datacenters and M WAN links between
     them (e.g M = 2 -> back and forth)
     """
@@ -209,7 +232,7 @@ def instance_nets(num_datacenters=2, num_links=2):
          # ^  instance_scenario_id == NS Record id
          'sce_net_id': uuid('vld%d' % l),
          # ^  scenario net id == VLD id
-         'status': 'BUILD',
+         'status': status,
          'vim_net_id': None,
          'created': True}
         for k in range(num_datacenters)
index 920182b..cee3c96 100644 (file)
@@ -47,7 +47,7 @@ from ...tests.db_helpers import (
     disable_foreign_keys,
     uuid,
 )
-from ..persistence import WimPersistence
+from ..persistence import WimPersistence, preprocess_record
 from ..wan_link_actions import WanLinkCreate, WanLinkDelete
 from ..wimconn import WimConnectorError
 
@@ -118,22 +118,118 @@ class TestCreate(TestActionsWithDb):
         self.assertIn('issue with the local networks', action.error_msg)
         self.assertIn('SCHEDULED_DELETION', action.error_msg)
 
+    def prepare_create__rules(self):
+        db_state = eg.consistent_set(num_wims=1, num_tenants=1,
+                                     num_datacenters=2,
+                                     external_ports_config=True)
+
+        instance_nets = eg.instance_nets(num_datacenters=2, num_links=1,
+                                         status='ACTIVE')
+        for i, net in enumerate(instance_nets):
+            net['vim_info'] = {}
+            net['vim_info']['provider:physical_network'] = 'provider'
+            net['vim_info']['encapsulation_type'] = 'vlan'
+            net['vim_info']['encapsulation_id'] = i
+            net['sdn_net_id'] = uuid('sdn-net%d' % i)
+
+        instance_action = eg.instance_action(action_id='ACTION-000')
+
+        db_state += [
+            {'instance_wim_nets': eg.instance_wim_nets()},
+            {'instance_nets': [preprocess_record(r) for r in instance_nets]},
+            {'instance_actions': instance_action}]
+
+        action = WanLinkCreate(
+            eg.wim_actions('CREATE', action_id='ACTION-000')[0])
+        # --> ensure it is in the database for updates --> #
+        action_record = action.as_record()
+        action_record['extra'] = json.dumps(action_record['extra'])
+        db_state += [{'vim_wim_actions': action_record}]
+
+        return db_state, action
+
+    @disable_foreign_keys
+    def test_process__rules(self):
+        # Given we want 1 WAN link between 2 datacenters
+        # and the local network in each datacenter is already created
+        db_state, action = self.prepare_create__rules()
+        self.populate(db_state)
+
+        instance_action = self.persist.get_by_uuid(
+            'instance_actions', action.instance_action_id)
+        number_done = instance_action['number_done']
+        number_failed = instance_action['number_failed']
+
+        # If the connector works fine
+        with patch.object(self.connector, 'create_connectivity_service',
+                          lambda *_, **__: (uuid('random-id'), None)):
+            # When we try to process a CREATE action that refers to the same
+            # instance_scenario_id and sce_net_id
+            action.process(self.connector, self.persist, self.ovim)
+
+        # Then the action should be succeeded
+        db_action = self.persist.query_one('vim_wim_actions', WHERE={
+            'instance_action_id': action.instance_action_id,
+            'task_index': action.task_index})
+        self.assertEqual(db_action['status'], 'DONE')
+
+        instance_action = self.persist.get_by_uuid(
+            'instance_actions', action.instance_action_id)
+        self.assertEqual(instance_action['number_done'], number_done + 1)
+        self.assertEqual(instance_action['number_failed'], number_failed)
+
+    @disable_foreign_keys
+    def test_process__rules_fail(self):
+        # Given we want 1 WAN link between 2 datacenters
+        # and the local network in each datacenter is already created
+        db_state, action = self.prepare_create__rules()
+        self.populate(db_state)
+
+        instance_action = self.persist.get_by_uuid(
+            'instance_actions', action.instance_action_id)
+        number_done = instance_action['number_done']
+        number_failed = instance_action['number_failed']
+
+        # If the connector raises an error
+        with patch.object(self.connector, 'create_connectivity_service',
+                          MagicMock(side_effect=WimConnectorError('foobar'))):
+            # When we try to process a CREATE action that refers to the same
+            # instance_scenario_id and sce_net_id
+            action.process(self.connector, self.persist, self.ovim)
+
+        # Then the action should be fail
+        db_action = self.persist.query_one('vim_wim_actions', WHERE={
+            'instance_action_id': action.instance_action_id,
+            'task_index': action.task_index})
+        self.assertEqual(db_action['status'], 'FAILED')
+
+        instance_action = self.persist.get_by_uuid(
+            'instance_actions', action.instance_action_id)
+        self.assertEqual(instance_action['number_done'], number_done)
+        self.assertEqual(instance_action['number_failed'], number_failed + 1)
+
     def prepare_create__sdn(self):
-        db_state = [{'nfvo_tenants': eg.tenant()}] + eg.wim_set()
+        db_state = eg.consistent_set(num_wims=1, num_tenants=1,
+                                     num_datacenters=2,
+                                     external_ports_config=False)
+
+        # Make sure all port_mappings are predictable
+        switch = 'AA:AA:AA:AA:AA:AA:AA:AA'
+        port = 1
+        port_mappings = next(r['wim_port_mappings']
+                             for r in db_state if 'wim_port_mappings' in r)
+        for mapping in port_mappings:
+            mapping['pop_switch_dpid'] = switch
+            mapping['pop_switch_port'] = port
 
-        instance_nets = eg.instance_nets(num_datacenters=2, num_links=1)
-        port_mappings = [
-            eg.wim_port_mapping(0, 0),
-            eg.wim_port_mapping(0, 1)
-        ]
         instance_action = eg.instance_action(action_id='ACTION-000')
+        instance_nets = eg.instance_nets(num_datacenters=2, num_links=1,
+                                         status='ACTIVE')
         for i, net in enumerate(instance_nets):
-            net['status'] = 'ACTIVE'
             net['sdn_net_id'] = uuid('sdn-net%d' % i)
 
         db_state += [{'instance_nets': instance_nets},
                      {'instance_wim_nets': eg.instance_wim_nets()},
-                     {'wim_port_mappings': port_mappings},
                      {'instance_actions': instance_action}]
 
         action = WanLinkCreate(
@@ -141,15 +237,21 @@ class TestCreate(TestActionsWithDb):
         # --> ensure it is in the database for updates --> #
         action_record = action.as_record()
         action_record['extra'] = json.dumps(action_record['extra'])
-        self.populate([{'vim_wim_actions': action_record}])
+        db_state += [{'vim_wim_actions': action_record}]
 
-        return db_state, action
+        ovim_patch = patch.object(
+            self.ovim, 'get_ports', MagicMock(return_value=[{
+                'switch_dpid': switch,
+                'switch_port': port,
+            }]))
+
+        return db_state, action, ovim_patch
 
     @disable_foreign_keys
     def test_process__sdn(self):
         # Given we want 1 WAN link between 2 datacenters
         # and the local network in each datacenter is already created
-        db_state, action = self.prepare_create__sdn()
+        db_state, action, ovim_patch = self.prepare_create__sdn()
         self.populate(db_state)
 
         instance_action = self.persist.get_by_uuid(
@@ -161,12 +263,6 @@ class TestCreate(TestActionsWithDb):
             self.connector, 'create_connectivity_service',
             lambda *_, **__: (uuid('random-id'), None))
 
-        ovim_patch = patch.object(
-            self.ovim, 'get_ports', MagicMock(return_value=[{
-                'switch_dpid': 'AA:AA:AA:AA:AA:AA:AA:AA',
-                'switch_port': 1,
-            }]))
-
         # If the connector works fine
         with connector_patch, ovim_patch:
             # When we try to process a CREATE action that refers to the same
@@ -188,7 +284,7 @@ class TestCreate(TestActionsWithDb):
     def test_process__sdn_fail(self):
         # Given we want 1 WAN link between 2 datacenters
         # and the local network in each datacenter is already created
-        db_state, action = self.prepare_create__sdn()
+        db_state, action, ovim_patch = self.prepare_create__sdn()
         self.populate(db_state)
 
         instance_action = self.persist.get_by_uuid(
@@ -200,12 +296,6 @@ class TestCreate(TestActionsWithDb):
             self.connector, 'create_connectivity_service',
             MagicMock(side_effect=WimConnectorError('foobar')))
 
-        ovim_patch = patch.object(
-            self.ovim, 'get_ports', MagicMock(return_value=[{
-                'switch_dpid': 'AA:AA:AA:AA:AA:AA:AA:AA',
-                'switch_port': 1,
-            }]))
-
         # If the connector throws an error
         with connector_patch, ovim_patch:
             # When we try to process a CREATE action that refers to the same
@@ -243,29 +333,32 @@ class TestDelete(TestActionsWithDb):
         assert action.is_done
 
     def prepare_delete(self):
-        db_state = [{'nfvo_tenants': eg.tenant()}] + eg.wim_set()
+        db_state = eg.consistent_set(num_wims=1, num_tenants=1,
+                                     num_datacenters=2,
+                                     external_ports_config=True)
 
-        instance_nets = eg.instance_nets(num_datacenters=2, num_links=1)
-        port_mappings = [
-            eg.wim_port_mapping(0, 0),
-            eg.wim_port_mapping(0, 1)
-        ]
-        instance_action = eg.instance_action(action_id='ACTION-000')
+        instance_nets = eg.instance_nets(num_datacenters=2, num_links=1,
+                                         status='ACTIVE')
         for i, net in enumerate(instance_nets):
-            net['status'] = 'ACTIVE'
+            net['vim_info'] = {}
+            net['vim_info']['provider:physical_network'] = 'provider'
+            net['vim_info']['encapsulation_type'] = 'vlan'
+            net['vim_info']['encapsulation_id'] = i
             net['sdn_net_id'] = uuid('sdn-net%d' % i)
 
-        db_state += [{'instance_nets': instance_nets},
-                     {'instance_wim_nets': eg.instance_wim_nets()},
-                     {'wim_port_mappings': port_mappings},
-                     {'instance_actions': instance_action}]
+        instance_action = eg.instance_action(action_id='ACTION-000')
+
+        db_state += [
+            {'instance_wim_nets': eg.instance_wim_nets()},
+            {'instance_nets': [preprocess_record(r) for r in instance_nets]},
+            {'instance_actions': instance_action}]
 
         action = WanLinkDelete(
             eg.wim_actions('DELETE', action_id='ACTION-000')[0])
         # --> ensure it is in the database for updates --> #
         action_record = action.as_record()
         action_record['extra'] = json.dumps(action_record['extra'])
-        self.populate([{'vim_wim_actions': action_record}])
+        db_state += [{'vim_wim_actions': action_record}]
 
         return db_state, action
 
@@ -331,8 +424,9 @@ class TestDelete(TestActionsWithDb):
     def test_create_and_delete(self):
         # Given a CREATE action was well succeeded
         db_state, delete_action = self.prepare_delete()
-        delete_action.save(self.persist, task_index=1)
         self.populate(db_state)
+
+        delete_action.save(self.persist, task_index=1)
         create_action = self.create_action()
 
         connector_patch = patch.multiple(
@@ -341,13 +435,7 @@ class TestDelete(TestActionsWithDb):
             create_connectivity_service=(
                 lambda *_, **__: (uuid('random-id'), None)))
 
-        ovim_patch = patch.object(
-            self.ovim, 'get_ports', MagicMock(return_value=[{
-                'switch_dpid': 'AA:AA:AA:AA:AA:AA:AA:AA',
-                'switch_port': 1,
-            }]))
-
-        with connector_patch, ovim_patch:
+        with connector_patch:  # , ovim_patch:
             create_action.process(self.connector, self.persist, self.ovim)
 
         # When we try to process a CREATE action that refers to the same
index 6fb2d8c..9bb7bca 100644 (file)
@@ -163,8 +163,7 @@ class TestWimEngine(unittest.TestCase):
         # When we receive a list of 4 instance nets, representing
         # 2 VLDs connecting 2 datacenters each
         instance_nets = eg.instance_nets(2, 2)
-        wan_links = engine.derive_wan_links(
-            instance_nets, uuid('tenant0'))
+        wan_links = engine.derive_wan_links({}, instance_nets, uuid('tenant0'))
 
         # Then we should derive 2 wan_links with the same instance_scenario_id
         # and different scenario_network_id
index 04577e4..428b1ce 100644 (file)
@@ -137,6 +137,44 @@ class TestHttpHandler(TestCaseWithDatabasePerTest):
             merge_dicts(eg.wim(1), name='My-New-Name'),
             response.json['wim'])
 
+    def test_edit_wim__port_mappings(self):
+        # Given a WIM exists in the database
+        self.populate()
+        # when a PUT /wims/<wim_id> request arrives
+        wim_id = uuid('wim1')
+        response = self.app.put_json(
+            '/wims/{}'.format(wim_id), {
+                'wim': dict(
+                    name='My-New-Name',
+                    config={'wim_port_mapping': [{
+                        'datacenter_name': 'dc0',
+                        'pop_wan_mappings': [{
+                            'pop_switch_dpid': '00:AA:11:BB:22:CC:33:DD',
+                            'pop_switch_port': 1,
+                            'wan_service_mapping_info': {
+                                'mapping_type': 'dpid-port',
+                                'wan_switch_dpid': 'BB:BB:BB:BB:BB:BB:BB:0A',
+                                'wan_switch_port': 1
+                            }
+                        }]}]
+                    }
+                )
+            }
+        )
+
+        # then the request should be well succeeded
+        self.assertEqual(response.status_code, OK)
+        # and the registered wim (wim1) should be present
+        self.assertDictContainsSubset(
+            merge_dicts(eg.wim(1), name='My-New-Name'),
+            response.json['wim'])
+        # and the port mappings hould be updated
+        mappings = response.json['wim']['config']['wim_port_mapping']
+        self.assertEqual(len(mappings), 1)
+        self.assertEqual(
+            mappings[0]['pop_wan_mappings'][0]['pop_switch_dpid'],
+            '00:AA:11:BB:22:CC:33:DD')
+
     def test_delete_wim(self):
         # Given a WIM exists in the database
         self.populate()
@@ -178,6 +216,35 @@ class TestHttpHandler(TestCaseWithDatabasePerTest):
         self.assertEqual(response.status_code, OK)
         self.assertEqual(response.json['wim']['name'], 'wim999')
 
+    def test_create_wim__port_mappings(self):
+        self.populate()
+        # when a POST /wims request arrives with the right payload
+        response = self.app.post_json(
+            '/wims', {
+                'wim': merge_dicts(
+                    eg.wim(999),
+                    config={'wim_port_mapping': [{
+                        'datacenter_name': 'dc0',
+                        'pop_wan_mappings': [{
+                            'pop_switch_dpid': 'AA:AA:AA:AA:AA:AA:AA:01',
+                            'pop_switch_port': 1,
+                            'wan_service_mapping_info': {
+                                'mapping_type': 'dpid-port',
+                                'wan_switch_dpid': 'BB:BB:BB:BB:BB:BB:BB:01',
+                                'wan_switch_port': 1
+                            }
+                        }]}]
+                    }
+                )
+            }
+        )
+
+        # then the request should be well succeeded
+        self.assertEqual(response.status_code, OK)
+        self.assertEqual(response.json['wim']['name'], 'wim999')
+        self.assertEqual(
+            len(response.json['wim']['config']['wim_port_mapping']), 1)
+
     def test_create_wim_account(self):
         # Given a WIM and a NFVO tenant exist but are not associated
         self.populate([{'wims': [eg.wim(0)]},
index d09a116..e3e6cf6 100644 (file)
@@ -170,7 +170,7 @@ class TestWimPersistence(TestCaseWithDatabasePerTest):
                 self.assertIsNot(wim, None)
 
             # and a array of pairs 'wan' <> 'pop' connections
-            pairs = chain(*(m['wan_pop_port_mappings'] for m in mappings))
+            pairs = chain(*(m['pop_wan_mappings'] for m in mappings))
             self.assertEqual(len(list(pairs)), 2 * eg.NUM_WIMS)
 
     def test_get_wim_port_mappings_multiple(self):
@@ -198,14 +198,14 @@ class TestWimPersistence(TestCaseWithDatabasePerTest):
         self.assertEqual(mappings[0]['wim_id'], uuid('wim0'))
         self.assertEqual(mappings[0]['datacenter_id'], uuid('dc0'))
 
-        self.assertEqual(len(mappings[0]['wan_pop_port_mappings']), 3)
+        self.assertEqual(len(mappings[0]['pop_wan_mappings']), 3)
 
         # when we retreive the mappings for more then one wim/datacenter
         # the grouping should still work properly
         mappings = self.persist.get_wim_port_mappings(
             wim=['wim0', 'wim1'], datacenter=['dc0', 'dc1'])
         self.assertEqual(len(mappings), 4)
-        pairs = chain(*(m['wan_pop_port_mappings'] for m in mappings))
+        pairs = chain(*(m['pop_wan_mappings'] for m in mappings))
         self.assertEqual(len(list(pairs)), 6)
 
     def test_get_actions_in_group(self):
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,
index a13d6a2..fa64fbb 100644 (file)
@@ -51,7 +51,7 @@ from time import time, sleep
 from six import reraise
 from six.moves import queue
 
-from . import wan_link_actions, wimconn_odl  # wimconn_tapi
+from . import wan_link_actions, wimconn_odl, wimconn_dynpac # wimconn_tapi
 from ..utils import ensure, partition, pipe
 from .actions import IGNORE, PENDING, REFRESH
 from .errors import (
@@ -71,6 +71,7 @@ CONNECTORS = {
     "odl": wimconn_odl.OdlConnector,
     # "tapi": wimconn_tapi
     # Add extra connectors here
+    "dynpac": wimconn_dynpac.DynpacConnector
 }
 
 
diff --git a/osm_ro/wim/wimconn_dynpac.py b/osm_ro/wim/wimconn_dynpac.py
new file mode 100644 (file)
index 0000000..1816937
--- /dev/null
@@ -0,0 +1,235 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+##
+# Copyright 2018 David GarcĂ­a, University of the Basque Country
+# Copyright 2018 University of the Basque Country
+# This file is part of openmano
+# All Rights Reserved.
+# Contact information at http://i2t.ehu.eus
+#
+# # 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 requests
+import json
+import logging
+from enum import Enum
+
+from wimconn import WimConnector, WimConnectorError
+
+
+class WimError(Enum):
+    UNREACHABLE = 'Unable to reach the WIM.',
+    SERVICE_TYPE_ERROR = 'Unexpected service_type. Only "L2" is accepted.',
+    CONNECTION_POINTS_SIZE = \
+        'Unexpected number of connection points: 2 expected.',
+    ENCAPSULATION_TYPE = \
+        'Unexpected service_endpoint_encapsulation_type. \
+         Only "dotq1" is accepted.',
+    BANDWIDTH = 'Unable to get the bandwidth.',
+    STATUS = 'Unable to get the status for the service.',
+    DELETE = 'Unable to delete service.',
+    CLEAR_ALL = 'Unable to clear all the services',
+    UNKNOWN_ACTION = 'Unknown action invoked.',
+    BACKUP = 'Unable to get the backup parameter.',
+    UNSUPPORTED_FEATURE = "Unsupported feature",
+    UNAUTHORIZED = "Failed while authenticating"
+
+
+class WimAPIActions(Enum):
+    CHECK_CONNECTIVITY = "CHECK_CONNECTIVITY",
+    CREATE_SERVICE = "CREATE_SERVICE",
+    DELETE_SERVICE = "DELETE_SERVICE",
+    CLEAR_ALL = "CLEAR_ALL",
+    SERVICE_STATUS = "SERVICE_STATUS",
+
+
+class DynpacConnector(WimConnector):
+    __supported_service_types = ["ELINE (L2)"]
+    __supported_encapsulation_types = ["dot1q"]
+    __WIM_LOGGER = 'openmano.wimconn.dynpac'
+    __ENCAPSULATION_TYPE_PARAM = "service_endpoint_encapsulation_type"
+    __ENCAPSULATION_INFO_PARAM = "service_endpoint_encapsulation_info"
+    __BACKUP_PARAM = "backup"
+    __BANDWIDTH_PARAM = "bandwidth"
+    __SERVICE_ENDPOINT_PARAM = "service_endpoint_id"
+    __WAN_SERVICE_ENDPOINT_PARAM = "wan_service_endpoint_id"
+    __WAN_MAPPING_INFO_PARAM = "wan_service_mapping_info"
+    __SW_ID_PARAM = "wan_switch_dpid"
+    __SW_PORT_PARAM = "wan_switch_port"
+    __VLAN_PARAM = "vlan"
+
+    # Public functions exposed to the Resource Orchestrator
+    def __init__(self, wim, wim_account, config):
+        self.logger = logging.getLogger(self.__WIM_LOGGER)
+        self.__wim = wim
+        self.__wim_account = wim_account
+        self.__config = config
+        self.__wim_url = self.__wim.get("wim_url")
+        self.__user = wim_account.get("user")
+        self.__passwd = wim_account.get("passwd")
+        self.logger.info("Initialized.")
+
+    def create_connectivity_service(self,
+                                    service_type,
+                                    connection_points,
+                                    **kwargs):
+        self.__check_service(service_type, connection_points, kwargs)
+
+        body = self.__get_body(service_type, connection_points, kwargs)
+
+        headers = {'Content-type': 'application/x-www-form-urlencoded'}
+        endpoint = "{}/service/create".format(self.__wim_url)
+
+        try:
+            response = requests.post(endpoint, data=body, headers=headers)
+        except requests.exceptions.RequestException as e:
+            self.__exception(e.message, http_code=503)
+
+        if response.status_code != 200:
+            error = json.loads(response.content)
+            reason = "Reason: {}. ".format(error.get("code"))
+            description = "Description: {}.".format(error.get("description"))
+            exception = reason + description
+            self.__exception(exception, http_code=response.status_code)
+        uuid = response.content
+        self.logger.info("Service with uuid {} created.".format(uuid))
+        return (uuid, None)
+
+    def edit_connectivity_service(self, service_uuid,
+                                  conn_info, connection_points,
+                                  **kwargs):
+        self.__exception(WimError.UNSUPPORTED_FEATURE, http_code=501)
+
+    def get_connectivity_service_status(self, service_uuid):
+        endpoint = "{}/service/status/{}".format(self.__wim_url, service_uuid)
+        try:
+            response = requests.get(endpoint)
+        except requests.exceptions.RequestException as e:
+            self.__exception(e.message, http_code=503)
+
+        if response.status_code != 200:
+            self.__exception(WimError.STATUS, http_code=response.status_code)
+        self.logger.info("Status for service with uuid {}: {}"
+                         .format(service_uuid, response.content))
+        return response.content
+
+    def delete_connectivity_service(self, service_uuid, conn_info):
+        endpoint = "{}/service/delete/{}".format(self.__wim_url, service_uuid)
+        try:
+            response = requests.delete(endpoint)
+        except requests.exceptions.RequestException as e:
+            self.__exception(e.message, http_code=503)
+        if response.status_code != 200:
+            self.__exception(WimError.DELETE, http_code=response.status_code)
+
+        self.logger.info("Service with uuid: {} deleted".format(service_uuid))
+
+    def clear_all_connectivity_services(self):
+        endpoint = "{}/service/clearAll".format(self.__wim_url)
+        try:
+            response = requests.delete(endpoint)
+            http_code = response.status_code
+        except requests.exceptions.RequestException as e:
+            self.__exception(e.message, http_code=503)
+        if http_code != 200:
+            self.__exception(WimError.CLEAR_ALL, http_code=http_code)
+
+        self.logger.info("{} services deleted".format(response.content))
+        return "{} services deleted".format(response.content)
+
+    def check_connectivity(self):
+        endpoint = "{}/checkConnectivity".format(self.__wim_url)
+
+        try:
+            response = requests.get(endpoint)
+            http_code = response.status_code
+        except requests.exceptions.RequestException as e:
+            self.__exception(e.message, http_code=503)
+
+        if http_code != 200:
+            self.__exception(WimError.UNREACHABLE, http_code=http_code)
+        self.logger.info("Connectivity checked")
+
+    def check_credentials(self):
+        endpoint = "{}/checkCredentials".format(self.__wim_url)
+        auth = (self.__user, self.__passwd)
+
+        try:
+            response = requests.get(endpoint, auth=auth)
+            http_code = response.status_code
+        except requests.exceptions.RequestException as e:
+            self.__exception(e.message, http_code=503)
+
+        if http_code != 200:
+            self.__exception(WimError.UNAUTHORIZED, http_code=http_code)
+        self.logger.info("Credentials checked")
+
+    # Private functions
+    def __exception(self, x, **kwargs):
+        http_code = kwargs.get("http_code")
+        if hasattr(x, "value"):
+            error = x.value
+        else:
+            error = x
+        self.logger.error(error)
+        raise WimConnectorError(error, http_code=http_code)
+
+    def __check_service(self, service_type, connection_points, kwargs):
+        if service_type not in self.__supported_service_types:
+            self.__exception(WimError.SERVICE_TYPE_ERROR, http_code=400)
+
+        if len(connection_points) != 2:
+            self.__exception(WimError.CONNECTION_POINTS_SIZE, http_code=400)
+
+        for connection_point in connection_points:
+            enc_type = connection_point.get(self.__ENCAPSULATION_TYPE_PARAM)
+            if enc_type not in self.__supported_encapsulation_types:
+                self.__exception(WimError.ENCAPSULATION_TYPE, http_code=400)
+
+        bandwidth = kwargs.get(self.__BANDWIDTH_PARAM)
+        if not isinstance(bandwidth, int):
+            self.__exception(WimError.BANDWIDTH, http_code=400)
+
+        backup = kwargs.get(self.__BACKUP_PARAM)
+        if not isinstance(backup, bool):
+            self.__exception(WimError.BACKUP, http_code=400)
+
+    def __get_body(self, service_type, connection_points, kwargs):
+        port_mapping = self.__config.get("port_mapping")
+        selected_ports = []
+        for connection_point in connection_points:
+            endpoint_id = connection_point.get(self.__SERVICE_ENDPOINT_PARAM)
+            port = filter(lambda x: x.get(self.__WAN_SERVICE_ENDPOINT_PARAM)
+                          == endpoint_id, port_mapping)[0]
+            wsmpi_json = port.get(self.__WAN_MAPPING_INFO_PARAM)
+            port_info = json.loads(wsmpi_json)
+            selected_ports.append(port_info)
+        if service_type == "ELINE (L2)":
+            service_type = "L2"
+        body = {
+            "connection_points": [{
+                "wan_switch_dpid": selected_ports[0].get(self.__SW_ID_PARAM),
+                "wan_switch_port": selected_ports[0].get(self.__SW_PORT_PARAM),
+                "wan_vlan": connection_points[0].get(self.__ENCAPSULATION_INFO_PARAM).get(self.__VLAN_PARAM)
+            }, {
+                "wan_switch_dpid": selected_ports[1].get(self.__SW_ID_PARAM),
+                "wan_switch_port": selected_ports[1].get(self.__SW_PORT_PARAM),
+                "wan_vlan":    connection_points[1].get(self.__ENCAPSULATION_INFO_PARAM).get(self.__VLAN_PARAM)
+            }],
+            "bandwidth": kwargs.get(self.__BANDWIDTH_PARAM),
+            "service_type": service_type,
+            "backup": kwargs.get(self.__BACKUP_PARAM)
+        }
+        return "body={}".format(json.dumps(body))