Commit 4630c247 authored by Ville Hallivuori's avatar Ville Hallivuori
Browse files

* 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)
parent 30b0185c
Loading
Loading
Loading
Loading
+42 −10
Original line number Diff line number Diff line
@@ -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:

+4 −2
Original line number Diff line number Diff line
@@ -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):
+0 −0

File mode changed from 100644 to 100755.

+17 −17
Original line number Diff line number Diff line
@@ -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:
+3 −0
Original line number Diff line number Diff line
@@ -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:
Loading