From 4630c247f79e4ef8283194d9f254e0350eff827b Mon Sep 17 00:00:00 2001 From: Ville Hallivuori Date: Tue, 7 Feb 2023 12:36:40 +0200 Subject: [PATCH 1/3] * Support IPM API 0.8.31 (removal of component identifier from REST API object URLs) * Support TeraFlow 2.0 changed service URL format * Support TeraFlow 2.0 persistence (handle delete requests for persisted objects properly) --- src/device/service/drivers/xr/README_XR.md | 52 ++++++++++++---- src/device/service/drivers/xr/XrDriver.py | 6 +- src/device/service/drivers/xr/cm-cli.py | 0 .../service/drivers/xr/cm/cm_connection.py | 34 +++++------ .../service/drivers/xr/cm/connection.py | 3 + .../drivers/xr/cm/tests/test_cm_connection.py | 4 +- .../xr/cm/tests/test_xr_service_set_config.py | 60 +++++++++---------- 7 files changed, 98 insertions(+), 61 deletions(-) mode change 100644 => 100755 src/device/service/drivers/xr/cm-cli.py diff --git a/src/device/service/drivers/xr/README_XR.md b/src/device/service/drivers/xr/README_XR.md index 3bfdf5b01..108a36769 100644 --- a/src/device/service/drivers/xr/README_XR.md +++ b/src/device/service/drivers/xr/README_XR.md @@ -25,6 +25,19 @@ cd ~/.kube microk8s config > config ``` +Helm 3 is mandatory as of February 2023. Enable it with microk8s command. Then create wrapper shell script to expose it with standard name: + +``` +sudo su - +cat > /usr/bin/helm3 +#!/bin/sh +microk8s.helm3 "$@" +^D +chmod 755 /usr/bin/helm3 +``` + +Using symbolic link does not work, because snap wraps the real binary and won't work if name is different. + Local Docker registry is needed for build results. Use the following command to start local registry (docker will pull necessary images from Internet) ```bash @@ -32,23 +45,33 @@ docker run -d -p 32000:5000 --restart=always --name registry registry:2 ``` Setup mydeploy script outside the git repo. E.g. following will do. SOURCE IT ON ALL SHELLS. - -IMPORTANT: September 2022 version of controller has a bug where any update to device trigger update to device -until GRPC endpoints are so loaded that K8s kills device service. XR does not need automation service, so it can -be left out. +Use https://labs.etsi.org/rep/tfs/controller/-/blob/develop/my_deploy.sh as example. +Script requires more variables than before as of February 2023. ```bash +# See https://labs.etsi.org/rep/tfs/controller/-/blob/develop/my_deploy.sh +# Use docker run -d -p 32000:5000 --restart=always --name registry registry:2 export TFS_REGISTRY_IMAGE="http://localhost:32000/tfs/" -# Without automation service (see note above) -export TFS_COMPONENTS="context device pathcomp service slice compute monitoring webui" -# Correct setting -# export TFS_COMPONENTS="context device automation pathcomp service slice compute monitoring webui" -# Pre-rebase -#export TFS_COMPONENTS="context device automation service compute monitoring webui" +export TFS_COMPONENTS="context device automation monitoring pathcomp service slice compute webui load_generator" export TFS_IMAGE_TAG="dev" export TFS_K8S_NAMESPACE="tfs" export TFS_EXTRA_MANIFESTS="manifests/nginx_ingress_http.yaml" export TFS_GRAFANA_PASSWORD="admin123+" +#export TFS_SKIP_BUILD="" +export CRDB_NAMESPACE="crdb" +export CRDB_USERNAME="tfs" +export CRDB_PASSWORD="tfs123" +export CRDB_DATABASE="tfs" +export CRDB_DEPLOY_MODE="single" +export CRDB_DROP_DATABASE_IF_EXISTS="" +export CRDB_REDEPLOY="" +export NATS_NAMESPACE="nats" +export NATS_REDEPLOY="" +export QDB_NAMESPACE="qdb" +export QDB_USERNAME="admin" +export QDB_PASSWORD="quest" +export QDB_TABLE="tfs_monitoring" +export QDB_REDEPLOY="" ``` Build is containerized, pytest used for setup is not. Teraflow has some third party venv suggestion in docs. However standard venv works. Create: @@ -119,6 +142,15 @@ Good logs to check are: * kubectl logs service/deviceservice --namespace tfs * kubectl logs service/webuiservice --namespace tfs +New 2.0 version of Teraflow has persistent database. To clean up any failed state +(e.g. from debugging session), set before deploy: + +``` +export CRDB_DROP_DATABASE_IF_EXISTS=YES +``` + +In normal test runs it is not necessary to clear the database. However DO NOT RE-UPLOAD THE TOPOLOGY JSON FILE if DB has not been cleared. + ## Unit Tests Run in src directory (src under repo top level) with command: diff --git a/src/device/service/drivers/xr/XrDriver.py b/src/device/service/drivers/xr/XrDriver.py index 565e3692f..605f4ce8d 100644 --- a/src/device/service/drivers/xr/XrDriver.py +++ b/src/device/service/drivers/xr/XrDriver.py @@ -106,8 +106,10 @@ class XrDriver(_Driver): def SetConfig(self, resources: List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: LOGGER.info(f"SetConfig[{self}]: {resources=}") # Logged config seems like: + # Pre-February 2023 #[('/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"}')] - + # Post February 2023 + #[('/services/service[e1b9184c-767d-44b9-bf83-a1f643d82bef]', '{"capacity_unit": "GHz", "capacity_value": 50.0, "direction": "UNIDIRECTIONAL", "input_sip": "XR LEAF 1|XR-T1", "layer_protocol_name": "PHOTONIC_MEDIA", "layer_protocol_qualifier": "tapi-photonic-media:PHOTONIC_LAYER_QUALIFIER_NMC", "output_sip": "XR HUB 1|XR-T4", "uuid": "e1b9184c-767d-44b9-bf83-a1f643d82bef"}')] with self.__lock: if self.__constellation is None: self.__constellation = self.__cm_connection.get_constellation_by_hub_name(self.__hub_module_name) @@ -157,7 +159,7 @@ class XrDriver(_Driver): else: LOGGER.info(f"DeleteConfig: Connection {service_uuid} delete failure (was {str(connection)})") - if self.__constellation.is_vti_mode(): + if connection.is_vti_mode(): active_tc = self.__cm_connection.get_transport_capacity_by_teraflow_uuid(service_uuid) if active_tc is not None: if self.__cm_connection.delete_transport_capacity(active_tc.href): diff --git a/src/device/service/drivers/xr/cm-cli.py b/src/device/service/drivers/xr/cm-cli.py old mode 100644 new mode 100755 diff --git a/src/device/service/drivers/xr/cm/cm_connection.py b/src/device/service/drivers/xr/cm/cm_connection.py index 8ee9ee236..712849451 100644 --- a/src/device/service/drivers/xr/cm/cm_connection.py +++ b/src/device/service/drivers/xr/cm/cm_connection.py @@ -241,7 +241,7 @@ class CmConnection: return self.__acquire_access_token() def list_constellations(self) -> List[Constellation]: - r = self.__get("/api/v1/ns/xr-networks?content=expanded") + r = self.__get("/api/v1/xr-networks?content=expanded") if not r.is_valid_json_list_with_status(200): return [] return [Constellation(c) for c in r.json] @@ -252,13 +252,13 @@ class CmConnection: ('content', 'expanded'), ('q', '{"hubModule.state.module.moduleName": "' + hub_module_name + '"}') ] - r = self.__get("/api/v1/ns/xr-networks?content=expanded", params=qparams) + r = self.__get("/api/v1/xr-networks?content=expanded", params=qparams) if not r.is_valid_json_list_with_status(200, 1, 1): return None return Constellation(r.json[0]) def get_transport_capacities(self) -> List[TransportCapacity]: - r= self.__get("/api/v1/ns/transport-capacities?content=expanded") + r= self.__get("/api/v1/transport-capacities?content=expanded") if not r.is_valid_json_list_with_status(200): return [] return [TransportCapacity(from_json=t) for t in r.json] @@ -268,7 +268,7 @@ class CmConnection: ('content', 'expanded'), ('q', '{"state.name": "' + tc_name + '"}') ] - r = self.__get("/api/v1/ns/transport-capacities?content=expanded", params=qparams) + r = self.__get("/api/v1/transport-capacities?content=expanded", params=qparams) if not r.is_valid_json_list_with_status(200, 1, 1): return TransportCapacity(from_json=r.json[0]) else: @@ -280,17 +280,17 @@ class CmConnection: def create_transport_capacity(self, tc: TransportCapacity) -> Optional[str]: # Create wants a list, so wrap connection to list tc_config = [tc.create_config()] - resp = self.__post("/api/v1/ns/transport-capacities", tc_config) + resp = self.__post("/api/v1/transport-capacities", tc_config) if resp.is_valid_json_list_with_status(202, 1, 1) and "href" in resp.json[0]: tc.href = resp.json[0]["href"] LOGGER.info(f"Created transport-capcity {tc}") - #LOGGER.info(self.__get(f"/api/v1/ns/transport-capacities{tc.href}?content=expanded")) + #LOGGER.info(self.__get(f"/api/v1/transport-capacities{tc.href}?content=expanded")) return tc.href else: return None def delete_transport_capacity(self, href: str) -> bool: - resp = self.__delete(f"/api/v1/ns/transport-capacities{href}") + resp = self.__delete(f"/api/v1/transport-capacities{href}") # Returns empty body if resp.is_valid_with_status_ignore_body(202): @@ -399,7 +399,7 @@ class CmConnection: # Create wants a list, so wrap connection to list cfg = [connection.create_config()] - resp = self.__post("/api/v1/ncs/network-connections", cfg) + resp = self.__post("/api/v1/network-connections", cfg) if resp.is_valid_json_list_with_status(202, 1, 1) and "href" in resp.json[0]: connection.href = resp.json[0]["href"] LOGGER.info(f"IPM accepted create request for connection {connection}") @@ -433,7 +433,7 @@ class CmConnection: # Perform deletes for ep_href in ep_deletes: - resp = self.__delete(f"/api/v1/ncs{ep_href}") + resp = self.__delete(f"/api/v1{ep_href}") if resp.is_valid_with_status_ignore_body(202): LOGGER.info(f"update_connection: EP-UPDATE: Deleted connection endpoint {ep_href}") else: @@ -441,21 +441,21 @@ class CmConnection: # Update capacities for otherwise similar endpoints for ep_href, ep_cfg in ep_updates: - resp = self.__put(f"/api/v1/ncs{ep_href}", ep_cfg) + resp = self.__put(f"/api/v1{ep_href}", ep_cfg) if resp.is_valid_with_status_ignore_body(202): LOGGER.info(f"update_connection: EP-UPDATE: Updated connection endpoint {ep_href} with {ep_cfg}") else: LOGGER.info(f"update_connection: EP-UPDATE: Failed to update connection endpoint {ep_href} with {ep_cfg}: {resp}") # Perform adds - resp = self.__post(f"/api/v1/ncs{href}/endpoints", ep_creates) + resp = self.__post(f"/api/v1{href}/endpoints", ep_creates) if resp.is_valid_json_list_with_status(202, 1, 1) and "href" in resp.json[0]: LOGGER.info(f"update_connection: EP-UPDATE: Created connection endpoints {resp.json[0]} with {ep_creates}") else: LOGGER.info(f"update_connection: EP-UPDATE: Failed to create connection endpoints {resp.json[0] if resp.json else None} with {ep_creates}: {resp}") # Connection update (excluding endpoints) - resp = self.__put(f"/api/v1/ncs{href}", cfg) + resp = self.__put(f"/api/v1{href}", cfg) # Returns empty body if resp.is_valid_with_status_ignore_body(202): LOGGER.info(f"update_connection: Updated connection {connection}") @@ -466,7 +466,7 @@ class CmConnection: return None def delete_connection(self, href: str) -> bool: - resp = self.__delete(f"/api/v1/ncs{href}") + resp = self.__delete(f"/api/v1{href}") #print(resp) # Returns empty body if resp.is_valid_with_status_ignore_body(202): @@ -489,7 +489,7 @@ class CmConnection: ('content', 'expanded'), ('q', '{"state.name": "' + connection_name + '"}') ] - r = self.__get("/api/v1/ncs/network-connections", params=qparams) + r = self.__get("/api/v1/network-connections", params=qparams) if r.is_valid_json_list_with_status(200, 1, 1): return Connection(from_json=r.json[0]) else: @@ -499,7 +499,7 @@ class CmConnection: qparams = [ ('content', 'expanded'), ] - r = self.__get(f"/api/v1/ncs{href}", params=qparams) + r = self.__get(f"/api/v1{href}", params=qparams) if r.is_valid_json_obj_with_status(200): return Connection(from_json=r.json) else: @@ -509,14 +509,14 @@ class CmConnection: return self.get_connection_by_name(f"TF:{uuid}") def get_connections(self): - r = self.__get("/api/v1/ncs/network-connections?content=expanded") + r = self.__get("/api/v1/network-connections?content=expanded") if r.is_valid_json_list_with_status(200): return [Connection(from_json=c) for c in r.json] else: return [] def service_uuid(self, key: str) -> Optional[str]: - service = re.match(r"^/service\[(.+)\]$", key) + service = re.match(r"^(?:/services)/service\[(.+)\]$", key) if service: return service.group(1) else: diff --git a/src/device/service/drivers/xr/cm/connection.py b/src/device/service/drivers/xr/cm/connection.py index 98736cce5..321922b1c 100644 --- a/src/device/service/drivers/xr/cm/connection.py +++ b/src/device/service/drivers/xr/cm/connection.py @@ -165,6 +165,9 @@ class Connection: endpoints = ", ".join((str(ep) for ep in self.endpoints)) return f"name: {name}, id: {self.href}, service-mode: {self.serviceMode}, end-points: [{endpoints}]" + def is_vti_mode(self) -> bool: + return "XR-VTI-P2P" == self.serviceMode + def __guess_service_mode_from_emulated_enpoints(self): for ep in self.endpoints: if ep.vlan is not None: diff --git a/src/device/service/drivers/xr/cm/tests/test_cm_connection.py b/src/device/service/drivers/xr/cm/tests/test_cm_connection.py index 4f45be686..a7944ed22 100644 --- a/src/device/service/drivers/xr/cm/tests/test_cm_connection.py +++ b/src/device/service/drivers/xr/cm/tests/test_cm_connection.py @@ -59,7 +59,7 @@ def test_cmc_connect(): def test_cmc_get_constellations(): with mock_cm_connectivity() as m: - m.get("https://127.0.0.1:9999/api/v1/ns/xr-networks?content=expanded", text=res_constellations) + m.get("https://127.0.0.1:9999/api/v1/xr-networks?content=expanded", text=res_constellations) cm = CmConnection("127.0.0.1", 9999, "xr-user", "xr-password", tls_verify=False) assert cm.Connect() @@ -73,7 +73,7 @@ def test_cmc_get_constellations(): ['XR HUB 1|XR-T1', 'XR HUB 1|XR-T2', 'XR HUB 1|XR-T3', 'XR HUB 1|XR-T4', 'XR LEAF 1|XR-T1', 'XR LEAF 2|XR-T1']] # Get constellation by hub module name - m.get("https://127.0.0.1:9999/api/v1/ns/xr-networks?content=expanded&content=expanded&q=%7B%22hubModule.state.module.moduleName%22%3A+%22XR+HUB+1%22%7D", text=res_constellation_by_name_hub1) + m.get("https://127.0.0.1:9999/api/v1/xr-networks?content=expanded&content=expanded&q=%7B%22hubModule.state.module.moduleName%22%3A+%22XR+HUB+1%22%7D", text=res_constellation_by_name_hub1) constellation = cm.get_constellation_by_hub_name("XR HUB 1") assert constellation assert constellation.ifnames() == ['XR HUB 1|XR-T1', 'XR HUB 1|XR-T2', 'XR HUB 1|XR-T3', 'XR HUB 1|XR-T4', 'XR LEAF 1|XR-T1', 'XR LEAF 2|XR-T1'] diff --git a/src/device/service/drivers/xr/cm/tests/test_xr_service_set_config.py b/src/device/service/drivers/xr/cm/tests/test_xr_service_set_config.py index 3bfd63def..e9b16b620 100644 --- a/src/device/service/drivers/xr/cm/tests/test_xr_service_set_config.py +++ b/src/device/service/drivers/xr/cm/tests/test_xr_service_set_config.py @@ -38,8 +38,8 @@ with open(os.path.join(resources, "connections-expanded.json"), "r", encoding="U def mock_cm(): m = requests_mock.Mocker() m.post('https://127.0.0.1:9999/realms/xr-cm/protocol/openid-connect/token', text=access_token) - m.get("https://127.0.0.1:9999/api/v1/ns/xr-networks?content=expanded&content=expanded&q=%7B%22hubModule.state.module.moduleName%22%3A+%22XR+HUB+1%22%7D", text=res_constellation_by_name_hub1) - m.post("https://127.0.0.1:9999/api/v1/ncs/network-connections", text='[{"href":"/network-connections/c3b31608-0bb7-4a4f-9f9a-88b24a059432","rt":["cm.network-connection"]}]', status_code=202) + m.get("https://127.0.0.1:9999/api/v1/xr-networks?content=expanded&content=expanded&q=%7B%22hubModule.state.module.moduleName%22%3A+%22XR+HUB+1%22%7D", text=res_constellation_by_name_hub1) + m.post("https://127.0.0.1:9999/api/v1/network-connections", text='[{"href":"/network-connections/c3b31608-0bb7-4a4f-9f9a-88b24a059432","rt":["cm.network-connection"]}]', status_code=202) return m uuid = "12345ABCDEFGHIJKLMN" @@ -69,9 +69,9 @@ def test_xr_set_config(): called_mocks = [(r._request.method, r._request.url) for r in m._adapter.request_history] expected_mocks = [ ('POST', 'https://127.0.0.1:9999/realms/xr-cm/protocol/openid-connect/token'), # Authentication - ('GET', 'https://127.0.0.1:9999/api/v1/ns/xr-networks?content=expanded&content=expanded&q=%7B%22hubModule.state.module.moduleName%22%3A+%22XR+HUB+1%22%7D'), # Hub module by name - ('GET', 'https://127.0.0.1:9999/api/v1/ncs/network-connections?content=expanded&q=%7B%22state.name%22%3A+%22TF%3A12345ABCDEFGHIJKLMN%22%7D'), # Get by name, determine update or create - ('POST', 'https://127.0.0.1:9999/api/v1/ncs/network-connections') # Create + ('GET', 'https://127.0.0.1:9999/api/v1/xr-networks?content=expanded&content=expanded&q=%7B%22hubModule.state.module.moduleName%22%3A+%22XR+HUB+1%22%7D'), # Hub module by name + ('GET', 'https://127.0.0.1:9999/api/v1/network-connections?content=expanded&q=%7B%22state.name%22%3A+%22TF%3A12345ABCDEFGHIJKLMN%22%7D'), # Get by name, determine update or create + ('POST', 'https://127.0.0.1:9999/api/v1/network-connections') # Create ] assert called_mocks == expected_mocks @@ -97,7 +97,7 @@ def test_xr_set_config_consistency_lifecycle(): json_non_terminal = copy.deepcopy(json_terminal) json_non_terminal["state"]["lifecycleState"] = "pendingConfiguration" # We go trough 404 and non-terminal lstate first and then terminal state. - m.get("https://127.0.0.1:9999/api/v1/ncs/network-connections/c3b31608-0bb7-4a4f-9f9a-88b24a059432", + m.get("https://127.0.0.1:9999/api/v1/network-connections/c3b31608-0bb7-4a4f-9f9a-88b24a059432", [{'text': '', 'status_code': 404}, { 'json': json_non_terminal, 'status_code': 200 }, {'json': json_terminal, 'status_code': 200 }]) @@ -108,19 +108,19 @@ def test_xr_set_config_consistency_lifecycle(): called_mocks = [(r._request.method, r._request.url) for r in m._adapter.request_history] expected_mocks = [ ('POST', 'https://127.0.0.1:9999/realms/xr-cm/protocol/openid-connect/token'), # Authentication - ('GET', 'https://127.0.0.1:9999/api/v1/ns/xr-networks?content=expanded&content=expanded&q=%7B%22hubModule.state.module.moduleName%22%3A+%22XR+HUB+1%22%7D'), # Hub module by name - ('GET', 'https://127.0.0.1:9999/api/v1/ncs/network-connections?content=expanded&q=%7B%22state.name%22%3A+%22TF%3A12345ABCDEFGHIJKLMN%22%7D'), # Get by name, determine update or create - ('POST', 'https://127.0.0.1:9999/api/v1/ncs/network-connections'), # Create - ('GET', 'https://127.0.0.1:9999/api/v1/ncs/network-connections/c3b31608-0bb7-4a4f-9f9a-88b24a059432?content=expanded'), # Life cycle state check --> no REST API object - ('GET', 'https://127.0.0.1:9999/api/v1/ncs/network-connections/c3b31608-0bb7-4a4f-9f9a-88b24a059432?content=expanded'), # Life cycle state check --> non-terminal - ('GET', 'https://127.0.0.1:9999/api/v1/ncs/network-connections/c3b31608-0bb7-4a4f-9f9a-88b24a059432?content=expanded') # Life cycle state check --> terminal + ('GET', 'https://127.0.0.1:9999/api/v1/xr-networks?content=expanded&content=expanded&q=%7B%22hubModule.state.module.moduleName%22%3A+%22XR+HUB+1%22%7D'), # Hub module by name + ('GET', 'https://127.0.0.1:9999/api/v1/network-connections?content=expanded&q=%7B%22state.name%22%3A+%22TF%3A12345ABCDEFGHIJKLMN%22%7D'), # Get by name, determine update or create + ('POST', 'https://127.0.0.1:9999/api/v1/network-connections'), # Create + ('GET', 'https://127.0.0.1:9999/api/v1/network-connections/c3b31608-0bb7-4a4f-9f9a-88b24a059432?content=expanded'), # Life cycle state check --> no REST API object + ('GET', 'https://127.0.0.1:9999/api/v1/network-connections/c3b31608-0bb7-4a4f-9f9a-88b24a059432?content=expanded'), # Life cycle state check --> non-terminal + ('GET', 'https://127.0.0.1:9999/api/v1/network-connections/c3b31608-0bb7-4a4f-9f9a-88b24a059432?content=expanded') # Life cycle state check --> terminal ] assert called_mocks == expected_mocks ################################################################################ # Same as before, but without life cycle progress m.reset_mock() - m.get("https://127.0.0.1:9999/api/v1/ncs/network-connections/c3b31608-0bb7-4a4f-9f9a-88b24a059432", + m.get("https://127.0.0.1:9999/api/v1/network-connections/c3b31608-0bb7-4a4f-9f9a-88b24a059432", [{'text': '', 'status_code': 401}, { 'json': json_non_terminal, 'status_code': 200 }]) @@ -129,10 +129,10 @@ def test_xr_set_config_consistency_lifecycle(): called_mocks = [(r._request.method, r._request.url) for r in m._adapter.request_history] expected_mocks_no_connect = [ - ('GET', 'https://127.0.0.1:9999/api/v1/ncs/network-connections?content=expanded&q=%7B%22state.name%22%3A+%22TF%3A12345ABCDEFGHIJKLMN%22%7D'), # Get by name, determine update or create - ('POST', 'https://127.0.0.1:9999/api/v1/ncs/network-connections'), # Create - ('GET', 'https://127.0.0.1:9999/api/v1/ncs/network-connections/c3b31608-0bb7-4a4f-9f9a-88b24a059432?content=expanded'), # Life cycle state check --> no REST API object - ('GET', 'https://127.0.0.1:9999/api/v1/ncs/network-connections/c3b31608-0bb7-4a4f-9f9a-88b24a059432?content=expanded'), # Life cycle state check --> non-terminal + ('GET', 'https://127.0.0.1:9999/api/v1/network-connections?content=expanded&q=%7B%22state.name%22%3A+%22TF%3A12345ABCDEFGHIJKLMN%22%7D'), # Get by name, determine update or create + ('POST', 'https://127.0.0.1:9999/api/v1/network-connections'), # Create + ('GET', 'https://127.0.0.1:9999/api/v1/network-connections/c3b31608-0bb7-4a4f-9f9a-88b24a059432?content=expanded'), # Life cycle state check --> no REST API object + ('GET', 'https://127.0.0.1:9999/api/v1/network-connections/c3b31608-0bb7-4a4f-9f9a-88b24a059432?content=expanded'), # Life cycle state check --> non-terminal ] assert called_mocks == repeat_last_expected(expected_mocks_no_connect, called_mocks) @@ -143,7 +143,7 @@ def test_xr_set_config_consistency_lifecycle(): assert cm.Connect() constellation = cm.get_constellation_by_hub_name("XR HUB 1") assert constellation - m.get("https://127.0.0.1:9999/api/v1/ncs/network-connections/c3b31608-0bb7-4a4f-9f9a-88b24a059432", + m.get("https://127.0.0.1:9999/api/v1/network-connections/c3b31608-0bb7-4a4f-9f9a-88b24a059432", [{'text': '', 'status_code': 401}, { 'json': json_non_terminal, 'status_code': 200 }]) result = set_config_for_service(cm, constellation, uuid, config) @@ -158,7 +158,7 @@ def test_xr_set_config_consistency_lifecycle(): assert cm.Connect() constellation = cm.get_constellation_by_hub_name("XR HUB 1") assert constellation - m.get("https://127.0.0.1:9999/api/v1/ncs/network-connections/c3b31608-0bb7-4a4f-9f9a-88b24a059432", + m.get("https://127.0.0.1:9999/api/v1/network-connections/c3b31608-0bb7-4a4f-9f9a-88b24a059432", [{'text': '', 'status_code': 401}]) result = set_config_for_service(cm, constellation, uuid, config) _validate_result(result, False) @@ -175,15 +175,15 @@ def test_xr_set_config_update_case(): assert constellation # Fake existing service (--> update path is taken) - m.get("https://127.0.0.1:9999/api/v1/ncs/network-connections?content=expanded&q=%7B%22state.name%22%3A+%22TF%3A12345ABCDEFGHIJKLMN%22%7D", json=res_connection_by_name_json) + m.get("https://127.0.0.1:9999/api/v1/network-connections?content=expanded&q=%7B%22state.name%22%3A+%22TF%3A12345ABCDEFGHIJKLMN%22%7D", json=res_connection_by_name_json) # Delete endpoint that is no longer necessary - m.delete("https://127.0.0.1:9999/api/v1/ncs/network-connections/4505d5d3-b2f3-40b8-8ec2-4a5b28523c03/endpoints/1d58ba8f-4d51-4213-83e1-97a0e0bdd388", text="", status_code = 202) + m.delete("https://127.0.0.1:9999/api/v1/network-connections/4505d5d3-b2f3-40b8-8ec2-4a5b28523c03/endpoints/1d58ba8f-4d51-4213-83e1-97a0e0bdd388", text="", status_code = 202) # Update changed endpoint - m.put("https://127.0.0.1:9999/api/v1/ncs/network-connections/4505d5d3-b2f3-40b8-8ec2-4a5b28523c03/endpoints/230516d0-7e38-44b1-b174-1ba7d4454ee6", text="", status_code = 202) + m.put("https://127.0.0.1:9999/api/v1/network-connections/4505d5d3-b2f3-40b8-8ec2-4a5b28523c03/endpoints/230516d0-7e38-44b1-b174-1ba7d4454ee6", text="", status_code = 202) # Create the newly added endpoint - m.post("https://127.0.0.1:9999/api/v1/ncs/network-connections/4505d5d3-b2f3-40b8-8ec2-4a5b28523c03/endpoints", json=[{"href":"/network-connections/4505d5d3-b2f3-40b8-8ec2-4a5b28523c03/endpoint/somethingplausible","rt":["plausible"]}], status_code=202) + m.post("https://127.0.0.1:9999/api/v1/network-connections/4505d5d3-b2f3-40b8-8ec2-4a5b28523c03/endpoints", json=[{"href":"/network-connections/4505d5d3-b2f3-40b8-8ec2-4a5b28523c03/endpoint/somethingplausible","rt":["plausible"]}], status_code=202) # Update the connection itself - m.put("https://127.0.0.1:9999/api/v1/ncs/network-connections/4505d5d3-b2f3-40b8-8ec2-4a5b28523c03", text="", status_code=202) + m.put("https://127.0.0.1:9999/api/v1/network-connections/4505d5d3-b2f3-40b8-8ec2-4a5b28523c03", text="", status_code=202) result = set_config_for_service(cm, constellation, uuid, config) _validate_result(result, True) @@ -191,11 +191,11 @@ def test_xr_set_config_update_case(): called_mocks = [(r._request.method, r._request.url) for r in m._adapter.request_history] expected_mocks = [ ('POST', 'https://127.0.0.1:9999/realms/xr-cm/protocol/openid-connect/token'), # Authentication - ('GET', 'https://127.0.0.1:9999/api/v1/ns/xr-networks?content=expanded&content=expanded&q=%7B%22hubModule.state.module.moduleName%22%3A+%22XR+HUB+1%22%7D'), # Hub module by name - ('GET', 'https://127.0.0.1:9999/api/v1/ncs/network-connections?content=expanded&q=%7B%22state.name%22%3A+%22TF%3A12345ABCDEFGHIJKLMN%22%7D'), # Get by name, determine update or create - ('DELETE', 'https://127.0.0.1:9999/api/v1/ncs/network-connections/4505d5d3-b2f3-40b8-8ec2-4a5b28523c03/endpoints/1d58ba8f-4d51-4213-83e1-97a0e0bdd388'), # Delete unnecessary endpoint - ('PUT', 'https://127.0.0.1:9999/api/v1/ncs/network-connections/4505d5d3-b2f3-40b8-8ec2-4a5b28523c03/endpoints/230516d0-7e38-44b1-b174-1ba7d4454ee6'), # Update changed endpoint - ('POST', 'https://127.0.0.1:9999/api/v1/ncs/network-connections/4505d5d3-b2f3-40b8-8ec2-4a5b28523c03/endpoints'), # Add new endpoint - ('PUT', 'https://127.0.0.1:9999/api/v1/ncs/network-connections/4505d5d3-b2f3-40b8-8ec2-4a5b28523c03') # Update the connection itself + ('GET', 'https://127.0.0.1:9999/api/v1/xr-networks?content=expanded&content=expanded&q=%7B%22hubModule.state.module.moduleName%22%3A+%22XR+HUB+1%22%7D'), # Hub module by name + ('GET', 'https://127.0.0.1:9999/api/v1/network-connections?content=expanded&q=%7B%22state.name%22%3A+%22TF%3A12345ABCDEFGHIJKLMN%22%7D'), # Get by name, determine update or create + ('DELETE', 'https://127.0.0.1:9999/api/v1/network-connections/4505d5d3-b2f3-40b8-8ec2-4a5b28523c03/endpoints/1d58ba8f-4d51-4213-83e1-97a0e0bdd388'), # Delete unnecessary endpoint + ('PUT', 'https://127.0.0.1:9999/api/v1/network-connections/4505d5d3-b2f3-40b8-8ec2-4a5b28523c03/endpoints/230516d0-7e38-44b1-b174-1ba7d4454ee6'), # Update changed endpoint + ('POST', 'https://127.0.0.1:9999/api/v1/network-connections/4505d5d3-b2f3-40b8-8ec2-4a5b28523c03/endpoints'), # Add new endpoint + ('PUT', 'https://127.0.0.1:9999/api/v1/network-connections/4505d5d3-b2f3-40b8-8ec2-4a5b28523c03') # Update the connection itself ] assert called_mocks == expected_mocks -- GitLab From b851152b92a12ae0e45179cc7604940acdf70d6c Mon Sep 17 00:00:00 2001 From: Ville Hallivuori Date: Thu, 9 Feb 2023 09:11:07 +0200 Subject: [PATCH 2/3] Clean up XR emulation json --- src/tests/ofc22/descriptors_emulated_xr.json | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/tests/ofc22/descriptors_emulated_xr.json b/src/tests/ofc22/descriptors_emulated_xr.json index d6a2f0234..4e247bb30 100644 --- a/src/tests/ofc22/descriptors_emulated_xr.json +++ b/src/tests/ofc22/descriptors_emulated_xr.json @@ -31,7 +31,10 @@ "device_config": {"config_rules": [ {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, - {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": "{\"endpoints\": [{\"sample_types\": [], \"type\": \"optical\", \"uuid\": \"13/0/0\"}, {\"sample_types\": [101, 102, 201, 202], \"type\": \"copper\", \"uuid\": \"13/1/2\"}]}"}} + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "13/0/0", "type": "optical", "sample_types": []}, + {"uuid": "13/1/2", "type": "copper", "sample_types": [101, 102, 201, 202]} + ]}}} ]}, "device_operational_status": 1, "device_drivers": [0], @@ -43,7 +46,10 @@ "device_config": {"config_rules": [ {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, - {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": "{\"endpoints\": [{\"sample_types\": [], \"type\": \"optical\", \"uuid\": \"13/0/0\"}, {\"sample_types\": [101, 102, 201, 202], \"type\": \"copper\", \"uuid\": \"13/1/2\"}]}"}} + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "13/0/0", "type": "optical", "sample_types": []}, + {"uuid": "13/1/2", "type": "copper", "sample_types": [101, 102, 201, 202]} + ]}}} ]}, "device_operational_status": 1, "device_drivers": [0], @@ -55,7 +61,10 @@ "device_config": {"config_rules": [ {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, - {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": "{\"endpoints\": [{\"sample_types\": [], \"type\": \"optical\", \"uuid\": \"13/0/0\"}, {\"sample_types\": [101, 102, 201, 202], \"type\": \"copper\", \"uuid\": \"13/1/2\"}]}"}} + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "13/0/0", "type": "optical", "sample_types": []}, + {"uuid": "13/1/2", "type": "copper", "sample_types": [101, 102, 201, 202]} + ]}}} ]}, "device_operational_status": 1, "device_drivers": [0], @@ -67,7 +76,10 @@ "device_config": {"config_rules": [ {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, - {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": "{\"endpoints\": [{\"sample_types\": [], \"type\": \"optical\", \"uuid\": \"13/0/0\"}, {\"sample_types\": [101, 102, 201, 202], \"type\": \"copper\", \"uuid\": \"13/1/2\"}]}"}} + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "13/0/0", "type": "optical", "sample_types": []}, + {"uuid": "13/1/2", "type": "copper", "sample_types": [101, 102, 201, 202]} + ]}}} ]}, "device_operational_status": 1, "device_drivers": [0], -- GitLab From 542c5abaeb32d3efc1637fc7f9f33aa770fe862d Mon Sep 17 00:00:00 2001 From: Ville Hallivuori Date: Fri, 10 Feb 2023 14:26:06 +0200 Subject: [PATCH 3/3] Add service-cli tool for improved testing of XR services --- src/device/service/drivers/xr/README_XR.md | 12 ++ src/device/service/drivers/xr/service-cli.py | 144 +++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100755 src/device/service/drivers/xr/service-cli.py diff --git a/src/device/service/drivers/xr/README_XR.md b/src/device/service/drivers/xr/README_XR.md index 108a36769..c741c3e80 100644 --- a/src/device/service/drivers/xr/README_XR.md +++ b/src/device/service/drivers/xr/README_XR.md @@ -137,6 +137,18 @@ Setup service by following commands in src directory. Kubernetes endpoins change python -m pytest --verbose tests/ofc22/tests/test_functional_create_service_xr.py ``` +For topology different than used by the test_functional_create/delete_service_xr.py, one can also +use service-cli.py tool in the xr module directory. It allows creation of ELINE services between +arbitrary endpoints in the topology (with consequent underlying XR service instantiation). Run in +*xr module directory*. Representative examples: +``` + PYTHONPATH=../../../../ ./service-cli.py create 1 R1-EMU 13/1/2 500 2 R3-EMU 13/1/2 500 + PYTHONPATH=../../../../ ./service-cli.py list + PYTHONPATH=../../../../ ./service-cli.py delete 43a8046a-5dec-463d-82f7-7cc3442dbf4f +``` +The PYTHONPATH is mandatory. Suitable topology JSON must have been loaded before. With the +CocroachDB persistence, it is sufficient to load the topology once and it will persist. + Good logs to check are: * kubectl logs service/deviceservice --namespace tfs diff --git a/src/device/service/drivers/xr/service-cli.py b/src/device/service/drivers/xr/service-cli.py new file mode 100755 index 000000000..01bd2aaa1 --- /dev/null +++ b/src/device/service/drivers/xr/service-cli.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +#pylint: disable=invalid-name, missing-function-docstring, line-too-long, logging-fstring-interpolation, missing-class-docstring, missing-module-docstring +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Manage L2 services (with underlying XR connectivity) without need to use unit test +# files or excessive JSON definitions +# +# Run in this directory with PYTHONPATH=../../../../ +# E.g.: +# PYTHONPATH=../../../../ ./service-cli.py create 1 R1-EMU 13/1/2 500 2 R3-EMU 13/1/2 500 +# PYTHONPATH=../../../../ ./service-cli.py list +# PYTHONPATH=../../../../ ./service-cli.py delete 43a8046a-5dec-463d-82f7-7cc3442dbf4f + + +import argparse +import logging +import traceback +from contextlib import contextmanager + +from common.Settings import get_setting +from context.client.ContextClient import ContextClient +from tests.tools.mock_osm.MockOSM import MockOSM +from common.proto.context_pb2 import ContextId, ServiceTypeEnum, ServiceStatusEnum +from common.tools.grpc.Tools import grpc_message_to_json_string + +LOGGER = logging.getLogger(__name__) + +WIM_USERNAME = 'admin' +WIM_PASSWORD = 'admin' + +@contextmanager +def make_context_client(): + try: + _client = ContextClient(get_setting('CONTEXTSERVICE_SERVICE_HOST'), get_setting('CONTEXTSERVICE_SERVICE_PORT_GRPC')) + yield _client + finally: + _client.close() + +def make_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) + +logging.basicConfig(level=logging.ERROR) + +parser = argparse.ArgumentParser(description='TF Service Management Utility') +subparsers = parser.add_subparsers(dest="command") +subparsers.required = True + +create_parser = subparsers.add_parser('create') +create_parser.add_argument('site1', type=int, help='One endpoint of the service, e.g. 1') +create_parser.add_argument('device1', type=str, help='One endpoint of the service, e.g. R1-EMU') +create_parser.add_argument('interface1', type=str, help='One endpoint of the service, e.g. 13/1/2') +create_parser.add_argument('vlan1', type=int, help='VLAN in first endpoint, e.g. 500') + +create_parser.add_argument('site2', type=int, help='One endpoint of the service, e.g. 2') +create_parser.add_argument('device2', type=str, help='One endpoint of the service, e.g. R3-EMU') +create_parser.add_argument('interface2', type=str, help='One endpoint of the service, e.g. 13/1/2') +create_parser.add_argument('vlan2', type=int, help='VLAN in first endpoint, e.g. 500') + +delete_parser = subparsers.add_parser('delete') +delete_parser.add_argument('service_uuid', type=str, help='UUID of the service to be deleted') + +list_parser = subparsers.add_parser('list') + +args = parser.parse_args() + +WIM_SERVICE_TYPE = 'ELINE' +CONTEXT_ID = {'context_uuid': {'uuid': 'admin'}} + +if args.command == "create": + endpoint1 = f"{args.device1}:{args.interface1}" + endpoint2 = f"{args.device2}:{args.interface2}" + + WIM_MAPPING = [ + {'device-id': args.device1, 'service_endpoint_id': endpoint1, + 'service_mapping_info': {'bearer': {'bearer-reference': endpoint1}, 'site-id': args.site1}}, + {'device-id': args.device2, 'service_endpoint_id': endpoint2, + 'service_mapping_info': {'bearer': {'bearer-reference': endpoint2}, 'site-id': args.site2}}, + ] + WIM_SERVICE_CONNECTION_POINTS = [ + {'service_endpoint_id': endpoint1, + 'service_endpoint_encapsulation_type': 'dot1q', + 'service_endpoint_encapsulation_info': {'vlan': args.vlan1}}, + {'service_endpoint_id': endpoint2, + 'service_endpoint_encapsulation_type': 'dot1q', + 'service_endpoint_encapsulation_info': {'vlan': args.vlan2}}, + ] +else: + WIM_MAPPING = [] + WIM_SERVICE_CONNECTION_POINTS = [] + +#print(str(args)) +print(f"=== WIM_SERVICE_TYPE: {WIM_SERVICE_TYPE}") +print(f"=== WIM_SERVICE_CONNECTION_POINTS: {WIM_SERVICE_CONNECTION_POINTS}") +print(f"=== WIM_MAPPING: {WIM_MAPPING}") + +with make_context_client() as client: + osm_wim = make_osm_wim(); + + if args.command == "create": + service_uuid = osm_wim.create_connectivity_service(WIM_SERVICE_TYPE, WIM_SERVICE_CONNECTION_POINTS) + print(f"*** Create connectivity service --> {service_uuid}") + status = osm_wim.get_connectivity_service_status(service_uuid) + print(f"*** Get created service status --> {str(status)}") + + elif args.command == "delete": + osm_wim.wim.check_credentials() + try: + osm_wim.wim.delete_connectivity_service(args.service_uuid) + print(f"*** Service {args.service_uuid} is no longer present (delete was successfull or service did not exist)") + except Exception as e: + print(f"*** Failed to delete service {args.service_uuid}, {e}") + elif args.command == "list": + response = client.ListServices(ContextId(**CONTEXT_ID)) + + #print('Services[{:d}] = {:s}'.format(len(response.services), grpc_message_to_json_string(response))) + for service in response.services: + scs = "" + + # See if there are endpoint constraints that might be regognizable by the user. + # Keys do not necessarily exist, so catch exceptions and ignore those constraints + # that we cannot easily represent. + for sc in service.service_constraints: + try: + scs += f"{sc.endpoint_location.endpoint_id.device_id.device_uuid.uuid}:{sc.endpoint_location.endpoint_id.endpoint_uuid.uuid} " + except Exception: + pass + + print(f"{service.service_id.service_uuid.uuid:36} {ServiceTypeEnum.Name(service.service_type):40} {ServiceStatusEnum.Name(service.service_status.service_status)} {scs}") + + -- GitLab