From: tierno Date: Mon, 21 Jan 2019 09:55:46 +0000 (+0000) Subject: Merge branch 'WIM' X-Git-Tag: v5.0.3~4 X-Git-Url: https://osm.etsi.org/gitweb/?a=commitdiff_plain;h=db1b22de8331161d80ef3a65ffd41ba8ea206cb9;hp=6d40da28d2152a84c564bdd8ce2ff54dbdd9d114;p=osm%2FRO.git Merge branch 'WIM' Change-Id: Id314390ba524d22d4e1f74cfe8ef7c808bb8ac0e Signed-off-by: tierno --- diff --git a/openmano b/openmano index 15776561..13a93da4 100755 --- 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", diff --git a/osm_ro/nfvo.py b/osm_ro/nfvo.py index 00738e98..4341422f 100644 --- a/osm_ro/nfvo.py +++ b/osm_ro/nfvo.py @@ -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)) diff --git a/osm_ro/vimconn_openstack.py b/osm_ro/vimconn_openstack.py index 876fa2f4..0930eb03 100644 --- a/osm_ro/vimconn_openstack.py +++ b/osm_ro/vimconn_openstack.py @@ -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): diff --git a/osm_ro/wim/engine.py b/osm_ro/wim/engine.py index dde5dee4..6ff2b4fc 100644 --- a/osm_ro/wim/engine.py +++ b/osm_ro/wim/engine.py @@ -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 ] diff --git a/osm_ro/wim/errors.py b/osm_ro/wim/errors.py index 16c53b55..ca8c2b73 100644 --- a/osm_ro/wim/errors.py +++ b/osm_ro/wim/errors.py @@ -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) diff --git a/osm_ro/wim/http_handler.py b/osm_ro/wim/http_handler.py index f5eeed99..b88dab3f 100644 --- a/osm_ro/wim/http_handler.py +++ b/osm_ro/wim/http_handler.py @@ -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') diff --git a/osm_ro/wim/persistence.py b/osm_ro/wim/persistence.py index 8a74d491..b956965b 100644 --- a/osm_ro/wim/persistence.py +++ b/osm_ro/wim/persistence.py @@ -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')) diff --git a/osm_ro/wim/schemas.py b/osm_ro/wim/schemas.py index a040405b..fb65fdd3 100644 --- a/osm_ro/wim/schemas.py +++ b/osm_ro/wim/schemas.py @@ -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"] } diff --git a/osm_ro/wim/tests/fixtures.py b/osm_ro/wim/tests/fixtures.py index 1b52e497..cb662ab0 100644 --- a/osm_ro/wim/tests/fixtures.py +++ b/osm_ro/wim/tests/fixtures.py @@ -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) diff --git a/osm_ro/wim/tests/test_actions.py b/osm_ro/wim/tests/test_actions.py index 920182bd..cee3c96f 100644 --- a/osm_ro/wim/tests/test_actions.py +++ b/osm_ro/wim/tests/test_actions.py @@ -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 diff --git a/osm_ro/wim/tests/test_engine.py b/osm_ro/wim/tests/test_engine.py index 6fb2d8cb..9bb7bca0 100644 --- a/osm_ro/wim/tests/test_engine.py +++ b/osm_ro/wim/tests/test_engine.py @@ -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 diff --git a/osm_ro/wim/tests/test_http_handler.py b/osm_ro/wim/tests/test_http_handler.py index 04577e46..428b1ce8 100644 --- a/osm_ro/wim/tests/test_http_handler.py +++ b/osm_ro/wim/tests/test_http_handler.py @@ -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/ 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)]}, diff --git a/osm_ro/wim/tests/test_persistence.py b/osm_ro/wim/tests/test_persistence.py index d09a1163..e3e6cf61 100644 --- a/osm_ro/wim/tests/test_persistence.py +++ b/osm_ro/wim/tests/test_persistence.py @@ -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): diff --git a/osm_ro/wim/wan_link_actions.py b/osm_ro/wim/wan_link_actions.py index 61c6dd9f..034e4159 100644 --- a/osm_ro/wim/wan_link_actions.py +++ b/osm_ro/wim/wan_link_actions.py @@ -33,15 +33,19 @@ ## # 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, diff --git a/osm_ro/wim/wim_thread.py b/osm_ro/wim/wim_thread.py index a13d6a25..fa64fbba 100644 --- a/osm_ro/wim/wim_thread.py +++ b/osm_ro/wim/wim_thread.py @@ -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 index 00000000..18169370 --- /dev/null +++ b/osm_ro/wim/wimconn_dynpac.py @@ -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))