diff --git a/my_deploy.sh b/my_deploy.sh index 67ec8615e16f41fde24254316ae7a16171bd2e86..88be82b63e9e79a97ee79702de886f69a6152f94 100755 --- a/my_deploy.sh +++ b/my_deploy.sh @@ -70,6 +70,10 @@ export TFS_COMPONENTS="context device pathcomp service slice nbi webui load_gene # export TLS_CERT_PATH="src/dlt/gateway/keys/ca.crt" #fi +# Uncomment to activate QKD App +#export TFS_COMPONENTS="${TFS_COMPONENTS} app" + + # Set the tag you want to use for your images. export TFS_IMAGE_TAG="dev" diff --git a/proto/context.proto b/proto/context.proto index 87f69132df022e2aa4a0766dc9f0a7a7fae36d59..fa9b1959b54746a6b9a426215874840e1cda8d10 100644 --- a/proto/context.proto +++ b/proto/context.proto @@ -214,6 +214,7 @@ enum DeviceDriverEnum { DEVICEDRIVER_OPTICAL_TFS = 9; DEVICEDRIVER_IETF_ACTN = 10; DEVICEDRIVER_OC = 11; + DEVICEDRIVER_QKD = 12; } enum DeviceOperationalStatusEnum { @@ -300,6 +301,7 @@ enum ServiceTypeEnum { SERVICETYPE_TE = 4; SERVICETYPE_E2E = 5; SERVICETYPE_OPTICAL_CONNECTIVITY = 6; + SERVICETYPE_QKD = 7; } enum ServiceStatusEnum { diff --git a/src/common/DeviceTypes.py b/src/common/DeviceTypes.py index 9ed321d5328aa17a856a3a6401bc35576eef679f..23ebe19d681bd0ba774c8f3f4435c233369d0e28 100644 --- a/src/common/DeviceTypes.py +++ b/src/common/DeviceTypes.py @@ -47,6 +47,7 @@ class DeviceTypeEnum(Enum): PACKET_ROUTER = 'packet-router' PACKET_SWITCH = 'packet-switch' XR_CONSTELLATION = 'xr-constellation' + QKD_NODE = 'qkd-node' # ETSI TeraFlowSDN controller TERAFLOWSDN_CONTROLLER = 'teraflowsdn' diff --git a/src/context/service/database/models/enums/DeviceDriver.py b/src/context/service/database/models/enums/DeviceDriver.py index 06d73177036d8bfe8b6f72d6c97b03f78aaf7531..cf900ed6df3b4699a4e56f53873174ddd997cd53 100644 --- a/src/context/service/database/models/enums/DeviceDriver.py +++ b/src/context/service/database/models/enums/DeviceDriver.py @@ -34,6 +34,7 @@ class ORM_DeviceDriverEnum(enum.Enum): OPTICAL_TFS = DeviceDriverEnum.DEVICEDRIVER_OPTICAL_TFS IETF_ACTN = DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN OC = DeviceDriverEnum.DEVICEDRIVER_OC + QKD = DeviceDriverEnum.DEVICEDRIVER_QKD 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 cb6158b965a21c974605a27582340620f368bdb9..a5e7f377113342b98203a23a426540f6188f784e 100644 --- a/src/device/service/drivers/__init__.py +++ b/src/device/service/drivers/__init__.py @@ -178,3 +178,14 @@ if LOAD_ALL_DEVICE_DRIVERS: FilterFieldEnum.DRIVER : DeviceDriverEnum.DEVICEDRIVER_OC, } ])) + +if LOAD_ALL_DEVICE_DRIVERS: + from .qkd.QKDDriver2 import QKDDriver # pylint: disable=wrong-import-position + DRIVERS.append( + (QKDDriver, [ + { + # Close enough, it does optical switching + FilterFieldEnum.DEVICE_TYPE: DeviceTypeEnum.QKD_NODE, + FilterFieldEnum.DRIVER : DeviceDriverEnum.DEVICEDRIVER_QKD, + } + ])) diff --git a/src/device/service/drivers/qkd/QKDDriver.py b/src/device/service/drivers/qkd/QKDDriver.py new file mode 100644 index 0000000000000000000000000000000000000000..a49144d6fc0840498c5f5f1267d1cef25cb1177a --- /dev/null +++ b/src/device/service/drivers/qkd/QKDDriver.py @@ -0,0 +1,168 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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, config_getter, create_connectivity_link + +LOGGER = logging.getLogger(__name__) + +DRIVER_NAME = 'qkd' +METRICS_POOL = MetricsPool('Device', 'Driver', labels={'driver': DRIVER_NAME}) + + +class QKDDriver(_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.__qkd_root = '{:s}://{:s}:{:d}'.format(scheme, self.address, int(self.port)) + self.__timeout = int(self.settings.get('timeout', 120)) + self.__node_ids = set(self.settings.get('node_ids', [])) + token = self.settings.get('token') + self.__headers = {'Authorization': 'Bearer ' + token} + self.__initial_data = None + + def Connect(self) -> bool: + url = self.__qkd_root + '/restconf/data/etsi-qkd-sdn-node:qkd_node' + with self.__lock: + if self.__started.is_set(): return True + r = None + try: + LOGGER.info(f'requests.get("{url}", timeout={self.__timeout}, verify=False, auth={self.__auth}, headers={self.__headers})') + r = requests.get(url, timeout=self.__timeout, verify=False, auth=self.__auth, headers=self.__headers) + LOGGER.info(f'R: {r}') + LOGGER.info(f'Text: {r.text}') + LOGGER.info(f'Json: {r.json()}') + except requests.exceptions.Timeout: + LOGGER.exception('Timeout connecting {:s}'.format(str(self.__qkd_root))) + return False + except Exception: # pylint: disable=broad-except + LOGGER.exception('Exception connecting {:s}'.format(str(self.__qkd_root))) + return False + else: + self.__started.set() + self.__initial_data = r.json() + 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 self.__initial_data + + @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) + results.extend(config_getter( + self.__qkd_root, resource_key, timeout=self.__timeout, auth=self.__auth, + node_ids=self.__node_ids, headers=self.__headers)) + 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_key, resource_value in resources: + LOGGER.info('resource = {:s}'.format(str(resource_key))) + + if resource_key.startswith('/link'): + try: + resource_value = json.loads(resource_value) + link_uuid = resource_value['uuid'] + + node_id_src = resource_value['src_qkdn_id'] + interface_id_src = resource_value['src_interface_id'] + node_id_dst = resource_value['dst_qkdn_id'] + interface_id_dst = resource_value['dst_interface_id'] + virt_prev_hop = resource_value.get('virt_prev_hop') + virt_next_hops = resource_value.get('virt_next_hops') + virt_bandwidth = resource_value.get('virt_bandwidth') + + + data = create_connectivity_link( + self.__qkd_root, link_uuid, node_id_src, interface_id_src, node_id_dst, interface_id_dst, + virt_prev_hop, virt_next_hops, virt_bandwidth, + timeout=self.__timeout, auth=self.__auth, headers=self.__headers + ) + + #data = create_connectivity_link( + # self.__qkd_root, link_uuid, node_id_src, interface_id_src, node_id_dst, interface_id_dst, + # timeout=self.__timeout, auth=self.__auth + #) + results.append(True) + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Unhandled error processing resource_key({:s})'.format(str(resource_key))) + results.append(e) + else: + results.append(True) + + LOGGER.info('Test keys: ' + str([x for x,y in resources])) + LOGGER.info('Test values: ' + str(results)) + 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))) + uuid = find_key(resource, 'uuid') + results.extend(delete_connectivity_service( + self.__qkd_root, uuid, timeout=self.__timeout, auth=self.__auth)) + return results + ''' + + @metered_subclass_method(METRICS_POOL) + def SubscribeState(self, subscriptions : List[Tuple[str, float, float]]) -> List[Union[bool, Exception]]: + # TODO: QKD API Driver does not support monitoring by now + LOGGER.info(f'Subscribe {self.address}: {subscriptions}') + return [True for _ in subscriptions] + + @metered_subclass_method(METRICS_POOL) + def UnsubscribeState(self, subscriptions : List[Tuple[str, float, float]]) -> List[Union[bool, Exception]]: + # TODO: QKD API Driver 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]]: + # TODO: QKD API Driver does not support monitoring by now + LOGGER.info(f'GetState {self.address} called') + return [] diff --git a/src/device/service/drivers/qkd/QKDDriver2.py b/src/device/service/drivers/qkd/QKDDriver2.py new file mode 100644 index 0000000000000000000000000000000000000000..c73a83141d92955d01a6a00912389b671fe7ef98 --- /dev/null +++ b/src/device/service/drivers/qkd/QKDDriver2.py @@ -0,0 +1,216 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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 os +import json +import logging +import requests +import threading +from requests.auth import HTTPBasicAuth +from typing import Any, 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 .Tools2 import config_getter, create_connectivity_link +from device.service.driver_api._Driver import _Driver +from . import ALL_RESOURCE_KEYS + +LOGGER = logging.getLogger(__name__) + +DRIVER_NAME = 'qkd' +METRICS_POOL = MetricsPool('Device', 'Driver', labels={'driver': DRIVER_NAME}) + + +class QKDDriver(_Driver): + def __init__(self, address: str, port: int, **settings) -> None: + LOGGER.info(f"Initializing QKDDriver with address={address}, port={port}, settings={settings}") + super().__init__(DRIVER_NAME, address, port, **settings) + self.__lock = threading.Lock() + self.__started = threading.Event() + self.__terminate = threading.Event() + self.__auth = None + self.__headers = {} + self.__qkd_root = os.getenv('QKD_API_URL', f"http://{self.address}:{self.port}") # Simplified URL management + self.__timeout = int(self.settings.get('timeout', 120)) + self.__node_ids = set(self.settings.get('node_ids', [])) + self.__initial_data = None + + # Optionally pass headers for authentication (e.g., JWT) + self.__headers = settings.get('headers', {}) + self.__auth = settings.get('auth', None) + + LOGGER.info(f"QKDDriver initialized with QKD root URL: {self.__qkd_root}") + + def Connect(self) -> bool: + url = self.__qkd_root + '/restconf/data/etsi-qkd-sdn-node:qkd_node' + with self.__lock: + LOGGER.info(f"Starting connection to {url}") + if self.__started.is_set(): + LOGGER.info("Already connected, skipping re-connection.") + return True + + try: + LOGGER.info(f'Attempting to connect to {url} with headers {self.__headers} and timeout {self.__timeout}') + response = requests.get(url, timeout=self.__timeout, verify=False, headers=self.__headers, auth=self.__auth) + LOGGER.info(f'Received response: {response.status_code}, content: {response.text}') + response.raise_for_status() + self.__initial_data = response.json() + self.__started.set() + LOGGER.info('Connection successful') + return True + except requests.exceptions.RequestException as e: + LOGGER.error(f'Connection failed: {e}') + return False + + def Disconnect(self) -> bool: + LOGGER.info("Disconnecting QKDDriver") + with self.__lock: + self.__terminate.set() + LOGGER.info("QKDDriver disconnected successfully") + return True + + @metered_subclass_method(METRICS_POOL) + def GetInitialConfig(self) -> List[Tuple[str, Any]]: + LOGGER.info("Getting initial configuration") + with self.__lock: + if isinstance(self.__initial_data, dict): + initial_config = [('qkd_node', self.__initial_data.get('qkd_node', {}))] + LOGGER.info(f"Initial configuration: {initial_config}") + return initial_config + LOGGER.warning("Initial data is not a dictionary") + 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) + LOGGER.info(f"Getting configuration for resource_keys: {resource_keys}") + results = [] + with self.__lock: + if not resource_keys: + resource_keys = ALL_RESOURCE_KEYS + for i, resource_key in enumerate(resource_keys): + chk_string(f'resource_key[{i}]', resource_key, allow_empty=False) + LOGGER.info(f"Retrieving resource key: {resource_key}") + resource_results = config_getter( + self.__qkd_root, resource_key, timeout=self.__timeout, headers=self.__headers, auth=self.__auth) + results.extend(resource_results) + LOGGER.info(f"Resource results for {resource_key}: {resource_results}") + LOGGER.info(f"Final configuration results: {results}") + 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_key, resource_value in resources: + LOGGER.info('Processing resource_key = {:s}'.format(str(resource_key))) + + # Only process '/link' keys + if resource_key.startswith('/link'): + try: + # Ensure resource_value is deserialized + if isinstance(resource_value, str): + resource_value = json.loads(resource_value) + + # Extract values from resource_value dictionary + link_uuid = resource_value['uuid'] + node_id_src = resource_value['src_qkdn_id'] + interface_id_src = resource_value['src_interface_id'] + node_id_dst = resource_value['dst_qkdn_id'] + interface_id_dst = resource_value['dst_interface_id'] + virt_prev_hop = resource_value.get('virt_prev_hop') + virt_next_hops = resource_value.get('virt_next_hops') + virt_bandwidth = resource_value.get('virt_bandwidth') + + # Call create_connectivity_link with the extracted values + LOGGER.info(f"Creating connectivity link with UUID: {link_uuid}") + data = create_connectivity_link( + self.__qkd_root, link_uuid, node_id_src, interface_id_src, node_id_dst, interface_id_dst, + virt_prev_hop, virt_next_hops, virt_bandwidth, + timeout=self.__timeout, auth=self.__auth + ) + + # Append success result + results.append(True) + LOGGER.info(f"Connectivity link {link_uuid} created successfully") + + except Exception as e: + # Catch and log any unhandled exceptions + LOGGER.exception(f'Unhandled error processing resource_key({resource_key})') + results.append(e) + else: + # Skip unsupported resource keys and append success + results.append(True) + + # Logging test results + LOGGER.info('Test keys: ' + str([x for x,y in resources])) + LOGGER.info('Test values: ' + str(results)) + + return results + + @metered_subclass_method(METRICS_POOL) + def DeleteConfig(self, resources: List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + LOGGER.info(f"Deleting configuration for resources: {resources}") + results = [] + if not resources: + LOGGER.warning("No resources provided for DeleteConfig") + return results + with self.__lock: + for resource in resources: + LOGGER.info(f'Resource to delete: {resource}') + uuid = resource[1].get('uuid') + if uuid: + LOGGER.info(f'Resource with UUID {uuid} deleted successfully') + results.append(True) + else: + LOGGER.warning(f"UUID not found in resource: {resource}") + results.append(False) + LOGGER.info(f"DeleteConfig results: {results}") + return results + + @metered_subclass_method(METRICS_POOL) + def SubscribeState(self, subscriptions: List[Tuple[str, float, float]]) -> List[Union[bool, Exception]]: + LOGGER.info(f"Subscribing to state updates: {subscriptions}") + results = [True for _ in subscriptions] + LOGGER.info(f"Subscription results: {results}") + return results + + @metered_subclass_method(METRICS_POOL) + def UnsubscribeState(self, subscriptions: List[Tuple[str, float, float]]) -> List[Union[bool, Exception]]: + LOGGER.info(f"Unsubscribing from state updates: {subscriptions}") + results = [True for _ in subscriptions] + LOGGER.info(f"Unsubscription results: {results}") + return results + + @metered_subclass_method(METRICS_POOL) + def GetState(self, blocking=False, terminate: Optional[threading.Event] = None) -> Union[dict, list]: + LOGGER.info(f"GetState called with blocking={blocking}, terminate={terminate}") + url = self.__qkd_root + '/restconf/data/etsi-qkd-sdn-node:qkd_node' + try: + LOGGER.info(f"Making GET request to {url} to retrieve state") + response = requests.get(url, timeout=self.__timeout, verify=False, headers=self.__headers, auth=self.__auth) + LOGGER.info(f"Received state response: {response.status_code}, content: {response.text}") + response.raise_for_status() + state_data = response.json() + LOGGER.info(f"State data retrieved: {state_data}") + return state_data + except requests.exceptions.Timeout: + LOGGER.error(f'Timeout getting state from {self.__qkd_root}') + return [] + except Exception as e: + LOGGER.error(f'Exception getting state from {self.__qkd_root}: {e}') + return [] diff --git a/src/device/service/drivers/qkd/Tools.py b/src/device/service/drivers/qkd/Tools.py new file mode 100644 index 0000000000000000000000000000000000000000..c17d01915dcdda55b36317c683fd60524c97239b --- /dev/null +++ b/src/device/service/drivers/qkd/Tools.py @@ -0,0 +1,173 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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 Dict, Optional, Set +from device.service.driver_api._Driver import RESOURCE_ENDPOINTS, RESOURCE_INTERFACES, RESOURCE_NETWORK_INSTANCES +from . import RESOURCE_APPS, RESOURCE_LINKS, RESOURCE_CAPABILITES, RESOURCE_NODE + + +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 config_getter( + root_url : str, resource_key : str, auth : Optional[HTTPBasicAuth] = None, timeout : Optional[int] = None, + node_ids : Set[str] = set(), headers={} +): + # getting endpoints + + url = root_url + '/restconf/data/etsi-qkd-sdn-node:qkd_node/' + + + result = [] + + try: + if resource_key in [RESOURCE_ENDPOINTS, RESOURCE_INTERFACES]: + url += 'qkd_interfaces/' + r = requests.get(url, timeout=timeout, verify=False, auth=auth, headers=headers) + interfaces = r.json()['qkd_interfaces']['qkd_interface'] + + # If it's a physical endpoint + if resource_key == RESOURCE_ENDPOINTS: + for interface in interfaces: + resource_value = interface.get('qkdi_att_point', {}) + if 'device' in resource_value and 'port' in resource_value: + uuid = '{}:{}'.format(resource_value['device'], resource_value['port']) + resource_key = '/endpoints/endpoint[{:s}]'.format(uuid) + resource_value['uuid'] = uuid + + sample_types = {} + metric_name = 'KPISAMPLETYPE_LINK_TOTAL_CAPACITY_GBPS' + metric_id = 301 + metric_name = metric_name.lower().replace('kpisampletype_', '') + monitoring_resource_key = '{:s}/state/{:s}'.format(resource_key, metric_name) + sample_types[metric_id] = monitoring_resource_key + + + + resource_value['sample_types'] = sample_types + + + + result.append((resource_key, resource_value)) + else: + for interface in interfaces: + resource_key = '/interface[{:s}]'.format(interface['qkdi_id']) + endpoint_value = interface.get('qkdi_att_point', {}) + + if 'device' in endpoint_value and 'port' in endpoint_value: + name = '{}:{}'.format(endpoint_value['device'], endpoint_value['port']) + interface['name'] = name + interface['enabled'] = True # For test purpose only + + result.append((resource_key, interface)) + + elif resource_key in [RESOURCE_LINKS, RESOURCE_NETWORK_INSTANCES]: + url += 'qkd_links/' + r = requests.get(url, timeout=timeout, verify=False, auth=auth, headers=headers) + links = r.json()['qkd_links']['qkd_link'] + + if resource_key == RESOURCE_LINKS: + for link in links: + link_type = link.get('qkdl_type', 'Direct') + + if link_type == 'Direct': + resource_key = '/link[{:s}]'.format(link['qkdl_id']) + result.append((resource_key, link)) + else: + for link in links: + link_type = link.get('qkdl_type', 'Direct') + + if link_type == 'Virtual': + resource_key = '/service[{:s}]'.format(link['qkdl_id']) + result.append((resource_key, link)) + + elif resource_key == RESOURCE_APPS: + url += 'qkd_applications/' + r = requests.get(url, timeout=timeout, verify=False, auth=auth, headers=headers) + apps = r.json()['qkd_applications']['qkd_app'] + + for app in apps: + resource_key = '/app[{:s}]'.format(app['app_id']) + result.append((resource_key, app)) + + + elif resource_key == RESOURCE_CAPABILITES: + url += 'qkdn_capabilities/' + r = requests.get(url, timeout=timeout, verify=False, auth=auth, headers=headers) + capabilities = r.json()['qkdn_capabilities'] + + result.append((resource_key, capabilities)) + + elif resource_key == RESOURCE_NODE: + r = requests.get(url, timeout=timeout, verify=False, auth=auth, headers=headers) + node = r.json()['qkd_node'] + + result.append((resource_key, node)) + + except requests.exceptions.Timeout: + LOGGER.exception('Timeout connecting {:s}'.format(url)) + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Exception retrieving/parsing endpoints for {:s}'.format(resource_key)) + result.append((resource_key, e)) + + + return result + + + +def create_connectivity_link( + root_url, link_uuid, node_id_src, interface_id_src, node_id_dst, interface_id_dst, + virt_prev_hop = None, virt_next_hops = None, virt_bandwidth = None, + auth : Optional[HTTPBasicAuth] = None, timeout : Optional[int] = None, headers={} +): + + url = root_url + '/restconf/data/etsi-qkd-sdn-node:qkd_node/qkd_links/' + is_virtual = bool(virt_prev_hop or virt_next_hops) + + qkd_link = { + 'qkdl_id': link_uuid, + 'qkdl_type': 'etsi-qkd-node-types:' + ('VIRT' if is_virtual else 'PHYS'), + 'qkdl_local': { + 'qkdn_id': node_id_src, + 'qkdi_id': interface_id_src + }, + 'qkdl_remote': { + 'qkdn_id': node_id_dst, + 'qkdi_id': interface_id_dst + } + } + + if is_virtual: + qkd_link['virt_prev_hop'] = virt_prev_hop + qkd_link['virt_next_hop'] = virt_next_hops or [] + qkd_link['virt_bandwidth'] = virt_bandwidth + + + data = {'qkd_links': {'qkd_link': [qkd_link]}} + + requests.post(url, json=data, headers=headers) + diff --git a/src/device/service/drivers/qkd/Tools2.py b/src/device/service/drivers/qkd/Tools2.py new file mode 100644 index 0000000000000000000000000000000000000000..c598c7443d276ea0eb76ce761d173de9944c3cfb --- /dev/null +++ b/src/device/service/drivers/qkd/Tools2.py @@ -0,0 +1,272 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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 +import logging +import requests +from typing import Dict, Optional, Set, List, Tuple, Union, Any +from device.service.driver_api._Driver import RESOURCE_ENDPOINTS, RESOURCE_INTERFACES, RESOURCE_NETWORK_INSTANCES +from . import RESOURCE_APPS, RESOURCE_LINKS, RESOURCE_CAPABILITES, RESOURCE_NODE + +LOGGER = logging.getLogger(__name__) + +HTTP_OK_CODES = {200, 201, 202, 204} + +def find_key(resource: Tuple[str, str], key: str) -> Any: + """ + Extracts a specific key from a JSON resource. + """ + return json.loads(resource[1]).get(key) + + +def config_getter( + root_url: str, resource_key: str, auth: Optional[Any] = None, timeout: Optional[int] = None, + node_ids: Set[str] = set(), headers: Dict[str, str] = {} +) -> List[Tuple[str, Union[Dict[str, Any], Exception]]]: + """ + Fetches configuration data from a QKD node for a specified resource key. + Returns a list of tuples containing the resource key and the corresponding data or exception. + The function is agnostic to authentication: headers and auth are passed from external sources. + """ + url = f"{root_url}/restconf/data/etsi-qkd-sdn-node:qkd_node/" + LOGGER.info(f"Fetching configuration for {resource_key} from {root_url}") + + try: + if resource_key in [RESOURCE_ENDPOINTS, RESOURCE_INTERFACES]: + return fetch_interfaces(url, resource_key, headers, auth, timeout) + + elif resource_key in [RESOURCE_LINKS, RESOURCE_NETWORK_INSTANCES]: + return fetch_links(url, resource_key, headers, auth, timeout) + + elif resource_key in [RESOURCE_APPS]: + return fetch_apps(url, resource_key, headers, auth, timeout) + + elif resource_key in [RESOURCE_CAPABILITES]: + return fetch_capabilities(url, resource_key, headers, auth, timeout) + + elif resource_key in [RESOURCE_NODE]: + return fetch_node(url, resource_key, headers, auth, timeout) + + else: + LOGGER.warning(f"Unknown resource key: {resource_key}") + return [(resource_key, ValueError(f"Unknown resource key: {resource_key}"))] + + except requests.exceptions.RequestException as e: + LOGGER.error(f'Error retrieving/parsing {resource_key} from {url}: {e}') + return [(resource_key, e)] + + +def fetch_interfaces(url: str, resource_key: str, headers: Dict[str, str], auth: Optional[Any], timeout: Optional[int]) -> List[Tuple[str, Union[Dict[str, Any], Exception]]]: + """ + Fetches interface data from the QKD node. Adapts to both mocked and real QKD data structures. + """ + result = [] + url += 'qkd_interfaces/' + + try: + r = requests.get(url, timeout=timeout, verify=False, auth=auth, headers=headers) + r.raise_for_status() + + # Handle both real and mocked QKD response structures + response_data = r.json() + + if isinstance(response_data.get('qkd_interfaces'), dict): + interfaces = response_data.get('qkd_interfaces', {}).get('qkd_interface', []) + else: + interfaces = response_data.get('qkd_interface', []) + + for interface in interfaces: + if resource_key in [RESOURCE_ENDPOINTS]: + # Handle real QKD data format + resource_value = interface.get('qkdi_att_point', {}) + if 'device' in resource_value and 'port' in resource_value: + uuid = f"{resource_value['device']}:{resource_value['port']}" + resource_key_with_uuid = f"/endpoints/endpoint[{uuid}]" + resource_value['uuid'] = uuid + + # Add sample types (for demonstration purposes) + sample_types = {} + metric_name = 'KPISAMPLETYPE_LINK_TOTAL_CAPACITY_GBPS' + metric_id = 301 + metric_name = metric_name.lower().replace('kpisampletype_', '') + monitoring_resource_key = '{:s}/state/{:s}'.format(resource_key, metric_name) + sample_types[metric_id] = monitoring_resource_key + resource_value['sample_types'] = sample_types + + result.append((resource_key_with_uuid, resource_value)) + + else: + # Handle both real and mocked QKD formats + endpoint_value = interface.get('qkdi_att_point', {}) + if 'device' in endpoint_value and 'port' in endpoint_value: + # Real QKD data format + interface_uuid = f"{endpoint_value['device']}:{endpoint_value['port']}" + interface['uuid'] = interface_uuid + interface['name'] = interface_uuid + interface['enabled'] = True # Assume enabled for real data + else: + # Mocked QKD data format + interface_uuid = interface.get('uuid', f"/interface[{interface['qkdi_id']}]") + interface['uuid'] = interface_uuid + interface['name'] = interface.get('name', interface_uuid) + interface['enabled'] = interface.get('enabled', False) # Mocked enabled status + + result.append((f"/interface[{interface['qkdi_id']}]", interface)) + + except requests.RequestException as e: + LOGGER.error(f"Error fetching interfaces from {url}: {e}") + result.append((resource_key, e)) + + return result + +def fetch_links(url: str, resource_key: str, headers: Dict[str, str], auth: Optional[Any], timeout: Optional[int]) -> List[Tuple[str, Union[Dict[str, Any], Exception]]]: + """ + Fetches link data from the QKD node. Adapts to both mocked and real QKD data structures. + """ + result = [] + + if resource_key in [RESOURCE_LINKS, RESOURCE_NETWORK_INSTANCES]: + url += 'qkd_links/' + + try: + r = requests.get(url, timeout=timeout, verify=False, auth=auth, headers=headers) + r.raise_for_status() + + # Handle real and mocked QKD data structures + links = r.json().get('qkd_links', []) + + for link in links: + # For real QKD format (QKD links returned as dictionary objects) + if isinstance(link, dict): + qkdl_id = link.get('qkdl_id') + link_type = link.get('qkdl_type', 'Direct') + + # Handle both real (PHYS, VIRT) and mocked (DIRECT) link types + if link_type == 'PHYS' or link_type == 'VIRT': + resource_key_direct = f"/link[{qkdl_id}]" + result.append((resource_key_direct, link)) + elif link_type == 'DIRECT': + # Mocked QKD format has a slightly different structure + result.append((f"/link/link[{qkdl_id}]", link)) + + # For mocked QKD format (QKD links returned as lists) + elif isinstance(link, list): + for l in link: + qkdl_id = l.get('uuid') + link_type = l.get('type', 'Direct') + + if link_type == 'DIRECT': + resource_key_direct = f"/link/link[{qkdl_id}]" + result.append((resource_key_direct, l)) + + except requests.RequestException as e: + LOGGER.error(f"Error fetching links from {url}: {e}") + result.append((resource_key, e)) + + return result + +def fetch_apps(url: str, resource_key: str, headers: Dict[str, str], auth: Optional[Any], timeout: Optional[int]) -> List[Tuple[str, Union[Dict[str, Any], Exception]]]: + """ + Fetches application data from the QKD node. + """ + result = [] + url += 'qkd_applications/' + + try: + r = requests.get(url, timeout=timeout, verify=False, auth=auth, headers=headers) + r.raise_for_status() + + apps = r.json().get('qkd_applications', {}).get('qkd_app', []) + for app in apps: + result.append((f"/app[{app['app_id']}]", app)) + except requests.RequestException as e: + LOGGER.error(f"Error fetching applications from {url}: {e}") + result.append((resource_key, e)) + + return result + + +def fetch_capabilities(url: str, resource_key: str, headers: Dict[str, str], auth: Optional[Any], timeout: Optional[int]) -> List[Tuple[str, Union[Dict[str, Any], Exception]]]: + """ + Fetches capabilities data from the QKD node. + """ + result = [] + url += 'qkdn_capabilities/' + + try: + r = requests.get(url, timeout=timeout, verify=False, auth=auth, headers=headers) + r.raise_for_status() + result.append((resource_key, r.json())) + except requests.RequestException as e: + LOGGER.error(f"Error fetching capabilities from {url}: {e}") + result.append((resource_key, e)) + + return result + + +def fetch_node(url: str, resource_key: str, headers: Dict[str, str], auth: Optional[Any], timeout: Optional[int]) -> List[Tuple[str, Union[Dict[str, Any], Exception]]]: + """ + Fetches node data from the QKD node. + """ + result = [] + + try: + r = requests.get(url, timeout=timeout, verify=False, auth=auth, headers=headers) + r.raise_for_status() + result.append((resource_key, r.json().get('qkd_node', {}))) + except requests.RequestException as e: + LOGGER.error(f"Error fetching node from {url}: {e}") + result.append((resource_key, e)) + + return result + + +def create_connectivity_link( + root_url: str, link_uuid: str, node_id_src: str, interface_id_src: str, node_id_dst: str, interface_id_dst: str, + virt_prev_hop: Optional[str] = None, virt_next_hops: Optional[List[str]] = None, virt_bandwidth: Optional[int] = None, + auth: Optional[Any] = None, timeout: Optional[int] = None, headers: Dict[str, str] = {} +) -> Union[bool, Exception]: + """ + Creates a connectivity link between QKD nodes using the provided parameters. + """ + url = f"{root_url}/restconf/data/etsi-qkd-sdn-node:qkd_node/qkd_links/" + + qkd_link = { + 'qkdl_id': link_uuid, + 'qkdl_type': 'etsi-qkd-node-types:' + ('VIRT' if virt_prev_hop or virt_next_hops else 'PHYS'), + 'qkdl_local': {'qkdn_id': node_id_src, 'qkdi_id': interface_id_src}, + 'qkdl_remote': {'qkdn_id': node_id_dst, 'qkdi_id': interface_id_dst} + } + + if virt_prev_hop or virt_next_hops: + qkd_link['virt_prev_hop'] = virt_prev_hop + qkd_link['virt_next_hop'] = virt_next_hops or [] + qkd_link['virt_bandwidth'] = virt_bandwidth + + data = {'qkd_links': {'qkd_link': [qkd_link]}} + + LOGGER.info(f"Creating connectivity link with payload: {json.dumps(data)}") + + try: + r = requests.post(url, json=data, timeout=timeout, verify=False, auth=auth, headers=headers) + r.raise_for_status() + if r.status_code in HTTP_OK_CODES: + LOGGER.info(f"Link {link_uuid} created successfully.") + return True + else: + LOGGER.error(f"Failed to create link {link_uuid}, status code: {r.status_code}") + return False + except requests.exceptions.RequestException as e: + LOGGER.error(f"Exception creating link {link_uuid} with payload {json.dumps(data)}: {e}") + return e diff --git a/src/device/service/drivers/qkd/__init__.py b/src/device/service/drivers/qkd/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e24e5523a216f79dcec21f2ac21b2262426acc04 --- /dev/null +++ b/src/device/service/drivers/qkd/__init__.py @@ -0,0 +1,27 @@ +from device.service.driver_api._Driver import RESOURCE_ENDPOINTS, RESOURCE_INTERFACES, RESOURCE_NETWORK_INSTANCES + +RESOURCE_LINKS = '__links__' +RESOURCE_APPS = '__apps__' +RESOURCE_CAPABILITES = '__capabilities__' +RESOURCE_NODE = '__node__' + + +ALL_RESOURCE_KEYS = [ + RESOURCE_ENDPOINTS, + RESOURCE_INTERFACES, + RESOURCE_NETWORK_INSTANCES, + RESOURCE_LINKS, + RESOURCE_APPS, + RESOURCE_CAPABILITES, + RESOURCE_NODE +] + +RESOURCE_KEY_MAPPINGS = { + RESOURCE_ENDPOINTS : 'component', + RESOURCE_INTERFACES : 'interface', + RESOURCE_NETWORK_INSTANCES: 'network_instance', + RESOURCE_LINKS : 'links', + RESOURCE_APPS : 'apps', + RESOURCE_CAPABILITES : 'capabilities', + RESOURCE_NODE : 'node' +} diff --git a/src/device/tests/qkd/integration/test_external_qkd_retrieve_information.py b/src/device/tests/qkd/integration/test_external_qkd_retrieve_information.py new file mode 100644 index 0000000000000000000000000000000000000000..0bb91191a96d0b3b6cfeef107b50a881c2261e60 --- /dev/null +++ b/src/device/tests/qkd/integration/test_external_qkd_retrieve_information.py @@ -0,0 +1,184 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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 pytest +import requests +import json +import os +from device.service.drivers.qkd.QKDDriver2 import QKDDriver +from device.service.drivers.qkd.Tools2 import ( + RESOURCE_INTERFACES, + RESOURCE_LINKS, + RESOURCE_CAPABILITES, + RESOURCE_NODE, + RESOURCE_APPS +) + +# Test ID: INT_LQ_Test_01 (QKD Node Authentication) +# Function to retrieve JWT token +def get_jwt_token(node_address, port, username, password): + """ Retrieve JWT token from a node's login endpoint if it's secured. """ + login_url = f"http://{node_address}:{port}/login" + payload = {'username': username, 'password': password} + try: + print(f"Attempting to retrieve JWT token from {login_url}...") + response = requests.post(login_url, headers={'Content-Type': 'application/x-www-form-urlencoded'}, data=payload) + response.raise_for_status() + print(f"Successfully retrieved JWT token from {login_url}") + return response.json().get('access_token') + except requests.exceptions.RequestException as e: + print(f"Failed to retrieve JWT token from {login_url}: {e}") + return None + + +# Environment variables for sensitive information +QKD1_ADDRESS = os.getenv("QKD1_ADDRESS") +QKD2_ADDRESS = os.getenv("QKD2_ADDRESS") +PORT = os.getenv("QKD_PORT") +USERNAME = os.getenv("QKD_USERNAME") +PASSWORD = os.getenv("QKD_PASSWORD") + +# Pytest fixture to initialize QKDDriver with token for Node 1 +@pytest.fixture +def driver_qkd1(): + token = get_jwt_token(QKD1_ADDRESS, PORT, USERNAME, PASSWORD) + headers = {'Authorization': f'Bearer {token}'} if token else {} + return QKDDriver(address=QKD1_ADDRESS, port=PORT, headers=headers) + +# Pytest fixture to initialize QKDDriver with token for Node 2 +@pytest.fixture +def driver_qkd2(): + token = get_jwt_token(QKD2_ADDRESS, PORT, USERNAME, PASSWORD) + headers = {'Authorization': f'Bearer {token}'} if token else {} + return QKDDriver(address=QKD2_ADDRESS, port=PORT, headers=headers) + +# Utility function to save data to a JSON file, filtering out non-serializable objects +def save_json_file(filename, data): + serializable_data = filter_serializable(data) + with open(filename, 'w') as f: + json.dump(serializable_data, f, indent=2) + print(f"Saved data to {filename}") + +# Function to filter out non-serializable objects like HTTPError +def filter_serializable(data): + if isinstance(data, list): + return [filter_serializable(item) for item in data if not isinstance(item, requests.exceptions.RequestException)] + elif isinstance(data, dict): + return {key: filter_serializable(value) for key, value in data.items() if not isinstance(value, requests.exceptions.RequestException)} + return data + +# Utility function to print the retrieved data for debugging, handling errors +def print_data(label, data): + try: + print(f"{label}: {json.dumps(data, indent=2)}") + except TypeError as e: + print(f"Error printing {label}: {e}, Data: {data}") + +# General function to retrieve and handle HTTP errors +def retrieve_data(driver_qkd, resource, resource_name): + try: + data = driver_qkd.GetConfig([resource]) + assert isinstance(data, list), f"Expected a list for {resource_name}" + assert len(data) > 0, f"No {resource_name} found in the system" + return data + except requests.exceptions.HTTPError as e: + print(f"HTTPError while fetching {resource_name}: {e}") + return None + except AssertionError as e: + print(f"AssertionError: {e}") + return None + +# Test ID: INT_LQ_Test_02 (QKD Node Capabilities) +def retrieve_capabilities(driver_qkd, node_name): + capabilities = retrieve_data(driver_qkd, RESOURCE_CAPABILITES, "capabilities") + if capabilities: + print_data(f"{node_name} Capabilities", capabilities) + return capabilities + +# Test ID: INT_LQ_Test_03 (QKD Interfaces) +def retrieve_interfaces(driver_qkd, node_name): + interfaces = retrieve_data(driver_qkd, RESOURCE_INTERFACES, "interfaces") + if interfaces: + print_data(f"{node_name} Interfaces", interfaces) + return interfaces + +# Test ID: INT_LQ_Test_04 (QKD Links) +def retrieve_links(driver_qkd, node_name): + links = retrieve_data(driver_qkd, RESOURCE_LINKS, "links") + if links: + print_data(f"{node_name} Links", links) + return links + +# Test ID: INT_LQ_Test_05 (QKD Link Metrics) +def retrieve_link_metrics(driver_qkd, node_name): + links = retrieve_links(driver_qkd, node_name) + if links: + for link in links: + if 'performance_metrics' in link[1]: + print_data(f"{node_name} Link Metrics", link[1]['performance_metrics']) + else: + print(f"No metrics found for link {link[0]}") + return links + +# Test ID: INT_LQ_Test_06 (QKD Applications) +def retrieve_applications(driver_qkd, node_name): + applications = retrieve_data(driver_qkd, RESOURCE_APPS, "applications") + if applications: + print_data(f"{node_name} Applications", applications) + return applications + +# Test ID: INT_LQ_Test_07 (System Health Check) +def retrieve_node_data(driver_qkd, node_name): + node_data = retrieve_data(driver_qkd, RESOURCE_NODE, "node data") + if node_data: + print_data(f"{node_name} Node Data", node_data) + return node_data + +# Main test to retrieve and save data from QKD1 and QKD2 to files +def test_retrieve_and_save_data(driver_qkd1, driver_qkd2): + # Retrieve data for QKD1 + qkd1_interfaces = retrieve_interfaces(driver_qkd1, "QKD1") + qkd1_links = retrieve_links(driver_qkd1, "QKD1") + qkd1_capabilities = retrieve_capabilities(driver_qkd1, "QKD1") + qkd1_node_data = retrieve_node_data(driver_qkd1, "QKD1") + qkd1_apps = retrieve_applications(driver_qkd1, "QKD1") + + qkd1_data = { + "interfaces": qkd1_interfaces, + "links": qkd1_links, + "capabilities": qkd1_capabilities, + "apps": qkd1_apps, + "node_data": qkd1_node_data + } + + # Save QKD1 data to file + save_json_file('qkd1_data.json', qkd1_data) + + # Retrieve data for QKD2 + qkd2_interfaces = retrieve_interfaces(driver_qkd2, "QKD2") + qkd2_links = retrieve_links(driver_qkd2, "QKD2") + qkd2_capabilities = retrieve_capabilities(driver_qkd2, "QKD2") + qkd2_node_data = retrieve_node_data(driver_qkd2, "QKD2") + qkd2_apps = retrieve_applications(driver_qkd2, "QKD2") + + qkd2_data = { + "interfaces": qkd2_interfaces, + "links": qkd2_links, + "capabilities": qkd2_capabilities, + "apps": qkd2_apps, + "node_data": qkd2_node_data + } + + # Save QKD2 data to file + save_json_file('qkd2_data.json', qkd2_data) diff --git a/src/device/tests/qkd/unit/PrepareScenario.py b/src/device/tests/qkd/unit/PrepareScenario.py new file mode 100644 index 0000000000000000000000000000000000000000..756b914d55df788472ed6e839e1fc29e356877e4 --- /dev/null +++ b/src/device/tests/qkd/unit/PrepareScenario.py @@ -0,0 +1,126 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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 pytest, os, time, logging +from common.Constants import ServiceNameEnum +from common.Settings import ( + ENVVAR_SUFIX_SERVICE_HOST, ENVVAR_SUFIX_SERVICE_PORT_HTTP, + get_env_var_name, get_service_port_http +) +from context.client.ContextClient import ContextClient +from nbi.service.rest_server.RestServer import RestServer +from nbi.service.rest_server.nbi_plugins.tfs_api import register_tfs_api +from device.client.DeviceClient import DeviceClient +from device.service.DeviceService import DeviceService +from device.service.driver_api.DriverFactory import DriverFactory +from device.service.driver_api.DriverInstanceCache import DriverInstanceCache +from device.service.drivers import DRIVERS +from device.tests.CommonObjects import CONTEXT, TOPOLOGY +from device.tests.MockService_Dependencies import MockService_Dependencies +from monitoring.client.MonitoringClient import MonitoringClient +from requests import codes as requests_codes +import requests + +# Constants +LOCAL_HOST = '127.0.0.1' +MOCKSERVICE_PORT = 8080 + +# Get dynamic port for NBI service +NBI_SERVICE_PORT = MOCKSERVICE_PORT + get_service_port_http(ServiceNameEnum.NBI) + +# Set environment variables for the NBI service host and port +os.environ[get_env_var_name(ServiceNameEnum.NBI, ENVVAR_SUFIX_SERVICE_HOST)] = str(LOCAL_HOST) +os.environ[get_env_var_name(ServiceNameEnum.NBI, ENVVAR_SUFIX_SERVICE_PORT_HTTP)] = str(NBI_SERVICE_PORT) + +# Expected status codes for requests +EXPECTED_STATUS_CODES = {requests_codes['OK'], requests_codes['CREATED'], requests_codes['ACCEPTED'], requests_codes['NO_CONTENT']} + +# Debugging output for the port number +print(f"MOCKSERVICE_PORT: {MOCKSERVICE_PORT}") +print(f"NBI_SERVICE_PORT: {NBI_SERVICE_PORT}") + +@pytest.fixture(scope='session') +def mock_service(): + _service = MockService_Dependencies(MOCKSERVICE_PORT) + _service.configure_env_vars() + _service.start() + yield _service + _service.stop() + +@pytest.fixture(scope='session') +def nbi_service_rest(mock_service): # Pass the `mock_service` as an argument if needed + _rest_server = RestServer() + register_tfs_api(_rest_server) # Register the TFS API with the REST server + _rest_server.start() + time.sleep(1) # Give time for the server to start + yield _rest_server + _rest_server.shutdown() + _rest_server.join() + +@pytest.fixture(scope='session') +def context_client(mock_service): + _client = ContextClient() + yield _client + _client.close() + +@pytest.fixture(scope='session') +def device_service(context_client, monitoring_client): + _driver_factory = DriverFactory(DRIVERS) + _driver_instance_cache = DriverInstanceCache(_driver_factory) + _service = DeviceService(_driver_instance_cache) + _service.start() + yield _service + _service.stop() + +@pytest.fixture(scope='session') +def device_client(device_service): + _client = DeviceClient() + yield _client + _client.close() + +# General request function +def do_rest_request(method, url, body=None, timeout=10, allow_redirects=True, logger=None): + # Construct the request URL with NBI service port + request_url = f"http://{LOCAL_HOST}:{NBI_SERVICE_PORT}{url}" + + # Log the request details for debugging + if logger: + msg = f"Request: {method.upper()} {request_url}" + if body: + msg += f" body={body}" + logger.warning(msg) + + # Send the request + reply = requests.request(method, request_url, timeout=timeout, json=body, allow_redirects=allow_redirects) + + # Log the response details for debugging + if logger: + logger.warning(f"Reply: {reply.text}") + + # Print status code and response for debugging instead of asserting + print(f"Status code: {reply.status_code}") + print(f"Response: {reply.text}") + + # Return the JSON response if present + if reply.content: + return reply.json() + return None + +# Function for GET requests +def do_rest_get_request(url, body=None, timeout=10, allow_redirects=True, logger=None): + return do_rest_request('get', url, body, timeout, allow_redirects, logger=logger) + +# Function for POST requests +def do_rest_post_request(url, body=None, timeout=10, allow_redirects=True, logger=None): + return do_rest_request('post', url, body, timeout, allow_redirects, logger=logger) diff --git a/src/device/tests/qkd/unit/retrieve_device_mock_information.py b/src/device/tests/qkd/unit/retrieve_device_mock_information.py new file mode 100644 index 0000000000000000000000000000000000000000..f6e18f51d4ce788cbc40dc6befe101df65ef765f --- /dev/null +++ b/src/device/tests/qkd/unit/retrieve_device_mock_information.py @@ -0,0 +1,104 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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 logging, urllib +from common.Constants import DEFAULT_CONTEXT_NAME, DEFAULT_TOPOLOGY_NAME +from common.proto.context_pb2 import ContextId +from common.tools.descriptor.Loader import DescriptorLoader +from context.client.ContextClient import ContextClient +from nbi.service.rest_server.RestServer import RestServer +from common.tools.object_factory.Context import json_context_id +from device.tests.qkd.unit.PrepareScenario import mock_service, nbi_service_rest, do_rest_get_request + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +JSON_ADMIN_CONTEXT_ID = json_context_id(DEFAULT_CONTEXT_NAME) +ADMIN_CONTEXT_ID = ContextId(**JSON_ADMIN_CONTEXT_ID) + + +# ----- Context -------------------------------------------------------------------------------------------------------- + +def test_rest_get_context_ids(nbi_service_rest: RestServer): # pylint: disable=redefined-outer-name, unused-argument + reply = do_rest_get_request('/tfs-api/context_ids') + print("Context IDs:", reply) + +def test_rest_get_contexts(nbi_service_rest: RestServer): # pylint: disable=redefined-outer-name, unused-argument + reply = do_rest_get_request('/tfs-api/contexts') + print("Contexts:", reply) + +def test_rest_get_context(nbi_service_rest: RestServer): # pylint: disable=redefined-outer-name, unused-argument + context_uuid = urllib.parse.quote(DEFAULT_CONTEXT_NAME) + reply = do_rest_get_request(f'/tfs-api/context/{context_uuid}') + print("Context data:", reply) + + +# ----- Topology ------------------------------------------------------------------------------------------------------- + +def test_rest_get_topology_ids(nbi_service_rest: RestServer): # pylint: disable=redefined-outer-name, unused-argument + context_uuid = urllib.parse.quote(DEFAULT_CONTEXT_NAME) + reply = do_rest_get_request(f'/tfs-api/context/{context_uuid}/topology_ids') + print("Topology IDs:", reply) + +def test_rest_get_topologies(nbi_service_rest: RestServer): # pylint: disable=redefined-outer-name, unused-argument + context_uuid = urllib.parse.quote(DEFAULT_CONTEXT_NAME) + reply = do_rest_get_request(f'/tfs-api/context/{context_uuid}/topologies') + print("Topologies:", reply) + +def test_rest_get_topology(nbi_service_rest: RestServer): # pylint: disable=redefined-outer-name, unused-argument + context_uuid = urllib.parse.quote(DEFAULT_CONTEXT_NAME) + topology_uuid = urllib.parse.quote(DEFAULT_TOPOLOGY_NAME) + reply = do_rest_get_request(f'/tfs-api/context/{context_uuid}/topology/{topology_uuid}') + print("Topology data:", reply) + + +# ----- Device --------------------------------------------------------------------------------------------------------- + +def test_rest_get_device_ids(nbi_service_rest: RestServer): # pylint: disable=redefined-outer-name, unused-argument + reply = do_rest_get_request('/tfs-api/device_ids') + print("Device IDs:", reply) + +def test_rest_get_devices(nbi_service_rest: RestServer): # pylint: disable=redefined-outer-name, unused-argument + reply = do_rest_get_request('/tfs-api/devices') + print("Devices:", reply) + + +# ----- Link ----------------------------------------------------------------------------------------------------------- + +def test_rest_get_link_ids(nbi_service_rest: RestServer): # pylint: disable=redefined-outer-name, unused-argument + reply = do_rest_get_request('/tfs-api/link_ids') + print("Link IDs:", reply) + +def test_rest_get_links(nbi_service_rest: RestServer): # pylint: disable=redefined-outer-name, unused-argument + reply = do_rest_get_request('/tfs-api/links') + print("Links:", reply) + + +# ----- Service -------------------------------------------------------------------------------------------------------- + +def test_rest_get_service_ids(nbi_service_rest: RestServer): # pylint: disable=redefined-outer-name, unused-argument + reply = do_rest_get_request('/tfs-api/link_ids') + print("Service IDs:", reply) + +def test_rest_get_topologies(nbi_service_rest: RestServer): # pylint: disable=redefined-outer-name, unused-argument + context_uuid = urllib.parse.quote(DEFAULT_CONTEXT_NAME) + reply = do_rest_get_request(f'/tfs-api/context/{context_uuid}/services') + print("Services:", reply) + +# ----- Apps ----------------------------------------------------------------------------------------------------------- + +def test_rest_get_apps(nbi_service_rest: RestServer): # pylint: disable=redefined-outer-name, unused-argument + context_uuid = urllib.parse.quote(DEFAULT_CONTEXT_NAME) # Context ID + reply = do_rest_get_request(f'/tfs-api/context/{context_uuid}/apps') + print("Apps:", reply) diff --git a/src/device/tests/qkd/unit/test_application_deployment.py b/src/device/tests/qkd/unit/test_application_deployment.py new file mode 100644 index 0000000000000000000000000000000000000000..92e16663b41556563aab884be2ee48518cd15ff7 --- /dev/null +++ b/src/device/tests/qkd/unit/test_application_deployment.py @@ -0,0 +1,47 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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 pytest +import json +from device.service.drivers.qkd.QKDDriver2 import QKDDriver + +MOCK_QKD_ADDRRESS = '127.0.0.1' +MOCK_PORT = 11111 + +@pytest.fixture +def qkd_driver(): + # Initialize the QKD driver with the appropriate settings + return QKDDriver(address=MOCK_QKD_ADDRRESS, port=MOCK_PORT, username='user', password='pass') + +def test_application_deployment(qkd_driver): + qkd_driver.Connect() + + # Application registration data + app_data = { + 'qkd_app': [ + { + 'app_id': '00000001-0001-0000-0000-000000000001', + 'client_app_id': [], + 'app_statistics': {'statistics': []}, + 'app_qos': {}, + 'backing_qkdl_id': [] + } + ] + } + + # Send a POST request to create the application + response = qkd_driver.SetConfig([('/qkd_applications/qkd_app', json.dumps(app_data))]) + + # Verify response + assert response[0] is True, "Expected application registration to succeed" diff --git a/src/device/tests/qkd/unit/test_mock_qkd_node.py b/src/device/tests/qkd/unit/test_mock_qkd_node.py new file mode 100644 index 0000000000000000000000000000000000000000..f679a8389476c35a6881b7dc6484baab8ef4e20b --- /dev/null +++ b/src/device/tests/qkd/unit/test_mock_qkd_node.py @@ -0,0 +1,31 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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 pytest +import requests +from requests.exceptions import ConnectionError + +def test_mock_qkd_node_responses(): + response = requests.get('http://127.0.0.1:11111/restconf/data/etsi-qkd-sdn-node:qkd_node') + assert response.status_code == 200 + data = response.json() + assert 'qkd_node' in data + +def test_mock_node_failure_scenarios(): + try: + response = requests.get('http://127.0.0.1:12345/restconf/data/etsi-qkd-sdn-node:qkd_node') + except ConnectionError as e: + assert isinstance(e, ConnectionError) + else: + pytest.fail("ConnectionError not raised as expected") diff --git a/src/device/tests/qkd/unit/test_qkd_compliance.py b/src/device/tests/qkd/unit/test_qkd_compliance.py new file mode 100644 index 0000000000000000000000000000000000000000..2f305888e952e7d9acc3e96ffc1e427a7cc85685 --- /dev/null +++ b/src/device/tests/qkd/unit/test_qkd_compliance.py @@ -0,0 +1,24 @@ + +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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 pytest +import requests +from tests.tools.mock_qkd_nodes.YangValidator import YangValidator + +def test_compliance_with_yang_models(): + validator = YangValidator('etsi-qkd-sdn-node', ['etsi-qkd-node-types']) + response = requests.get('http://127.0.0.1:11111/restconf/data/etsi-qkd-sdn-node:qkd_node') + data = response.json() + assert validator.parse_to_dict(data) is not None diff --git a/src/device/tests/qkd/unit/test_qkd_configuration.py b/src/device/tests/qkd/unit/test_qkd_configuration.py new file mode 100644 index 0000000000000000000000000000000000000000..15c4787c28a92ed07d8666b4c715954da1e690d6 --- /dev/null +++ b/src/device/tests/qkd/unit/test_qkd_configuration.py @@ -0,0 +1,230 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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 pytest +import json +from requests.exceptions import HTTPError +from device.service.drivers.qkd.QKDDriver2 import QKDDriver +import requests +from device.service.drivers.qkd.Tools2 import ( + RESOURCE_INTERFACES, + RESOURCE_LINKS, + RESOURCE_ENDPOINTS, + RESOURCE_APPS, + RESOURCE_CAPABILITES, + RESOURCE_NODE +) + +MOCK_QKD_ADDRRESS = '127.0.0.1' +MOCK_PORT = 11111 + +@pytest.fixture +def qkd_driver(): + # Initialize the QKD driver with the appropriate settings, ensure correct JWT headers are included + token = "YOUR_JWT_TOKEN" # Replace with your actual JWT token + if not token: + pytest.fail("JWT token is missing. Make sure to generate a valid JWT token.") + headers = {"Authorization": f"Bearer {token}"} + return QKDDriver(address=MOCK_QKD_ADDRRESS, port=MOCK_PORT, headers=headers) + +# Utility function to print the retrieved data for debugging +def print_data(label, data): + print(f"{label}: {json.dumps(data, indent=2)}") + +# Test ID: SBI_Test_03 (Initial Config Retrieval) +def test_initial_config_retrieval(qkd_driver): + qkd_driver.Connect() + + # Retrieve and validate the initial configuration + config = qkd_driver.GetInitialConfig() + + # Since GetInitialConfig returns a list, adjust the assertions accordingly + assert isinstance(config, list), "Expected a list for initial config" + assert len(config) > 0, "Initial config should not be empty" + + # Output for debugging + print_data("Initial Config", config) + +# Test ID: INT_LQ_Test_05 (QKD Devices Retrieval) +def test_retrieve_devices(qkd_driver): + qkd_driver.Connect() + + # Retrieve and validate device information + devices = qkd_driver.GetConfig([RESOURCE_NODE]) + assert isinstance(devices, list), "Expected a list of devices" + + if not devices: + pytest.skip("No devices found in the system. Skipping device test.") + + for device in devices: + assert isinstance(device, tuple), "Each device entry must be a tuple" + assert isinstance(device[1], dict), "Device data must be a dictionary" + if isinstance(device[1], Exception): + pytest.fail(f"Error retrieving devices: {device[1]}") + + # Output for debugging + print_data("Devices", devices) + +# Test ID: INT_LQ_Test_04 (QKD Links Retrieval) +def test_retrieve_links(qkd_driver): + qkd_driver.Connect() + + try: + # Fetch the links using the correct resource key + links = qkd_driver.GetConfig([RESOURCE_LINKS]) + assert isinstance(links, list), "Expected a list of tuples (resource key, data)." + + if len(links) == 0: + pytest.skip("No links found in the system, skipping link validation.") + + for link in links: + assert isinstance(link, tuple), "Each link entry must be a tuple" + resource_key, link_data = link # Unpack the tuple + + # Handle HTTPError or exception in the response + if isinstance(link_data, requests.exceptions.HTTPError): + pytest.fail(f"Failed to retrieve links due to HTTP error: {link_data}") + + if isinstance(link_data, dict): + # For real QKD data (links as dictionaries) + assert 'qkdl_id' in link_data, "Missing 'qkdl_id' in link data" + assert 'qkdl_local' in link_data, "Missing 'qkdl_local' in link data" + assert 'qkdl_remote' in link_data, "Missing 'qkdl_remote' in link data" + assert 'qkdl_type' in link_data, "Missing 'qkdl_type' in link data" + + # Check 'virt_prev_hop' only for virtual links (VIRT) + if link_data['qkdl_type'] == 'etsi-qkd-node-types:VIRT': + virt_prev_hop = link_data.get('virt_prev_hop') + assert virt_prev_hop is None or re.match(r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}', str(virt_prev_hop)), \ + f"Invalid 'virt_prev_hop': {virt_prev_hop}" + + # Print out the link details for debugging + print(f"Link ID: {link_data['qkdl_id']}") + print(f"Link Type: {link_data['qkdl_type']}") + print(f"Local QKD: {json.dumps(link_data['qkdl_local'], indent=2)}") + print(f"Remote QKD: {json.dumps(link_data['qkdl_remote'], indent=2)}") + + elif isinstance(link_data, list): + # For mocked QKD data (links as lists of dictionaries) + for mock_link in link_data: + assert 'uuid' in mock_link, "Missing 'uuid' in mocked link data" + assert 'src_qkdn_id' in mock_link, "Missing 'src_qkdn_id' in mocked link data" + assert 'dst_qkdn_id' in mock_link, "Missing 'dst_qkdn_id' in mocked link data" + + # Print out the mocked link details for debugging + print(f"Mock Link ID: {mock_link['uuid']}") + print(f"Source QKD ID: {mock_link['src_qkdn_id']}") + print(f"Destination QKD ID: {mock_link['dst_qkdn_id']}") + + else: + pytest.fail(f"Unexpected link data format: {type(link_data)}") + + except HTTPError as e: + pytest.fail(f"HTTP error occurred while retrieving links: {e}") + except Exception as e: + pytest.fail(f"An unexpected error occurred: {e}") + +# Test for QKD Services +def test_retrieve_services(qkd_driver): + qkd_driver.Connect() + services = qkd_driver.GetConfig([RESOURCE_ENDPOINTS]) + assert isinstance(services, list), "Expected a list of services" + + if not services: + pytest.skip("No services found in the system. Skipping service test.") + + for service in services: + assert isinstance(service, tuple), "Each service entry must be a tuple" + assert isinstance(service[1], dict), "Service data must be a dictionary" + if isinstance(service[1], Exception): + pytest.fail(f"Error retrieving services: {service[1]}") + + print("Services:", json.dumps(services, indent=2)) + +# Test ID: INT_LQ_Test_07 (QKD Applications Retrieval) +def test_retrieve_applications(qkd_driver): + qkd_driver.Connect() + + # Retrieve and validate applications information + applications = qkd_driver.GetConfig([RESOURCE_APPS]) # Adjust to fetch applications using the correct key + assert isinstance(applications, list), "Expected a list of applications" + + if not applications: + pytest.skip("No applications found in the system. Skipping applications test.") + + for app in applications: + assert isinstance(app, tuple), "Each application entry must be a tuple" + assert isinstance(app[1], dict), "Application data must be a dictionary" + if isinstance(app[1], Exception): + pytest.fail(f"Error retrieving applications: {app[1]}") + + # Output for debugging + print_data("Applications", applications) + +# Test ID: INT_LQ_Test_03 (QKD Interfaces Retrieval) +def test_retrieve_interfaces(qkd_driver): + qkd_driver.Connect() + + # Retrieve and validate interface information + interfaces = qkd_driver.GetConfig([RESOURCE_INTERFACES]) + + assert isinstance(interfaces, list), "Expected a list of interfaces" + assert len(interfaces) > 0, "No interfaces found in the system" + + for interface in interfaces: + assert isinstance(interface, tuple), "Each interface entry must be a tuple" + assert isinstance(interface[1], dict), "Interface data must be a dictionary" + if isinstance(interface[1], Exception): + pytest.fail(f"Error retrieving interfaces: {interface[1]}") + + # Output for debugging + print_data("Interfaces", interfaces) + +# Test ID: INT_LQ_Test_02 (QKD Capabilities Retrieval) +def test_retrieve_capabilities(qkd_driver): + qkd_driver.Connect() + + # Retrieve and validate capabilities information + capabilities = qkd_driver.GetConfig([RESOURCE_CAPABILITES]) + + assert isinstance(capabilities, list), "Expected a list of capabilities" + assert len(capabilities) > 0, "No capabilities found in the system" + + for capability in capabilities: + assert isinstance(capability, tuple), "Each capability entry must be a tuple" + assert isinstance(capability[1], dict), "Capability data must be a dictionary" + if isinstance(capability[1], Exception): + pytest.fail(f"Error retrieving capabilities: {capability[1]}") + + # Output for debugging + print_data("Capabilities", capabilities) + +# Test ID: INT_LQ_Test_03 (QKD Endpoints Retrieval) +def test_retrieve_endpoints(qkd_driver): + qkd_driver.Connect() + + # Retrieve and validate endpoint information + endpoints = qkd_driver.GetConfig([RESOURCE_ENDPOINTS]) + + assert isinstance(endpoints, list), "Expected a list of endpoints" + assert len(endpoints) > 0, "No endpoints found in the system" + + for endpoint in endpoints: + assert isinstance(endpoint, tuple), "Each endpoint entry must be a tuple" + assert isinstance(endpoint[1], dict), "Endpoint data must be a dictionary" + if isinstance(endpoint[1], Exception): + pytest.fail(f"Error retrieving endpoints: {endpoint[1]}") + + # Output for debugging + print_data("Endpoints", endpoints) diff --git a/src/device/tests/qkd/unit/test_qkd_error_hanling.py b/src/device/tests/qkd/unit/test_qkd_error_hanling.py new file mode 100644 index 0000000000000000000000000000000000000000..d93e3711136de496fd39365563032f827cfbe913 --- /dev/null +++ b/src/device/tests/qkd/unit/test_qkd_error_hanling.py @@ -0,0 +1,93 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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 pytest, requests +from requests.exceptions import ConnectionError, HTTPError, Timeout +from device.service.drivers.qkd.QKDDriver2 import QKDDriver + +MOCK_QKD_ADDRRESS = '127.0.0.1' +MOCK_PORT = 11111 + +@pytest.fixture +def qkd_driver(): + # Initialize the QKD driver for testing + return QKDDriver(address=MOCK_QKD_ADDRRESS, port=MOCK_PORT, username='user', password='pass') + +def test_invalid_operations_on_network_links(qkd_driver): + """ + Test Case ID: SBI_Test_09 - Perform invalid operations and validate error handling. + Objective: Perform invalid operations on network links and ensure proper error handling and logging. + """ + qkd_driver.Connect() + + # Step 1: Perform invalid operation with an incorrect resource key + invalid_payload = { + "invalid_resource_key": { + "invalid_field": "invalid_value" + } + } + + try: + # Attempt to perform an invalid operation (simulate wrong resource key) + response = requests.post(f'http://{qkd_driver.address}/invalid_resource', json=invalid_payload) + response.raise_for_status() + + except HTTPError as e: + # Step 2: Validate proper error handling and user-friendly messages + print(f"Handled HTTPError: {e}") + assert e.response.status_code in [400, 404], "Expected 400 Bad Request or 404 Not Found for invalid operation." + if e.response.status_code == 404: + assert "Not Found" in e.response.text, "Expected user-friendly 'Not Found' message." + elif e.response.status_code == 400: + assert "Invalid resource key" in e.response.text, "Expected user-friendly 'Bad Request' message." + + except Exception as e: + # Log unexpected exceptions + pytest.fail(f"Unexpected error occurred: {e}") + + finally: + qkd_driver.Disconnect() + +def test_network_failure_simulation(qkd_driver): + """ + Test Case ID: SBI_Test_10 - Simulate network failures and validate resilience and recovery. + Objective: Simulate network failures (e.g., QKD node downtime) and validate system's resilience. + """ + qkd_driver.Connect() + + try: + # Step 1: Simulate network failure (disconnect QKD node, or use unreachable address/port) + qkd_driver_with_failure = QKDDriver(address='127.0.0.1', port=12345, username='user', password='pass') # Valid but incorrect port + + # Try to connect and retrieve state, expecting a failure + response = qkd_driver_with_failure.GetState() + + # Step 2: Validate resilience and recovery mechanisms + # Check if the response is empty, indicating a failure to retrieve state + if not response: + print("Network failure simulated successfully and handled.") + else: + pytest.fail("Expected network failure but received a valid response.") + + except HTTPError as e: + # Log HTTP errors as part of error handling + print(f"Handled network failure error: {e}") + + except Exception as e: + # Step 3: Log unexpected exceptions + print(f"Network failure encountered: {e}") + + finally: + # Step 4: Ensure driver disconnects properly + qkd_driver.Disconnect() diff --git a/src/device/tests/qkd/unit/test_qkd_mock_connectivity.py b/src/device/tests/qkd/unit/test_qkd_mock_connectivity.py new file mode 100644 index 0000000000000000000000000000000000000000..150d00fd079b0a036f383653c833562279bb4d72 --- /dev/null +++ b/src/device/tests/qkd/unit/test_qkd_mock_connectivity.py @@ -0,0 +1,40 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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 pytest, requests +from unittest.mock import patch +from device.service.drivers.qkd.QKDDriver import QKDDriver + +MOCK_QKD_ADDRRESS = '127.0.0.1' +MOCK_PORT = 11111 + +@pytest.fixture +def qkd_driver(): + return QKDDriver(address=MOCK_QKD_ADDRRESS, port=MOCK_PORT, username='user', password='pass') + +# Deliverable Test ID: SBI_Test_01 +def test_qkd_driver_connection(qkd_driver): + assert qkd_driver.Connect() is True + +# Deliverable Test ID: SBI_Test_01 +def test_qkd_driver_invalid_connection(): + qkd_driver = QKDDriver(address='127.0.0.1', port=12345, username='user', password='pass') # Use invalid port directly + assert qkd_driver.Connect() is False + +# Deliverable Test ID: SBI_Test_10 +@patch('device.service.drivers.qkd.QKDDriver2.requests.get') +def test_qkd_driver_timeout_connection(mock_get, qkd_driver): + mock_get.side_effect = requests.exceptions.Timeout + qkd_driver.timeout = 0.001 # Simulate very short timeout + assert qkd_driver.Connect() is False diff --git a/src/device/tests/qkd/unit/test_qkd_performance.py b/src/device/tests/qkd/unit/test_qkd_performance.py new file mode 100644 index 0000000000000000000000000000000000000000..b15d1ab070b61333c55f7674e39ba74aae891de6 --- /dev/null +++ b/src/device/tests/qkd/unit/test_qkd_performance.py @@ -0,0 +1,32 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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. + +# tests/unit/test_qkd_performance.py + +import pytest, time +from device.service.drivers.qkd.QKDDriver2 import QKDDriver + +MOCK_QKD_ADDRRESS = '127.0.0.1' +MOCK_PORT = 11111 + +def test_performance_under_load(): + driver = QKDDriver(address=MOCK_QKD_ADDRRESS, port=MOCK_PORT, username='user', password='pass') + driver.Connect() + + start_time = time.time() + for _ in range(1000): + driver.GetConfig(['/qkd_interfaces/qkd_interface']) + end_time = time.time() + + assert (end_time - start_time) < 60 diff --git a/src/device/tests/qkd/unit/test_qkd_security.py b/src/device/tests/qkd/unit/test_qkd_security.py new file mode 100644 index 0000000000000000000000000000000000000000..f2942fd4685dce13f89832528d4298b267707886 --- /dev/null +++ b/src/device/tests/qkd/unit/test_qkd_security.py @@ -0,0 +1,88 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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 +import os +import pytest +import requests +from requests.exceptions import HTTPError +from device.service.drivers.qkd.QKDDriver2 import QKDDriver +from device.service.drivers.qkd.Tools2 import RESOURCE_CAPABILITES + +# Helper function to print data in a formatted JSON style for debugging +def print_data(label, data): + print(f"{label}: {json.dumps(data, indent=2)}") + +# Environment variables for sensitive information +QKD1_ADDRESS = os.getenv("QKD1_ADDRESS") +MOCK_QKD_ADDRRESS = '127.0.0.1' +MOCK_PORT = 11111 +PORT = os.getenv("QKD_PORT") +USERNAME = os.getenv("QKD_USERNAME") +PASSWORD = os.getenv("QKD_PASSWORD") + + +# Utility function to retrieve JWT token +def get_jwt_token(address, port, username, password): + url = f"http://{address}:{port}/login" + headers = {"Content-Type": "application/x-www-form-urlencoded"} + payload = f"username={username}&password={password}" + + try: + response = requests.post(url, data=payload, headers=headers) + response.raise_for_status() + return response.json().get('access_token') + except requests.exceptions.RequestException as e: + print(f"Failed to retrieve JWT token: {e}") + return None + +# Real QKD Driver (Requires JWT token) +@pytest.fixture +def real_qkd_driver(): + token = get_jwt_token(QKD1_ADDRESS, PORT, USERNAME, PASSWORD) # Replace with actual details + if not token: + pytest.fail("Failed to retrieve JWT token.") + headers = {'Authorization': f'Bearer {token}'} + return QKDDriver(address=QKD1_ADDRESS, port=PORT, headers=headers) + +# Mock QKD Driver (No actual connection, mock capabilities) +@pytest.fixture +def mock_qkd_driver(): + # Initialize the mock QKD driver with mock settings + token = "mock_token" + headers = {"Authorization": f"Bearer {token}"} + return QKDDriver(address=MOCK_QKD_ADDRRESS, port=MOCK_PORT, headers=headers) + +# General function to retrieve and test capabilities +def retrieve_capabilities(qkd_driver, driver_name): + try: + qkd_driver.Connect() + capabilities = qkd_driver.GetConfig([RESOURCE_CAPABILITES]) + assert isinstance(capabilities, list), "Expected a list of capabilities" + assert len(capabilities) > 0, f"No capabilities found for {driver_name}" + print_data(f"{driver_name} Capabilities", capabilities) + except HTTPError as e: + pytest.fail(f"HTTPError while fetching capabilities for {driver_name}: {e}") + except AssertionError as e: + pytest.fail(f"AssertionError: {e}") + except Exception as e: + pytest.fail(f"An unexpected error occurred: {e}") + +# Test for Real QKD Capabilities +def test_real_qkd_capabilities(real_qkd_driver): + retrieve_capabilities(real_qkd_driver, "Real QKD") + +# Test for Mock QKD Capabilities +def test_mock_qkd_capabilities(mock_qkd_driver): + retrieve_capabilities(mock_qkd_driver, "Mock QKD") diff --git a/src/device/tests/qkd/unit/test_qkd_subscription.py b/src/device/tests/qkd/unit/test_qkd_subscription.py new file mode 100644 index 0000000000000000000000000000000000000000..883fe2a1a6defe85a995c12e824a5e7d88159981 --- /dev/null +++ b/src/device/tests/qkd/unit/test_qkd_subscription.py @@ -0,0 +1,53 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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 pytest +from typing import List, Tuple +from device.service.drivers.qkd.QKDDriver2 import QKDDriver + +MOCK_QKD_ADDRRESS = '127.0.0.1' +MOCK_PORT = 11111 + + +@pytest.fixture +def qkd_driver(): + # Initialize the QKD driver + return QKDDriver(address=MOCK_QKD_ADDRRESS, port=MOCK_PORT, username='user', password='pass') + + +def test_state_subscription(qkd_driver): + """ + Test Case ID: SBI_Test_06 - Subscribe to state changes and validate the subscription process. + """ + qkd_driver.Connect() + + try: + # Step 1: Define the subscription + subscriptions = [ + ('00000001-0000-0000-0000-000000000000', 60, 10) # (node_id, frequency, timeout) + ] + + # Step 2: Subscribe to state changes using the driver method + subscription_results = qkd_driver.SubscribeState(subscriptions) + + # Step 3: Validate that the subscription was successful + assert all(result is True for result in subscription_results), "Subscription to state changes failed." + + print("State subscription successful:", subscription_results) + + except Exception as e: + pytest.fail(f"An unexpected error occurred during state subscription: {e}") + + finally: + qkd_driver.Disconnect() diff --git a/src/device/tests/qkd/unit/test_qkd_unsubscription.py b/src/device/tests/qkd/unit/test_qkd_unsubscription.py new file mode 100644 index 0000000000000000000000000000000000000000..883fe2a1a6defe85a995c12e824a5e7d88159981 --- /dev/null +++ b/src/device/tests/qkd/unit/test_qkd_unsubscription.py @@ -0,0 +1,53 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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 pytest +from typing import List, Tuple +from device.service.drivers.qkd.QKDDriver2 import QKDDriver + +MOCK_QKD_ADDRRESS = '127.0.0.1' +MOCK_PORT = 11111 + + +@pytest.fixture +def qkd_driver(): + # Initialize the QKD driver + return QKDDriver(address=MOCK_QKD_ADDRRESS, port=MOCK_PORT, username='user', password='pass') + + +def test_state_subscription(qkd_driver): + """ + Test Case ID: SBI_Test_06 - Subscribe to state changes and validate the subscription process. + """ + qkd_driver.Connect() + + try: + # Step 1: Define the subscription + subscriptions = [ + ('00000001-0000-0000-0000-000000000000', 60, 10) # (node_id, frequency, timeout) + ] + + # Step 2: Subscribe to state changes using the driver method + subscription_results = qkd_driver.SubscribeState(subscriptions) + + # Step 3: Validate that the subscription was successful + assert all(result is True for result in subscription_results), "Subscription to state changes failed." + + print("State subscription successful:", subscription_results) + + except Exception as e: + pytest.fail(f"An unexpected error occurred during state subscription: {e}") + + finally: + qkd_driver.Disconnect() diff --git a/src/device/tests/qkd/unit/test_set_new_configuration.py b/src/device/tests/qkd/unit/test_set_new_configuration.py new file mode 100644 index 0000000000000000000000000000000000000000..438e46d74f0ad1204f496aaf99e29e21f41c5805 --- /dev/null +++ b/src/device/tests/qkd/unit/test_set_new_configuration.py @@ -0,0 +1,126 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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 pytest, requests, uuid +from requests.exceptions import HTTPError +from device.service.drivers.qkd.QKDDriver2 import QKDDriver +from device.service.drivers.qkd.Tools2 import RESOURCE_APPS + +MOCK_QKD1_ADDRRESS = '127.0.0.1' +MOCK_PORT1 = 11111 +MOCK_QKD3_ADDRRESS = '127.0.0.1' +MOCK_PORT3 = 33333 + +@pytest.fixture +def qkd_driver1(): + # Initialize the QKD driver for QKD1 + return QKDDriver(address=MOCK_QKD1_ADDRRESS, port=MOCK_PORT1, username='user', password='pass') + +@pytest.fixture +def qkd_driver3(): + # Initialize the QKD driver for QKD3 + return QKDDriver(address=MOCK_QKD3_ADDRRESS, port=MOCK_PORT3, username='user', password='pass') + +def create_qkd_app(driver, qkdn_id, backing_qkdl_id, client_app_id=None): + """ + Helper function to create QKD applications on the given driver. + """ + server_app_id = str(uuid.uuid4()) # Generate a unique server_app_id + + app_payload = { + 'app': { + 'server_app_id': server_app_id, + 'client_app_id': client_app_id if client_app_id else [], # Add client_app_id if provided + 'app_status': 'ON', + 'local_qkdn_id': qkdn_id, + 'backing_qkdl_id': backing_qkdl_id + } + } + + try: + # Log the payload being sent + print(f"Sending payload to {driver.address}: {app_payload}") + + # Send POST request to create the application + response = requests.post(f'http://{driver.address}/app/create_qkd_app', json=app_payload) + + # Check if the request was successful (HTTP 2xx) + response.raise_for_status() + + # Validate the response + assert response.status_code == 200, f"Failed to create QKD app for {driver.address}: {response.text}" + + response_data = response.json() + assert response_data.get('status') == 'success', "Application creation failed." + + # Log the response from the server + print(f"Server {driver.address} response: {response_data}") + + return server_app_id # Return the created server_app_id + + except HTTPError as e: + pytest.fail(f"HTTP error occurred while creating the QKD application on {driver.address}: {e}") + except Exception as e: + pytest.fail(f"An unexpected error occurred: {e}") + +def test_create_qkd_application_bidirectional(qkd_driver1, qkd_driver3): + """ + Create QKD applications on both qkd1 and qkd3, and validate the complete creation in both directions. + """ + + qkd_driver1.Connect() + qkd_driver3.Connect() + + try: + # Step 1: Create QKD application for qkd1, referencing qkd3 as the backing QKDL + server_app_id_qkd1 = create_qkd_app( + qkd_driver1, + qkdn_id='00000001-0000-0000-0000-000000000000', + backing_qkdl_id=['00000003-0002-0000-0000-000000000000'] # qkd3's QKDL + ) + + # Step 2: Create QKD application for qkd3, referencing qkd1 as the backing QKDL, and setting client_app_id to qkd1's app + create_qkd_app( + qkd_driver3, + qkdn_id='00000003-0000-0000-0000-000000000000', + backing_qkdl_id=['00000003-0002-0000-0000-000000000000'], # qkd3's QKDL + client_app_id=[server_app_id_qkd1] # Set qkd1 as the client + ) + + # Step 3: Fetch applications from both qkd1 and qkd3 to validate that the applications exist + apps_qkd1 = qkd_driver1.GetConfig([RESOURCE_APPS]) + apps_qkd3 = qkd_driver3.GetConfig([RESOURCE_APPS]) + + print(f"QKD1 applications config: {apps_qkd1}") + print(f"QKD3 applications config: {apps_qkd3}") + + # Debugging: Print the full structure of the apps to understand what is returned + for app in apps_qkd1: + print(f"QKD1 App: {app}") + + # Debugging: Print the full structure of the apps to understand what is returned + for app in apps_qkd3: + print(f"QKD3 App: {app}") + + # Step 4: Validate the applications are created using app_id instead of server_app_id + assert any(app[1].get('app_id') == '00000001-0001-0000-0000-000000000000' for app in apps_qkd1), "QKD app not created on qkd1." + assert any(app[1].get('app_id') == '00000003-0001-0000-0000-000000000000' for app in apps_qkd3), "QKD app not created on qkd3." + + print("QKD applications created successfully in both directions.") + + except Exception as e: + pytest.fail(f"An unexpected error occurred: {e}") + finally: + qkd_driver1.Disconnect() + qkd_driver3.Disconnect() diff --git a/src/tests/tools/mock_qkd_nodes/YangValidator.py b/src/tests/tools/mock_qkd_nodes/YangValidator.py new file mode 100644 index 0000000000000000000000000000000000000000..2056d5df64a1d841fc74c1be73aa6408051ab738 --- /dev/null +++ b/src/tests/tools/mock_qkd_nodes/YangValidator.py @@ -0,0 +1,42 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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 libyang, os +from typing import Dict, Optional + +YANG_DIR = os.path.join(os.path.dirname(__file__), 'yang') + +class YangValidator: + def __init__(self, main_module : str, dependency_modules : [str]) -> None: + self._yang_context = libyang.Context(YANG_DIR) + + self._yang_module = self._yang_context.load_module(main_module) + mods = [self._yang_context.load_module(mod) for mod in dependency_modules] + [self._yang_module] + + for mod in mods: + mod.feature_enable_all() + + + + def parse_to_dict(self, message : Dict) -> Dict: + dnode : Optional[libyang.DNode] = self._yang_module.parse_data_dict( + message, validate_present=True, validate=True, strict=True + ) + if dnode is None: raise Exception('Unable to parse Message({:s})'.format(str(message))) + message = dnode.print_dict() + dnode.free() + return message + + def destroy(self) -> None: + self._yang_context.destroy() diff --git a/src/tests/tools/mock_qkd_nodes/mock.py b/src/tests/tools/mock_qkd_nodes/mock.py new file mode 100644 index 0000000000000000000000000000000000000000..7a606f6cac855fee9852f620c595908fbb3d36da --- /dev/null +++ b/src/tests/tools/mock_qkd_nodes/mock.py @@ -0,0 +1,368 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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 os +from flask import Flask, request +from YangValidator import YangValidator + +app = Flask(__name__) + + +yang_validator = YangValidator('etsi-qkd-sdn-node', ['etsi-qkd-node-types']) + + +nodes = { + '127.0.0.1:11111': {'node': { + 'qkdn_id': '00000001-0000-0000-0000-000000000000', + }, + 'qkdn_capabilities': { + }, + 'qkd_applications': { + 'qkd_app': [ + { + 'app_id': '00000001-0001-0000-0000-000000000000', + 'client_app_id': [], + 'app_statistics': { + 'statistics': [] + }, + 'app_qos': { + }, + 'backing_qkdl_id': [] + } + ] + }, + 'qkd_interfaces': { + 'qkd_interface': [ + { + 'qkdi_id': '100', + 'qkdi_att_point': { + }, + 'qkdi_capabilities': { + } + }, + { + 'qkdi_id': '101', + 'qkdi_att_point': { + 'device':'127.0.0.1', + 'port':'1001' + }, + 'qkdi_capabilities': { + } + } + ] + }, + 'qkd_links': { + 'qkd_link': [ + + ] + } + }, + + '127.0.0.1:22222': {'node': { + 'qkdn_id': '00000002-0000-0000-0000-000000000000', + }, + 'qkdn_capabilities': { + }, + 'qkd_applications': { + 'qkd_app': [ + { + 'app_id': '00000002-0001-0000-0000-000000000000', + 'client_app_id': [], + 'app_statistics': { + 'statistics': [] + }, + 'app_qos': { + }, + 'backing_qkdl_id': [] + } + ] + }, + 'qkd_interfaces': { + 'qkd_interface': [ + { + 'qkdi_id': '200', + 'qkdi_att_point': { + }, + 'qkdi_capabilities': { + } + }, + { + 'qkdi_id': '201', + 'qkdi_att_point': { + 'device':'127.0.0.1', + 'port':'2001' + }, + 'qkdi_capabilities': { + } + }, + { + 'qkdi_id': '202', + 'qkdi_att_point': { + 'device':'127.0.0.1', + 'port':'2002' + }, + 'qkdi_capabilities': { + } + } + ] + }, + 'qkd_links': { + 'qkd_link': [ + + ] + } + }, + + '127.0.0.1:33333': {'node': { + 'qkdn_id': '00000003-0000-0000-0000-000000000000', + }, + 'qkdn_capabilities': { + }, + 'qkd_applications': { + 'qkd_app': [ + { + 'app_id': '00000003-0001-0000-0000-000000000000', + 'client_app_id': [], + 'app_statistics': { + 'statistics': [] + }, + 'app_qos': { + }, + 'backing_qkdl_id': [] + } + ] + }, + 'qkd_interfaces': { + 'qkd_interface': [ + { + 'qkdi_id': '300', + 'qkdi_att_point': { + }, + 'qkdi_capabilities': { + } + }, + { + 'qkdi_id': '301', + 'qkdi_att_point': { + 'device':'127.0.0.1', + 'port':'3001' + }, + 'qkdi_capabilities': { + } + } + ] + }, + 'qkd_links': { + 'qkd_link': [ + + ] + } + } +} + + +def get_side_effect(url): + + steps = url.lstrip('https://').lstrip('http://').rstrip('/') + ip_port, _, _, header, *steps = steps.split('/') + + header_splitted = header.split(':') + + module = header_splitted[0] + assert(module == 'etsi-qkd-sdn-node') + + tree = {'qkd_node': nodes[ip_port]['node'].copy()} + + if len(header_splitted) == 1 or not header_splitted[1]: + value = nodes[ip_port].copy() + value.pop('node') + tree['qkd_node'].update(value) + + return tree, tree + + root = header_splitted[1] + assert(root == 'qkd_node') + + if not steps: + return tree, tree + + + endpoint, *steps = steps + + value = nodes[ip_port][endpoint] + + if not steps: + return_value = {endpoint:value} + tree['qkd_node'].update(return_value) + + return return_value, tree + + + + ''' + element, *steps = steps + + container, key = element.split('=') + + # value = value[container][key] + + if not steps: + return_value['qkd_node'][endpoint] = [value] + return return_value + + ''' + raise Exception('Url too long') + + + +def edit(from_dict, to_dict, create): + for key, value in from_dict.items(): + if isinstance(value, dict): + if key not in to_dict and create: + to_dict[key] = {} + edit(from_dict[key], to_dict[key], create) + elif isinstance(value, list): + to_dict[key].extend(value) + else: + to_dict[key] = value + + + +def edit_side_effect(url, json, create): + steps = url.lstrip('https://').lstrip('http://').rstrip('/') + ip_port, _, _, header, *steps = steps.split('/') + + module, root = header.split(':') + + assert(module == 'etsi-qkd-sdn-node') + assert(root == 'qkd_node') + + if not steps: + edit(json, nodes[ip_port]['node']) + return + + endpoint, *steps = steps + + if not steps: + edit(json[endpoint], nodes[ip_port][endpoint], create) + return + + + ''' + element, *steps = steps + + container, key = element.split('=') + + if not steps: + if key not in nodes[ip_port][endpoint][container] and create: + nodes[ip_port][endpoint][container][key] = {} + + edit(json, nodes[ip_port][endpoint][container][key], create) + return 0 + ''' + + raise Exception('Url too long') + + + + + + +@app.get('/', defaults={'path': ''}) +@app.get("/") +@app.get('/') +def get(path): + msg, msg_validate = get_side_effect(request.base_url) + print(msg_validate) + yang_validator.parse_to_dict(msg_validate) + return msg + + +@app.post('/', defaults={'path': ''}) +@app.post("/") +@app.post('/') +def post(path): + success = True + reason = '' + try: + edit_side_effect(request.base_url, request.json, True) + except Exception as e: + reason = str(e) + success = False + return {'success': success, 'reason': reason} + + + +@app.route('/', defaults={'path': ''}, methods=['PUT', 'PATCH']) +@app.route("/", methods=['PUT', 'PATCH']) +@app.route('/', methods=['PUT', 'PATCH']) +def patch(path): + success = True + reason = '' + try: + edit_side_effect(request.base_url, request.json, False) + except Exception as e: + reason = str(e) + success = False + return {'success': success, 'reason': reason} + + + + + +# import json +# from mock import requests +# import pyangbind.lib.pybindJSON as enc +# from pyangbind.lib.serialise import pybindJSONDecoder as dec +# from yang.sbi.qkd.templates.etsi_qkd_sdn_node import etsi_qkd_sdn_node + +# module = etsi_qkd_sdn_node() +# url = 'https://1.1.1.1/restconf/data/etsi-qkd-sdn-node:' + +# # Get node all info +# z = requests.get(url).json() +# var = dec.load_json(z, None, None, obj=module) +# print(enc.dumps(var)) + + +# Reset module variable because it is already filled +# module = etsi_qkd_sdn_node() + +# # Get node basic info +# node = module.qkd_node +# z = requests.get(url + 'qkd_node').json() +# var = dec.load_json(z, None, None, obj=node) +# print(enc.dumps(var)) + + +# # Get all apps +# apps = node.qkd_applications +# z = requests.get(url + 'qkd_node/qkd_applications').json() +# var = dec.load_json(z, None, None, obj=apps) +# print(enc.dumps(var)) + +# # Edit app 0 +# app = apps.qkd_app['00000000-0001-0000-0000-000000000000'] +# app.client_app_id = 'id_0' +# requests.put(url + 'qkd_node/qkd_applications/qkd_app=00000000-0001-0000-0000-000000000000', json=json.loads(enc.dumps(app))) + +# # Create app 1 +# app = apps.qkd_app.add('00000000-0001-0000-0000-000000000001') +# requests.post(url + 'qkd_node/qkd_applications/qkd_app=00000000-0001-0000-0000-000000000001', json=json.loads(enc.dumps(app))) + +# # Get all apps +# apps = node.qkd_applications +# z = requests.get(url + 'qkd_node/qkd_applications').json() +# var = dec.load_json(z, None, None, obj=apps) +# print(enc.dumps(var)) diff --git a/src/tests/tools/mock_qkd_nodes/start.sh b/src/tests/tools/mock_qkd_nodes/start.sh new file mode 100755 index 0000000000000000000000000000000000000000..b1bc56d5a7f90809e81c73a54803fb2dc11bacd9 --- /dev/null +++ b/src/tests/tools/mock_qkd_nodes/start.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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. + +cd "$(dirname "$0")" + +killbg() { + for p in "${pids[@]}" ; do + kill "$p"; + done +} + +trap killbg EXIT +pids=() +flask --app mock run --host 0.0.0.0 --port 11111 & +pids+=($!) +flask --app mock run --host 0.0.0.0 --port 22222 & +pids+=($!) +flask --app mock run --host 0.0.0.0 --port 33333 diff --git a/src/tests/tools/mock_qkd_nodes/yang/etsi-qkd-node-types.yang b/src/tests/tools/mock_qkd_nodes/yang/etsi-qkd-node-types.yang new file mode 100644 index 0000000000000000000000000000000000000000..04bbd8a875445a9bcf19266f21b792439bf9005c --- /dev/null +++ b/src/tests/tools/mock_qkd_nodes/yang/etsi-qkd-node-types.yang @@ -0,0 +1,326 @@ +/* Copyright 2022 ETSI +Licensed under the BSD-3 Clause (https://forge.etsi.org/legal-matters) */ + +module etsi-qkd-node-types { + + yang-version "1"; + + namespace "urn:etsi:qkd:yang:etsi-qkd-node-types"; + + prefix "etsi-qkdn-types"; + + organization "ETSI ISG QKD"; + + contact + "https://www.etsi.org/committee/qkd + vicente@fi.upm.es"; + + description + "This module contains the base types created for + the software-defined QKD node information models + specified in ETSI GS QKD 015 V2.1.1 + - QKD-TECHNOLOGY-TYPES + - QKDN-STATUS-TYPES + - QKD-LINK-TYPES + - QKD-ROLE-TYPES + - QKD-APP-TYPES + - Wavelength + "; + + revision "2022-01-30" { + description + "Refinement of the YANG model to make it compatible with the ETSI ISG QKD 018. Minor fixes."; + } + + revision "2020-09-30" { + description + "First definition based on initial requirement analysis."; + } + + identity QKD-TECHNOLOGY-TYPES { + description "Quantum Key Distribution System base technology types."; + } + + identity CV-QKD { + base QKD-TECHNOLOGY-TYPES; + description "Continuous Variable base technology."; + } + + identity DV-QKD { + base QKD-TECHNOLOGY-TYPES; + description "Discrete Variable base technology."; + } + + identity DV-QKD-COW { + base QKD-TECHNOLOGY-TYPES; + description "COW base technology."; + } + + identity DV-QKD-2Ws { + base QKD-TECHNOLOGY-TYPES; + description "2-Ways base technology."; + } + + typedef qkd-technology-types { + type identityref { + base QKD-TECHNOLOGY-TYPES; + } + description "This type represents the base technology types of the SD-QKD system."; + } + + identity QKDN-STATUS-TYPES { + description "Base identity used to identify the SD-QKD node status."; + } + + identity NEW { + base QKDN-STATUS-TYPES; + description "The QKD node is installed."; + } + + identity OPERATING { + base QKDN-STATUS-TYPES; + description "The QKD node is up."; + } + + identity DOWN { + base QKDN-STATUS-TYPES; + description "The QKD node is not working as expected."; + } + + identity FAILURE { + base QKDN-STATUS-TYPES; + description "The QKD node cannot be accessed by SDN controller with communication failure."; + } + + identity OUT { + base QKDN-STATUS-TYPES; + description "The QKD node is switched off and uninstalled."; + } + + typedef qkdn-status-types { + type identityref { + base QKDN-STATUS-TYPES; + } + description "This type represents the status of the SD-QKD node."; + } + + identity QKD-LINK-TYPES { + description "QKD key association link types."; + } + + identity VIRT { + base QKD-LINK-TYPES; + description "Virtual Link."; + } + + identity PHYS { + base QKD-LINK-TYPES; + description "Physical Link."; + } + + typedef qkd-link-types { + type identityref { + base QKD-LINK-TYPES; + } + description "This type represents the key association link type between two SD-QKD nodes."; + } + + identity QKD-ROLE-TYPES { + description "QKD Role Type."; + } + + identity TRANSMITTER { + base QKD-ROLE-TYPES; + description "QKD module working as transmitter."; + } + + identity RECEIVER { + base QKD-ROLE-TYPES; + description "QKD module working as receiver."; + } + + identity TRANSCEIVER { + base QKD-ROLE-TYPES; + description "QKD System that can work as a transmitter or receiver."; + } + + typedef qkd-role-types { + type identityref { + base QKD-ROLE-TYPES; + } + description "This type represents the working mode of a SD-QKD module."; + } + + identity QKD-APP-TYPES { + description "Application types."; + } + + identity CLIENT { + base QKD-APP-TYPES; + description "Application working as client."; + } + + identity INTERNAL { + base QKD-APP-TYPES; + description "Internal QKD node application."; + } + + typedef qkd-app-types { + type identityref { + base QKD-APP-TYPES; + } + description "This type represents the application class consuming key from SD-QKD nodes."; + } + + identity PHYS-PERF-TYPES { + description "Physical performance types."; + } + + identity QBER { + base PHYS-PERF-TYPES; + description "Quantum Bit Error Rate."; + } + + identity SNR { + base PHYS-PERF-TYPES; + description "Signal to Noise Ratio."; + } + + typedef phys-perf-types { + type identityref { + base PHYS-PERF-TYPES; + } + description "This type represents physical performance types."; + } + + identity LINK-STATUS-TYPES { + description "Status of the key association QKD link (physical and virtual)."; + } + + identity ACTIVE { + base LINK-STATUS-TYPES; + description "Link actively generating keys."; + } + + identity PASSIVE { + base LINK-STATUS-TYPES; + description "No key generation on key association QKD link but a pool of keys + are still available."; + } + + identity PENDING { + base LINK-STATUS-TYPES; + description "Waiting for activation and no keys are available."; + } + + identity OFF { + base LINK-STATUS-TYPES; + description "No key generation and no keys are available."; + } + + typedef link-status-types { + type identityref { + base LINK-STATUS-TYPES; + } + description "This type represents the status of a key association QKD link, both physical and virtual."; + } + + /// + + identity IFACE-STATUS-TYPES { + description "Interface Status."; + } + + identity ENABLED { + base IFACE-STATUS-TYPES; + description "The interfaces is up."; + } + + identity DISABLED { + base IFACE-STATUS-TYPES; + description "The interfaces is down."; + } + + identity FAILED { + base IFACE-STATUS-TYPES; + description "The interfaces has failed."; + } + + typedef iface-status-types { + type identityref { + base IFACE-STATUS-TYPES; + } + description "This type represents the status of a interface between a SD-QKD node and a SD-QKD module."; + } + + identity APP-STATUS-TYPES { + description "Application types."; + } + + identity ON { + base APP-STATUS-TYPES; + description "The application is on."; + } + + identity DISCONNECTED { + base APP-STATUS-TYPES; + description "The application is disconnected."; + } + + identity OUT-OF-TIME { + base APP-STATUS-TYPES; + description "The application is out of time."; + } + + identity ZOMBIE { + base APP-STATUS-TYPES; + description "The application is in a zombie state."; + } + + typedef app-status-types { + type identityref { + base APP-STATUS-TYPES; + } + description "This type represents the status of an application consuming key from SD-QKD nodes."; + } + + identity SEVERITY-TYPES { + description "Error/Failure severity levels."; + } + + identity MAJOR { + base SEVERITY-TYPES; + description "Major error/failure."; + } + + identity MINOR { + base SEVERITY-TYPES; + description "Minor error/failure."; + } + + typedef severity-types { + type identityref { + base SEVERITY-TYPES; + } + description "This type represents the Error/Failure severity levels."; + } + + typedef wavelength { + type string { + pattern "([1-9][0-9]{0,3})"; + } + description + "A WDM channel number (starting at 1). For example: 20"; + } + + //Pattern from "A Yang Data Model for WSON Optical Networks". + typedef wavelength-range-type { + type string { + pattern "([1-9][0-9]{0,3}(-[1-9][0-9]{0,3})?" + + "(,[1-9][0-9]{0,3}(-[1-9][0-9]{0,3})?)*)"; + } + description + "A list of WDM channel numbers (starting at 1) + in ascending order. For example: 1,12-20,40,50-80"; + } +} diff --git a/src/tests/tools/mock_qkd_nodes/yang/etsi-qkd-sdn-node.yang b/src/tests/tools/mock_qkd_nodes/yang/etsi-qkd-sdn-node.yang new file mode 100644 index 0000000000000000000000000000000000000000..d07004cdc5b558adc5a9c0b6acb32adac0d7cc11 --- /dev/null +++ b/src/tests/tools/mock_qkd_nodes/yang/etsi-qkd-sdn-node.yang @@ -0,0 +1,941 @@ +/* Copyright 2022 ETSI +Licensed under the BSD-3 Clause (https://forge.etsi.org/legal-matters) */ + +module etsi-qkd-sdn-node { + + yang-version "1"; + + namespace "urn:etsi:qkd:yang:etsi-qkd-node"; + + prefix "etsi-qkdn"; + + import ietf-yang-types { prefix "yang"; } + import ietf-inet-types { prefix "inet"; } + import etsi-qkd-node-types { prefix "etsi-qkdn-types"; } + + // meta + organization "ETSI ISG QKD"; + + contact + "https://www.etsi.org/committee/qkd + vicente@fi.upm.es"; + + description + "This module contains the groupings and containers composing + the software-defined QKD node information models + specified in ETSI GS QKD 015 V2.1.1"; + + revision "2022-01-30" { + description + "Refinement of the YANG model to make it compatible with the ETSI ISG QKD 018. Minor fixes."; + reference + "ETSI GS QKD 015 V2.1.1 (2022-01)"; + } + + revision "2020-09-30" { + description + "First definition based on initial requirement analysis."; + reference + "ETSI GS QKD 015 V1.1.1 (2021-03)"; + } + + grouping qkdn_id { + description "Grouping of qkdn_id leaf."; + + leaf qkdn_id { + type yang:uuid; + mandatory true; + description + "This value reflects the unique ID of the SD-QKD node."; + } + } + + grouping qkdn_version { + description "Grouping of qkdn_version leaf."; + + leaf qkdn_version { + type string; + description "Hardware or software version of the SD-QKD node."; + } + } + + grouping qkdn_location_id { + description "Grouping of qkdn_location_id leaf."; + + leaf qkdn_location_id { + type string; + default ""; + description + "This value enables the location of the secure + area that contains the SD-QKD node to be specified."; + } + } + + grouping qkdn_status { + description "Grouping of qkdn_status leaf."; + + leaf qkdn_status { + type etsi-qkdn-types:qkdn-status-types; + config false; + description "Status of the SD-QKD node."; + } + } + + grouping qkdn_capabilities { + description "Grouping of the capabilities of the SD-QKD node."; + + container qkdn_capabilities { + description "Capabilities of the SD-QKD node."; + + leaf link_stats_support { + type boolean; + default true; + description + "If true, this node exposes link-related statistics (secure key + generation rate-SKR, link consumption, status, QBER)."; + } + + leaf application_stats_support { + type boolean; + default true; + description "If true, this node exposes application related + statistics (application consumption, alerts)."; + } + + leaf key_relay_mode_enable { + type boolean; + default true; + description "If true, this node supports key relay (multi-hop) mode services."; + } + } + } + + grouping app_id { + description "Grouping of app_id leaf."; + + leaf app_id { + type yang:uuid; + description + "Unique ID that identifies a QKD application consisting of a set of entities + that are allowed to receive keys shared with each other from the SD-QKD nodes + they connect to. This value is similar to a key ID or key handle."; + } + } + + grouping app_basic { + description "Grouping of app's basic parameters."; + + uses app_id; + + leaf app_status { + type etsi-qkdn-types:app-status-types; + config false; + description "Status of the application."; + } + } + + grouping app_priority { + description "Grouping of app_priority leaf."; + + leaf app_priority { + type uint32; + default 0; + description "Priority of the association/application + might be defined by the user but usually + handled by a network administrator."; + } + } + + grouping app_details { + description "Grouping of app's details parameters."; + + leaf app_type { + type etsi-qkdn-types:qkd-app-types; + description "Type of the registered application. These + values, defined within the types module, can be client + (if an external applications requesting keys) + or internal (application is defined to maintain + the QKD - e.g. multi-hop, authentication or + other encryption operations)."; + } + + leaf server_app_id { + type inet:uri; + description "ID that identifies the entity that initiated the + creation of the QKD application to receive keys shared with one + or more specified target entity identified by client_app_id. + It is a client in the interface to the SD-QKD node and the name + server_app_id reflects that it requested the QKD application to + be initiated."; + } + + leaf-list client_app_id { + type inet:uri; + description "List of IDs that identifies the one or more + entities that are allowed to receive keys from SD-QKD + node(s) under the QKD application in addition to the + initiating entity identified by server_app_id."; + } + + uses app_priority; + } + + grouping local_qkdn_id { + description "Grouping of local_qkdn_id leaf."; + + leaf local_qkdn_id { + type yang:uuid; + description "Unique ID of the local SD-QKD node which + is providing QKD keys to the local application."; + } + } + + grouping app_time { + description "Grouping of app's time parameters."; + + leaf creation_time { + type yang:date-and-time; + config false; + description "Date and time of the service creation."; + } + + leaf expiration_time { + type yang:date-and-time; + description "Date and time of the service expiration."; + } + } + + grouping app_statistics { + description "Grouping of app's statistic parameters."; + + container app_statistics { + description "Statistical information relating to a specific statistic period of time."; + + list statistics { + key "end_time"; + config false; + description "List of statistics."; + + leaf end_time { + type yang:date-and-time; + config false; + description "End time for the statistic period."; + } + + leaf start_time { + type yang:date-and-time; + config false; + description "Start time for the statistic period."; + } + + leaf consumed_bits { + type uint32; + config false; + description "Consumed secret key amount (in bits) for a statistics collection period of time."; + } + } + } + } + + grouping app_qos { + description "Grouping of app's basic qos parameters."; + + container app_qos { + description "Requested Quality of Service."; + + leaf max_bandwidth { + type uint32; + description "Maximum bandwidth (in bits per second) allowed for + this specific application. Exceeding this value will raise an + error from the local key store to the appl. This value might + be internally configured (or by an admin) with a default value."; + } + + leaf min_bandwidth { + type uint32; + description "This value is an optional QoS parameter which + enables to require a minimum key rate (in bits per second) + for the application."; + } + + leaf jitter { + type uint32; + description "This value allows to specify the maximum jitter + (in msec) to be provided by the key delivery API for + applications requiring fast rekeying. This value can be + coordinated with the other QoS to provide a wide enough + QoS definition."; + } + + leaf ttl { + type uint32; + description "This value is used to specify the maximum time + (in seconds) that a key could be kept in the key store for + a given application without being used."; + } + } + } + + grouping augmented_app_qos { + description "Grouping of app's detailed qos parameters."; + + uses app_qos { + augment app_qos { + description "Augmentation of app's basic parameters with app's detailed qos parameters."; + + leaf clients_shared_path_enable { + type boolean; + default false; + description "If true, multiple clients for this + application might share keys to reduce service + impact (consumption)."; + } + + leaf clients_shared_keys_required { + type boolean; + default false; + description "If true, multiple clients for this application + might share keys to reduce service impact (consumption)."; + } + } + } + } + + grouping qkd_applications { + description "Grouping of the list of applications container."; + + container qkd_applications { + description "List of applications container."; + + list qkd_app { + key "app_id"; + description "List of applications that are currently registered + in the SD-QKD node. Any entity consuming QKD-derived keys (either + for internal or external purposes) is considered an application."; + + uses app_basic; + + uses app_details; + + uses app_time; + + uses app_statistics; + + uses augmented_app_qos; + + leaf-list backing_qkdl_id { + type yang:uuid; + description "Unique ID of the key association link which is + providing QKD keys to these applications."; + } + + uses local_qkdn_id; + + leaf remote_qkdn_id { + type yang:uuid; + description "Unique ID of the remote SD-QKD node which + is providing QKD keys to the remote application. + While unknown, the local SD-QKD will not be able to + provide keys to the local application."; + } + } + } + } + + grouping qkdi_status { + description "Grouping of qkdi_status leaf."; + + leaf qkdi_status { + type etsi-qkdn-types:iface-status-types; + config false; + description "Status of a QKD interface of the SD-QKD node."; + } + } + + grouping qkdi_model { + description "Grouping of qkdi_model leaf."; + + leaf qkdi_model { + type string; + description "Device model (vendor/device)."; + } + } + + grouping qkdi_type { + description "Grouping of qkdi_type leaf."; + + leaf qkdi_type { + type etsi-qkdn-types:qkd-technology-types; + description "Interface type (QKD technology)."; + } + } + + grouping qkdi_att_point { + description "Grouping of the interface attachment points to an optical switch."; + + container qkdi_att_point { + description "Interface attachment point to an optical switch."; + + leaf device { + type string; + description "Unique ID of the optical switch (or + passive component) to which the interface is connected."; + } + + leaf port { + type uint32; + description "Port ID of the device to which the interface + is connected."; + } + } + } + + grouping qkdi_id { + description "Grouping of qkdi_id leaf."; + + leaf qkdi_id { + type uint32; + description "Interface id. It is described as a locally unique number, + which is globally unique when combined with the SD-QKD node ID."; + } + } + + grouping qkd_interface_item { + description "Grouping of the interface parameters."; + + uses qkdi_id; + + uses qkdi_model; + + uses qkdi_type; + + uses qkdi_att_point; + + container qkdi_capabilities { + description "Capabilities of the QKD system (interface)."; + + leaf role_support { + type etsi-qkdn-types:qkd-role-types; + description "QKD node support for key relay mode services."; + } + + leaf wavelength_range { + type etsi-qkdn-types:wavelength-range-type; + description "Range of supported wavelengths (nm) (multiple + if it contains a tunable laser)."; + } + + leaf max_absorption { + type decimal64 { + fraction-digits 3; + } + description "Maximum absorption supported (in dB)."; + } + } + } + + grouping qkd_interfaces { + description "Grouping of the list of interfaces."; + + container qkd_interfaces { + description "List of interfaces container."; + + list qkd_interface { + key "qkdi_id"; + description "List of physical QKD modules in a secure location, + abstracted as interfaces of the SD-QKD node."; + + uses qkd_interface_item; + + uses qkdi_status; + + } + } + } + + grouping qkdl_id { + description "Grouping of qkdl_id leaf."; + + leaf qkdl_id { + type yang:uuid; + description "Unique ID of the QKD link (key association)."; + } + } + + grouping qkdl_status { + description "Grouping of qkdl_status leaf."; + + leaf qkdl_status { + type etsi-qkdn-types:link-status-types; + description "Status of the QKD key association link."; + } + } + + grouping common_performance { + description "Grouping of common performance parameters."; + + leaf expected_consumption { + type uint32; + config false; + description "Sum of all the application's bandwidth (in bits per + second) on this particular key association link."; + } + + leaf skr { + type uint32; + config false; + description "Secret key rate generation (in bits per second) + of the key association link."; + } + + leaf eskr { + type uint32; + config false; + description "Effective secret key rate (in bits per second) generation + of the key association link available after internal consumption."; + } + } + + grouping physical_link_perf { + description "Grouping of the list of physical performance parameters."; + + list phys_perf { + key "perf_type"; + config false; + description "List of physical performance parameters."; + + leaf perf_type { + type etsi-qkdn-types:phys-perf-types; + config false; + description "Type of the physical performance value to be + exposed to the controller."; + } + + leaf value { + type decimal64 { + fraction-digits 3; + } + config false; + description "Numerical value for the performance parameter + type specified above."; + } + } + } + + grouping virtual_link_spec { + description "Grouping of the virtual link's parameters."; + + leaf virt_prev_hop { + type yang:uuid; + description "Previous hop in a multi-hop/virtual key + association link config."; + } + + leaf-list virt_next_hop { + type yang:uuid; + description "Next hop(s) in a multihop/virtual key + association link config. Defined as a list for multicast + over shared sub-paths."; + } + + leaf virt_bandwidth { + type uint32; + description "Required bandwidth (in bits per second) for that key association link. + Used to reserve bandwidth from the physical QKD links to support the virtual key + association link as an internal application."; + } + } + + grouping physical_link_spec { + description "Grouping of the physical link's parameters."; + + leaf phys_channel_att { + type decimal64 { + fraction-digits 3; + } + description "Expected attenuation on the quantum channel (in dB) + between the Source/qkd_node and Destination/qkd_node."; + + } + + leaf phys_wavelength { + type etsi-qkdn-types:wavelength; + description "Wavelength (in nm) to be used for the quantum channel. + If the interface is not tunable, this configuration could be bypassed"; + } + + leaf phys_qkd_role { + type etsi-qkdn-types:qkd-role-types; + description "Transmitter/receiver mode for the QKD module. + If there is no multi-role support, this could be ignored."; + } + } + + grouping qkd_links { + description "Grouping of the list of links."; + + container qkd_links { + description "List of links container"; + + list qkd_link { + key "qkdl_id"; + description "List of (key association) links to other SD-QKD nodes in the network. + The links can be physical (direct quantum channel) or virtual multi-hop + connection doing key-relay through several nodes."; + + uses qkdl_id; + + uses qkdl_status; + + leaf qkdl_enable { + type boolean; + default true; + description "This value allows to enable of disable the key generation + process for a given link."; + + } + + container qkdl_local { + description "Source (local) node of the SD-QKD link."; + + leaf qkdn_id { + type yang:uuid; + description "Unique ID of the local SD-QKD node."; + } + + leaf qkdi_id { + type uint32; + description "Interface used to create the key association link."; + } + } + + container qkdl_remote { + description "Destination (remote) unique SD-QKD node."; + + leaf qkdn_id { + type yang:uuid; + description "Unique ID of the remote SD-QKD node. This value is + provided by the SDN controller when the key association link + request arrives."; + } + + leaf qkdi_id { + type uint32; + description "Interface used to create the link."; + } + } + + leaf qkdl_type { + type etsi-qkdn-types:qkd-link-types; + description "Key Association Link type: Virtual (multi-hop) or Direct."; + } + + leaf-list qkdl_applications { + type yang:uuid; + description "Applications which are consuming keys from + this key association link."; + } + + uses virtual_link_spec { + when "qkdl_type = 'etsi-qkd-node-types:VIRT'" { + description "Virtual key association link specific configuration."; + } + } + + uses physical_link_spec { + when "qkdl_type = 'etsi-qkd-node-types:PHYS'" { + description "Physical key association link specific configuration."; + } + } + + container qkdl_performance { + description "Container of link's performace parameters."; + + uses common_performance; + + uses physical_link_perf { + when "../qkdl_type = 'PHYS'" { + description "Performance of the specific physical link."; + } + } + } + } + } + } + + container qkd_node { + description + "Top module describing a software-defined QKD node (SD-QKD node)."; + + uses qkdn_id; + + uses qkdn_status; + + uses qkdn_version; + + uses qkdn_location_id; + + uses qkdn_capabilities; + + uses qkd_applications; + + uses qkd_interfaces; + + uses qkd_links; + } + + grouping message { + description "Grouping of message leaf."; + + leaf message { + type string; + description "Placeholder for the message."; + } + } + + grouping severity { + description "Grouping of severity leaf."; + + leaf severity { + type etsi-qkdn-types:severity-types; + description "Placeholder for the severity."; + } + } + + grouping reason { + description "Grouping of reason leaf."; + + leaf reason { + type string; + description "Auxiliary parameter to include additional + information about the reason for link failure."; + } + } + + notification sdqkdn_application_new { + description "Defined for the controller to detect new applications + requesting keys from a QKD node. This maps with the workflow shown + in clause 5.2 'QKD Application Registration'. Parameters such as + client and server app IDs, local QKD node identifier, priority and + QoS are sent in the notification."; + + container qkd_application { + description "'sdqkdn_application_new' notification's qkd_application parameters."; + + uses app_details; + + uses local_qkdn_id; + + uses augmented_app_qos; + + } + } + + notification sdqkdn_application_qos_update { + description "Notification that includes information about priority or + QoS changes on an existing and already registered application."; + + container qkd_application { + description "'sdqkdn_application_qos_update' notification's qkd_application parameters."; + + uses app_id; + + uses augmented_app_qos; + + uses app_priority; + + } + } + + notification sdqkdn_application_disconnected { + description "Includes the application identifier to inform that the + application is no longer registered and active in the QKD node."; + + container qkd_application { + description "'sdqkdn_application_disconnected' notification's qkd_application parameters."; + + uses app_id; + + } + } + + notification sdqkdn_interface_new { + description "Includes all the information about the new QKD system + installed in the secure location of a given QKD node."; + + container qkd_interface { + description "'sdqkdn_interface_new' notification's qkd_interface parameters."; + + uses qkd_interface_item; + + } + } + + notification sdqkdn_interface_down { + description "Identifies an interface within a QKD node which is not + working as expected, allowing additional information to be included + in a 'reason' string field."; + + container qkd_interface { + description "'sdqkdn_interface_down' notification's qkd_interface parameters."; + + uses qkdi_id; + + uses reason; + + } + } + + notification sdqkdn_interface_out { + description "Contains the ID of an interface which is switch off and + uninstall from a QKD node. This information can be gathered from this + notification or from regular polling from the controller's side."; + + container qkd_interface { + description "'sdqkdn_interface_out' notification's qkd_interface parameters."; + + uses qkdi_id; + + } + } + + notification sdqkdn_link_down { + description "As in the interface down event, this notification contains + the identifier of a given link which has gone down unexpectedly. + In addition, further information can be sent in the 'reason' field."; + + container qkd_link { + description "'sdqkdn_link_down' notification's qkd_link parameters."; + + uses qkdl_id; + + uses reason; + + } + } + + notification sdqkdn_link_perf_update { + description "This notification allows to inform of any mayor + modification in the performance of an active link. The identifier + of the link is sent together with the performance parameters of the link."; + + container qkd_link { + description "'sdqkdn_link_perf_update' notification's qkd_link parameters."; + + uses qkdl_id; + + container performance { + description "'sdqkdn_link_perf_update' notification's performance parameters."; + + uses common_performance; + + uses physical_link_perf; + + } + } + } + + notification sdqkdn_link_overloaded { + description "This notification is sent when the link cannot cope with the + demand. The link identifier is sent with the expected consumption and + general performance parameters."; + + container qkd_link { + description "'sdqkdn_link_overloaded' notification's qkd_link parameters."; + + uses qkdl_id; + + container performance { + description "'sdqkdn_link_overloaded' notification's performance parameters."; + + uses common_performance; + + } + } + } + + notification alarm { + description "'alarm' notification."; + + container link { + description "'alarm' notification's link parameters."; + + uses qkdl_id; + + uses qkdl_status; + + uses message; + + uses severity; + + } + + container interface { + description "'alarm' notification's interface parameters."; + + uses qkdi_id; + + uses qkdi_status; + + uses message; + + uses severity; + + } + + container application { + description "'alarm' notification's application parameters."; + + uses app_basic; + + uses message; + + uses severity; + + } + + } + + notification event { + description "'event' notification."; + + container link { + description "'alarm' notification's link parameters."; + + uses qkdl_id; + + uses qkdl_status; + + uses message; + + uses severity; + + } + + container interface { + description "'alarm' notification's interface parameters."; + + uses qkdi_id; + + uses qkdi_status; + + uses message; + + uses severity; + + } + + container application { + description "'alarm' notification's application parameters."; + + uses app_basic; + + uses message; + + uses severity; + + } + + } + +} diff --git a/src/tests/tools/mock_qkd_nodes/yang/ietf-inet-types.yang b/src/tests/tools/mock_qkd_nodes/yang/ietf-inet-types.yang new file mode 100644 index 0000000000000000000000000000000000000000..eacefb6363de1beb543567a0fa705571b7dc57a2 --- /dev/null +++ b/src/tests/tools/mock_qkd_nodes/yang/ietf-inet-types.yang @@ -0,0 +1,458 @@ +module ietf-inet-types { + + namespace "urn:ietf:params:xml:ns:yang:ietf-inet-types"; + prefix "inet"; + + organization + "IETF NETMOD (NETCONF Data Modeling Language) Working Group"; + + contact + "WG Web: + WG List: + + WG Chair: David Kessens + + + WG Chair: Juergen Schoenwaelder + + + Editor: Juergen Schoenwaelder + "; + + description + "This module contains a collection of generally useful derived + YANG data types for Internet addresses and related things. + + Copyright (c) 2013 IETF Trust and the persons identified as + authors of the code. All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, is permitted pursuant to, and subject + to the license terms contained in, the Simplified BSD License + set forth in Section 4.c of the IETF Trust's Legal Provisions + Relating to IETF Documents + (http://trustee.ietf.org/license-info). + + This version of this YANG module is part of RFC 6991; see + the RFC itself for full legal notices."; + + revision 2013-07-15 { + description + "This revision adds the following new data types: + - ip-address-no-zone + - ipv4-address-no-zone + - ipv6-address-no-zone"; + reference + "RFC 6991: Common YANG Data Types"; + } + + revision 2010-09-24 { + description + "Initial revision."; + reference + "RFC 6021: Common YANG Data Types"; + } + + /*** collection of types related to protocol fields ***/ + + typedef ip-version { + type enumeration { + enum unknown { + value "0"; + description + "An unknown or unspecified version of the Internet + protocol."; + } + enum ipv4 { + value "1"; + description + "The IPv4 protocol as defined in RFC 791."; + } + enum ipv6 { + value "2"; + description + "The IPv6 protocol as defined in RFC 2460."; + } + } + description + "This value represents the version of the IP protocol. + + In the value set and its semantics, this type is equivalent + to the InetVersion textual convention of the SMIv2."; + reference + "RFC 791: Internet Protocol + RFC 2460: Internet Protocol, Version 6 (IPv6) Specification + RFC 4001: Textual Conventions for Internet Network Addresses"; + } + + typedef dscp { + type uint8 { + range "0..63"; + } + description + "The dscp type represents a Differentiated Services Code Point + that may be used for marking packets in a traffic stream. + In the value set and its semantics, this type is equivalent + to the Dscp textual convention of the SMIv2."; + reference + "RFC 3289: Management Information Base for the Differentiated + Services Architecture + RFC 2474: Definition of the Differentiated Services Field + (DS Field) in the IPv4 and IPv6 Headers + RFC 2780: IANA Allocation Guidelines For Values In + the Internet Protocol and Related Headers"; + } + + typedef ipv6-flow-label { + type uint32 { + range "0..1048575"; + } + description + "The ipv6-flow-label type represents the flow identifier or Flow + Label in an IPv6 packet header that may be used to + discriminate traffic flows. + + In the value set and its semantics, this type is equivalent + to the IPv6FlowLabel textual convention of the SMIv2."; + reference + "RFC 3595: Textual Conventions for IPv6 Flow Label + RFC 2460: Internet Protocol, Version 6 (IPv6) Specification"; + } + + typedef port-number { + type uint16 { + range "0..65535"; + } + description + "The port-number type represents a 16-bit port number of an + Internet transport-layer protocol such as UDP, TCP, DCCP, or + SCTP. Port numbers are assigned by IANA. A current list of + all assignments is available from . + + Note that the port number value zero is reserved by IANA. In + situations where the value zero does not make sense, it can + be excluded by subtyping the port-number type. + In the value set and its semantics, this type is equivalent + to the InetPortNumber textual convention of the SMIv2."; + reference + "RFC 768: User Datagram Protocol + RFC 793: Transmission Control Protocol + RFC 4960: Stream Control Transmission Protocol + RFC 4340: Datagram Congestion Control Protocol (DCCP) + RFC 4001: Textual Conventions for Internet Network Addresses"; + } + + /*** collection of types related to autonomous systems ***/ + + typedef as-number { + type uint32; + description + "The as-number type represents autonomous system numbers + which identify an Autonomous System (AS). An AS is a set + of routers under a single technical administration, using + an interior gateway protocol and common metrics to route + packets within the AS, and using an exterior gateway + protocol to route packets to other ASes. IANA maintains + the AS number space and has delegated large parts to the + regional registries. + + Autonomous system numbers were originally limited to 16 + bits. BGP extensions have enlarged the autonomous system + number space to 32 bits. This type therefore uses an uint32 + base type without a range restriction in order to support + a larger autonomous system number space. + + In the value set and its semantics, this type is equivalent + to the InetAutonomousSystemNumber textual convention of + the SMIv2."; + reference + "RFC 1930: Guidelines for creation, selection, and registration + of an Autonomous System (AS) + RFC 4271: A Border Gateway Protocol 4 (BGP-4) + RFC 4001: Textual Conventions for Internet Network Addresses + RFC 6793: BGP Support for Four-Octet Autonomous System (AS) + Number Space"; + } + + /*** collection of types related to IP addresses and hostnames ***/ + + typedef ip-address { + type union { + type inet:ipv4-address; + type inet:ipv6-address; + } + description + "The ip-address type represents an IP address and is IP + version neutral. The format of the textual representation + implies the IP version. This type supports scoped addresses + by allowing zone identifiers in the address format."; + reference + "RFC 4007: IPv6 Scoped Address Architecture"; + } + + typedef ipv4-address { + type string { + pattern + '(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.){3}' + + '([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])' + + '(%[\p{N}\p{L}]+)?'; + } + description + "The ipv4-address type represents an IPv4 address in + dotted-quad notation. The IPv4 address may include a zone + index, separated by a % sign. + + The zone index is used to disambiguate identical address + values. For link-local addresses, the zone index will + typically be the interface index number or the name of an + interface. If the zone index is not present, the default + zone of the device will be used. + + The canonical format for the zone index is the numerical + format"; + } + + typedef ipv6-address { + type string { + pattern '((:|[0-9a-fA-F]{0,4}):)([0-9a-fA-F]{0,4}:){0,5}' + + '((([0-9a-fA-F]{0,4}:)?(:|[0-9a-fA-F]{0,4}))|' + + '(((25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])\.){3}' + + '(25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])))' + + '(%[\p{N}\p{L}]+)?'; + pattern '(([^:]+:){6}(([^:]+:[^:]+)|(.*\..*)))|' + + '((([^:]+:)*[^:]+)?::(([^:]+:)*[^:]+)?)' + + '(%.+)?'; + } + description + "The ipv6-address type represents an IPv6 address in full, + mixed, shortened, and shortened-mixed notation. The IPv6 + address may include a zone index, separated by a % sign. + + The zone index is used to disambiguate identical address + values. For link-local addresses, the zone index will + typically be the interface index number or the name of an + interface. If the zone index is not present, the default + zone of the device will be used. + + The canonical format of IPv6 addresses uses the textual + representation defined in Section 4 of RFC 5952. The + canonical format for the zone index is the numerical + format as described in Section 11.2 of RFC 4007."; + reference + "RFC 4291: IP Version 6 Addressing Architecture + RFC 4007: IPv6 Scoped Address Architecture + RFC 5952: A Recommendation for IPv6 Address Text + Representation"; + } + + typedef ip-address-no-zone { + type union { + type inet:ipv4-address-no-zone; + type inet:ipv6-address-no-zone; + } + description + "The ip-address-no-zone type represents an IP address and is + IP version neutral. The format of the textual representation + implies the IP version. This type does not support scoped + addresses since it does not allow zone identifiers in the + address format."; + reference + "RFC 4007: IPv6 Scoped Address Architecture"; + } + + typedef ipv4-address-no-zone { + type inet:ipv4-address { + pattern '[0-9\.]*'; + } + description + "An IPv4 address without a zone index. This type, derived from + ipv4-address, may be used in situations where the zone is + known from the context and hence no zone index is needed."; + } + + typedef ipv6-address-no-zone { + type inet:ipv6-address { + pattern '[0-9a-fA-F:\.]*'; + } + description + "An IPv6 address without a zone index. This type, derived from + ipv6-address, may be used in situations where the zone is + known from the context and hence no zone index is needed."; + reference + "RFC 4291: IP Version 6 Addressing Architecture + RFC 4007: IPv6 Scoped Address Architecture + RFC 5952: A Recommendation for IPv6 Address Text + Representation"; + } + + typedef ip-prefix { + type union { + type inet:ipv4-prefix; + type inet:ipv6-prefix; + } + description + "The ip-prefix type represents an IP prefix and is IP + version neutral. The format of the textual representations + implies the IP version."; + } + + typedef ipv4-prefix { + type string { + pattern + '(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.){3}' + + '([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])' + + '/(([0-9])|([1-2][0-9])|(3[0-2]))'; + } + description + "The ipv4-prefix type represents an IPv4 address prefix. + The prefix length is given by the number following the + slash character and must be less than or equal to 32. + + A prefix length value of n corresponds to an IP address + mask that has n contiguous 1-bits from the most + significant bit (MSB) and all other bits set to 0. + + The canonical format of an IPv4 prefix has all bits of + the IPv4 address set to zero that are not part of the + IPv4 prefix."; + } + + typedef ipv6-prefix { + type string { + pattern '((:|[0-9a-fA-F]{0,4}):)([0-9a-fA-F]{0,4}:){0,5}' + + '((([0-9a-fA-F]{0,4}:)?(:|[0-9a-fA-F]{0,4}))|' + + '(((25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])\.){3}' + + '(25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])))' + + '(/(([0-9])|([0-9]{2})|(1[0-1][0-9])|(12[0-8])))'; + pattern '(([^:]+:){6}(([^:]+:[^:]+)|(.*\..*)))|' + + '((([^:]+:)*[^:]+)?::(([^:]+:)*[^:]+)?)' + + '(/.+)'; + } + + description + "The ipv6-prefix type represents an IPv6 address prefix. + The prefix length is given by the number following the + slash character and must be less than or equal to 128. + + A prefix length value of n corresponds to an IP address + mask that has n contiguous 1-bits from the most + significant bit (MSB) and all other bits set to 0. + + The IPv6 address should have all bits that do not belong + to the prefix set to zero. + + The canonical format of an IPv6 prefix has all bits of + the IPv6 address set to zero that are not part of the + IPv6 prefix. Furthermore, the IPv6 address is represented + as defined in Section 4 of RFC 5952."; + reference + "RFC 5952: A Recommendation for IPv6 Address Text + Representation"; + } + + /*** collection of domain name and URI types ***/ + + typedef domain-name { + type string { + pattern + '((([a-zA-Z0-9_]([a-zA-Z0-9\-_]){0,61})?[a-zA-Z0-9]\.)*' + + '([a-zA-Z0-9_]([a-zA-Z0-9\-_]){0,61})?[a-zA-Z0-9]\.?)' + + '|\.'; + length "1..253"; + } + description + "The domain-name type represents a DNS domain name. The + name SHOULD be fully qualified whenever possible. + + Internet domain names are only loosely specified. Section + 3.5 of RFC 1034 recommends a syntax (modified in Section + 2.1 of RFC 1123). The pattern above is intended to allow + for current practice in domain name use, and some possible + future expansion. It is designed to hold various types of + domain names, including names used for A or AAAA records + (host names) and other records, such as SRV records. Note + that Internet host names have a stricter syntax (described + in RFC 952) than the DNS recommendations in RFCs 1034 and + 1123, and that systems that want to store host names in + schema nodes using the domain-name type are recommended to + adhere to this stricter standard to ensure interoperability. + + The encoding of DNS names in the DNS protocol is limited + to 255 characters. Since the encoding consists of labels + prefixed by a length bytes and there is a trailing NULL + byte, only 253 characters can appear in the textual dotted + notation. + + The description clause of schema nodes using the domain-name + type MUST describe when and how these names are resolved to + IP addresses. Note that the resolution of a domain-name value + may require to query multiple DNS records (e.g., A for IPv4 + and AAAA for IPv6). The order of the resolution process and + which DNS record takes precedence can either be defined + explicitly or may depend on the configuration of the + resolver. + + Domain-name values use the US-ASCII encoding. Their canonical + format uses lowercase US-ASCII characters. Internationalized + domain names MUST be A-labels as per RFC 5890."; + reference + "RFC 952: DoD Internet Host Table Specification + RFC 1034: Domain Names - Concepts and Facilities + RFC 1123: Requirements for Internet Hosts -- Application + and Support + RFC 2782: A DNS RR for specifying the location of services + (DNS SRV) + RFC 5890: Internationalized Domain Names in Applications + (IDNA): Definitions and Document Framework"; + } + + typedef host { + type union { + type inet:ip-address; + type inet:domain-name; + } + description + "The host type represents either an IP address or a DNS + domain name."; + } + + typedef uri { + type string; + description + "The uri type represents a Uniform Resource Identifier + (URI) as defined by STD 66. + + Objects using the uri type MUST be in US-ASCII encoding, + and MUST be normalized as described by RFC 3986 Sections + 6.2.1, 6.2.2.1, and 6.2.2.2. All unnecessary + percent-encoding is removed, and all case-insensitive + characters are set to lowercase except for hexadecimal + digits, which are normalized to uppercase as described in + Section 6.2.2.1. + + The purpose of this normalization is to help provide + unique URIs. Note that this normalization is not + sufficient to provide uniqueness. Two URIs that are + textually distinct after this normalization may still be + equivalent. + + Objects using the uri type may restrict the schemes that + they permit. For example, 'data:' and 'urn:' schemes + might not be appropriate. + + A zero-length URI is not a valid URI. This can be used to + express 'URI absent' where required. + + In the value set and its semantics, this type is equivalent + to the Uri SMIv2 textual convention defined in RFC 5017."; + reference + "RFC 3986: Uniform Resource Identifier (URI): Generic Syntax + RFC 3305: Report from the Joint W3C/IETF URI Planning Interest + Group: Uniform Resource Identifiers (URIs), URLs, + and Uniform Resource Names (URNs): Clarifications + and Recommendations + RFC 5017: MIB Textual Conventions for Uniform Resource + Identifiers (URIs)"; + } + +} diff --git a/src/tests/tools/mock_qkd_nodes/yang/ietf-yang-types.yang b/src/tests/tools/mock_qkd_nodes/yang/ietf-yang-types.yang new file mode 100644 index 0000000000000000000000000000000000000000..ee58fa3ab0042120d5607b8713d21fa0ba845895 --- /dev/null +++ b/src/tests/tools/mock_qkd_nodes/yang/ietf-yang-types.yang @@ -0,0 +1,474 @@ +module ietf-yang-types { + + namespace "urn:ietf:params:xml:ns:yang:ietf-yang-types"; + prefix "yang"; + + organization + "IETF NETMOD (NETCONF Data Modeling Language) Working Group"; + + contact + "WG Web: + WG List: + + WG Chair: David Kessens + + + WG Chair: Juergen Schoenwaelder + + + Editor: Juergen Schoenwaelder + "; + + description + "This module contains a collection of generally useful derived + YANG data types. + + Copyright (c) 2013 IETF Trust and the persons identified as + authors of the code. All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, is permitted pursuant to, and subject + to the license terms contained in, the Simplified BSD License + set forth in Section 4.c of the IETF Trust's Legal Provisions + Relating to IETF Documents + (http://trustee.ietf.org/license-info). + + This version of this YANG module is part of RFC 6991; see + the RFC itself for full legal notices."; + + revision 2013-07-15 { + description + "This revision adds the following new data types: + - yang-identifier + - hex-string + - uuid + - dotted-quad"; + reference + "RFC 6991: Common YANG Data Types"; + } + + revision 2010-09-24 { + description + "Initial revision."; + reference + "RFC 6021: Common YANG Data Types"; + } + + /*** collection of counter and gauge types ***/ + + typedef counter32 { + type uint32; + description + "The counter32 type represents a non-negative integer + that monotonically increases until it reaches a + maximum value of 2^32-1 (4294967295 decimal), when it + wraps around and starts increasing again from zero. + + Counters have no defined 'initial' value, and thus, a + single value of a counter has (in general) no information + content. Discontinuities in the monotonically increasing + value normally occur at re-initialization of the + management system, and at other times as specified in the + description of a schema node using this type. If such + other times can occur, for example, the creation of + a schema node of type counter32 at times other than + re-initialization, then a corresponding schema node + should be defined, with an appropriate type, to indicate + the last discontinuity. + + The counter32 type should not be used for configuration + schema nodes. A default statement SHOULD NOT be used in + combination with the type counter32. + + In the value set and its semantics, this type is equivalent + to the Counter32 type of the SMIv2."; + reference + "RFC 2578: Structure of Management Information Version 2 + (SMIv2)"; + } + + typedef zero-based-counter32 { + type yang:counter32; + default "0"; + description + "The zero-based-counter32 type represents a counter32 + that has the defined 'initial' value zero. + + A schema node of this type will be set to zero (0) on creation + and will thereafter increase monotonically until it reaches + a maximum value of 2^32-1 (4294967295 decimal), when it + wraps around and starts increasing again from zero. + + Provided that an application discovers a new schema node + of this type within the minimum time to wrap, it can use the + 'initial' value as a delta. It is important for a management + station to be aware of this minimum time and the actual time + between polls, and to discard data if the actual time is too + long or there is no defined minimum time. + + In the value set and its semantics, this type is equivalent + to the ZeroBasedCounter32 textual convention of the SMIv2."; + reference + "RFC 4502: Remote Network Monitoring Management Information + Base Version 2"; + } + + typedef counter64 { + type uint64; + description + "The counter64 type represents a non-negative integer + that monotonically increases until it reaches a + maximum value of 2^64-1 (18446744073709551615 decimal), + when it wraps around and starts increasing again from zero. + + Counters have no defined 'initial' value, and thus, a + single value of a counter has (in general) no information + content. Discontinuities in the monotonically increasing + value normally occur at re-initialization of the + management system, and at other times as specified in the + description of a schema node using this type. If such + other times can occur, for example, the creation of + a schema node of type counter64 at times other than + re-initialization, then a corresponding schema node + should be defined, with an appropriate type, to indicate + the last discontinuity. + + The counter64 type should not be used for configuration + schema nodes. A default statement SHOULD NOT be used in + combination with the type counter64. + + In the value set and its semantics, this type is equivalent + to the Counter64 type of the SMIv2."; + reference + "RFC 2578: Structure of Management Information Version 2 + (SMIv2)"; + } + + typedef zero-based-counter64 { + type yang:counter64; + default "0"; + description + "The zero-based-counter64 type represents a counter64 that + has the defined 'initial' value zero. + + A schema node of this type will be set to zero (0) on creation + and will thereafter increase monotonically until it reaches + a maximum value of 2^64-1 (18446744073709551615 decimal), + when it wraps around and starts increasing again from zero. + + Provided that an application discovers a new schema node + of this type within the minimum time to wrap, it can use the + 'initial' value as a delta. It is important for a management + station to be aware of this minimum time and the actual time + between polls, and to discard data if the actual time is too + long or there is no defined minimum time. + + In the value set and its semantics, this type is equivalent + to the ZeroBasedCounter64 textual convention of the SMIv2."; + reference + "RFC 2856: Textual Conventions for Additional High Capacity + Data Types"; + } + + typedef gauge32 { + type uint32; + description + "The gauge32 type represents a non-negative integer, which + may increase or decrease, but shall never exceed a maximum + value, nor fall below a minimum value. The maximum value + cannot be greater than 2^32-1 (4294967295 decimal), and + the minimum value cannot be smaller than 0. The value of + a gauge32 has its maximum value whenever the information + being modeled is greater than or equal to its maximum + value, and has its minimum value whenever the information + being modeled is smaller than or equal to its minimum value. + If the information being modeled subsequently decreases + below (increases above) the maximum (minimum) value, the + gauge32 also decreases (increases). + + In the value set and its semantics, this type is equivalent + to the Gauge32 type of the SMIv2."; + reference + "RFC 2578: Structure of Management Information Version 2 + (SMIv2)"; + } + + typedef gauge64 { + type uint64; + description + "The gauge64 type represents a non-negative integer, which + may increase or decrease, but shall never exceed a maximum + value, nor fall below a minimum value. The maximum value + cannot be greater than 2^64-1 (18446744073709551615), and + the minimum value cannot be smaller than 0. The value of + a gauge64 has its maximum value whenever the information + being modeled is greater than or equal to its maximum + value, and has its minimum value whenever the information + being modeled is smaller than or equal to its minimum value. + If the information being modeled subsequently decreases + below (increases above) the maximum (minimum) value, the + gauge64 also decreases (increases). + + In the value set and its semantics, this type is equivalent + to the CounterBasedGauge64 SMIv2 textual convention defined + in RFC 2856"; + reference + "RFC 2856: Textual Conventions for Additional High Capacity + Data Types"; + } + + /*** collection of identifier-related types ***/ + + typedef object-identifier { + type string { + pattern '(([0-1](\.[1-3]?[0-9]))|(2\.(0|([1-9]\d*))))' + + '(\.(0|([1-9]\d*)))*'; + } + description + "The object-identifier type represents administratively + assigned names in a registration-hierarchical-name tree. + + Values of this type are denoted as a sequence of numerical + non-negative sub-identifier values. Each sub-identifier + value MUST NOT exceed 2^32-1 (4294967295). Sub-identifiers + are separated by single dots and without any intermediate + whitespace. + + The ASN.1 standard restricts the value space of the first + sub-identifier to 0, 1, or 2. Furthermore, the value space + of the second sub-identifier is restricted to the range + 0 to 39 if the first sub-identifier is 0 or 1. Finally, + the ASN.1 standard requires that an object identifier + has always at least two sub-identifiers. The pattern + captures these restrictions. + + Although the number of sub-identifiers is not limited, + module designers should realize that there may be + implementations that stick with the SMIv2 limit of 128 + sub-identifiers. + + This type is a superset of the SMIv2 OBJECT IDENTIFIER type + since it is not restricted to 128 sub-identifiers. Hence, + this type SHOULD NOT be used to represent the SMIv2 OBJECT + IDENTIFIER type; the object-identifier-128 type SHOULD be + used instead."; + reference + "ISO9834-1: Information technology -- Open Systems + Interconnection -- Procedures for the operation of OSI + Registration Authorities: General procedures and top + arcs of the ASN.1 Object Identifier tree"; + } + + typedef object-identifier-128 { + type object-identifier { + pattern '\d*(\.\d*){1,127}'; + } + description + "This type represents object-identifiers restricted to 128 + sub-identifiers. + + In the value set and its semantics, this type is equivalent + to the OBJECT IDENTIFIER type of the SMIv2."; + reference + "RFC 2578: Structure of Management Information Version 2 + (SMIv2)"; + } + + typedef yang-identifier { + type string { + length "1..max"; + pattern '[a-zA-Z_][a-zA-Z0-9\-_.]*'; + pattern '.|..|[^xX].*|.[^mM].*|..[^lL].*'; + } + description + "A YANG identifier string as defined by the 'identifier' + rule in Section 12 of RFC 6020. An identifier must + start with an alphabetic character or an underscore + followed by an arbitrary sequence of alphabetic or + numeric characters, underscores, hyphens, or dots. + + A YANG identifier MUST NOT start with any possible + combination of the lowercase or uppercase character + sequence 'xml'."; + reference + "RFC 6020: YANG - A Data Modeling Language for the Network + Configuration Protocol (NETCONF)"; + } + + /*** collection of types related to date and time***/ + + typedef date-and-time { + type string { + pattern '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?' + + '(Z|[\+\-]\d{2}:\d{2})'; + } + description + "The date-and-time type is a profile of the ISO 8601 + standard for representation of dates and times using the + Gregorian calendar. The profile is defined by the + date-time production in Section 5.6 of RFC 3339. + + The date-and-time type is compatible with the dateTime XML + schema type with the following notable exceptions: + + (a) The date-and-time type does not allow negative years. + + (b) The date-and-time time-offset -00:00 indicates an unknown + time zone (see RFC 3339) while -00:00 and +00:00 and Z + all represent the same time zone in dateTime. + + (c) The canonical format (see below) of data-and-time values + differs from the canonical format used by the dateTime XML + schema type, which requires all times to be in UTC using + the time-offset 'Z'. + + This type is not equivalent to the DateAndTime textual + convention of the SMIv2 since RFC 3339 uses a different + separator between full-date and full-time and provides + higher resolution of time-secfrac. + + The canonical format for date-and-time values with a known time + zone uses a numeric time zone offset that is calculated using + the device's configured known offset to UTC time. A change of + the device's offset to UTC time will cause date-and-time values + to change accordingly. Such changes might happen periodically + in case a server follows automatically daylight saving time + (DST) time zone offset changes. The canonical format for + date-and-time values with an unknown time zone (usually + referring to the notion of local time) uses the time-offset + -00:00."; + reference + "RFC 3339: Date and Time on the Internet: Timestamps + RFC 2579: Textual Conventions for SMIv2 + XSD-TYPES: XML Schema Part 2: Datatypes Second Edition"; + } + + typedef timeticks { + type uint32; + description + "The timeticks type represents a non-negative integer that + represents the time, modulo 2^32 (4294967296 decimal), in + hundredths of a second between two epochs. When a schema + node is defined that uses this type, the description of + the schema node identifies both of the reference epochs. + + In the value set and its semantics, this type is equivalent + to the TimeTicks type of the SMIv2."; + reference + "RFC 2578: Structure of Management Information Version 2 + (SMIv2)"; + } + + typedef timestamp { + type yang:timeticks; + description + "The timestamp type represents the value of an associated + timeticks schema node at which a specific occurrence + happened. The specific occurrence must be defined in the + description of any schema node defined using this type. When + the specific occurrence occurred prior to the last time the + associated timeticks attribute was zero, then the timestamp + value is zero. Note that this requires all timestamp values + to be reset to zero when the value of the associated timeticks + attribute reaches 497+ days and wraps around to zero. + + The associated timeticks schema node must be specified + in the description of any schema node using this type. + + In the value set and its semantics, this type is equivalent + to the TimeStamp textual convention of the SMIv2."; + reference + "RFC 2579: Textual Conventions for SMIv2"; + } + + /*** collection of generic address types ***/ + + typedef phys-address { + type string { + pattern '([0-9a-fA-F]{2}(:[0-9a-fA-F]{2})*)?'; + } + + description + "Represents media- or physical-level addresses represented + as a sequence octets, each octet represented by two hexadecimal + numbers. Octets are separated by colons. The canonical + representation uses lowercase characters. + + In the value set and its semantics, this type is equivalent + to the PhysAddress textual convention of the SMIv2."; + reference + "RFC 2579: Textual Conventions for SMIv2"; + } + + typedef mac-address { + type string { + pattern '[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}'; + } + description + "The mac-address type represents an IEEE 802 MAC address. + The canonical representation uses lowercase characters. + + In the value set and its semantics, this type is equivalent + to the MacAddress textual convention of the SMIv2."; + reference + "IEEE 802: IEEE Standard for Local and Metropolitan Area + Networks: Overview and Architecture + RFC 2579: Textual Conventions for SMIv2"; + } + + /*** collection of XML-specific types ***/ + + typedef xpath1.0 { + type string; + description + "This type represents an XPATH 1.0 expression. + + When a schema node is defined that uses this type, the + description of the schema node MUST specify the XPath + context in which the XPath expression is evaluated."; + reference + "XPATH: XML Path Language (XPath) Version 1.0"; + } + + /*** collection of string types ***/ + + typedef hex-string { + type string { + pattern '([0-9a-fA-F]{2}(:[0-9a-fA-F]{2})*)?'; + } + description + "A hexadecimal string with octets represented as hex digits + separated by colons. The canonical representation uses + lowercase characters."; + } + + typedef uuid { + type string { + pattern '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-' + + '[0-9a-fA-F]{4}-[0-9a-fA-F]{12}'; + } + description + "A Universally Unique IDentifier in the string representation + defined in RFC 4122. The canonical representation uses + lowercase characters. + + The following is an example of a UUID in string representation: + f81d4fae-7dec-11d0-a765-00a0c91e6bf6 + "; + reference + "RFC 4122: A Universally Unique IDentifier (UUID) URN + Namespace"; + } + + typedef dotted-quad { + type string { + pattern + '(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.){3}' + + '([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])'; + } + description + "An unsigned 32-bit number expressed in the dotted-quad + notation, i.e., four octets written as decimal numbers + and separated with the '.' (full stop) character."; + } +}