diff --git a/README_INFINERA.md b/README_INFINERA.md index 4d3c1a3f00a537827324cced1ccf830481dfe372..339c516ae93f2a72910ae1fc5ccabdeff10ca997 100644 --- a/README_INFINERA.md +++ b/README_INFINERA.md @@ -57,6 +57,15 @@ SOURCE VENV ACTIVATE ON ANY SHELL USED FOR PYTHON RELATED WORK (e.g. pytest). Use apt-get to install any missing tools (e.g. jq is required). +For host based Python development (e.g. VS Code) and test script execution, generate protobuf stubs: + +```bash +cd proto +./generate_code_python.sh +cd ../src/context +ln -s ../../proto/src/python proto +``` + ## Building Run deploy script to build in docker containers and then instantiate to configured K8s cluster. Deploy script must be sources for this to work! @@ -80,3 +89,19 @@ Good logs to check are: * kubectl logs service/deviceservice --namespace tfs * kubectl logs service/webuiservice --namespace tfs + +## cm-cli + +The tool cm-cli in the xr driver directory can be use to connect to CM and test the connectivity. For example: + +```bash +./cm-cli.py 172.19.219.44 443 xr-user-1 xr-user-1 --show-constellation-by-hub-name="XR HUB 1" +./cm-cli.py 172.19.219.44 443 xr-user-1 xr-user-1 --list-constellations +./cm-cli.py 172.19.219.44 443 xr-user-1 xr-user-1 --create-connection="FOO;XR HUB 1|XR-T4;XR LEAF 1|XR-T1" +./cm-cli.py 172.19.219.44 443 xr-user-1 xr-user-1 --show-connection-by-name="FooBar123" +./cm-cli.py 172.19.219.44 443 xr-user-1 xr-user-1 --list-connections +# Modify argumens: href;uuid;ifname;ifname or href;uuid +# uuid translates to name TF:uuid +./cm-cli.py 172.19.219.44 443 xr-user-1 xr-user-1 --modify-connection="/network-connections/138f0cc0-3dc6-4195-97c0-2cbed5fd59ba;FooBarAaa" +./cm-cli.py 172.19.219.44 443 xr-user-1 xr-user-1 --delete-connection=/network-connections/138f0cc0-3dc6-4195-97c0-2cbed5fd59ba +``` diff --git a/src/device/service/drivers/xr/CmConnection.py b/src/device/service/drivers/xr/CmConnection.py new file mode 100644 index 0000000000000000000000000000000000000000..490597860ff1df2883bd0d64f7e7da0347b4805e --- /dev/null +++ b/src/device/service/drivers/xr/CmConnection.py @@ -0,0 +1,379 @@ +#pylint: disable=invalid-name, missing-function-docstring, line-too-long, logging-fstring-interpolation, missing-class-docstring, missing-module-docstring +import logging +import json +import time +from typing import Tuple, Optional #Any, Iterator, List, , Union +import requests +import urllib3 +import re + +LOGGER = logging.getLogger(__name__) + +class InvalidIfnameError(Exception): + def __init__(self, ifname): + # Call the base class constructor with the parameters it needs + super().__init__(f"Invalid interface name {ifname}, expecting format \"MODULENAME|PORTNAME\"") + +class ConnectionDeserializationError(Exception): + def __init__(self, msg): + # Call the base class constructor with the parameters it needs + super().__init__(msg) + +def ifname_to_module_and_aid(ifname: str) -> Tuple[str, str]: + a = ifname.split("|") + if len(a) != 2: + raise InvalidIfnameError(ifname) + return (a[0], a[1]) + +class Connection: + def __init__(self, from_json=None): + def get_endpoint_ifname(endpoint): + try: + return endpoint["state"]["moduleIf"]["moduleName"] + "|" + endpoint["state"]["moduleIf"]["clientIfAid"] + except KeyError: + return None + + if from_json: + try: + state = from_json["state"] + self.name = state["name"] if "name" in state else None #Name is optional + self.serviceMode = state["serviceMode"] + self.href = from_json["href"] + + self.endpoints = [] + for ep in from_json["endpoints"]: + ifname = get_endpoint_ifname(ep) + if ifname: + self.endpoints.append(ifname) + except KeyError as e: + raise ConnectionDeserializationError(f"Missing mandatory key, f{str(e)}") + else: + # May support other initializations in future + raise ConnectionDeserializationError("JSON dict missing") + + def __str__(self): + name = self.name if self.name else "<NO NAME>" + endpoints = ", ".join(self.endpoints) + return f"name: {name}, id: {self.href}, service-mode: {self.serviceMode}, end-points: [{endpoints}]" + +class ExpiringValue: + def __init__(self, value, expiry): + self.__value = value + self.__expiry = expiry + self.__created = time.monotonic() + + def get_value(self): + return self.__value + + def is_valid_for(self, duration): + if self.__created + self.__expiry >= time.monotonic()+duration: + return True + else: + return False + +class CmConnection: + def __init__(self, address: str, port: int, username: str, password: str, timeout=30, tls_verify=True) -> None: + self.__tls_verify = tls_verify + if not tls_verify: + urllib3.disable_warnings() + + self.__timeout = timeout + self.__username = username + self.__password = password + self.__cm_root = 'https://' + address + ':' + str(port) + self.__access_token = None + + def __post_w_headers(self, path, data, headers, data_as_json=True): + url = self.__cm_root + path + try: + if data_as_json: + response = requests.post(url, headers=headers, json=data, timeout=self.__timeout, verify=self.__tls_verify) + else: + response = requests.post(url, headers=headers, data=data, timeout=self.__timeout, verify=self.__tls_verify) + + LOGGER.info(f"POST: {url} ==> {response.status_code}") + resp = json.loads(response.text) + return (response.status_code, resp) + except requests.exceptions.Timeout: + LOGGER.info(f"POST: {url} ==> timeout") + return None + except json.JSONDecodeError as json_err: + LOGGER.info(f"POST: {url} ==> response json decode error: {str(json_err)}") + return None + except Exception as e: # pylint: disable=broad-except + es=str(e) + LOGGER.info(f"POST: {url} ==> unexpected exception: {es}") + return None + + def __post(self, path, data, data_as_json=True): + return self.__post_w_headers(path, data, self.__http_headers(), data_as_json=data_as_json) + + def __put(self, path, data, data_as_json=True): + url = self.__cm_root + path + headers = self.__http_headers() + try: + if data_as_json: + response = requests.put(url, headers=headers, json=data, timeout=self.__timeout, verify=self.__tls_verify) + else: + response = requests.put(url, headers=headers, data=data, timeout=self.__timeout, verify=self.__tls_verify) + + LOGGER.info(f"PUT: {url} ==> {response.status_code}") + + if response.content == b'null': + return (response.status_code, None) + resp = json.loads(response.text) + return (response.status_code, resp) + except requests.exceptions.Timeout: + LOGGER.info(f"PUT: {url} ==> timeout") + return None + except json.JSONDecodeError as json_err: + LOGGER.info(f"PUT: {url} ==> response json decode error: {str(json_err)}") + return None + except Exception as e: # pylint: disable=broad-except + es=str(e) + LOGGER.info(f"PUT: {url} ==> unexpected exception: {es}") + return None + + def __delete(self, path, data=None): + url = self.__cm_root + path + headers = self.__http_headers() + try: + response = requests.delete(url, headers=headers, data=data, timeout=self.__timeout, verify=self.__tls_verify) + LOGGER.info(f"DELETE: {url} ==> {response.status_code}") + + if response.content == b'null': + return (response.status_code, None) + resp = json.loads(response.text) + return (response.status_code, resp) + except requests.exceptions.Timeout: + LOGGER.info(f"DELETE: {url} ==> timeout") + return None + except json.JSONDecodeError as json_err: + LOGGER.info(f"DELETE: {url} ==> response json decode error: {str(json_err)}") + return None + except Exception as e: # pylint: disable=broad-except + es=str(e) + LOGGER.info(f"DELETE: {url} ==> unexpected exception: {es}") + return None + + def __http_headers(self): + self.__ensure_valid_access_token() + if self.__access_token: + return {'Authorization': 'Bearer '+ self.__access_token.get_value()} + else: + return {} + + def __get_json(self, path, params=None): + url = self.__cm_root + path + try: + response = requests.get(url,headers=self.__http_headers(), timeout=self.__timeout,verify=self.__tls_verify, params=params) + LOGGER.info(f"GET: {url} {params=} ==> {response.status_code}") + resp = json.loads(response.text) + return (response.status_code, resp) + except requests.exceptions.Timeout: + LOGGER.info(f"GET: {url} {params=} ==> timeout") + return None + except json.JSONDecodeError as json_err: + LOGGER.info(f"GET: {url} {params=} ==> response json decode error: {str(json_err)}") + return None + except Exception as e: # pylint: disable=broad-except + es=str(e) + LOGGER.info(f"GET: {url} {params=} ==> unexpected exception: {es}") + return None + + def __acquire_access_token(self): + path = '/realms/xr-cm/protocol/openid-connect/token' + req = { + "username": self.__username, + "password": self.__password, + "grant_type": "password", + "client_secret": "xr-web-client", + "client_id": "xr-web-client" + } + (status_code, response) = self.__post_w_headers(path, req, None, data_as_json=False) + if 200 != status_code or 'access_token' not in response: + LOGGER.error(f"Authentication failure, status code {status_code}, data {response}") + return False + access_token = response['access_token'] + expires = int(response["expires_in"]) if "expires_in" in response else 0 + LOGGER.info(f"Obtained access token {access_token}, expires in {expires}") + self.__access_token = ExpiringValue(access_token, expires) + return True + + def __ensure_valid_access_token(self): + if not self.__access_token or not self.__access_token.is_valid_for(60): + self.__acquire_access_token() + + def Connect(self) -> bool: + return self.__acquire_access_token() + + @staticmethod + def get_constellation_module_ifnames(module): + ifnames = [] + try: + module_state = module["state"] + module_name = module_state["module"]["moduleName"] + if "endpoints" in module_state: + for endpoint in module_state["endpoints"]: + try: + ifname = endpoint["moduleIf"]["clientIfAid"] + ifnames.append(f"{module_name}|{ifname}") + except KeyError: + pass + except KeyError: + pass + return ifnames + + @staticmethod + def get_constellation_ifnames(constellation): + ifnames = [] + if "hubModule" in constellation: + hub = constellation["hubModule"] + ifnames.extend(CmConnection.get_constellation_module_ifnames(hub)) + + if "leafModules" in constellation: + for leaf in constellation["leafModules"]: + ifnames.extend(CmConnection.get_constellation_module_ifnames(leaf)) + return ifnames + + @staticmethod + def get_ifnames_per_constellation(constellation): + ifnames = [] + try: + ports = CmConnection.get_constellation_ifnames(constellation) + constellation_id = constellation["id"] + for port in ports: + ifnames.append(port) + except KeyError: + return None + + return (constellation_id, ifnames) + + def list_constellations(self): + status_code, constellations = self.__get_json("/api/v1/ns/xr-networks?content=expanded") + if not constellations or status_code != 200: + return [] + return [CmConnection.get_ifnames_per_constellation(c) for c in constellations] + + def get_constellation_by_hub_name(self, hub_module_name: str): + qparams = [ + ('content', 'expanded'), + ('q', '{"hubModule.state.module.moduleName": "' + hub_module_name + '"}') + ] + status_code, constellations = self.__get_json("/api/v1/ns/xr-networks?content=expanded", params=qparams) + if not constellations or status_code != 200 or len(constellations) != 1: + return None + return CmConnection.get_ifnames_per_constellation(constellations[0]) + + @staticmethod + def create_connection_config(uid: str, serviceMode: Optional[str], mod1: Optional[str], aid1: Optional[str], mod2: Optional[str], aid2: Optional[str]) -> Connection: + name = f"TF:{uid}" + def create_endpoint(mod, aid): + ep = { + "selector": { + "ifSelectorByModuleName": { + "moduleName": mod, + "moduleClientIfAid": aid, + } + } + } + return ep + + connection = { "name" : name} + if serviceMode: + connection["serviceMode"] = serviceMode + endpoints = [] + if mod1: + endpoints.append(create_endpoint(mod1, aid1)) + if mod2: + endpoints.append(create_endpoint(mod2, aid2)) + if len(endpoints) > 0: + connection["endpoints"] = endpoints + return connection + + # All arguments are mandatory + def create_connection(self, uid, mod1, aid1, mod2, aid2) -> Optional[str]: + # Create wants a list, so wrap connection to list + connection = [CmConnection.create_connection_config(uid, "portMode", mod1, aid1, mod2, aid2)] + resp = self.__post("/api/v1/ncs/network-connections", connection) + if resp and resp[0] == 202 and len(resp[1]) == 1 and "href" in resp[1][0]: + created_resource = resp[1][0]["href"] + LOGGER.info(f"Created connection {created_resource} {uid=}, {mod1=}, {aid1=}, {mod2=}, {aid2=}") + # FIXME: remove + LOGGER.info(self.__get_json(f"/api/v1/ncs{created_resource}?content=expanded")) + return created_resource + else: + return None + + # Modules and aids are optional. Uid is Teraflow UID, and is stored in mae field + def modify_connection(self, href: str, uid: str, service_mode: Optional[str], mod1: Optional[str]=None, aid1: Optional[str]=None, mod2: Optional[str]=None, aid2: Optional[str]=None) -> Optional[str]: + connection = CmConnection.create_connection_config(uid, service_mode, mod1, aid1, mod2, aid2) + resp = self.__put(f"/api/v1/ncs{href}", connection) + # Returns empty body + if resp and resp[0] == 202: + LOGGER.info(f"Updated connection {href=}, {uid=}, {service_mode=}, {mod1=}, {aid1=}, {mod2=}, {aid2=}") + # Return href used for update to be consisten with create + return href + else: + return None + + def delete_connection(self, href: str) -> bool: + resp = self.__delete(f"/api/v1/ncs{href}") + print(resp) + # Returns empty body + if resp and resp[0] == 202: + LOGGER.info(f"Deleted connection {href=}") + return True + else: + return False + + def create_connection_ifnames(self, uid: str, ifname1: str, ifname2: str): + module1, aid1 = ifname_to_module_and_aid(ifname1) + module2, aid2 = ifname_to_module_and_aid(ifname2) + return self.create_connection(uid, module1, aid1, module2, aid2) + + def modify_connection_ifnames(self, href: str, uid: str, ifname1: Optional[str], ifname2: Optional[str], service_mode: Optional[str] =None): + # Only uid and href are mandatory + module1, aid1 = ifname_to_module_and_aid(ifname1) if ifname1 else (None, None) + module2, aid2 = ifname_to_module_and_aid(ifname2) if ifname2 else (None, None) + return self.modify_connection(href, uid, service_mode, module1, aid1, module2, aid2) + + # Always does the correct thing, that is update if present, otherwise create + def create_or_update_connection_ifnames(self, uid: str, ifname1: str, ifname2: str) -> Optional[str]: + module1, aid1 = ifname_to_module_and_aid(ifname1) + module2, aid2 = ifname_to_module_and_aid(ifname2) + + name = f"TF:{uid}" + existing_connection = self.get_connection_by_name(name) + if existing_connection: + return self.modify_connection(existing_connection.href, uid, module1, aid1, module2, aid2) + else: + return self.create_connection(uid, module1, aid1, module2, aid2) + + def get_connection_by_name(self, connection_name: str) -> Optional[Connection]: + qparams = [ + ('content', 'expanded'), + ('q', '{"state.name": "' + connection_name + '"}') + ] + r = self.__get_json("/api/v1/ncs/network-connections", params=qparams) + if r and r[0] == 200 and len(r[1]) == 1: + return Connection(from_json=r[1][0]) + else: + return None + + def get_connection_by_teraflow_uuid(self, uuid: str) -> Optional[Connection]: + return self.get_connection_by_name(f"TF:{uuid}") + + def get_connections(self): + r = self.__get_json("/api/v1/ncs/network-connections?content=expanded") + if r and r[0] == 200: + return [Connection(from_json=c) for c in r[1]] + else: + return [] + + def service_uuid(self, key: str) -> Optional[str]: + service = re.match(r"^/service\[(.+)\]$", key) + if service: + return service.group(1) + else: + return None diff --git a/src/device/service/drivers/xr/XrDriver.py b/src/device/service/drivers/xr/XrDriver.py index 81898d7c73d4263b71b0b1e81fa0684affed070f..c40224cb4e07e9346874b5f6064c2abacd51155e 100644 --- a/src/device/service/drivers/xr/XrDriver.py +++ b/src/device/service/drivers/xr/XrDriver.py @@ -11,6 +11,7 @@ # 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. +#pylint: disable=invalid-name, missing-function-docstring, line-too-long, logging-fstring-interpolation, missing-class-docstring, missing-module-docstring import logging, requests, threading from typing import Any, Iterator, List, Optional, Tuple, Union @@ -19,6 +20,12 @@ from device.service.driver_api._Driver import _Driver from . import ALL_RESOURCE_KEYS #from .Tools import create_connectivity_service, find_key, config_getter, delete_connectivity_service import json +from .CmConnection import CmConnection + +# Don't complain about non-verified SSL certificate. This driver is demo only +# and CM is not provisioned in demos with a proper certificate. +import urllib3 +urllib3.disable_warnings() LOGGER = logging.getLogger(__name__) @@ -27,43 +34,22 @@ class XrDriver(_Driver): self.__lock = threading.Lock() self.__started = threading.Event() self.__terminate = threading.Event() - self.__cm_root = 'https://' + address + ':' + str(port) self.__timeout = int(settings.get('timeout', 120)) - self.__verify = False; # Currently using self signed certificates - self.__audience = settings["audience"] if "audience" in settings else "test" - self.__client_id = settings["client_id"] if "client_id" in settings else "test" + # Mandatory key, an exception will get thrown if missing + self.__hub_module_name = settings["hub_module_name"] + + tls_verify = False # Currently using self signed certificates + username = settings["username"] if "username" in settings else "xr-user-1" + password = settings["password"] if "password" in settings else "xr-user-1" - self.__services = {} + self.__cm_connection = CmConnection(address, int(port), username, password, self.__timeout, tls_verify = tls_verify) - # FIXME: remove - LOGGER.info(f"FIXME!!! XrDriver, cm {address}:{port}, {settings=}"); + LOGGER.info(f"XrDriver instantiated, cm {address}:{port}, {settings=}") def Connect(self) -> bool: - url = self.__cm_root + '/oauth/token' with self.__lock: if self.__started.is_set(): return True - try: - # TODO: could also do get: https://${HOSTNAME}:443/oauth/token?client_id=test&audience=test" - req = {"grant_type":"client_credentials","client_id": self.__client_id, "audience": self.__audience} - response = requests.post(url,data=req,timeout=self.__timeout,verify=self.__verify) - resp = json.loads(response.text) - if 'access_token' in resp: - self.__access_token=resp['access_token'] - LOGGER.info(f"FIXME!!! CM connected, {self.__access_token=}") ## TODO: remove - - # Use in subsequend requests as named argument headers=self.__cm_http_headers - self.__cm_http_headers = {'Authorization': 'Bearer '+ self.__access_token} - else: - LOGGER.exception('No access token provided by {:s}'.format(str(self.__cm_root))) - return False - except requests.exceptions.Timeout: - LOGGER.exception('Timeout connecting {:s}'.format(str(self.__cm_root))) - return False - except json.JSONDecodeError as json_err: - LOGGER.exception(f"Exception parsing JSON access token from {str(self.__cm_root)}, {str(json_err)}") - return False - except Exception: # pylint: disable=broad-except - LOGGER.exception('Exception connecting {:s}'.format(str(self.__cm_root))) + if not self.__cm_connection.Connect(): return False else: self.__started.set() @@ -78,62 +64,70 @@ class XrDriver(_Driver): with self.__lock: return [] - def fake_interface_names(self) -> List[str]: - interfaces = [] - # Using 4 as max leaf and lane to keep prints small during development - for lane in range(0,4): - interfaces.append(f"HUB-LANE-{lane:02}") - for leaf in range(1,5): - for lane in range(0,4): - interfaces.append(f"LEAF-{leaf:02}-LANE-{lane:02}") - return interfaces - def GetConfig(self, resource_keys : List[str] = []) -> List[Tuple[str, Union[Any, None, Exception]]]: chk_type('resources', resource_keys, list) - results = [] - - # TODO: Completely fake interface information until we get same info from CM - for ifname in self.fake_interface_names(): - results.append((f"/endpoints/endpoint[{ifname}]", {'uuid': ifname, 'type': 'optical', 'sample_types': {}})) - return results + constellation = self.__cm_connection.get_constellation_by_hub_name(self.__hub_module_name) + if constellation: + _cid, if_list = constellation + return [(f"/endpoints/endpoint[{ifname}]", {'uuid': ifname, 'type': 'optical', 'sample_types': {}}) for ifname in if_list] + else: + return [] def SetConfig(self, resources: List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: - LOGGER.info(f"FIXME!!! XrDriver, SetConfig {resources=}"); + LOGGER.info(f"SetConfig {resources=}"); # Logged config seems like: - #[('/service[44ca3570-4e1a-49b5-8aab-06c92f239fab:optical]', '{"capacity_unit": "GHz", "capacity_value": 1, "direction": "UNIDIRECTIONAL", "input_sip": "HUB-LANE-01", "layer_protocol_name": "PHOTONIC_MEDIA", "layer_protocol_qualifier": "tapi-photonic-media:PHOTONIC_LAYER_QUALIFIER_NMC", "output_sip": "LEAF-02-LANE-01", "uuid": "44ca3570-4e1a-49b5-8aab-06c92f239fab:optical"}')] + #[('/service[52ff5f0f-fda4-40bd-a0b1-066f4ff04079:optical]', '{"capacity_unit": "GHz", "capacity_value": 1, "direction": "UNIDIRECTIONAL", "input_sip": "XR HUB 1|XR-T4", "layer_protocol_name": "PHOTONIC_MEDIA", "layer_protocol_qualifier": "tapi-photonic-media:PHOTONIC_LAYER_QUALIFIER_NMC", "output_sip": "XR LEAF 1|XR-T1", "uuid": "52ff5f0f-fda4-40bd-a0b1-066f4ff04079:optical"}')] + results = [] if len(resources) == 0: return results - # Temporary dummy version for key, config in resources: - self.__services[key] = config - - # TODO: config to CM - # Ignore "direction=UNIDIRECITONAL", it seems that controller only creates one direction... - results.append(True) + service_uuid = self.__cm_connection.service_uuid(key) + if service_uuid: + config = json.loads(config) + href = self.__cm_connection.create_or_update_connection_ifnames(service_uuid, config["input_sip"], config["output_sip"]) + if href: + LOGGER.info(f"SetConfig: Created service {service_uuid} as {href}") + results.append(True) + else: + LOGGER.error(f"SetConfig: Service creation failure for {service_uuid}") + results.append(False) + else: + results.append(False) return results def DeleteConfig(self, resources: List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: - LOGGER.info(f"FIXME!!! XrDriver, DeleteConfig {resources=}"); + LOGGER.info(f"DeleteConfig {resources=}"); + + # Input looks like: + # resources=[('/service[c8a35e81-88d8-4468-9afc-a8abd92a64d0:optical]', '{"uuid": "c8a35e81-88d8-4468-9afc-a8abd92a64d0:optical"}')] results = [] if len(resources) == 0: return results # Temporary dummy version - for key, config in resources: - if key in self.__services[key]: - del self.__services[key] - # TODO: Delete config from CM - results.append(True) + for key, _config in resources: + service_uuid = self.__cm_connection.service_uuid(key) + if service_uuid: + connection = self.__cm_connection.get_connection_by_teraflow_uuid(service_uuid) + if connection is None: + LOGGER.info(f"DeleteConfig: Connection {service_uuid} does not exist, delete is no-op") + results.append(True) + else: + was_deleted = self.__cm_connection.delete_connection(connection.href) + if was_deleted: + LOGGER.info(f"DeleteConfig: Connection {service_uuid} deleted (was {str(connection)})") + else: + LOGGER.info(f"DeleteConfig: Connection {service_uuid} delete failure (was {str(connection)})") + results.append(was_deleted) else: results.append(False) - return results def SubscribeState(self, subscriptions : List[Tuple[str, float, float]]) -> List[Union[bool, Exception]]: diff --git a/src/device/service/drivers/xr/cm-cli.py b/src/device/service/drivers/xr/cm-cli.py new file mode 100755 index 0000000000000000000000000000000000000000..2bd23fdb842989574824e8423a90e65b758081b6 --- /dev/null +++ b/src/device/service/drivers/xr/cm-cli.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 + +# Test program for CmConnection + +import CmConnection +import argparse +import logging + +logging.basicConfig(level=logging.INFO) + +parser = argparse.ArgumentParser(description='CM Connectin Test Utility') +parser.add_argument('ip', help='CM IP address or domain name') +parser.add_argument('port', help='CM port', type=int) +parser.add_argument('username', help='Username') +parser.add_argument('password', help='Password') + +parser.add_argument('--list-constellations', action='store_true') +parser.add_argument('--show-constellation-by-hub-name', nargs='?', type=str) +parser.add_argument('--create-connection', nargs='?', type=str, help="uuid;ifname;ifname") +parser.add_argument('--modify-connection', nargs='?', type=str, help="href;uuid;ifname;ifname") +parser.add_argument('--show-connection-by-name', nargs='?', type=str) +parser.add_argument('--list-connections', action='store_true') +parser.add_argument('--delete-connection', nargs='?', type=str, help="connection id, e.g. \"/network-connections/4505d5d3-b2f3-40b8-8ec2-4a5b28523c03\"") + +args = parser.parse_args() + +cm = CmConnection.CmConnection(args.ip, args.port, args.username, args.password, tls_verify=False) +if not cm.Connect(): + exit(-1) + +if args.list_constellations: + constellations = cm.list_constellations() + for cid, if_list in constellations: + print("Constellation:", cid) + for if_name in if_list: + print(f" {if_name}") + +if args.show_constellation_by_hub_name: + constellation = cm.get_constellation_by_hub_name(args.show_constellation_by_hub_name) + if constellation: + (cid, if_list) = constellation + print("Constellation:", cid) + for if_name in if_list: + print(f" {if_name}") + +if args.create_connection: + cc_args = args.create_connection.split(";") + if len(cc_args) != 3: + print("Invalid create connection arguments. Expecting \"oid;ifname1;ifname2\", where ifname is form \"MODULE|PORT\"") + exit(-1) + cm.create_connection_ifnames(*cc_args) + +if args.modify_connection: + mc_args = args.modify_connection.split(";") + if len(mc_args) == 2: + cm.modify_connection_ifnames(mc_args[0], mc_args[1], None, None) + elif len(mc_args) == 4: + cm.modify_connection_ifnames(*mc_args) + else: + print("Invalid modify connection arguments. Expecting \"href;oid\" or \"href;oid;ifname1;ifname2\", where ifname is form \"MODULE|PORT\"") + exit(-1) + +if args.show_connection_by_name: + connection = cm.get_connection_by_name(args.show_connection_by_name) + if connection: + print(str(connection)) + +if args.list_connections: + connections = cm.get_connections() + for c in connections: + print(str(c)) + +if args.delete_connection: + was_deleted = cm.delete_connection(args.delete_connection) + if was_deleted: + print(f"Successfully deleted {args.delete_connection}") + else: + print(f"Failed to delete {args.delete_connection}") + diff --git a/src/tests/ofc22/descriptors_emulated_xr.json b/src/tests/ofc22/descriptors_emulated_xr.json index a3fc07bcfa89206a509da4e9e8048aea2ae79def..30bd97dddeb94f836d3fe66e51fce729c34ceced 100644 --- a/src/tests/ofc22/descriptors_emulated_xr.json +++ b/src/tests/ofc22/descriptors_emulated_xr.json @@ -68,7 +68,7 @@ "device_config": {"config_rules": [ {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "172.19.219.44"}}, {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "443"}}, - {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": "{\"endpoints\": [{\"sample_types\": [], \"type\": \"optical\", \"uuid\": \"HUB-LANE-01\"}, {\"sample_types\": [], \"type\": \"optical\", \"uuid\": \"LEAF-01-LANE-01\"}]}"}} + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": "{\"username\": \"xr-user-1\", \"password\": \"xr-user-1\", \"hub_module_name\": \"XR HUB 1\"}"}} ]}, "device_operational_status": 1, "device_drivers": [6], @@ -77,31 +77,31 @@ ], "links": [ { - "link_id": {"link_uuid": {"uuid": "R1-EMU/13/0/0==XR1-HUB-LANE-01"}}, + "link_id": {"link_uuid": {"uuid": "R1-EMU/13/0/0==XR HUB 1|XR-T4"}}, "link_endpoint_ids": [ {"device_id": {"device_uuid": {"uuid": "R1-EMU"}}, "endpoint_uuid": {"uuid": "13/0/0"}}, - {"device_id": {"device_uuid": {"uuid": "X1-XR-CONSTELLATION"}}, "endpoint_uuid": {"uuid": "HUB-LANE-01"}} + {"device_id": {"device_uuid": {"uuid": "X1-XR-CONSTELLATION"}}, "endpoint_uuid": {"uuid": "XR HUB 1|XR-T4"}} ] }, { - "link_id": {"link_uuid": {"uuid": "R2-EMU/13/0/0==XR1-LEAF-01-LANE-01"}}, + "link_id": {"link_uuid": {"uuid": "R2-EMU/13/0/0==XR HUB 1|XR-T3"}}, "link_endpoint_ids": [ {"device_id": {"device_uuid": {"uuid": "R2-EMU"}}, "endpoint_uuid": {"uuid": "13/0/0"}}, - {"device_id": {"device_uuid": {"uuid": "X1-XR-CONSTELLATION"}}, "endpoint_uuid": {"uuid": "LEAF-01-LANE-01"}} + {"device_id": {"device_uuid": {"uuid": "X1-XR-CONSTELLATION"}}, "endpoint_uuid": {"uuid": "XR HUB 1|XR-T3"}} ] }, { - "link_id": {"link_uuid": {"uuid": "R3-EMU/13/0/0==XR1-LEAF-02-LANE-01"}}, + "link_id": {"link_uuid": {"uuid": "R3-EMU/13/0/0==XR1-XR LEAF 1|XR-T1"}}, "link_endpoint_ids": [ {"device_id": {"device_uuid": {"uuid": "R3-EMU"}}, "endpoint_uuid": {"uuid": "13/0/0"}}, - {"device_id": {"device_uuid": {"uuid": "X1-XR-CONSTELLATION"}}, "endpoint_uuid": {"uuid": "LEAF-02-LANE-01"}} + {"device_id": {"device_uuid": {"uuid": "X1-XR-CONSTELLATION"}}, "endpoint_uuid": {"uuid": "XR LEAF 1|XR-T1"}} ] }, { - "link_id": {"link_uuid": {"uuid": "R4-EMU/13/0/0==XR1-LEAF-03-LANE-01"}}, + "link_id": {"link_uuid": {"uuid": "R4-EMU/13/0/0==XR LEAF 2|XR-T1"}}, "link_endpoint_ids": [ {"device_id": {"device_uuid": {"uuid": "R4-EMU"}}, "endpoint_uuid": {"uuid": "13/0/0"}}, - {"device_id": {"device_uuid": {"uuid": "X1-XR-CONSTELLATION"}}, "endpoint_uuid": {"uuid": "LEAF-03-LANE-01"}} + {"device_id": {"device_uuid": {"uuid": "X1-XR-CONSTELLATION"}}, "endpoint_uuid": {"uuid": "XR LEAF 2|XR-T1"}} ] } ] diff --git a/src/tests/ofc22/tests/ObjectsXr.py b/src/tests/ofc22/tests/ObjectsXr.py index 12d2b48eeed96c274da9756161727f64e5eaa399..e1ca0450b59eda995d4b1959caf73d58cbacfb4c 100644 --- a/src/tests/ofc22/tests/ObjectsXr.py +++ b/src/tests/ofc22/tests/ObjectsXr.py @@ -143,20 +143,26 @@ DEVICE_R4_CONNECT_RULES = json_device_emulated_connect_rules(DEVICE_R4_ENDPOINT_ DEVICE_X1_UUID = 'X1-XR-CONSTELLATION' DEVICE_X1_TIMEOUT = 120 DEVICE_X1_ENDPOINT_DEFS = [ - ('HUB-LANE-01', 'optical', []), - ('LEAF-01-LANE-01', 'optical', []), - ('LEAF-02-LANE-01', 'optical', []), - ('LEAF-03-LANE-01', 'optical', []), + ('XR HUB 1|XR-T1', 'optical', []), + ('XR HUB 1|XR-T2', 'optical', []), + ('XR HUB 1|XR-T3', 'optical', []), + ('XR HUB 1|XR-T4', 'optical', []), + ('XR LEAF 1|XR-T1', 'optical', []), + ('XR LEAF 2|XR-T1', 'optical', []), ] DEVICE_X1_ID = json_device_id(DEVICE_X1_UUID) DEVICE_X1 = json_device_tapi_disabled(DEVICE_X1_UUID) DEVICE_X1_ENDPOINT_IDS = json_endpoint_ids(DEVICE_X1_ID, DEVICE_X1_ENDPOINT_DEFS) -ENDPOINT_ID_X1_EP1 = DEVICE_X1_ENDPOINT_IDS[0] -ENDPOINT_ID_X1_EP2 = DEVICE_X1_ENDPOINT_IDS[1] -ENDPOINT_ID_X1_EP3 = DEVICE_X1_ENDPOINT_IDS[2] -ENDPOINT_ID_X1_EP4 = DEVICE_X1_ENDPOINT_IDS[3] +# These match JSON, hence indexes are what theyt are +ENDPOINT_ID_X1_EP1 = DEVICE_X1_ENDPOINT_IDS[3] +ENDPOINT_ID_X1_EP2 = DEVICE_X1_ENDPOINT_IDS[2] +ENDPOINT_ID_X1_EP3 = DEVICE_X1_ENDPOINT_IDS[4] +ENDPOINT_ID_X1_EP4 = DEVICE_X1_ENDPOINT_IDS[5] DEVICE_X1_CONNECT_RULES = json_device_connect_rules(DEVICE_X1_ADDRESS, DEVICE_X1_PORT, { 'timeout' : DEVICE_X1_TIMEOUT, + "username": "xr-user-1", + "password": "xr-user-1", + "hub_module_name": "XR HUB 1" }) # Always using real device (CM, whether CM has emulated backend is another story) #if USE_REAL_DEVICES else json_device_emulated_connect_rules(DEVICE_X1_ENDPOINT_DEFS) diff --git a/src/tests/ofc22/tests/test_functional_create_service_xr.py b/src/tests/ofc22/tests/test_functional_create_service_xr.py index 882950cd57ab7836f1f52d28d9c4fb0dd5d7bf65..7913aa9d73c5af0103b0490bfe89ff20eeb6fc35 100644 --- a/src/tests/ofc22/tests/test_functional_create_service_xr.py +++ b/src/tests/ofc22/tests/test_functional_create_service_xr.py @@ -32,8 +32,7 @@ LOGGER = logging.getLogger(__name__) LOGGER.setLevel(logging.DEBUG) DEVTYPE_EMU_PR = DeviceTypeEnum.EMULATED_PACKET_ROUTER.value -#DEVTYPE_EMU_OLS = DeviceTypeEnum.EMULATED_OPTICAL_LINE_SYSTEM.value -DEVTYPE_EMU_OLS = DeviceTypeEnum.XR_CONSTELLATION.value +DEVTYPE_XR_CONSTELLATION = DeviceTypeEnum.XR_CONSTELLATION.value @pytest.fixture(scope='session') @@ -80,7 +79,7 @@ def test_service_creation(context_client : ContextClient, osm_wim : MockOSM): # # ----- Validate collected events ---------------------------------------------------------------------------------- packet_connection_uuid = '{:s}:{:s}'.format(service_uuid, DEVTYPE_EMU_PR) - optical_connection_uuid = '{:s}:optical:{:s}'.format(service_uuid, DEVTYPE_EMU_OLS) + optical_connection_uuid = '{:s}:optical:{:s}'.format(service_uuid, DEVTYPE_XR_CONSTELLATION) optical_service_uuid = '{:s}:optical'.format(service_uuid) expected_events = [ diff --git a/src/tests/ofc22/tests/test_functional_delete_service_xr.py b/src/tests/ofc22/tests/test_functional_delete_service_xr.py new file mode 100644 index 0000000000000000000000000000000000000000..efef7484a9a9691e3d17d7073c298ab05e953bd9 --- /dev/null +++ b/src/tests/ofc22/tests/test_functional_delete_service_xr.py @@ -0,0 +1,133 @@ +# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) +# +# 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, pytest +from common.DeviceTypes import DeviceTypeEnum +from common.Settings import get_setting +from common.tests.EventTools import EVENT_REMOVE, EVENT_UPDATE, check_events +from common.tools.object_factory.Connection import json_connection_id +from common.tools.object_factory.Device import json_device_id +from common.tools.object_factory.Service import json_service_id +from common.tools.grpc.Tools import grpc_message_to_json_string +from compute.tests.mock_osm.MockOSM import MockOSM +from context.client.ContextClient import ContextClient +from context.client.EventsCollector import EventsCollector +from context.proto.context_pb2 import ContextId, Empty +from .ObjectsXr import ( + CONTEXT_ID, CONTEXTS, DEVICE_X1_UUID, DEVICE_R1_UUID, DEVICE_R3_UUID, DEVICES, LINKS, TOPOLOGIES, WIM_MAPPING, + WIM_PASSWORD, WIM_USERNAME) + + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +DEVTYPE_EMU_PR = DeviceTypeEnum.EMULATED_PACKET_ROUTER.value +DEVTYPE_XR_CONSTELLATION = DeviceTypeEnum.XR_CONSTELLATION.value + +@pytest.fixture(scope='session') +def context_client(): + _client = ContextClient(get_setting('CONTEXTSERVICE_SERVICE_HOST'), get_setting('CONTEXTSERVICE_SERVICE_PORT_GRPC')) + yield _client + _client.close() + + +@pytest.fixture(scope='session') +def osm_wim(): + wim_url = 'http://{:s}:{:s}'.format( + get_setting('COMPUTESERVICE_SERVICE_HOST'), str(get_setting('COMPUTESERVICE_SERVICE_PORT_HTTP'))) + return MockOSM(wim_url, WIM_MAPPING, WIM_USERNAME, WIM_PASSWORD) + + +def test_scenario_is_correct(context_client : ContextClient): # pylint: disable=redefined-outer-name + # ----- List entities - Ensure service is created ------------------------------------------------------------------ + response = context_client.ListContexts(Empty()) + assert len(response.contexts) == len(CONTEXTS) + + response = context_client.ListTopologies(ContextId(**CONTEXT_ID)) + assert len(response.topologies) == len(TOPOLOGIES) + + response = context_client.ListDevices(Empty()) + assert len(response.devices) == len(DEVICES) + + response = context_client.ListLinks(Empty()) + assert len(response.links) == len(LINKS) + + response = context_client.ListServices(ContextId(**CONTEXT_ID)) + LOGGER.info('Services[{:d}] = {:s}'.format(len(response.services), grpc_message_to_json_string(response))) + assert len(response.services) == 2 # L3NM + TAPI + for service in response.services: + service_id = service.service_id + response = context_client.ListConnections(service_id) + LOGGER.info(' ServiceId[{:s}] => Connections[{:d}] = {:s}'.format( + grpc_message_to_json_string(service_id), len(response.connections), grpc_message_to_json_string(response))) + assert len(response.connections) == 1 # one connection per service + + +def test_service_removal(context_client : ContextClient, osm_wim : MockOSM): # pylint: disable=redefined-outer-name + # ----- Start the EventsCollector ---------------------------------------------------------------------------------- + events_collector = EventsCollector(context_client, log_events_received=True) + events_collector.start() + + # ----- Delete Service --------------------------------------------------------------------------------------------- + response = context_client.ListServiceIds(ContextId(**CONTEXT_ID)) + LOGGER.info('Services[{:d}] = {:s}'.format(len(response.service_ids), grpc_message_to_json_string(response))) + assert len(response.service_ids) == 2 # L3NM + TAPI + service_uuids = set() + for service_id in response.service_ids: + service_uuid = service_id.service_uuid.uuid + if service_uuid.endswith(':optical'): continue + service_uuids.add(service_uuid) + osm_wim.conn_info[service_uuid] = {} + + assert len(service_uuids) == 1 # assume a single service has been created + service_uuid = set(service_uuids).pop() + + osm_wim.delete_connectivity_service(service_uuid) + + # ----- Validate collected events ---------------------------------------------------------------------------------- + packet_connection_uuid = '{:s}:{:s}'.format(service_uuid, DEVTYPE_EMU_PR) + optical_connection_uuid = '{:s}:optical:{:s}'.format(service_uuid, DEVTYPE_XR_CONSTELLATION) + optical_service_uuid = '{:s}:optical'.format(service_uuid) + + expected_events = [ + ('ConnectionEvent', EVENT_REMOVE, json_connection_id(packet_connection_uuid)), + ('DeviceEvent', EVENT_UPDATE, json_device_id(DEVICE_R1_UUID)), + ('DeviceEvent', EVENT_UPDATE, json_device_id(DEVICE_R3_UUID)), + ('ServiceEvent', EVENT_REMOVE, json_service_id(service_uuid, context_id=CONTEXT_ID)), + ('ConnectionEvent', EVENT_REMOVE, json_connection_id(optical_connection_uuid)), + ('DeviceEvent', EVENT_UPDATE, json_device_id(DEVICE_X1_UUID)), + ('ServiceEvent', EVENT_REMOVE, json_service_id(optical_service_uuid, context_id=CONTEXT_ID)), + ] + check_events(events_collector, expected_events) + + # ----- Stop the EventsCollector ----------------------------------------------------------------------------------- + events_collector.stop() + + +def test_services_removed(context_client : ContextClient): # pylint: disable=redefined-outer-name + # ----- List entities - Ensure service is removed ------------------------------------------------------------------ + response = context_client.ListContexts(Empty()) + assert len(response.contexts) == len(CONTEXTS) + + response = context_client.ListTopologies(ContextId(**CONTEXT_ID)) + assert len(response.topologies) == len(TOPOLOGIES) + + response = context_client.ListDevices(Empty()) + assert len(response.devices) == len(DEVICES) + + response = context_client.ListLinks(Empty()) + assert len(response.links) == len(LINKS) + + response = context_client.ListServices(ContextId(**CONTEXT_ID)) + assert len(response.services) == 0