diff --git a/src/context/service/database/models/enums/DeviceDriver.py b/src/context/service/database/models/enums/DeviceDriver.py index 66635decc5369c8b7601863da85f497626d70ac8..f8483360191dcea1a56cdc372b681ae2d03c9ef1 100644 --- a/src/context/service/database/models/enums/DeviceDriver.py +++ b/src/context/service/database/models/enums/DeviceDriver.py @@ -31,6 +31,7 @@ class ORM_DeviceDriverEnum(enum.Enum): XR = DeviceDriverEnum.DEVICEDRIVER_XR IETF_L2VPN = DeviceDriverEnum.DEVICEDRIVER_IETF_L2VPN GNMI_OPENCONFIG = DeviceDriverEnum.DEVICEDRIVER_GNMI_OPENCONFIG + FLEXSCALE = DeviceDriverEnum.DEVICEDRIVER_FLEXSCALE grpc_to_enum__device_driver = functools.partial( grpc_to_enum, DeviceDriverEnum, ORM_DeviceDriverEnum) diff --git a/src/device/service/drivers/__init__.py b/src/device/service/drivers/__init__.py index 0d85e8ff9668c5715dfc9d830027a5ae1faed9b5..442acf839c8e4615237d338d7b485be297d1a4ff 100644 --- a/src/device/service/drivers/__init__.py +++ b/src/device/service/drivers/__init__.py @@ -148,3 +148,13 @@ if LOAD_ALL_DEVICE_DRIVERS: FilterFieldEnum.DRIVER : DeviceDriverEnum.DEVICEDRIVER_XR, } ])) + +if LOAD_ALL_DEVICE_DRIVERS: + from .flexscale.FlexScaleDriver import FlexScaleDriver # pylint: disable=wrong-import-position + DRIVERS.append( + (FlexScaleDriver, [ + { + FilterFieldEnum.DEVICE_TYPE: DeviceTypeEnum.OPEN_LINE_SYSTEM, + FilterFieldEnum.DRIVER: DeviceDriverEnum.DEVICEDRIVER_FLEXSCALE, + } + ])) diff --git a/src/device/service/drivers/flexscale/FlexScaleDriver.py b/src/device/service/drivers/flexscale/FlexScaleDriver.py new file mode 100644 index 0000000000000000000000000000000000000000..f91ee1cebbd686cceed2370df98445aa247d5990 --- /dev/null +++ b/src/device/service/drivers/flexscale/FlexScaleDriver.py @@ -0,0 +1,151 @@ +# 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 json, logging, requests, threading +from requests.auth import HTTPBasicAuth +from typing import Any, Iterator, List, Optional, Tuple, Union +from common.method_wrappers.Decorator import MetricsPool, metered_subclass_method +from common.type_checkers.Checkers import chk_string, chk_type +from device.service.driver_api._Driver import _Driver +from . import ALL_RESOURCE_KEYS +from .Tools import find_key, add_lightpath, del_lightpath, get_lightpaths +from device.service.driver_api._Driver import _Driver, RESOURCE_ENDPOINTS +from device.service.drivers.ietf_l2vpn.TfsDebugApiClient import TfsDebugApiClient +from device.service.driver_api.ImportTopologyEnum import ImportTopologyEnum, get_import_topology + +LOGGER = logging.getLogger(__name__) + +DRIVER_NAME = 'flexscale' +METRICS_POOL = MetricsPool('Device', 'Driver', labels={'driver': DRIVER_NAME}) + + +class FlexScaleDriver(_Driver): + def __init__(self, address: str, port: int, **settings) -> None: + super().__init__(DRIVER_NAME, address, port, **settings) + self.__lock = threading.Lock() + self.__started = threading.Event() + self.__terminate = threading.Event() + username = self.settings.get('username') + password = self.settings.get('password') + self.__auth = HTTPBasicAuth(username, password) if username is not None and password is not None else None + scheme = self.settings.get('scheme', 'http') + self.dac = TfsDebugApiClient(self.address, int(self.port), scheme=scheme, username=username, password=password) + self.__flexscale_root = '{:s}://{:s}:{:d}'.format(scheme, self.address, int(self.port)) + self.__timeout = int(self.settings.get('timeout', 120)) + + # Options are: + # disabled --> just import endpoints as usual + # devices --> imports sub-devices but not links connecting them. + # (a remotely-controlled transport domain might exist between them) + # topology --> imports sub-devices and links connecting them. + # (not supported by XR driver) + self.__import_topology = get_import_topology(self.settings, default=ImportTopologyEnum.TOPOLOGY) + + + def Connect(self) -> bool: + url = self.__flexscale_root + '/OpticalTFS/GetLightpaths' + with self.__lock: + if self.__started.is_set(): return True + try: + requests.get(url, timeout=self.__timeout, verify=False, auth=self.__auth) + except requests.exceptions.Timeout: + LOGGER.exception('Timeout connecting {:s}'.format(str(self.__tapi_root))) + return False + except Exception: # pylint: disable=broad-except + LOGGER.exception('Exception connecting {:s}'.format(str(self.__tapi_root))) + return False + else: + self.__started.set() + return True + + def Disconnect(self) -> bool: + with self.__lock: + self.__terminate.set() + return True + + @metered_subclass_method(METRICS_POOL) + def GetInitialConfig(self) -> List[Tuple[str, Any]]: + with self.__lock: + return [] + + @metered_subclass_method(METRICS_POOL) + def GetConfig(self, resource_keys : List[str] = []) -> List[Tuple[str, Union[Any, None, Exception]]]: + chk_type('resources', resource_keys, list) + results = [] + with self.__lock: + if len(resource_keys) == 0: resource_keys = ALL_RESOURCE_KEYS + for i, resource_key in enumerate(resource_keys): + str_resource_name = 'resource_key[#{:d}]'.format(i) + chk_string(str_resource_name, resource_key, allow_empty=False) + + if resource_key == RESOURCE_ENDPOINTS: + # return endpoints through debug-api and list-devices method + results.extend(self.dac.get_devices_endpoints(self.__import_topology)) + + # results.extend(get_lightpaths( + # self.__flexscale_root, resource_key, timeout=self.__timeout, auth=self.__auth)) + return results + + @metered_subclass_method(METRICS_POOL) + def SetConfig(self, resources: List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + results = [] + if len(resources) == 0: + return results + with self.__lock: + for _, resource in resources: + LOGGER.info('resource = {:s}'.format(str(resource))) + + src_node = find_key(resource, 'src_node') + dst_node = find_key(resource, 'dst_node') + bitrate = find_key(resource, 'bitrate') + + response = add_lightpath(self.__flexscale_root, src_node, dst_node, bitrate, + auth=self.__auth, timeout=self.__timeout) + + results.extend(response) + return results + + @metered_subclass_method(METRICS_POOL) + def DeleteConfig(self, resources: List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + results = [] + if len(resources) == 0: + return results + with self.__lock: + for _, resource in resources: + LOGGER.info('resource = {:s}'.format(str(resource))) + flow_id = find_key(resource, 'flow_id') + src_node = find_key(resource, 'src_node') + dst_node = find_key(resource, 'dst_node') + bitrate = find_key(resource, 'bitrate') + + response = del_lightpath(self.__flexscale_root, flow_id, src_node, dst_node, bitrate) + results.extend(response) + + return results + + @metered_subclass_method(METRICS_POOL) + def SubscribeState(self, subscriptions : List[Tuple[str, float, float]]) -> List[Union[bool, Exception]]: + # FlexScale does not support monitoring by now + return [False for _ in subscriptions] + + @metered_subclass_method(METRICS_POOL) + def UnsubscribeState(self, subscriptions : List[Tuple[str, float, float]]) -> List[Union[bool, Exception]]: + # FlexScale does not support monitoring by now + return [False for _ in subscriptions] + + def GetState( + self, blocking=False, terminate : Optional[threading.Event] = None + ) -> Iterator[Tuple[float, str, Any]]: + # FlexScale does not support monitoring by now + return [] diff --git a/src/device/service/drivers/flexscale/Tools.py b/src/device/service/drivers/flexscale/Tools.py new file mode 100644 index 0000000000000000000000000000000000000000..2f74f36571e3918fdc6a963f4ab221ddbe3216e4 --- /dev/null +++ b/src/device/service/drivers/flexscale/Tools.py @@ -0,0 +1,143 @@ +# 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 json, logging, requests +from requests.auth import HTTPBasicAuth +from typing import Optional + +LOGGER = logging.getLogger(__name__) + +HTTP_OK_CODES = { + 200, # OK + 201, # Created + 202, # Accepted + 204, # No Content +} + +def find_key(resource, key): + return json.loads(resource[1])[key] + +def get_lightpaths(root_url : str, resource_key : str,auth : Optional[HTTPBasicAuth] = None, + timeout : Optional[int] = None): + headers = {'accept': 'application/json'} + url = '{:s}/OpticalTFS/GetLightpaths'.format(root_url) + + result = [] + try: + response = requests.get(url, timeout=timeout, headers=headers, verify=False, auth=auth) + except requests.exceptions.Timeout: + LOGGER.exception('Timeout connecting {:s}'.format(url)) + return result + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Exception retrieving {:s}'.format(resource_key)) + result.append((resource_key, e)) + return result + + try: + flows = json.loads(response.content) + except Exception as e: # pylint: disable=broad-except + LOGGER.warning('Unable to decode reply: {:s}'.format(str(response.content))) + result.append((resource_key, e)) + return result + + for flow in flows: + flow_id = flow.get('flow_id') + source = flow.get('src') + destination = flow.get('dst') + bitrate = flow.get('bitrate') + + endpoint_url = '/flows/flow[{:s}]'.format(flow_id) + endpoint_data = {'flow_id': flow_id, 'src': source, 'dst': destination, 'bitrate': bitrate} + result.append((endpoint_url, endpoint_data)) + + return result + + +def add_lightpath(root_url, src_node, dst_node, bitrate, + auth : Optional[HTTPBasicAuth] = None, timeout : Optional[int] = None): + + headers = {'accept': 'application/json'} + url = '{:s}/OpticalTFS/AddLightpath/{:s}/{:s}/{:s}'.format( + root_url, src_node, dst_node, bitrate) + + results = [] + try: + LOGGER.info('Lightpath request: {:s} <-> {:s} with {:s} bitrate'.format( + str(src_node), str(dst_node), str(bitrate))) + response = requests.put(url=url, timeout=timeout, headers=headers, verify=False, auth=auth) + results.append(response.json()) + LOGGER.info('Response: {:s}'.format(str(response))) + + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Exception requesting Lightpath: {:s} <-> {:s} with {:s} bitrate'.format( + str(src_node), str(dst_node), str(bitrate))) + results.append(e) + else: + if response.status_code not in HTTP_OK_CODES: + msg = 'Could not create Lightpath(status_code={:s} reply={:s}' + LOGGER.error(msg.format(str(response.status_code), str(response))) + results.append(response.status_code in HTTP_OK_CODES) + + return results + + + +def del_lightpath(root_url, flow_id, src_node, dst_node, bitrate, + auth : Optional[HTTPBasicAuth] = None, timeout : Optional[int] = None): + url = '{:s}/OpticalTFS/DelLightpath/{:s}/{:s}/{:s}/{:s}'.format( + root_url, flow_id, src_node, dst_node, bitrate) + headers = {'accept': 'application/json'} + + results = [] + + try: + response = requests.delete( + url=url, timeout=timeout, headers=headers, verify=False, auth=auth) + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Exception deleting Lightpath(uuid={:s})'.format(str(flow_id))) + results.append(e) + else: + if response.status_code not in HTTP_OK_CODES: + msg = 'Could not delete Lightpath(flow_id={:s}). status_code={:s} reply={:s}' + LOGGER.error(msg.format(str(flow_id), str(response.status_code), str(response))) + results.append(response.status_code in HTTP_OK_CODES) + + return results + + +def get_topology(root_url : str, resource_key : str,auth : Optional[HTTPBasicAuth] = None, + timeout : Optional[int] = None): + headers = {'accept': 'application/json'} + url = '{:s}/OpticalTFS/GetLinks'.format(root_url) + + result = [] + try: + response = requests.get(url, timeout=timeout, headers=headers, verify=False, auth=auth) + except requests.exceptions.Timeout: + LOGGER.exception('Timeout connecting {:s}'.format(url)) + return result + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Exception retrieving {:s}'.format(resource_key)) + result.append((resource_key, e)) + return result + + try: + response = json.loads(response.content) + except Exception as e: # pylint: disable=broad-except + LOGGER.warning('Unable to decode reply: {:s}'.format(str(response.content))) + result.append((resource_key, e)) + return result + + result.append(response) + return result diff --git a/src/device/service/drivers/flexscale/__init__.py b/src/device/service/drivers/flexscale/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d5073c330b89bed63f08b0da86c4a7649c87b3dd --- /dev/null +++ b/src/device/service/drivers/flexscale/__init__.py @@ -0,0 +1,20 @@ +# 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. + +from device.service.driver_api._Driver import RESOURCE_ENDPOINTS, RESOURCE_SERVICES + +ALL_RESOURCE_KEYS = [ + RESOURCE_ENDPOINTS, + RESOURCE_SERVICES, +] diff --git a/src/device/service/drivers/ietf_l2vpn/TfsDebugApiClient.py b/src/device/service/drivers/ietf_l2vpn/TfsDebugApiClient.py index 2d3901695abc4c0124a7f443ffa59f825d4e13bf..06c55c5dc1b0feb77d817581ae5d735e1158e38d 100644 --- a/src/device/service/drivers/ietf_l2vpn/TfsDebugApiClient.py +++ b/src/device/service/drivers/ietf_l2vpn/TfsDebugApiClient.py @@ -44,6 +44,7 @@ MAPPING_DRIVER = { 'DEVICEDRIVER_XR' : 6, 'DEVICEDRIVER_IETF_L2VPN' : 7, 'DEVICEDRIVER_GNMI_OPENCONFIG' : 8, + 'DEVICEDRIVER_FLEXSCALE' : 9, } MSG_ERROR = 'Could not retrieve devices in remote TeraFlowSDN instance({:s}). status_code={:s} reply={:s}' diff --git a/src/tests/tools/mock_flexscale_opt_ctrl/MockFlexscaleOptCtrl.py b/src/tests/tools/mock_flexscale_opt_ctrl/MockFlexscaleOptCtrl.py new file mode 100644 index 0000000000000000000000000000000000000000..c4cc2b69911b99600c79b61d3634427268827b66 --- /dev/null +++ b/src/tests/tools/mock_flexscale_opt_ctrl/MockFlexscaleOptCtrl.py @@ -0,0 +1,73 @@ +# 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 functools, logging, sys, time +from flask import Flask, jsonify, make_response, request +from flask_restful import Api, Resource +from data import ADDLIGHTPATH_REPLY + +BIND_ADDRESS = '0.0.0.0' +BIND_PORT = 8443 +BASE_URL = '/OpticalTFS' +STR_ENDPOINT = 'https://{:s}:{:s}{:s}'.format(str(BIND_ADDRESS), str(BIND_PORT), str(BASE_URL)) +LOG_LEVEL = logging.DEBUG + + +logging.basicConfig(level=LOG_LEVEL, format="[%(asctime)s] %(levelname)s:%(name)s:%(message)s") +LOGGER = logging.getLogger(__name__) +logging.getLogger('werkzeug').setLevel(logging.WARNING) + +def log_request(logger : logging.Logger, response): + timestamp = time.strftime('[%Y-%b-%d %H:%M]') + logger.info('%s %s %s %s %s', timestamp, request.remote_addr, request.method, request.full_path, response.status) + return response + +class AddLightpath(Resource): + def put(self, src_node: str, dst_node: str, bitrate: int): + return make_response(jsonify(ADDLIGHTPATH_REPLY), 200) + +class DelLightpath(Resource): + def delete(self, flow_id: str, src_node: str, dst_node: str, bitrate: int): + return make_response(jsonify({}), 200) + +class GetLightpaths(Resource): + def get(self): + return make_response(jsonify({}), 200) + +class GetLinks(Resource): + def get(self): + return make_response(jsonify({}), 200) + + +def main(): + LOGGER.info('Starting...') + + app = Flask(__name__) + app.after_request(functools.partial(log_request, LOGGER)) + + api = Api(app, prefix=BASE_URL) + api.add_resource(AddLightpath, '/AddLightpath///') + api.add_resource(DelLightpath, '/DelLightpath////') + api.add_resource(GetLightpaths, '/GetLightpaths') + api.add_resource(GetLinks, '/GetLinks') + + LOGGER.info('Listening on {:s}...'.format(str(STR_ENDPOINT))) + app.run(debug=True, host=BIND_ADDRESS, port=BIND_PORT) + + LOGGER.info('Bye') + return 0 + +if __name__ == '__main__': + sys.exit(main()) diff --git a/src/tests/tools/mock_flexscale_opt_ctrl/data.py b/src/tests/tools/mock_flexscale_opt_ctrl/data.py new file mode 100644 index 0000000000000000000000000000000000000000..20a0340165e56b71d6fdf016246fe59b1a9ddc71 --- /dev/null +++ b/src/tests/tools/mock_flexscale_opt_ctrl/data.py @@ -0,0 +1,98 @@ +# 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. + + +ADDLIGHTPATH_REPLY = { + "flow_id": 1, + "src": "t1", + "dst": "t2", + "bitrate": 100, + "bidir": 1, + "flows": { + "t1": [ + { + "in": 0, + "out": "1" + }, + { + "in": "1", + "out": 0 + } + ], + "r1": [ + { + "in": "101R", + "out": "1T" + }, + { + "in": "1R", + "out": "101T" + } + ], + "r2": [ + { + "in": "1R", + "out": "101T" + }, + { + "in": "101R", + "out": "1T" + } + ], + "t2": [ + { + "in": "1", + "out": 0 + }, + { + "in": 0, + "out": "1" + } + ] + }, + "band_type": "c_slots", + "slots": [ + 1, + 2, + 3, + 4 + ], + "fiber_forward": { + "t1-r1": "M1", + "r1-r2": "d1-1", + "r2-t2": "S1" + }, + "fiber_backward": { + "r1-t1": "S1", + "r2-r1": "d1-1", + "t2-r2": "M1" + }, + "op-mode": 1, + "n_slots": 4, + "links": [ + "t1-r1", + "r1-r2", + "r2-t2" + ], + "path": [ + "t1", + "r1", + "r2", + "t2" + ], + "band": 50, + "freq": 192031.25, + "is_active": True +} + diff --git a/src/tests/tools/mock_flexscale_opt_ctrl/run.sh b/src/tests/tools/mock_flexscale_opt_ctrl/run.sh new file mode 100755 index 0000000000000000000000000000000000000000..183df7a030dca352ee2bb5fdfd0ac081cdcec960 --- /dev/null +++ b/src/tests/tools/mock_flexscale_opt_ctrl/run.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env 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. + +python MockFlexscaleOptCtrl.py