diff --git a/scripts/run_tests_locally-device-ietf-actn.sh b/scripts/run_tests_locally-device-ietf-actn.sh new file mode 100755 index 0000000000000000000000000000000000000000..c694b6424ae83500720002b5a471bbb773586ce1 --- /dev/null +++ b/scripts/run_tests_locally-device-ietf-actn.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) +# +# 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. + + +PROJECTDIR=`pwd` + +cd $PROJECTDIR/src +RCFILE=$PROJECTDIR/coverage/.coveragerc + +# Run unitary tests and analyze coverage of code at same time +coverage run --rcfile=$RCFILE --append -m pytest --log-level=INFO --verbose \ + device/tests/test_unitary_ietf_actn.py diff --git a/src/common/tools/object_factory/Device.py b/src/common/tools/object_factory/Device.py index bc5c28740d5635df99c26ef56124c471d2c77d91..76959232a947ec50918afbc77a992d8af0d0723f 100644 --- a/src/common/tools/object_factory/Device.py +++ b/src/common/tools/object_factory/Device.py @@ -46,6 +46,10 @@ DEVICE_P4_DRIVERS = [DeviceDriverEnum.DEVICEDRIVER_P4] DEVICE_TFS_TYPE = DeviceTypeEnum.TERAFLOWSDN_CONTROLLER.value DEVICE_TFS_DRIVERS = [DeviceDriverEnum.DEVICEDRIVER_IETF_L2VPN] +DEVICE_IETF_ACTN_TYPE = DeviceTypeEnum.OPEN_LINE_SYSTEM.value +DEVICE_IETF_ACTN_DRIVERS = [DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN] + + def json_device_id(device_uuid : str): return {'device_uuid': {'uuid': device_uuid}} @@ -136,6 +140,14 @@ def json_device_tfs_disabled( device_uuid, DEVICE_TFS_TYPE, DEVICE_DISABLED, name=name, endpoints=endpoints, config_rules=config_rules, drivers=drivers) +def json_device_ietf_actn_disabled( + device_uuid : str, name : Optional[str] = None, endpoints : List[Dict] = [], config_rules : List[Dict] = [], + drivers : List[Dict] = DEVICE_IETF_ACTN_DRIVERS + ): + return json_device( + device_uuid, DEVICE_IETF_ACTN_TYPE, DEVICE_DISABLED, name=name, endpoints=endpoints, config_rules=config_rules, + drivers=drivers) + def json_device_connect_rules(address : str, port : int, settings : Dict = {}) -> List[Dict]: return [ json_config_rule_set('_connect/address', address), diff --git a/src/device/service/drivers/__init__.py b/src/device/service/drivers/__init__.py index 442acf839c8e4615237d338d7b485be297d1a4ff..27c61f89f15c735b44ad2724df01e08a51dda6ba 100644 --- a/src/device/service/drivers/__init__.py +++ b/src/device/service/drivers/__init__.py @@ -84,6 +84,15 @@ DRIVERS.append( } ])) +from .ietf_actn.IetfActnDriver import IetfActnDriver # pylint: disable=wrong-import-position +DRIVERS.append( + (IetfActnDriver, [ + { + FilterFieldEnum.DEVICE_TYPE: DeviceTypeEnum.OPEN_LINE_SYSTEM, + FilterFieldEnum.DRIVER: DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN, + } + ])) + if LOAD_ALL_DEVICE_DRIVERS: from .openconfig.OpenConfigDriver import OpenConfigDriver # pylint: disable=wrong-import-position DRIVERS.append( diff --git a/src/device/service/drivers/ietf_actn/data.txt b/src/device/service/drivers/ietf_actn/data.txt deleted file mode 100644 index 7248933e60abc52bc059cd9ec40e4799372ca377..0000000000000000000000000000000000000000 --- a/src/device/service/drivers/ietf_actn/data.txt +++ /dev/null @@ -1,14 +0,0 @@ - -osu_tunnel_1: - delay = 20 - te_odu_number = 40 - src_ttp_channel_name = 'och:1-odu2:1-oduflex:1-osuflex:2' - dst_ttp_channel_name = 'och:1-odu2:1-oduflex:3-osuflex:1' - -etht_service_1: - etht_svc_type = 'op-mp2mp-svc' - src_endpoint_static_route_list: - dst='128.32.10.5', mask=24 => next_hop='128.32.33.5' - dst='128.32.20.5', mask=24 => next_hop='128.32.33.5' - dst_endpoint_static_route_list: - dst='172.1.101.22', mask=24 => next_hop='172.10.33.5' diff --git a/src/device/service/drivers/ietf_actn/handlers/EthtServiceHandler.py b/src/device/service/drivers/ietf_actn/handlers/EthtServiceHandler.py index ff0dadbbcbcd0d58508ce4dacb1830f2ef170bad..ac9a966337c7d5d718f946bad1090113ee99f5b5 100644 --- a/src/device/service/drivers/ietf_actn/handlers/EthtServiceHandler.py +++ b/src/device/service/drivers/ietf_actn/handlers/EthtServiceHandler.py @@ -176,10 +176,10 @@ class EthtServiceHandler: 'dst_node_id' : dst_endpoint['node-id'], 'dst_tp_id' : dst_endpoint['tp-id'], - 'dst_vlan_tag' : src_endpoint['outer-tag']['vlan-value'], + 'dst_vlan_tag' : dst_endpoint['outer-tag']['vlan-value'], 'dst_static_routes': [ (static_route['destination'], static_route['destination-mask'], static_route['next-hop']) - for static_route in src_endpoint.get('static-route-list', list()) + for static_route in dst_endpoint.get('static-route-list', list()) ], } etht_services.append(etht_service) diff --git a/src/device/service/drivers/ietf_actn/handlers/OsuTunnelHandler.py b/src/device/service/drivers/ietf_actn/handlers/OsuTunnelHandler.py index 2cb6f46f0b940cc03f8956904749f5bd3bf319b8..960ad70d782db3b6b420067cf46491ea1870dae7 100644 --- a/src/device/service/drivers/ietf_actn/handlers/OsuTunnelHandler.py +++ b/src/device/service/drivers/ietf_actn/handlers/OsuTunnelHandler.py @@ -127,11 +127,11 @@ class OsuTunnelHandler: osu_tunnel = { 'name' : item['name'], 'src_node_id' : src_endpoint['node-id'], - 'src_tp_id' : src_endpoint['node-id'], + 'src_tp_id' : src_endpoint['tp-id'], 'src_ttp_channel_name': src_endpoint['ttp-channel-name'], 'dst_node_id' : dst_endpoint['node-id'], - 'dst_tp_id' : dst_endpoint['node-id'], - 'dst_ttp_channel_name': src_endpoint['ttp-channel-name'], + 'dst_tp_id' : dst_endpoint['tp-id'], + 'dst_ttp_channel_name': dst_endpoint['ttp-channel-name'], 'odu_type' : item['te-bandwidth']['odu-type'], 'osuflex_number' : item['te-bandwidth']['number'], 'delay' : item['delay'], diff --git a/src/device/service/drivers/ietf_actn/handlers/RestApiClient.py b/src/device/service/drivers/ietf_actn/handlers/RestApiClient.py index 7154dfc483a038c2cd20b7ac5c429ae759ce9a65..25bc9fc712968d76be0c702ac9fe4247a8eaf963 100644 --- a/src/device/service/drivers/ietf_actn/handlers/RestApiClient.py +++ b/src/device/service/drivers/ietf_actn/handlers/RestApiClient.py @@ -19,7 +19,7 @@ from typing import Any, Dict, List, Set, Tuple, Union LOGGER = logging.getLogger(__name__) DEFAULT_BASE_URL = '/restconf/v2/data' -DEFAULT_SCHEMA = 'https' +DEFAULT_SCHEME = 'https' DEFAULT_TIMEOUT = 120 DEFAULT_VERIFY = False @@ -41,12 +41,12 @@ class RestApiClient: password = settings.get('password') self._auth = HTTPBasicAuth(username, password) if username is not None and password is not None else None - scheme = settings.get('scheme', DEFAULT_SCHEMA ) + scheme = settings.get('scheme', DEFAULT_SCHEME ) base_url = settings.get('base_url', DEFAULT_BASE_URL) self._base_url = '{:s}://{:s}:{:d}{:s}'.format(scheme, address, int(port), base_url) self._timeout = int(settings.get('timeout', DEFAULT_TIMEOUT)) - self._verify = int(settings.get('verify', DEFAULT_VERIFY )) + self._verify = bool(settings.get('verify', DEFAULT_VERIFY)) def get( self, object_name : str, url : str, filters : List[Tuple[str, str]], diff --git a/src/device/tests/test_unitary_ietf_actn.py b/src/device/tests/test_unitary_ietf_actn.py new file mode 100644 index 0000000000000000000000000000000000000000..e6afd4fffee3a6885308b3e19ea3b19c610e1ce8 --- /dev/null +++ b/src/device/tests/test_unitary_ietf_actn.py @@ -0,0 +1,223 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) +# +# 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 copy, logging, os, pytest, time +from flask import Flask +from common.proto.context_pb2 import ConfigActionEnum, Device, DeviceId +from common.tools.grpc.Tools import grpc_message_to_json_string +from common.tools.object_factory.ConfigRule import json_config_rule_delete, json_config_rule_set +from common.tools.object_factory.Device import ( + json_device_connect_rules, json_device_id, json_device_ietf_actn_disabled +) +from common.tools.service.GenericRestServer import GenericRestServer +from context.client.ContextClient import ContextClient +from device.client.DeviceClient import DeviceClient +from device.service.DeviceService import DeviceService +from device.service.driver_api._Driver import _Driver +from tests.tools.mock_ietf_actn_sdn_ctrl.ResourceEthServices import EthService, EthServices +from tests.tools.mock_ietf_actn_sdn_ctrl.ResourceOsuTunnels import OsuTunnel, OsuTunnels + +os.environ['DEVICE_EMULATED_ONLY'] = 'TRUE' +from .PrepareTestScenario import ( # pylint: disable=unused-import + # be careful, order of symbols is important here! + mock_service, device_service, context_client, device_client, test_prepare_environment +) + +DEVICE_UUID = 'DEVICE-IETF-ACTN' +DEVICE_ADDRESS = '127.0.0.1' +DEVICE_PORT = 8080 +DEVICE_USERNAME = 'admin' +DEVICE_PASSWORD = 'admin' +DEVICE_SCHEME = 'http' +DEVICE_BASE_URL = '/restconf/v2/data' +DEVICE_TIMEOUT = 120 +DEVICE_VERIFY = False + +DEVICE_ID = json_device_id(DEVICE_UUID) +DEVICE = json_device_ietf_actn_disabled(DEVICE_UUID) + +DEVICE_CONNECT_RULES = json_device_connect_rules(DEVICE_ADDRESS, DEVICE_PORT, { + 'scheme' : DEVICE_SCHEME, + 'username': DEVICE_USERNAME, + 'password': DEVICE_PASSWORD, + 'base_url': DEVICE_BASE_URL, + 'timeout' : DEVICE_TIMEOUT, + 'verify' : DEVICE_VERIFY, +}) + +DEVICE_CONFIG_RULES = [ + json_config_rule_set('/osu_tunnels/osu_tunnel[osu_tunnel_1]', { + 'name' : 'osu_tunnel_1', + 'src_node_id' : '10.0.10.1', + 'src_tp_id' : '200', + 'src_ttp_channel_name': 'och:1-odu2:1-oduflex:1-osuflex:2', + 'dst_node_id' : '10.0.30.1', + 'dst_tp_id' : '200', + 'dst_ttp_channel_name': 'och:1-odu2:1-oduflex:3-osuflex:1', + 'odu_type' : 'osuflex', + 'osuflex_number' : 40, + 'delay' : 20, + 'bidirectional' : True, + }), + json_config_rule_set('/etht_services/etht_service[etht_service_1]', { + 'name' : 'etht_service_1', + 'service_type' : 'op-mp2mp-svc', + 'osu_tunnel_name' : 'osu_tunnel_1', + 'src_node_id' : '10.0.10.1', + 'src_tp_id' : '200', + 'src_vlan_tag' : 21, + 'src_static_routes': [('128.32.10.5', 24, '128.32.33.5'), ('128.32.20.5', 24, '128.32.33.5')], + 'dst_node_id' : '10.0.30.1', + 'dst_tp_id' : '200', + 'dst_vlan_tag' : 101, + 'dst_static_routes': [('172.1.101.22', 24, '172.10.33.5')], + }), +] + +DEVICE_DECONFIG_RULES = [ + json_config_rule_delete('/osu_tunnels/osu_tunnel[osu_tunnel_1]', {'name': 'osu_tunnel_1'}), + json_config_rule_delete('/etht_services/etht_service[etht_service_1]', {'name': 'etht_service_1'}), +] + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +@pytest.fixture(scope='session') +def ietf_actn_sdn_ctrl( + device_service: DeviceService, # pylint: disable=redefined-outer-name +) -> Flask: + _rest_server = GenericRestServer(DEVICE_PORT, DEVICE_BASE_URL, bind_address=DEVICE_ADDRESS) + _rest_server.app.debug = True + _rest_server.app.env = 'development' + _rest_server.app.testing = True + + _rest_server.add_resource(OsuTunnels, '/ietf-te:tunnel') + _rest_server.add_resource(OsuTunnel, '/ietf-te:tunnel[name=<string:name>]') + _rest_server.add_resource(EthServices, '/ietf-eth-tran-service:etht-svc') + _rest_server.add_resource(EthService, '/ietf-eth-tran-service:etht-svc[etht-svc-name=<string:etht_svc_name>]') + + _rest_server.start() + time.sleep(1) # bring time for the server to start + yield _rest_server + _rest_server.shutdown() + _rest_server.join() + + +def test_device_ietf_actn_add_correct( + device_client: DeviceClient, # pylint: disable=redefined-outer-name + device_service: DeviceService, # pylint: disable=redefined-outer-name +) -> None: + DEVICE_WITH_CONNECT_RULES = copy.deepcopy(DEVICE) + DEVICE_WITH_CONNECT_RULES['device_config']['config_rules'].extend(DEVICE_CONNECT_RULES) + device_client.AddDevice(Device(**DEVICE_WITH_CONNECT_RULES)) + driver_instance_cache = device_service.device_servicer.driver_instance_cache + driver: _Driver = driver_instance_cache.get(DEVICE_UUID) + assert driver is not None + + +def test_device_ietf_actn_get( + context_client: ContextClient, # pylint: disable=redefined-outer-name + device_client: DeviceClient, # pylint: disable=redefined-outer-name +) -> None: + + initial_config = device_client.GetInitialConfig(DeviceId(**DEVICE_ID)) + LOGGER.info('initial_config = {:s}'.format(grpc_message_to_json_string(initial_config))) + + device_data = context_client.GetDevice(DeviceId(**DEVICE_ID)) + LOGGER.info('device_data = {:s}'.format(grpc_message_to_json_string(device_data))) + + +def test_device_ietf_actn_configure( + context_client: ContextClient, # pylint: disable=redefined-outer-name + device_client: DeviceClient, # pylint: disable=redefined-outer-name + device_service: DeviceService, # pylint: disable=redefined-outer-name +) -> None: + + driver_instance_cache = device_service.device_servicer.driver_instance_cache + driver : _Driver = driver_instance_cache.get(DEVICE_UUID) + assert driver is not None + + # Requires to retrieve data from device; might be slow. Uncomment only when needed and test does not pass directly. + #driver_config = sorted(driver.GetConfig(), key=operator.itemgetter(0)) + #LOGGER.info('driver_config = {:s}'.format(str(driver_config))) + + DEVICE_WITH_CONFIG_RULES = copy.deepcopy(DEVICE) + DEVICE_WITH_CONFIG_RULES['device_config']['config_rules'].extend(DEVICE_CONFIG_RULES) + device_client.ConfigureDevice(Device(**DEVICE_WITH_CONFIG_RULES)) + + # Requires to retrieve data from device; might be slow. Uncomment only when needed and test does not pass directly. + #driver_config = sorted(driver.GetConfig(), key=operator.itemgetter(0)) + #LOGGER.info('driver_config = {:s}'.format(str(driver_config))) + + device_data = context_client.GetDevice(DeviceId(**DEVICE_ID)) + config_rules = [ + (ConfigActionEnum.Name(config_rule.action), config_rule.custom.resource_key, config_rule.custom.resource_value) + for config_rule in device_data.device_config.config_rules + if config_rule.WhichOneof('config_rule') == 'custom' + ] + LOGGER.info('device_data.device_config.config_rules = \n{:s}'.format( + '\n'.join(['{:s} {:s} = {:s}'.format(*config_rule) for config_rule in config_rules]))) + for config_rule in DEVICE_CONFIG_RULES: + assert 'custom' in config_rule + config_rule = ( + ConfigActionEnum.Name(config_rule['action']), config_rule['custom']['resource_key'], + config_rule['custom']['resource_value']) + assert config_rule in config_rules + + +def test_device_ietf_actn_deconfigure( + context_client: ContextClient, # pylint: disable=redefined-outer-name + device_client: DeviceClient, # pylint: disable=redefined-outer-name + device_service: DeviceService, # pylint: disable=redefined-outer-name +) -> None: + + driver_instance_cache = device_service.device_servicer.driver_instance_cache + driver: _Driver = driver_instance_cache.get(DEVICE_UUID) + assert driver is not None + + # Requires to retrieve data from device; might be slow. Uncomment only when needed and test does not pass directly. + #driver_config = sorted(driver.GetConfig(), key=operator.itemgetter(0)) + #LOGGER.info('driver_config = {:s}'.format(str(driver_config))) + + DEVICE_WITH_DECONFIG_RULES = copy.deepcopy(DEVICE) + DEVICE_WITH_DECONFIG_RULES['device_config']['config_rules'].extend(DEVICE_DECONFIG_RULES) + device_client.ConfigureDevice(Device(**DEVICE_WITH_DECONFIG_RULES)) + + # Requires to retrieve data from device; might be slow. Uncomment only when needed and test does not pass directly. + #driver_config = sorted(driver.GetConfig(), key=operator.itemgetter(0)) + #LOGGER.info('driver_config = {:s}'.format(str(driver_config))) + + device_data = context_client.GetDevice(DeviceId(**DEVICE_ID)) + config_rules = [ + (ConfigActionEnum.Name(config_rule.action), config_rule.custom.resource_key, config_rule.custom.resource_value) + for config_rule in device_data.device_config.config_rules + if config_rule.WhichOneof('config_rule') == 'custom' + ] + LOGGER.info('device_data.device_config.config_rules = \n{:s}'.format( + '\n'.join(['{:s} {:s} = {:s}'.format(*config_rule) for config_rule in config_rules]))) + for config_rule in DEVICE_DECONFIG_RULES: + assert 'custom' in config_rule + action_set = ConfigActionEnum.Name(ConfigActionEnum.CONFIGACTION_SET) + config_rule = (action_set, config_rule['custom']['resource_key'], config_rule['custom']['resource_value']) + assert config_rule not in config_rules + + +def test_device_ietf_actn_delete( + device_client : DeviceClient, # pylint: disable=redefined-outer-name + device_service : DeviceService, # pylint: disable=redefined-outer-name +) -> None: + device_client.DeleteDevice(DeviceId(**DEVICE_ID)) + driver_instance_cache = device_service.device_servicer.driver_instance_cache + driver : _Driver = driver_instance_cache.get(DEVICE_UUID, {}) + assert driver is None