Commit 5c2ad17c authored by Shayan Hajipour's avatar Shayan Hajipour
Browse files

Merge branch 'feat/240-cttc-driver-required-to-interact-with-ietf-slice' into camara-integration

parents a1cb3462 f8d22900
Loading
Loading
Loading
Loading
+0 −172
Original line number Diff line number Diff line
# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
from typing import Dict, List, Optional

import requests
from requests.auth import HTTPBasicAuth

from device.service.driver_api.ImportTopologyEnum import ImportTopologyEnum

GET_DEVICES_URL = "{:s}://{:s}:{:d}/tfs-api/devices"
GET_LINKS_URL = "{:s}://{:s}:{:d}/tfs-api/links"
IETF_SLICE_URL = "{:s}://{:s}:{:d}/restconf/data/ietf-network-slice-service:ietf-nss"
TIMEOUT = 30

HTTP_OK_CODES = {
    200,  # OK
    201,  # Created
    202,  # Accepted
    204,  # No Content
}

MAPPING_STATUS = {
    "DEVICEOPERATIONALSTATUS_UNDEFINED": 0,
    "DEVICEOPERATIONALSTATUS_DISABLED": 1,
    "DEVICEOPERATIONALSTATUS_ENABLED": 2,
}

MAPPING_DRIVER = {
    "DEVICEDRIVER_UNDEFINED": 0,
    "DEVICEDRIVER_OPENCONFIG": 1,
    "DEVICEDRIVER_TRANSPORT_API": 2,
    "DEVICEDRIVER_P4": 3,
    "DEVICEDRIVER_IETF_NETWORK_TOPOLOGY": 4,
    "DEVICEDRIVER_ONF_TR_532": 5,
    "DEVICEDRIVER_XR": 6,
    "DEVICEDRIVER_IETF_L2VPN": 7,
    "DEVICEDRIVER_GNMI_OPENCONFIG": 8,
    "DEVICEDRIVER_OPTICAL_TFS": 9,
    "DEVICEDRIVER_IETF_ACTN": 10,
    "DEVICEDRIVER_OC": 11,
}

MSG_ERROR = "Could not retrieve devices in remote TeraFlowSDN instance({:s}). status_code={:s} reply={:s}"

LOGGER = logging.getLogger(__name__)


class TfsApiClient:
    def __init__(
        self,
        address: str,
        port: int,
        scheme: str = "http",
        username: Optional[str] = None,
        password: Optional[str] = None,
    ) -> None:
        self._devices_url = GET_DEVICES_URL.format(scheme, address, port)
        self._links_url = GET_LINKS_URL.format(scheme, address, port)
        self._slice_url = IETF_SLICE_URL.format(scheme, address, port)
        self._auth = None
        # (
        #     HTTPBasicAuth(username, password)
        #     if username is not None and password is not None
        #     else None
        # )

    # def get_devices_endpoints(
    #     self, import_topology: ImportTopologyEnum = ImportTopologyEnum.DEVICES
    # ) -> List[Dict]:
    #     LOGGER.debug("[get_devices_endpoints] begin")
    #     LOGGER.debug(
    #         "[get_devices_endpoints] import_topology={:s}".format(str(import_topology))
    #     )

    #     reply = requests.get(self._devices_url, timeout=TIMEOUT, auth=self._auth)
    #     if reply.status_code not in HTTP_OK_CODES:
    #         msg = MSG_ERROR.format(
    #             str(self._devices_url), str(reply.status_code), str(reply)
    #         )
    #         LOGGER.error(msg)
    #         raise Exception(msg)

    #     if import_topology == ImportTopologyEnum.DISABLED:
    #         raise Exception(
    #             "Unsupported import_topology mode: {:s}".format(str(import_topology))
    #         )

    #     result = list()
    #     for json_device in reply.json()["devices"]:
    #         device_uuid: str = json_device["device_id"]["device_uuid"]["uuid"]
    #         device_type: str = json_device["device_type"]
    #         device_status = json_device["device_operational_status"]
    #         device_url = "/devices/device[{:s}]".format(device_uuid)
    #         device_data = {
    #             "uuid": json_device["device_id"]["device_uuid"]["uuid"],
    #             "name": json_device["name"],
    #             "type": device_type,
    #             "status": MAPPING_STATUS[device_status],
    #             "drivers": [
    #                 MAPPING_DRIVER[driver] for driver in json_device["device_drivers"]
    #             ],
    #         }
    #         result.append((device_url, device_data))

    #         for json_endpoint in json_device["device_endpoints"]:
    #             endpoint_uuid = json_endpoint["endpoint_id"]["endpoint_uuid"]["uuid"]
    #             endpoint_url = "/endpoints/endpoint[{:s}]".format(endpoint_uuid)
    #             endpoint_data = {
    #                 "device_uuid": device_uuid,
    #                 "uuid": endpoint_uuid,
    #                 "name": json_endpoint["name"],
    #                 "type": json_endpoint["endpoint_type"],
    #             }
    #             result.append((endpoint_url, endpoint_data))

    #     if import_topology == ImportTopologyEnum.DEVICES:
    #         LOGGER.debug("[get_devices_endpoints] devices only; returning")
    #         return result

    #     reply = requests.get(self._links_url, timeout=TIMEOUT, auth=self._auth)
    #     if reply.status_code not in HTTP_OK_CODES:
    #         msg = MSG_ERROR.format(
    #             str(self._links_url), str(reply.status_code), str(reply)
    #         )
    #         LOGGER.error(msg)
    #         raise Exception(msg)

    #     for json_link in reply.json()["links"]:
    #         link_uuid: str = json_link["link_id"]["link_uuid"]["uuid"]
    #         link_url = "/links/link[{:s}]".format(link_uuid)
    #         link_endpoint_ids = [
    #             (
    #                 json_endpoint_id["device_id"]["device_uuid"]["uuid"],
    #                 json_endpoint_id["endpoint_uuid"]["uuid"],
    #             )
    #             for json_endpoint_id in json_link["link_endpoint_ids"]
    #         ]
    #         link_data = {
    #             "uuid": json_link["link_id"]["link_uuid"]["uuid"],
    #             "name": json_link["name"],
    #             "endpoints": link_endpoint_ids,
    #         }
    #         result.append((link_url, link_data))

    #     LOGGER.debug("[get_devices_endpoints] topology; returning")
    # #     return resu

    def create_slice(self, slice_data: dict) -> None:
        try:
            requests.post(self._slice_url, json=slice_data)
        except requests.exceptions.ConnectionError:
            raise Exception("faild to send post request to TFS L3VPN NBI")

    def delete_slice(self, slice_uuid: str) -> None:
        url = self.__url + f"/vpn-service={slice_uuid}"
        try:
            requests.delete(url, auth=self._auth)
        except requests.exceptions.ConnectionError:
            raise Exception("faild to send delete request to TFS L3VPN NBI")
+1 −194
Original line number Diff line number Diff line
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from typing import Any, Dict, Optional, Tuple, TypedDict
from typing import Any, Dict, Optional, Tuple

import requests

@@ -25,199 +25,6 @@ from .Constants import SPECIAL_RESOURCE_MAPPINGS
LOGGER = logging.getLogger(__name__)


def service_exists(wim_url: str, auth, service_uuid: str) -> bool:
    try:
        get_connectivity_service(wim_url, auth, service_uuid)
        return True
    except:  # pylint: disable=bare-except
        return False


def get_all_active_connectivity_services(wim_url: str, auth):
    try:
        LOGGER.info("Sending get all connectivity services")
        servicepoint = f"{wim_url}/restconf/data/ietf-l3vpn-svc:l3vpn-svc/vpn-services"
        response = requests.get(servicepoint, auth=auth)

        if response.status_code != requests.codes.ok:
            raise Exception(
                "Unable to get all connectivity services",
                http_code=response.status_code,
            )

        return response
    except requests.exceptions.ConnectionError:
        raise Exception("Request Timeout", http_code=408)


def get_connectivity_service(wim_url, auth, service_uuid):
    try:
        LOGGER.info("Sending get connectivity service")
        servicepoint = f"{wim_url}/restconf/data/ietf-l3vpn-svc:l3vpn-svc/vpn-services/vpn-service={service_uuid}"

        response = requests.get(servicepoint)

        if response.status_code != requests.codes.ok:
            raise Exception(
                "Unable to get connectivity service{:s}".format(str(service_uuid)),
                http_code=response.status_code,
            )

        return response
    except requests.exceptions.ConnectionError:
        raise Exception("Request Timeout", http_code=408)


def create_slice_datamodel(resource_value: dict) -> dict:
    src_node_id: str = resource_value["src_node_id"]
    src_mgmt_ip_address: str = resource_value["src_mgmt_ip_address"]
    src_ac_node_id: str = resource_value["src_ac_node_id"]
    src_ac_ep_id: str = resource_value["src_ac_ep_id"]
    src_vlan: str = resource_value["src_vlan"]

    dst_node_id: str = resource_value["dst_node_id"]
    dst_mgmt_ip_address: str = resource_value["dst_mgmt_ip_address"]
    dst_ac_node_id: str = resource_value["dst_ac_node_id"]
    dst_ac_ep_id: str = resource_value["dst_ac_ep_id"]
    dst_vlan: str = resource_value["dst_vlan"]

    slice_id: str = resource_value["slice_id"]
    delay: str = resource_value["delay"]
    bandwidth: str = resource_value["bandwidth"]
    packet_loss: str = resource_value["packet_loss"]

    sdps = [
        {
            "id": "1",
            "node-id": src_node_id,
            "sdp-ip-address": [src_mgmt_ip_address],
            "service-match-criteria": {
                "match-criterion": [
                    {
                        "index": 1,
                        "match-type": [
                            {
                                "type": "ietf-network-slice-service:vlan",
                                "value": [src_vlan],
                            },
                        ],
                        "target-connection-group-id": "line1",
                    }
                ]
            },
            "attachment-circuits": {
                "attachment-circuit": [
                    {
                        "id": "0",
                        "description": "dsc",
                        "ac-node-id": src_ac_node_id,
                        "ac-tp-id": src_ac_ep_id,
                    }
                ]
            },
        },
        {
            "id": "2",
            "node-id": dst_node_id,
            "sdp-ip-address": [dst_mgmt_ip_address],
            "service-match-criteria": {
                "match-criterion": [
                    {
                        "index": 1,
                        "match-type": [
                            {
                                "type": "ietf-network-slice-service:vlan",
                                "value": [dst_vlan],
                            },
                        ],
                        "target-connection-group-id": "line1",
                    }
                ]
            },
            "attachment-circuits": {
                "attachment-circuit": [
                    {
                        "id": "0",
                        "description": "dsc",
                        "ac-node-id": dst_ac_node_id,
                        "ac-tp-id": dst_ac_ep_id,
                    }
                ]
            },
        },
    ]

    connection_groups = [
        {
            "id": "line1",
            "connectivity-type": "point-to-point",
            "connectivity-construct": [
                {
                    "id": 1,
                    "p2mp-sender-sdp": "1",
                    "p2mp-receiver-sdp": ["2"],
                    "service-slo-sle-policy": {
                        "slo-policy": {
                            "metric-bound": [
                                {
                                    "metric-type": "ietf-network-slice-service:one-way-delay-maximum",
                                    "metric-unit": "milliseconds",
                                    "bound": delay,
                                },
                                {
                                    "metric-type": "ietf-network-slice-service:one-way-bandwidth",
                                    "metric-unit": "Mbps",
                                    "bound": bandwidth,
                                },
                                {
                                    "metric-type": "ietf-network-slice-service:two-way-packet-loss",
                                    "metric-unit": "percentage",
                                    "percentile-value": packet_loss,
                                },
                            ]
                        }
                    },
                },
                {
                    "id": 2,
                    "p2mp-sender-sdp": "2",
                    "p2mp-receiver-sdp": ["1"],
                    "service-slo-sle-policy": {
                        "slo-policy": {
                            "metric-bound": [
                                {
                                    "metric-type": "ietf-network-slice-service:one-way-delay-maximum",
                                    "metric-unit": "milliseconds",
                                    "bound": delay,
                                },
                                {
                                    "metric-type": "ietf-network-slice-service:one-way-bandwidth",
                                    "metric-unit": "Mbps",
                                    "bound": bandwidth,
                                },
                                {
                                    "metric-type": "ietf-network-slice-service:two-way-packet-loss",
                                    "metric-unit": "percentage",
                                    "percentile-value": packet_loss,
                                },
                            ]
                        }
                    },
                },
            ],
        }
    ]

    slice_service = {
        "id": slice_id,
        "description": "dsc",
        "sdps": {"sdp": sdps},
        "connection-groups": {"connection-group": connection_groups},
    }
    slice_data_model = {"network-slice-services": {"slice-service": [slice_service]}}
    return slice_data_model


def process_optional_string_field(
    endpoint_data: Dict[str, Any],
    field_name: str,
+18 −27
Original line number Diff line number Diff line
@@ -41,11 +41,8 @@ from device.service.driver_api.ImportTopologyEnum import (
)

from .Constants import SPECIAL_RESOURCE_MAPPINGS
from .TfsApiClient import TfsApiClient
from .Tools import (
    compose_resource_endpoint,
    service_exists,
)
from .tfs_slice_nbi_client import TfsApiClient
from .Tools import compose_resource_endpoint

LOGGER = logging.getLogger(__name__)

@@ -145,8 +142,7 @@ class IetfSliceDriver(_Driver):
            if self.__started.is_set():
                return True
            try:
                # requests.get(url, timeout=self.__timeout, auth=self.__auth)
                ...
                requests.get(url, timeout=self.__timeout)
            except requests.exceptions.Timeout:
                LOGGER.exception("Timeout connecting {:s}".format(url))
                return False
@@ -195,7 +191,6 @@ class IetfSliceDriver(_Driver):
                        (resource_key, e)
                    )  # if validation fails, store the exception
                    continue

                resource_node = get_subnode(
                    resolver, self.__running, resource_path, default=None
                )
@@ -229,27 +224,23 @@ class IetfSliceDriver(_Driver):
                    continue
                try:
                    resource_value = json.loads(resource_value)

                    slice_name = resource_value["network-slice-services"][
                        "slice-service"
                    ][0]["connection-groups"]["connection-group"]
                    if operation_type == "create":
                        # create the underlying service
                        # self.tac.create_slice(resource_value)
                        ...
                    elif (
                        len(
                            resource_value["network-slice-services"]["slice-service"][
                                0
                            ]["connection-groups"]["connection-group"]
                        )
                        == 0
                        and operation_type == "update"
                    ):
                        # Remove the IP transport service
                        # self.tac.remove_slice(service_uuid)
                        ...
                        self.tac.create_slice(resource_value)
                    elif operation_type == "update":
                        # update the underlying service bandwidth
                        # self.tac.update_slice(resource_value)
                        ...
                        connection_groups = resource_value["network-slice-services"][
                            "slice-service"
                        ][0]["connection-groups"]["connection-group"]
                        if len(connection_groups) != 1:
                            raise Exception("only one connection group is supported")
                        connection_group = connection_groups[0]
                        self.tac.update_slice(
                            slice_name, connection_group["id"], connection_group
                        )
                    elif operation_type == "delete":
                        self.tac.delete_slice(slice_name)
                    results.append((resource_key, True))
                except Exception as e:  # pylint: disable=broad-except
                    LOGGER.exception(
+71 −0
Original line number Diff line number Diff line
# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
from typing import Optional

import requests
from requests.auth import HTTPBasicAuth

IETF_SLICE_URL = "{:s}://{:s}:{:d}/restconf/data/ietf-network-slice-service:ietf-nss"
TIMEOUT = 30

LOGGER = logging.getLogger(__name__)


class TfsApiClient:
    def __init__(
        self,
        address: str,
        port: int,
        scheme: str = "http",
        username: Optional[str] = None,
        password: Optional[str] = None,
    ) -> None:
        self._slice_url = IETF_SLICE_URL.format(scheme, address, port)
        self._auth = None
        # (
        #     HTTPBasicAuth(username, password)
        #     if username is not None and password is not None
        #     else None
        # )

    def create_slice(self, slice_data: dict) -> None:
        url = self._slice_url + "/network-slice-services"
        try:
            requests.post(url, json=slice_data)
        except requests.exceptions.ConnectionError:
            raise Exception("faild to send post request to TFS IETF Slice NBI")

    def update_slice(
        self,
        slice_name: str,
        connection_group_id: str,
        updated_connection_group_data: dict,
    ) -> None:
        url = (
            self._slice_url
            + f"/network-slice-services/slice-service={slice_name}/connection-groups/connection-group={connection_group_id}"
        )
        try:
            requests.put(url, json=updated_connection_group_data)
        except requests.exceptions.ConnectionError:
            raise Exception("faild to send update request to TFS IETF Slice NBI")

    def delete_slice(self, slice_name: str) -> None:
        url = self._slice_url + f"/network-slice-services/slice-service={slice_name}"
        try:
            requests.delete(url)
        except requests.exceptions.ConnectionError:
            raise Exception("faild to send delete request to TFS IETF Slice NBI")