Commit a5004f44 authored by Lluis Gifre Renom's avatar Lluis Gifre Renom
Browse files

Merge branch 'feat/xr_device_drive_tf_20_ipm_api_0_8_13' into 'develop'

feat/xr_device_drive_tf_20_ipm_api_0_8_13

See merge request !60
parents 0c1d58ef 542c5aba
Loading
Loading
Loading
Loading
+54 −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:
@@ -114,11 +137,32 @@ 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
* 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