From 87779f44cd6a224e9b233ee8a46f46097bc7974c Mon Sep 17 00:00:00 2001 From: Laskaratos Dimitris Date: Mon, 30 Jun 2025 10:16:21 +0300 Subject: [PATCH 1/7] Added fixes on edge cloud management api and deployment file --- .../Dockerfile | 3 + .../requirements.txt | 3 + .../src/adapters/__init__.py | 2 + .../src/adapters/common/__init__.py | 0 .../src/adapters/common/adapters_factory.py | 95 +++ .../src/adapters/common/sdk.py | 71 ++ .../src/adapters/common/sdk_factory.py | 90 ++ .../src/adapters/edgecloud/.env | 7 + .../src/adapters/edgecloud/__init__.py | 0 .../adapters/edgecloud/adapters/__init__.py | 0 .../edgecloud/adapters/aeros/__init__.py | 23 + .../edgecloud/adapters/aeros/client.py | 433 ++++++++++ .../edgecloud/adapters/aeros/config.py | 27 + .../adapters/aeros/continuum_client.py | 196 +++++ .../edgecloud/adapters/aeros/utils.py | 43 + .../src/adapters/edgecloud/adapters/errors.py | 5 + .../edgecloud/adapters/i2edge/__init__.py | 0 .../edgecloud/adapters/i2edge/client.py | 243 ++++++ .../edgecloud/adapters/i2edge/common.py | 100 +++ .../edgecloud/adapters/i2edge/schemas.py | 167 ++++ .../edgecloud/adapters/i2edge/utils.py | 152 ++++ .../edgecloud/adapters/kubernetes/__init__.py | 0 .../edgecloud/adapters/kubernetes/client.py | 263 ++++++ .../src/adapters/edgecloud/core/__init__.py | 0 .../edgecloud/core/edgecloud_interface.py | 123 +++ .../src/adapters/logger.py | 48 ++ .../src/adapters/network/__init__.py | 0 .../src/adapters/network/adapters/__init__.py | 0 .../src/adapters/network/adapters/errors.py | 3 + .../adapters/network/adapters/oai/__init__.py | 0 .../adapters/network/adapters/oai/client.py | 155 ++++ .../network/adapters/open5gcore/__init__.py | 0 .../network/adapters/open5gcore/client.py | 79 ++ .../network/adapters/open5gs/__init__.py | 0 .../network/adapters/open5gs/client.py | 96 +++ .../src/adapters/network/clients/__init__.py | 0 .../src/adapters/network/clients/errors.py | 3 + .../adapters/network/clients/oai/__init__.py | 0 .../adapters/network/clients/oai/client.py | 139 +++ .../network/clients/open5gcore/__init__.py | 0 .../network/clients/open5gcore/client.py | 49 ++ .../network/clients/open5gs/__init__.py | 0 .../network/clients/open5gs/client.py | 65 ++ .../src/adapters/network/core/__init__.py | 0 .../network/core/base_network_client.py | 469 ++++++++++ .../src/adapters/network/core/common.py | 122 +++ .../network/core/network_interface.py | 311 +++++++ .../src/adapters/network/core/schemas.py | 798 ++++++++++++++++++ .../src/adapters/o-ran/__init__.py | 0 .../src/adapters/o-ran/clients/__init__.py | 0 .../o-ran/clients/juniper-ric/__init__.py | 0 .../o-ran/clients/juniper-ric/client.py | 0 .../src/adapters/o-ran/core/__init__.py | 0 .../adapters/o-ran/core/o-ran_interface.py | 1 + .../src/adapters/oran/__init__.py | 0 .../src/adapters/oran/clients/__init__.py | 0 .../oran/clients/juniper_ric/__init__.py | 0 .../oran/clients/juniper_ric/client.py | 0 .../src/adapters/oran/core/__init__.py | 0 .../src/adapters/oran/core/oran_interface.py | 1 + .../controllers/app_instance_controller.py | 1 - .../edge_cloud_management_controller.py | 12 +- .../network_functions_controller.py | 17 +- .../src/controllers/nodes_controller.py | 24 +- .../src/controllers/operations_controller.py | 66 +- .../src/swagger/swagger.yaml | 45 +- srm-deployment.yaml | 503 +---------- sunrise6g-deployment.yaml | 193 +++++ 68 files changed, 4689 insertions(+), 557 deletions(-) create mode 100644 service-resource-manager-implementation/src/adapters/__init__.py create mode 100644 service-resource-manager-implementation/src/adapters/common/__init__.py create mode 100644 service-resource-manager-implementation/src/adapters/common/adapters_factory.py create mode 100644 service-resource-manager-implementation/src/adapters/common/sdk.py create mode 100644 service-resource-manager-implementation/src/adapters/common/sdk_factory.py create mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/.env create mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/__init__.py create mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/adapters/__init__.py create mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/__init__.py create mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/client.py create mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/config.py create mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/continuum_client.py create mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/utils.py create mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/adapters/errors.py create mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/__init__.py create mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/client.py create mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/common.py create mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/schemas.py create mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/utils.py create mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/adapters/kubernetes/__init__.py create mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/adapters/kubernetes/client.py create mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/core/__init__.py create mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/core/edgecloud_interface.py create mode 100644 service-resource-manager-implementation/src/adapters/logger.py create mode 100644 service-resource-manager-implementation/src/adapters/network/__init__.py create mode 100644 service-resource-manager-implementation/src/adapters/network/adapters/__init__.py create mode 100644 service-resource-manager-implementation/src/adapters/network/adapters/errors.py create mode 100644 service-resource-manager-implementation/src/adapters/network/adapters/oai/__init__.py create mode 100644 service-resource-manager-implementation/src/adapters/network/adapters/oai/client.py create mode 100644 service-resource-manager-implementation/src/adapters/network/adapters/open5gcore/__init__.py create mode 100644 service-resource-manager-implementation/src/adapters/network/adapters/open5gcore/client.py create mode 100644 service-resource-manager-implementation/src/adapters/network/adapters/open5gs/__init__.py create mode 100644 service-resource-manager-implementation/src/adapters/network/adapters/open5gs/client.py create mode 100644 service-resource-manager-implementation/src/adapters/network/clients/__init__.py create mode 100644 service-resource-manager-implementation/src/adapters/network/clients/errors.py create mode 100644 service-resource-manager-implementation/src/adapters/network/clients/oai/__init__.py create mode 100644 service-resource-manager-implementation/src/adapters/network/clients/oai/client.py create mode 100644 service-resource-manager-implementation/src/adapters/network/clients/open5gcore/__init__.py create mode 100644 service-resource-manager-implementation/src/adapters/network/clients/open5gcore/client.py create mode 100644 service-resource-manager-implementation/src/adapters/network/clients/open5gs/__init__.py create mode 100644 service-resource-manager-implementation/src/adapters/network/clients/open5gs/client.py create mode 100644 service-resource-manager-implementation/src/adapters/network/core/__init__.py create mode 100644 service-resource-manager-implementation/src/adapters/network/core/base_network_client.py create mode 100644 service-resource-manager-implementation/src/adapters/network/core/common.py create mode 100644 service-resource-manager-implementation/src/adapters/network/core/network_interface.py create mode 100644 service-resource-manager-implementation/src/adapters/network/core/schemas.py create mode 100644 service-resource-manager-implementation/src/adapters/o-ran/__init__.py create mode 100644 service-resource-manager-implementation/src/adapters/o-ran/clients/__init__.py create mode 100644 service-resource-manager-implementation/src/adapters/o-ran/clients/juniper-ric/__init__.py create mode 100644 service-resource-manager-implementation/src/adapters/o-ran/clients/juniper-ric/client.py create mode 100644 service-resource-manager-implementation/src/adapters/o-ran/core/__init__.py create mode 100644 service-resource-manager-implementation/src/adapters/o-ran/core/o-ran_interface.py create mode 100644 service-resource-manager-implementation/src/adapters/oran/__init__.py create mode 100644 service-resource-manager-implementation/src/adapters/oran/clients/__init__.py create mode 100644 service-resource-manager-implementation/src/adapters/oran/clients/juniper_ric/__init__.py create mode 100644 service-resource-manager-implementation/src/adapters/oran/clients/juniper_ric/client.py create mode 100644 service-resource-manager-implementation/src/adapters/oran/core/__init__.py create mode 100644 service-resource-manager-implementation/src/adapters/oran/core/oran_interface.py create mode 100644 sunrise6g-deployment.yaml diff --git a/service-resource-manager-implementation/Dockerfile b/service-resource-manager-implementation/Dockerfile index 3ef607e..fd4129c 100644 --- a/service-resource-manager-implementation/Dockerfile +++ b/service-resource-manager-implementation/Dockerfile @@ -29,6 +29,9 @@ ENV PYTHONUNBUFFERED=1 #RUN pip3 install --no-cache --upgrade pip setuptools +RUN python3 -m venv .venv +RUN source .venv/bin/activate + RUN pip3 install --upgrade pip RUN pip3 install wheel diff --git a/service-resource-manager-implementation/requirements.txt b/service-resource-manager-implementation/requirements.txt index c211cb2..235e5e2 100644 --- a/service-resource-manager-implementation/requirements.txt +++ b/service-resource-manager-implementation/requirements.txt @@ -34,3 +34,6 @@ psycopg2-binary #pandas==0.24.2 paramiko>=2.12.0 urllib3 +colorlog==6.8.2 +pydantic==2.10.6 +pydantic-extra-types==2.10.3 \ No newline at end of file diff --git a/service-resource-manager-implementation/src/adapters/__init__.py b/service-resource-manager-implementation/src/adapters/__init__.py new file mode 100644 index 0000000..c0e4bd9 --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from .common.sdk import Sdk diff --git a/service-resource-manager-implementation/src/adapters/common/__init__.py b/service-resource-manager-implementation/src/adapters/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service-resource-manager-implementation/src/adapters/common/adapters_factory.py b/service-resource-manager-implementation/src/adapters/common/adapters_factory.py new file mode 100644 index 0000000..42af9d2 --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/common/adapters_factory.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +## +# Copyright 2025-present by Software Networks Area, i2CAT. +# All rights reserved. +# +# This file is part of the Open SDK +# +# Contributors: +# - Adrián Pino Martínez (adrian.pino@i2cat.net) +## + +from src.adapters.edgecloud.adapters.aeros.client import ( + EdgeApplicationManager as AerosClient, +) +from src.adapters.edgecloud.adapters.i2edge.client import ( + EdgeApplicationManager as I2EdgeClient, +) +from src.adapters.edgecloud.adapters.kubernetes.client import ( + EdgeApplicationManager as kubernetesClient, +) +# from src.adapters.network.adapters.oai.client import ( +# NetworkManager as OaiCoreClient, +# ) +# from src.adapters.network.adapters.open5gcore.client import ( +# NetworkManager as Open5GCoreClient, +# ) +# from src.adapters.network.adapters.open5gs.client import ( +# NetworkManager as Open5GSClient, +# ) + + +def _edgecloud_adapters_factory(client_name: str, base_url: str, **kwargs): + if client_name == "i2edge": + if "flavour_id" not in kwargs: + raise ValueError("Missing required 'flavour_id' for i2edge client.") + + edge_cloud_factory = { + "aeros": lambda url, **kw: AerosClient(base_url=url, **kw), + "i2edge": lambda url, **kw: I2EdgeClient(base_url=url, **kw), + "kubernetes": lambda url, **kw: kubernetesClient(base_url=url, **kw), + } + try: + return edge_cloud_factory[client_name](base_url, **kwargs) + except KeyError: + raise ValueError( + f"Invalid edgecloud client '{client_name}'. Available: {list(edge_cloud_factory)}" + ) + + +# def _network_adapters_factory(client_name: str, base_url: str, **kwargs): +# if "scs_as_id" not in kwargs: +# raise ValueError("Missing required 'scs_as_id' for network adapters.") +# scs_as_id = kwargs.pop("scs_as_id") + +# network_factory = { +# "open5gs": lambda url, scs_id, **kw: Open5GSClient( +# base_url=url, scs_as_id=scs_id, **kw +# ), +# "oai": lambda url, scs_id, **kw: OaiCoreClient( +# base_url=url, scs_as_id=scs_id, **kw +# ), +# "open5gcore": lambda url, scs_id, **kw: Open5GCoreClient( +# base_url=url, scs_as_id=scs_id, **kw +# ), +# } +# try: +# return network_factory[client_name](base_url, scs_as_id, **kwargs) +# except KeyError: +# raise ValueError( +# f"Invalid network client '{client_name}'. Available: {list(network_factory)}" +# ) + + +# def _oran_adapters_factory(client_name: str, base_url: str): +# # TODO + + +class AdaptersFactory: + _domain_factories = { + "edgecloud": _edgecloud_adapters_factory, + # "network": _network_adapters_factory, + # "oran": _oran_adapters_factory, + } + + @classmethod + def instantiate_and_retrieve_adapters( + cls, domain: str, client_name: str, base_url: str, **kwargs + ): + try: + catalog = cls._domain_factories[domain] + except KeyError: + raise ValueError( + f"Unsupported domain '{domain}'. Supported: {list(cls._domain_factories)}" + ) + return catalog(client_name, base_url, **kwargs) \ No newline at end of file diff --git a/service-resource-manager-implementation/src/adapters/common/sdk.py b/service-resource-manager-implementation/src/adapters/common/sdk.py new file mode 100644 index 0000000..44ac013 --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/common/sdk.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +## +# Copyright 2025-present by Software Networks Area, i2CAT. +# All rights reserved. +# +# This file is part of the Open SDK +# +# Contributors: +# - Adrián Pino Martínez (adrian.pino@i2cat.net) +## +from typing import Dict + +from src.adapters.common.adapters_factory import AdaptersFactory + + +class Sdk: + @staticmethod + def create_adapters_from( + adapter_specs: Dict[str, Dict[str, str]], + ) -> Dict[str, object]: + """ + Create and return a dictionary of instantiated edgecloud/network/oran adapters + based on the provided specifications. + + Args: + adapter_specs (dict): A dictionary where each key is the client's domain (e.g., 'edgecloud', 'network'), + and each value is a dictionary containing: + - 'client_name' (str): The specific name of the client (e.g., 'i2edge', 'open5gs'). + - 'base_url' (str): The base URL for the client's API. + Additional parameters like 'scs_as_id' may also be included. + + Returns: + dict: A dictionary where keys are the 'client_name' (str) and values are + the instantiated client objects. + + # TODO: Update it + # Example: + >>> from src.common.universal_client_catalog import UniversalCatalogClient + >>> + >>> adapter_specs_example = { + >>> 'edgecloud': { + >>> 'client_name': 'i2edge', + >>> 'base_url': 'http://ip_edge_cloud:port', + >>> 'additionalEdgeCloudParamater1': 'example' + >>> }, + >>> 'network': { + >>> 'client_name': 'open5gs', + >>> 'base_url': 'http://ip_network:port', + >>> 'additionalNetworkParamater1': 'example' + >>> } + >>> } + >>> + """ + sdk_client = AdaptersFactory() + adapters = {} + + for domain, config in adapter_specs.items(): + client_name = config["client_name"] + base_url = config["base_url"] + + # Support of additional paramaters for specific adapters + kwargs = { + k: v for k, v in config.items() if k not in ("client_name", "base_url") + } + + client = sdk_client.instantiate_and_retrieve_adapters( + domain, client_name, base_url, **kwargs + ) + adapters[domain] = client + + return adapters diff --git a/service-resource-manager-implementation/src/adapters/common/sdk_factory.py b/service-resource-manager-implementation/src/adapters/common/sdk_factory.py new file mode 100644 index 0000000..0eaf792 --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/common/sdk_factory.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +## +# Copyright 2025-present by Software Networks Area, i2CAT. +# All rights reserved. +# +# This file is part of the Open SDK +# +# Contributors: +# - Adrián Pino Martínez (adrian.pino@i2cat.net) +## +from src.adapters.edgecloud.adapters.aeros.client import ( + EdgeApplicationManager as AerosClient, +) +from src.adapters.edgecloud.adapters.i2edge.client import ( + EdgeApplicationManager as I2EdgeClient, +) +from src.adapters.edgecloud.adapters.kubernetes.client import ( + EdgeApplicationManager as KubernetesClient, +) +# from src.clients.network.clients.oai.client import NetworkManager as OaiCoreClient +# from src.clients.network.clients.open5gcore.client import ( +# NetworkManager as Open5GCoreClient, +# ) +# from src.clients.network.clients.open5gs.client import ( +# NetworkManager as Open5GSClient, +# ) + +# + + +def _edgecloud_factory(client_name: str, base_url: str, **kwargs): + edge_cloud_factory = { + "aeros": lambda url, **kw: AerosClient(base_url=url, **kw), + "i2edge": lambda url: I2EdgeClient(base_url=url), + "kubernetes": lambda url, **kw: KubernetesClient(base_url=url, **kw), + } + try: + return edge_cloud_factory[client_name](base_url, **kwargs) + except KeyError: + raise ValueError( + f"Invalid edgecloud client '{client_name}'. Available: {list(edge_cloud_factory)}" + ) + + +# def _network_factory(client_name: str, base_url: str, **kwargs): +# if "scs_as_id" not in kwargs: +# raise ValueError("Missing required 'scs_as_id' for network clients.") +# scs_as_id = kwargs.pop("scs_as_id") + +# network_factory = { +# "open5gs": lambda url, scs_id, **kw: Open5GSClient( +# base_url=url, scs_as_id=scs_id, **kw +# ), +# "oai": lambda url, scs_id, **kw: OaiCoreClient( +# base_url=url, scs_as_id=scs_id, **kw +# ), +# "open5gcore": lambda url, scs_id, **kw: Open5GCoreClient( +# base_url=url, scs_as_id=scs_id, **kw +# ), +# } +# try: +# return network_factory[client_name](base_url, scs_as_id, **kwargs) +# except KeyError: +# raise ValueError( +# f"Invalid network client '{client_name}'. Available: {list(network_factory)}" +# ) + + +# def _oran_factory(client_name: str, base_url: str): +# # TODO + + +class SdkFactory: + _domain_factories = { + "edgecloud": _edgecloud_factory, + # "network": _network_factory, + # "oran": _oran_factory, + } + + @classmethod + def instantiate_and_retrieve_clients( + cls, domain: str, client_name: str, base_url: str, **kwargs + ): + try: + catalog = cls._domain_factories[domain] + except KeyError: + raise ValueError( + f"Unsupported domain '{domain}'. Supported: {list(cls._domain_factories)}" + ) + return catalog(client_name, base_url, **kwargs) diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/.env b/service-resource-manager-implementation/src/adapters/edgecloud/.env new file mode 100644 index 0000000..8bcda46 --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/edgecloud/.env @@ -0,0 +1,7 @@ +# #### Logging #### +# LOG_LEVEL="debug" +# LOG_FILE="edgecloud.log" + +#### EdgeCloud #### +# EDGE_CLOUD="i2edge" +EDGE_CLOUD_URL=http://192.168.123.86:30769 diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/__init__.py b/service-resource-manager-implementation/src/adapters/edgecloud/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/__init__.py b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/__init__.py b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/__init__.py new file mode 100644 index 0000000..47625e8 --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/__init__.py @@ -0,0 +1,23 @@ +""" +aerOS client + This module provides a client for interacting with the aerOS REST API. + It includes methods for onboarding/deploying applications, + and querying aerOS continuum entities + aerOS domain is exposed as zones + aerOS services and service components are exposed as applications + Client is initialized with a base URL for the aerOS API + and an access token for authentication. +""" + +from src.adapters.edgecloud.adapters.aeros import config +from src.adapters.logger import setup_logger + +logger = setup_logger(__name__, is_debug=True, file_name=config.LOG_FILE) + +# TODO: The following should only appear in case aerOS client is used +# Currently even if another client is used, the logs appear +# logger.info("aerOS client initialized") +# logger.debug("aerOS API URL: %s", config.aerOS_API_URL) +# logger.debug("aerOS access token: %s", config.aerOS_ACCESS_TOKEN) +# logger.debug("aerOS debug mode: %s", config.DEBUG) +# logger.debug("aerOS log file: %s", config.LOG_FILE) diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/client.py b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/client.py new file mode 100644 index 0000000..5d05af2 --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/client.py @@ -0,0 +1,433 @@ +## +# This file is part of the Open SDK +# +# Contributors: +# - Vasilis Pitsilis (vpitsilis@dat.demokritos.gr, vpitsilis@iit.demokritos.gr) +# - Andreas Sakellaropoulos (asakellaropoulos@iit.demokritos.gr) +## +import uuid +from typing import Any, Dict, List, Optional + +import yaml + +from src.adapters.edgecloud.adapters.aeros import config +from src.adapters.edgecloud.adapters.aeros.continuum_client import ContinuumClient +from src.adapters.edgecloud.adapters.errors import EdgeCloudPlatformError +from src.adapters.edgecloud.core.edgecloud_interface import ( + EdgeCloudManagementInterface, +) +from src.adapters.logger import setup_logger + + +class EdgeApplicationManager(EdgeCloudManagementInterface): + """ + aerOS Continuum Client + FIXME: Handle None responses from continuum client + """ + + def __init__(self, base_url: str, **kwargs): + self.base_url = base_url + self.logger = setup_logger(__name__, is_debug=True, file_name=config.LOG_FILE) + self._app_store: Dict[str, Dict] = {} + self._deployed_services: Dict[str, List[str]] = {} + self._stopped_services: Dict[str, List[str]] = {} + + # Overwrite config values if provided via kwargs + if "aerOS_API_URL" in kwargs: + config.aerOS_API_URL = kwargs["aerOS_API_URL"] + if "aerOS_ACCESS_TOKEN" in kwargs: + config.aerOS_ACCESS_TOKEN = kwargs["aerOS_ACCESS_TOKEN"] + if "aerOS_HLO_TOKEN" in kwargs: + config.aerOS_HLO_TOKEN = kwargs["aerOS_HLO_TOKEN"] + + if not config.aerOS_API_URL: + raise ValueError("Missing 'aerOS_API_URL'") + if not config.aerOS_ACCESS_TOKEN: + raise ValueError("Missing 'aerOS_ACCESS_TOKEN'") + if not config.aerOS_HLO_TOKEN: + raise ValueError("Missing 'aerOS_HLO_TOKEN'") + + def onboard_app(self, app_manifest: Dict) -> Dict: + app_id = app_manifest.get("appId") + if not app_id: + raise EdgeCloudPlatformError("Missing 'appId' in app manifest") + + if app_id in self._app_store: + raise EdgeCloudPlatformError( + f"Application with id '{app_id}' already exists" + ) + + self._app_store[app_id] = app_manifest + self.logger.debug("Onboarded application with id: %s", app_id) + return {"appId": app_id} + + def get_all_onboarded_apps(self) -> List[Dict]: + self.logger.debug("Onboarded applications: %s", list(self._app_store.keys())) + return list(self._app_store.values()) + + def get_onboarded_app(self, app_id: str) -> Dict: + if app_id not in self._app_store: + raise EdgeCloudPlatformError( + f"Application with id '{app_id}' does not exist" + ) + self.logger.debug("Retrieved application with id: %s", app_id) + return self._app_store[app_id] + + def delete_onboarded_app(self, app_id: str) -> None: + if app_id not in self._app_store: + raise EdgeCloudPlatformError( + f"Application with id '{app_id}' does not exist" + ) + service_instances = self._stopped_services.get(app_id, []) + self.logger.debug( + "Deleting application with id: %s and instances: %s", + app_id, + service_instances, + ) + for service_instance in service_instances: + self._purge_deployed_app_from_continuum(service_instance) + self.logger.debug( + "successfully purged service instance: %s", service_instance + ) + del self._stopped_services[app_id] # Clean up stopped services + del self._app_store[app_id] # Remove from onboarded apps + + def _generate_service_id(self, app_id: str) -> str: + return f"urn:ngsi-ld:Service:{app_id}-{uuid.uuid4().hex[:4]}" + + def _generate_tosca_yaml_dict( + self, app_manifest: Dict, app_zones: List[Dict] + ) -> Dict: + component = app_manifest.get("componentSpec", [{}])[0] + component_name = component.get("componentName", "application") + + image_path = app_manifest.get("appRepo", {}).get("imagePath", "") + image_file = image_path.split("/")[-1] + repository_url = ( + "/".join(image_path.split("/")[:-1]) if "/" in image_path else "docker_hub" + ) + zone_id = ( + app_zones[0].get("EdgeCloudZone", {}).get("edgeCloudZoneId", "default-zone") + ) + + # Extract minNodeMemory + min_node_memory = ( + app_manifest.get("requiredResources", {}) + .get("applicationResources", {}) + .get("cpuPool", {}) + .get("topology", {}) + .get("minNodeMemory", 1024) + ) + + ports = {} + for iface in component.get("networkInterfaces", []): + interface_id = iface.get("interfaceId", "default") + protocol = iface.get("protocol", "TCP").lower() + port = iface.get("port", 8080) + ports[interface_id] = { + "properties": {"protocol": [protocol], "source": port} + } + + expose_ports = any( + iface.get("visibilityType") == "VISIBILITY_EXTERNAL" + for iface in component.get("networkInterfaces", []) + ) + + yaml_dict = { + "tosca_definitions_version": "tosca_simple_yaml_1_3", + "description": f"TOSCA for {app_manifest.get('name', 'application')}", + "serviceOverlay": False, + "node_templates": { + component_name: { + "type": "tosca.nodes.Container.Application", + "isJob": False, + "requirements": [ + { + "network": { + "properties": { + "ports": ports, + "exposePorts": expose_ports, + } + } + }, + { + "host": { + "node_filter": { + "capabilities": [ + { + "host": { + "properties": { + "cpu_arch": {"equal": "x64"}, + "realtime": {"equal": False}, + "cpu_usage": { + "less_or_equal": "0.1" + }, + "mem_size": { + "greater_or_equal": str( + min_node_memory + ) + }, + "domain_id": {"equal": zone_id}, + } + } + } + ], + "properties": None, + } + } + }, + ], + "artifacts": { + "application_image": { + "file": image_file, + "type": "tosca.artifacts.Deployment.Image.Container.Docker", + "is_private": False, + "repository": repository_url, + } + }, + "interfaces": { + "Standard": { + "create": { + "implementation": "application_image", + "inputs": {"cliArgs": [], "envVars": []}, + } + } + }, + } + }, + } + + return yaml_dict + + def deploy_app(self, app_id: str, app_zones: List[Dict]) -> Dict: + # 1. Get app CAMARA manifest + app_manifest = self._app_store.get(app_id) + if not app_manifest: + raise EdgeCloudPlatformError( + f"Application with id '{app_id}' does not exist" + ) + + # 2. Generate unique service ID + service_id = self._generate_service_id(app_id) + + # 3. Convert dict to YAML string + yaml_dict = self._generate_tosca_yaml_dict(app_manifest, app_zones) + tosca_yaml = yaml.dump(yaml_dict, sort_keys=False) + self.logger.info("Generated TOSCA YAML:") + self.logger.info(tosca_yaml) + + # 4. Instantiate client and call continuum to deploy service + aeros_client = ContinuumClient(self.base_url) + response = aeros_client.onboard_and_deploy_service(service_id, tosca_yaml) + + if "serviceId" not in response: + raise EdgeCloudPlatformError( + "Invalid response from onboard_service: missing 'serviceId'" + ) + + # 5. Track deployment + if app_id not in self._deployed_services: + self._deployed_services[app_id] = [] + self._deployed_services[app_id].append(service_id) + + # 6. Return expected format + return {"appInstanceId": response["serviceId"]} + + def get_all_deployed_apps( + self, + app_id: Optional[str] = None, + app_instance_id: Optional[str] = None, + region: Optional[str] = None, + ) -> List[Dict]: + deployed = [] + for stored_app_id, instance_ids in self._deployed_services.items(): + for instance_id in instance_ids: + deployed.append({"appId": stored_app_id, "appInstanceId": instance_id}) + return deployed + + def _purge_deployed_app_from_continuum(self, app_id: str) -> None: + aeros_client = ContinuumClient(self.base_url) + response = aeros_client.purge_service(app_id) + if response: + self.logger.debug("Purged deployed application with id: %s", app_id) + else: + raise EdgeCloudPlatformError( + f"Failed to purg service with id from the continuum '{app_id}'" + ) + + def undeploy_app(self, app_instance_id: str) -> None: + # 1. Locate app_id corresponding to this instance + found_app_id = None + for app_id, instances in self._deployed_services.items(): + if app_instance_id in instances: + found_app_id = app_id + break + + if not found_app_id: + raise EdgeCloudPlatformError( + f"No deployed app instance with ID '{app_instance_id}' found" + ) + + # 2. Call the external undeploy_service + aeros_client = ContinuumClient(self.base_url) + try: + aeros_client.undeploy_service(app_instance_id) + except Exception as e: + raise EdgeCloudPlatformError( + f"Failed to undeploy app instance '{app_instance_id}': {str(e)}" + ) from e + + # We could do it here with a little wait but better all instances in the same app are purged at once + # 3. Purge the deployed app from continuum + # self._purge_deployed_app_from_continuum(app_instance_id) + + # 4. Clean up internal tracking + self._deployed_services[found_app_id].remove(app_instance_id) + # Add instance to _stopped_services to purge it later + if found_app_id not in self._stopped_services: + self._stopped_services[found_app_id] = [] + self._stopped_services[found_app_id].append(app_instance_id) + # If app has no instances left, remove it from deployed services + if not self._deployed_services[found_app_id]: + del self._deployed_services[found_app_id] + + def get_edge_cloud_zones( + self, region: Optional[str] = None, status: Optional[str] = None + ) -> List[Dict]: + aeros_client = ContinuumClient(self.base_url) + ngsild_params = "type=Domain&format=simplified" + aeros_domains = aeros_client.query_entities(ngsild_params) + return [ + { + "zoneId": domain["id"], + "status": domain["domainStatus"].split(":")[-1].lower(), + "geographyDetails": "NOT_USED", + } + for domain in aeros_domains + ] + + def get_edge_cloud_zones_details( + self, zone_id: str, flavour_id: Optional[str] = None + ) -> Dict: + """ + Get details of a specific edge cloud zone. + :param zone_id: The ID of the edge cloud zone + :param flavour_id: Optional flavour ID to filter the results + :return: Details of the edge cloud zone + """ + # Minimal mocked response based on required fields of 'ZoneRegisteredData' in GSMA OPG E/WBI API + # return { + # "zoneId": + # zone_id, + # "reservedComputeResources": [{ + # "cpuArchType": "ISA_X86_64", + # "numCPU": "4", + # "memory": 8192, + # }], + # "computeResourceQuotaLimits": [{ + # "cpuArchType": "ISA_X86_64", + # "numCPU": "8", + # "memory": 16384, + # }], + # "flavoursSupported": [{ + # "flavourId": + # "medium-x86", + # "cpuArchType": + # "ISA_X86_64", + # "supportedOSTypes": [{ + # "architecture": "x86_64", + # "distribution": "UBUNTU", + # "version": "OS_VERSION_UBUNTU_2204_LTS", + # "license": "OS_LICENSE_TYPE_FREE", + # }], + # "numCPU": + # 4, + # "memorySize": + # 8192, + # "storageSize": + # 100, + # }], + # # + # } + aeros_client = ContinuumClient(self.base_url) + ngsild_params = ( + f'format=simplified&type=InfrastructureElement&q=domain=="{zone_id}"' + ) + self.logger.debug( + "Querying infrastructure elements for zone %s with params: %s", + zone_id, + ngsild_params, + ) + # Query the infrastructure elements for the specified zonese + aeros_domain_ies = aeros_client.query_entities(ngsild_params) + # Transform the infrastructure elements into the required format + # and return the details of the edge cloud zone + response = self.transform_infrastructure_elements( + domain_ies=aeros_domain_ies, domain=zone_id + ) + self.logger.debug("Transformed response: %s", response) + # Return the transformed response + return response + + def transform_infrastructure_elements( + self, domain_ies: List[Dict[str, Any]], domain: str + ) -> Dict[str, Any]: + """ + Transform the infrastructure elements into a format suitable for the + edge cloud zone details. + :param domain_ies: List of infrastructure elements + :param domain: The ID of the edge cloud zone + :return: Transformed details of the edge cloud zone + """ + total_cpu = 0 + total_ram = 0 + total_disk = 0 + total_available_ram = 0 + total_available_disk = 0 + + flavours_supported = [] + + for element in domain_ies: + total_cpu += element.get("cpuCores", 0) + total_ram += element.get("ramCapacity", 0) + total_available_ram += element.get("availableRam", 0) + total_disk += element.get("diskCapacity", 0) + total_available_disk += element.get("availableDisk", 0) + + # Create a flavour per machine + flavour = { + "flavourId": f"{element.get('hostname')}-{element.get('containerTechnology')}", + "cpuArchType": f"{element.get('cpuArchitecture')}", + "supportedOSTypes": [ + { + "architecture": f"{element.get('cpuArchitecture')}", + "distribution": f"{element.get('operatingSystem')}", # assume + "version": "OS_VERSION_UBUNTU_2204_LTS", + "license": "OS_LICENSE_TYPE_FREE", + } + ], + "numCPU": element.get("cpuCores", 0), + "memorySize": element.get("ramCapacity", 0), + "storageSize": element.get("diskCapacity", 0), + } + flavours_supported.append(flavour) + + result = { + "zoneId": domain, + "reservedComputeResources": [ + { + "cpuArchType": "ISA_X86_64", + "numCPU": str(total_cpu), + "memory": total_ram, + } + ], + "computeResourceQuotaLimits": [ + { + "cpuArchType": "ISA_X86_64", + "numCPU": str(total_cpu * 2), # Assume quota is 2x total? + "memory": total_ram * 2, + } + ], + "flavoursSupported": flavours_supported, + } + return result diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/config.py b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/config.py new file mode 100644 index 0000000..794cba5 --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/config.py @@ -0,0 +1,27 @@ +## +# This file is part of the Open SDK +# +# Contributors: +# - Vasilis Pitsilis (vpitsilis@dat.demokritos.gr, vpitsilis@iit.demokritos.gr) +# - Andreas Sakellaropoulos (asakellaropoulos@iit.demokritos.gr) +## +""" +aerOS access configuration +Access tokens need to be provided in environment variables. +""" +# import os + +# aerOS_API_URL = os.environ.get("aerOS_API_URL") +aerOS_API_URL = "harcoded_api" +if not aerOS_API_URL: + raise ValueError("Environment variable 'aerOS_API_URL' is not set.") +# aerOS_ACCESS_TOKEN = os.environ.get("aerOS_ACCESS_TOKEN") +aerOS_ACCESS_TOKEN = "harcoded_access_token" +if not aerOS_ACCESS_TOKEN: + raise ValueError("Environment variable 'aerOS_ACCESS_TOKEN' is not set.") +# aerOS_HLO_TOKEN = os.environ.get("aerOS_HLO_TOKEN") +aerOS_HLO_TOKEN = "harcoded_hlo_token" +if not aerOS_HLO_TOKEN: + raise ValueError("Environment variable 'aerOS_HLO_TOKEN' is not set.") +DEBUG = False +LOG_FILE = ".log/aeros_client.log" diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/continuum_client.py b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/continuum_client.py new file mode 100644 index 0000000..6f8b7c5 --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/continuum_client.py @@ -0,0 +1,196 @@ +## +# This file is part of the Open SDK +# +# Contributors: +# - Vasilis Pitsilis (vpitsilis@dat.demokritos.gr, vpitsilis@iit.demokritos.gr) +# - Andreas Sakellaropoulos (asakellaropoulos@iit.demokritos.gr) +## +""" +aerOS REST API Client + This client is used to interact with the aerOS REST API. +""" + +import requests + +from src.adapters.edgecloud.adapters.aeros import config +from src.adapters.edgecloud.adapters.aeros.utils import catch_requests_exceptions +from src.adapters.logger import setup_logger + + +class ContinuumClient: + """ + Client to aerOS ngsi-ld based continuum exposure + """ + + def __init__(self, base_url: str = None): + """ + :param base_url: the base url of the aerOS API + """ + if base_url is None: + self.api_url = config.aerOS_API_URL + else: + self.api_url = base_url + self.logger = setup_logger(__name__, is_debug=True, file_name=config.LOG_FILE) + self.m2m_cb_token = config.aerOS_ACCESS_TOKEN + self.hlo_token = config.aerOS_HLO_TOKEN + self.headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "aerOS": "true", + "Authorization": f"Bearer {self.m2m_cb_token}", + } + self.hlo_headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "aerOS": "true", + "Authorization": f"Bearer {self.hlo_token}", + } + self.hlo_onboard_headers = { + "Content-Type": "application/yaml", + "Authorization": f"Bearer {self.hlo_token}", + } + + @catch_requests_exceptions + def query_entity(self, entity_id, ngsild_params) -> dict: + """ + Query entity with ngsi-ld params + :input + @param entity_id: the id of the queried entity + @param ngsi-ld: the query params + :output + ngsi-ld object + """ + entity_url = f"{self.api_url}/entities/{entity_id}?{ngsild_params}" + response = requests.get(entity_url, headers=self.headers, timeout=15) + if response is None: + return None + else: + if config.DEBUG: + self.logger.debug("Query entity URL: %s", entity_url) + self.logger.debug( + "Query entity response: %s %s", response.status_code, response.text + ) + return response.json() + + @catch_requests_exceptions + def query_entities(self, ngsild_params): + """ + Query entities with ngsi-ld params + :input + @param ngsi-ld: the query params + :output + ngsi-ld object + """ + entities_url = f"{self.api_url}/entities?{ngsild_params}" + response = requests.get(entities_url, headers=self.headers, timeout=15) + if response is None: + return None + # else: + # if config.DEBUG: + # self.logger.debug("Query entities URL: %s", entities_url) + # self.logger.debug("Query entities response: %s %s", + # response.status_code, response.text) + return response.json() + + @catch_requests_exceptions + def deploy_service(self, service_id: str) -> dict: + """ + Re-allocate (deploy) service on aerOS continuum + :input + @param service_id: the id of the service to be re-allocated + :output + the re-allocated service json object + """ + re_allocate_url = f"{self.api_url}/hlo_fe/services/{service_id}" + response = requests.put(re_allocate_url, headers=self.hlo_headers, timeout=15) + if response is None: + return None + else: + if config.DEBUG: + self.logger.debug("Re-allocate service URL: %s", re_allocate_url) + self.logger.debug( + "Re-allocate service response: %s %s", + response.status_code, + response.text, + ) + return response.json() + + @catch_requests_exceptions + def undeploy_service(self, service_id: str) -> dict: + """ + Undeploy service + :input + @param service_id: the id of the service to be undeployed + :output + the undeployed service json object + """ + undeploy_url = f"{self.api_url}/hlo_fe/services/{service_id}" + response = requests.delete(undeploy_url, headers=self.hlo_headers, timeout=15) + if response is None: + return None + else: + if config.DEBUG: + self.logger.debug("Re-allocate service URL: %s", undeploy_url) + self.logger.debug( + "Undeploy service response: %s %s", + response.status_code, + response.text, + ) + return response.json() + + @catch_requests_exceptions + def onboard_and_deploy_service(self, service_id: str, tosca_str: str) -> dict: + """ + Onboard (& deploy) service on aerOS continuum + :input + @param service_id: the id of the service to onboarded (& deployed) + @param tosca_str: the tosca whith all orchestration information + :output + the allocated service json object + """ + onboard_url = f"{self.api_url}/hlo_fe/services/{service_id}" + if config.DEBUG: + self.logger.debug("Onboard service URL: %s", onboard_url) + self.logger.debug( + "Onboard service request body (TOSCA-YAML): %s", tosca_str + ) + response = requests.post( + onboard_url, data=tosca_str, headers=self.hlo_onboard_headers, timeout=15 + ) + if response is None: + return None + else: + if config.DEBUG: + self.logger.debug("Onboard service URL: %s", onboard_url) + self.logger.debug( + "Onboard service response: %s %s", + response.status_code, + response.text, + ) + return response.json() + + @catch_requests_exceptions + def purge_service(self, service_id: str) -> bool: + """ + Purge service from aerOS continuum + :input + @param service_id: the id of the service to be purged + :output + the purge result message from aerOS continuum + """ + purge_url = f"{self.api_url}/hlo_fe/services/{service_id}/purge" + response = requests.delete(purge_url, headers=self.hlo_headers, timeout=15) + if response is None: + return False + else: + if config.DEBUG: + self.logger.debug("Purge service URL: %s", purge_url) + self.logger.debug( + "Purge service response: %s %s", + response.status_code, + response.text, + ) + if response.status_code != 200: + self.logger.error("Failed to purge service: %s", response.text) + return False + return True diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/utils.py b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/utils.py new file mode 100644 index 0000000..b552b37 --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/utils.py @@ -0,0 +1,43 @@ +## +# This file is part of the Open SDK +# +# Contributors: +# - Vasilis Pitsilis (vpitsilis@dat.demokritos.gr, vpitsilis@iit.demokritos.gr) +# - Andreas Sakellaropoulos (asakellaropoulos@iit.demokritos.gr) +## +""" +Docstring +""" +from requests.exceptions import HTTPError, RequestException, Timeout + +import src.adapters.edgecloud.adapters.aeros.config as config +from src.adapters.logger import setup_logger + + +def catch_requests_exceptions(func): + """ + Docstring + """ + logger = setup_logger(__name__, is_debug=True, file_name=config.LOG_FILE) + + def wrapper(*args, **kwargs): + try: + result = func(*args, **kwargs) + return result + except HTTPError as e: + logger.info("4xx or 5xx: %s \n", {e}) + return None # raise our custom exception or log, etc. + except ConnectionError as e: + logger.info( + "Raised for connection-related issues (e.g., DNS resolution failure, network issues): %s \n", + {e}, + ) + return None # raise our custom exception or log, etc. + except Timeout as e: + logger.info("Timeout occured: %s \n", {e}) + return None # raise our custom exception or log, etc. + except RequestException as e: + logger.info("Request failed: %s \n", {e}) + return None # raise our custom exception or log, etc. + + return wrapper diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/errors.py b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/errors.py new file mode 100644 index 0000000..97b14bc --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/errors.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + + +class EdgeCloudPlatformError(Exception): + pass diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/__init__.py b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/client.py b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/client.py new file mode 100644 index 0000000..0ee3aa9 --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/client.py @@ -0,0 +1,243 @@ +# -*- coding: utf-8 -*- +## +# Copyright 2025-present by Software Networks Area, i2CAT. +# All rights reserved. +# +# This file is part of the Open SDK +# +# Contributors: +# - Adrián Pino Martínez (adrian.pino@i2cat.net) +# - Sergio Giménez (sergio.gimenez@i2cat.net) +## +from typing import Dict, List, Optional + +from src.adapters import logger +from src.adapters.edgecloud.core.edgecloud_interface import ( + EdgeCloudManagementInterface, +) + +from ...adapters.i2edge import schemas +from .common import ( + I2EdgeError, + i2edge_delete, + i2edge_get, + i2edge_post, + i2edge_post_multiform_data, +) + +log = logger.get_logger(__name__) + + +class EdgeApplicationManager(EdgeCloudManagementInterface): + """ + i2Edge Client + """ + + def __init__(self, base_url: str, flavour_id: str): + self.base_url = base_url + self.flavour_id = flavour_id + + def get_edge_cloud_zones( + self, region: Optional[str] = None, status: Optional[str] = None + ) -> list[dict]: + url = "{}/zones/list".format(self.base_url) + params = {} + try: + response = i2edge_get(url, params=params) + log.info("Availability zones retrieved successfully") + return response + except I2EdgeError as e: + raise e + + def get_edge_cloud_zones_details( + self, zone_id: str, flavour_id: Optional[str] = None + ) -> Dict: + url = "{}zone/{}".format(self.base_url, zone_id) + params = {} + try: + response = i2edge_get(url, params=params) + log.info("Availability zone details retrieved successfully") + return response + except I2EdgeError as e: + raise e + + def _create_artefact( + self, + artefact_id: str, + artefact_name: str, + repo_name: str, + repo_type: str, + repo_url: str, + password: Optional[str] = None, + token: Optional[str] = None, + user_name: Optional[str] = None, + ): + repo_type = schemas.RepoType(repo_type) + url = "{}/artefact".format(self.base_url) + payload = schemas.ArtefactOnboarding( + artefact_id=artefact_id, + name=artefact_name, + repo_password=password, + repo_name=repo_name, + repo_type=repo_type, + repo_url=repo_url, + repo_token=token, + repo_user_name=user_name, + ) + try: + i2edge_post_multiform_data(url, payload) + log.info("Artifact added successfully") + except I2EdgeError as e: + raise e + + def _get_artefact(self, artefact_id: str) -> Dict: + url = "{}/artefact/{}".format(self.base_url, artefact_id) + try: + response = i2edge_get(url, artefact_id) + log.info("Artifact retrieved successfully") + return response + except I2EdgeError as e: + raise e + + def _get_all_artefacts(self) -> List[Dict]: + url = "{}/artefact".format(self.base_url) + try: + response = i2edge_get(url, {}) + log.info("Artifacts retrieved successfully") + return response + except I2EdgeError as e: + raise e + + def _delete_artefact(self, artefact_id: str): + url = "{}/artefact".format(self.base_url) + try: + i2edge_delete(url, artefact_id) + log.info("Artifact deleted successfully") + except I2EdgeError as e: + raise e + + def onboard_app(self, app_manifest: Dict) -> Dict: + try: + app_id = app_manifest["appId"] + artefact_id = app_id + + app_component_spec = schemas.AppComponentSpec(artefactId=artefact_id) + data = schemas.ApplicationOnboardingData( + app_id=app_id, appComponentSpecs=[app_component_spec] + ) + payload = schemas.ApplicationOnboardingRequest(profile_data=data) + url = "{}/application/onboarding".format(self.base_url) + i2edge_post(url, payload) + except I2EdgeError as e: + raise e + except KeyError as e: + raise I2EdgeError("Missing required field in app_manifest: {}".format(e)) + + def delete_onboarded_app(self, app_id: str) -> None: + url = "{}/application/onboarding".format(self.base_url) + try: + i2edge_delete(url, app_id) + except I2EdgeError as e: + raise e + + def get_onboarded_app(self, app_id: str) -> Dict: + url = "{}/application/onboarding/{}".format(self.base_url, app_id) + try: + response = i2edge_get(url, app_id) + return response + except I2EdgeError as e: + raise e + + def get_all_onboarded_apps(self) -> List[Dict]: + url = "{}/applications/onboarding".format(self.base_url) + params = {} + try: + response = i2edge_get(url, params) + return response + except I2EdgeError as e: + raise e + + # def _select_best_flavour_for_app(self, zone_id) -> str: + # # list_of_flavours = self.get_edge_cloud_zones_details(zone_id) + # # + # return flavourId + + def deploy_app(self, app_id: str, app_zones: List[Dict]) -> Dict: + appId = app_id + app = self.get_onboarded_app(appId) + profile_data = app["profile_data"] + appProviderId = profile_data["appProviderId"] + appVersion = profile_data["appMetaData"]["version"] + zone_info = app_zones[0]["EdgeCloudZone"] + zone_id = zone_info["edgeCloudZoneId"] + # TODO: atm the flavour id is specified as an input parameter + # flavourId = self._select_best_flavour_for_app(zone_id=zone_id) + app_deploy_data = schemas.AppDeployData( + appId=appId, + appProviderId=appProviderId, + appVersion=appVersion, + zoneInfo=schemas.ZoneInfo(flavourId=self.flavour_id, zoneId=zone_id), + ) + url = "{}/app/".format(self.base_url) + payload = schemas.AppDeploy(app_deploy_data=app_deploy_data) + try: + response = i2edge_post(url, payload) + log.info("App deployed successfully") + print(response) + return response + except I2EdgeError as e: + raise e + + def get_all_deployed_apps(self) -> List[Dict]: + url = "{}/app/".format(self.base_url) + params = {} + try: + response = i2edge_get(url, params=params) + log.info("All app instances retrieved successfully") + return response + except I2EdgeError as e: + raise e + + def get_deployed_app(self, app_id, zone_id) -> List[Dict]: + # Logic: Get all onboarded apps and filter the one where release_name == artifact name + + # Step 1) Extract "app_name" from the onboarded app using the "app_id" + onboarded_app = self.get_onboarded_app(app_id) + if not onboarded_app: + raise ValueError(f"No onboarded app found with ID: {app_id}") + + try: + app_name = onboarded_app["profile_data"]["appMetaData"]["appName"] + except KeyError as e: + raise ValueError(f"Onboarded app missing required field: {e}") + + # Step 2) Retrieve all deployed apps and filter the one(s) where release_name == app_name + deployed_apps = self.get_all_deployed_apps() + if not deployed_apps: + return [] + + # Filter apps where release_name matches our app_name and zone matches + for app_instance_name in deployed_apps: + if ( + app_instance_name.get("release_name") == app_name + and app_instance_name.get("zone_id") == zone_id + ): + return app_instance_name + return None + + url = "{}/app/{}/{}".format(self.base_url, zone_id, app_instance_name) + params = {} + try: + response = i2edge_get(url, params=params) + log.info("App instance retrieved successfully") + return response + except I2EdgeError as e: + raise e + + def undeploy_app(self, app_instance_id: str) -> None: + url = "{}/app".format(self.base_url) + try: + i2edge_delete(url, app_instance_id) + log.info("App instance deleted successfully") + except I2EdgeError as e: + raise e diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/common.py b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/common.py new file mode 100644 index 0000000..33c2d12 --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/common.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +## +# Copyright 2025-present by Software Networks Area, i2CAT. +# All rights reserved. +# +# This file is part of the Open SDK +# +# Contributors: +# - Sergio Giménez (sergio.gimenez@i2cat.net) +## +import json +from typing import Optional + +import requests +from pydantic import BaseModel + +from src.adapters import logger +from src.adapters.edgecloud.adapters.errors import EdgeCloudPlatformError + +log = logger.get_logger(__name__) + + +class I2EdgeError(EdgeCloudPlatformError): + pass + + +class I2EdgeErrorResponse(BaseModel): + message: str + detail: dict + + +def get_error_message_from(response: requests.Response) -> str: + try: + error_response = I2EdgeErrorResponse(**response.json()) + return error_response.message + except Exception as e: + log.error("Failed to parse error response from i2edge: {}".format(e)) + return response.text + + +def i2edge_post(url: str, model_payload: BaseModel) -> dict: + headers = { + "Content-Type": "application/json", + "accept": "application/json", + } + json_payload = json.dumps(model_payload.model_dump(mode="json")) + try: + response = requests.post(url, data=json_payload, headers=headers) + response.raise_for_status() + return response.json() + except requests.exceptions.HTTPError as e: + i2edge_err_msg = get_error_message_from(response) + err_msg = "Failed to deploy app: {}. Detail: {}".format(i2edge_err_msg, e) + log.error(err_msg) + raise I2EdgeError(err_msg) + + +def i2edge_post_multiform_data(url: str, model_payload: BaseModel) -> dict: + headers = { + "accept": "application/json", + } + payload_dict = model_payload.model_dump(mode="json") + payload_in_str = {k: str(v) for k, v in payload_dict.items()} + try: + response = requests.post(url, data=payload_in_str, headers=headers) + response.raise_for_status() + return response.json() + except requests.exceptions.HTTPError as e: + i2edge_err_msg = get_error_message_from(response) + err_msg = "Failed to deploy app: {}. Detail: {}".format(i2edge_err_msg, e) + log.error(err_msg) + raise I2EdgeError(err_msg) + + +def i2edge_delete(url: str, id: str) -> dict: + headers = {"accept": "application/json"} + try: + query = "{}/{}".format(url, id) + response = requests.delete(query, headers=headers) + response.raise_for_status() + return response.json() + except requests.exceptions.HTTPError as e: + i2edge_err_msg = get_error_message_from(response) + err_msg = "Failed to undeploy app: {}. Detail: {}".format(i2edge_err_msg, e) + log.error(err_msg) + raise I2EdgeError(err_msg) + + +def i2edge_get(url: str, params: Optional[dict]): + headers = {"accept": "application/json"} + try: + response = requests.get(url, params=params, headers=headers) + response.raise_for_status() + return response.json() + except requests.exceptions.HTTPError as e: + i2edge_err_msg = get_error_message_from(response) + err_msg = "Failed to get apps: {}. Detail: {}".format(i2edge_err_msg, e) + log.error(err_msg) + raise I2EdgeError(err_msg) diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/schemas.py b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/schemas.py new file mode 100644 index 0000000..c0d522f --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/schemas.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +## +# Copyright 2025-present by Software Networks Area, i2CAT. +# All rights reserved. +# +# This file is part of the Open SDK +# +# Contributors: +# - Sergio Giménez (sergio.gimenez@i2cat.net) +# - César Cajas (cesar.cajas@i2cat.net) +## +from enum import Enum +from typing import List, Optional + +from pydantic import BaseModel, ConfigDict, Field, field_validator + + +class ZoneInfo(BaseModel): + flavourId: str + zoneId: str + + +class AppParameters(BaseModel): + namespace: Optional[str] = None + + +class AppDeployData(BaseModel): + appId: str + appProviderId: str + appVersion: str + zoneInfo: ZoneInfo + + +class AppDeploy(BaseModel): + app_deploy_data: AppDeployData + app_parameters: Optional[AppParameters] = Field(default=AppParameters()) + + +# Artefact + + +class RepoType(str, Enum): + UPLOAD = "UPLOAD" + PUBLICREPO = "PUBLICREPO" + PRIVATEREPO = "PRIVATEREPO" + + +class ArtefactOnboarding(BaseModel): + artefact_id: str + name: str + # chart: Optional[bytes] = Field(default=None) # XXX AFAIK not supported by CAMARA. + repo_password: Optional[str] = None + repo_name: Optional[str] = None + repo_type: RepoType + repo_url: Optional[str] = None + repo_token: Optional[str] = None + repo_user_name: Optional[str] = None + model_config = ConfigDict(use_enum_values=True) + + +# Application Onboarding + +# XXX Leaving default values since i2edge only cares about appid and artifactid, at least for now. + + +class AppComponentSpec(BaseModel): + artefactId: str + componentName: str = Field(default="default_component") + serviceNameEW: str = Field(default="default_ew_service") + serviceNameNB: str = Field(default="default_nb_service") + + +class AppMetaData(BaseModel): + appDescription: str = Field(default="Default app description") + appName: str = Field(default="Default App") + category: str = Field(default="DEFAULT") + mobilitySupport: bool = Field(default=False) + version: str = Field(default="1.0") + + +class AppQoSProfile(BaseModel): + appProvisioning: bool = Field(default=True) + bandwidthRequired: int = Field(default=1) + latencyConstraints: str = Field(default="NONE") + multiUserClients: str = Field(default="APP_TYPE_SINGLE_USER") + noOfUsersPerAppInst: int = Field(default=1) + + +class ApplicationOnboardingData(BaseModel): + appComponentSpecs: List[AppComponentSpec] + appDeploymentZones: List[str] = Field(default=["default_zone"]) + app_id: str + appMetaData: AppMetaData = Field(default_factory=AppMetaData) + appProviderId: str = Field(default="default_provider") + appQoSProfile: AppQoSProfile = Field(default_factory=AppQoSProfile) + appStatusCallbackLink: Optional[str] = None + + +class ApplicationOnboardingRequest(BaseModel): + profile_data: ApplicationOnboardingData + + +# Flavour + + +class GPU(BaseModel): + gpuMemory: int = Field(default=0, description="GPU memory in MB") + gpuModeName: str = Field(default="", description="GPU mode name") + gpuVendorType: str = Field( + default="GPU_PROVIDER_NVIDIA", description="GPU vendor type" + ) + numGPU: int = Field(..., description="Number of GPUs") + + +class Hugepages(BaseModel): + number: int = Field(default=0, description="Number of hugepages") + pageSize: str = Field(default="2MB", description="Size of hugepages") + + +class SupportedOSTypes(BaseModel): + architecture: str = Field(default="x86_64", description="OS architecture") + distribution: str = Field(default="RHEL", description="OS distribution") + license: str = Field(default="OS_LICENSE_TYPE_FREE", description="OS license type") + version: str = Field(default="OS_VERSION_UBUNTU_2204_LTS", description="OS version") + + +class FlavourSupported(BaseModel): + cpuArchType: str = Field(default="ISA_X86", description="CPU architecture type") + cpuExclusivity: bool = Field(default=True, description="CPU exclusivity") + fpga: int = Field(default=0, description="Number of FPGAs") + gpu: Optional[List[GPU]] = Field(default=None, description="List of GPUs") + hugepages: List[Hugepages] = Field( + default_factory=lambda: [Hugepages()], description="List of hugepages" + ) + memorySize: str = Field(..., description="Memory size (e.g., '1024MB' or '2GB')") + numCPU: int = Field(..., description="Number of CPUs") + storageSize: int = Field(default=0, description="Storage size in GB") + supportedOSTypes: List[SupportedOSTypes] = Field( + default_factory=lambda: [SupportedOSTypes()], + description="List of supported OS types", + ) + vpu: int = Field(default=0, description="Number of VPUs") + + @field_validator("memorySize") + @classmethod + def validate_memory_size(cls, v): + if not (v.endswith("MB") or v.endswith("GB")): + raise ValueError("memorySize must end with MB or GB") + try: + int(v[:-2]) + except ValueError: + raise ValueError("memorySize must be a number followed by MB or GB") + return v + + +class Flavour(BaseModel): + flavour_supported: FlavourSupported + + +# EdgeCloud Zones + + +class Zone(BaseModel): + geographyDetails: str + geolocation: str + zoneId: str diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/utils.py b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/utils.py new file mode 100644 index 0000000..c02b79d --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/utils.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +## +# Copyright 2025-present by Software Networks Area, i2CAT. +# All rights reserved. +# +# This file is part of the Open SDK +# +# Contributors: +# - Sergio Giménez (sergio.gimenez@i2cat.net) +# - César Cajas (cesar.cajas@i2cat.net) +## +import uuid +from typing import Optional, Union +from uuid import UUID + +from src.edgecloud import logger + +from src.adapters.edgecloud.api.routers.lcm.schemas import RequiredResources +from src.adapters.edgecloud.core import utils as core_utils + +from .client import I2EdgeClient +from .common import I2EdgeError + +log = logger.get_logger(__name__) + + +def generate_namespace_name_from(app_id: str, app_instance_id: str) -> str: + max_length = 63 + combined_name = "{}-{}".format(app_id, app_instance_id) + if len(combined_name) > max_length: + combined_name = combined_name[:max_length] + return combined_name + + +def generate_unique_id() -> UUID: + return uuid.uuid4() + + +def instantiate_app_with( + camara_app_id: UUID, + zone_id: str, + required_resources: RequiredResources, + i2edge: I2EdgeClient, +) -> tuple[str, str]: + memory_size_str = "{}GB".format(required_resources.memory + 1) + num_gpus = core_utils.get_num_gpus_from(required_resources) + try: + flavour_id = i2edge.create_flavour( + zone_id=zone_id, + memory_size=memory_size_str, + num_cpu=required_resources.numCPU, + num_gpus=num_gpus, + ) + i2edge_instance_id = generate_unique_id() + application_k8s_namespace = generate_namespace_name_from( + str(camara_app_id), str(i2edge_instance_id) + ) + i2edge.deploy_app( + appId=str(camara_app_id), + zoneId=zone_id, + flavourId=flavour_id, + namespace=application_k8s_namespace, + ) + return flavour_id, application_k8s_namespace + except I2EdgeError as e: + err_msg = "Error instantiating app {} in zone {}".format(camara_app_id, zone_id) + log.error("{}. Detailed error: {}".format(err_msg, e)) + raise e + + +def onboard_app_with( + application_id: UUID, + artefact_id: UUID, + app_name: str, + app_version: Optional[str], # TODO pass this to i2edge + repo_type: str, + app_repo: str, + user_name: Optional[str], + password: Optional[str], + token: Optional[str], + i2edge: I2EdgeClient, +): + try: + # TODO Come back to handle errors when onboarding and perform rollbacks + i2edge.create_artefact( + artefact_id=str(artefact_id), + artefact_name=app_name, + repo_name=app_name, + repo_type=repo_type, + repo_url=app_repo, + user_name=user_name, + password=password, + token=token, + ) + + i2edge.onboard_app(app_id=str(application_id), artefact_id=str(application_id)) + except I2EdgeError as e: + err_msg = "Error onboarding app {} in i2edge".format(app_name) + log.error("{}. Detailed error: {}".format(err_msg, e)) + raise e + + +def delete_app_instance_by( + namespace: str, flavour_id: str, zone_id: str, i2edge: I2EdgeClient +): + i2edge_app_instance_name = get_app_name_from(namespace, i2edge) + if i2edge_app_instance_name is None: + err_msg = "Couldn't retrieve app instance from I2Edge." + log.error(err_msg) + raise I2EdgeError(err_msg) + i2edge.undeploy_app(i2edge_app_instance_name) + i2edge.delete_flavour(flavour_id=str(flavour_id), zone_id=zone_id) + + +def get_app_name_from(namespace: str, i2edge: I2EdgeClient) -> Union[str, None]: + try: + response = i2edge.get_all_deployed_apps() + for deployment in response: + if deployment.get("bodytosend", {}).get("namespace") == namespace: + return deployment.get("name") + return None + except I2EdgeError as e: + err_msg = "Error getting app name for namespace {}".format(namespace) + log.error("{}. Detailed error: {}".format(err_msg, e)) + raise e + + +def delete_app_by(app_id: UUID, artefact_id: UUID, i2edge: I2EdgeClient): + try: + i2edge.delete_onboarded_app(app_id=str(app_id)) + i2edge.delete_artefact(artefact_id=str(artefact_id)) + except I2EdgeError as e: + err_msg = "Error deleting app {}".format(app_id) + log.error("{}. Detailed error: {}".format(err_msg, e)) + raise e + + +def get_edgecloud_zones(i2edge: I2EdgeClient) -> list[str]: + try: + zone_ids = [] + response = i2edge.get_zones_list() + for zone in response: + zone_id = zone.get("zoneId") + if zone_id is not None: + zone_ids.append(zone_id) + return zone_ids + + except I2EdgeError as e: + err_msg = "Error getting zones from i2edge" + log.error("{}. Detailed error: {}".format(err_msg, e)) + raise e diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/kubernetes/__init__.py b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/kubernetes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/kubernetes/client.py b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/kubernetes/client.py new file mode 100644 index 0000000..a5207c4 --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/kubernetes/client.py @@ -0,0 +1,263 @@ +# Mocked API for testing purposes +import logging +from typing import Dict, List, Optional + +from kubernetes.client import V1Deployment + +from src.adapters.edgecloud.adapters.kubernetes.lib.core.piedge_encoder import ( + deploy_service_function, +) +from src.adapters.edgecloud.adapters.kubernetes.lib.models.app_manifest import ( + AppManifest, +) +from src.adapters.edgecloud.adapters.kubernetes.lib.models.deploy_service_function import ( + DeployServiceFunction, +) +from src.adapters.edgecloud.adapters.kubernetes.lib.models.service_function_registration_request import ( + ServiceFunctionRegistrationRequest, +) +from src.adapters.edgecloud.adapters.kubernetes.lib.utils.connector_db import ( + ConnectorDB, +) +from src.adapters.edgecloud.adapters.kubernetes.lib.utils.kubernetes_connector import ( + KubernetesConnector, +) +from src.adapters.edgecloud.core.edgecloud_interface import ( + EdgeCloudManagementInterface, +) + + +class EdgeApplicationManager(EdgeCloudManagementInterface): + + def __init__(self, base_url: str, **kwargs): + self.kubernetes_host = base_url + self.edge_cloud_provider = kwargs.get("PLATFORM_PROVIDER") + kubernetes_token = kwargs.get("KUBERNETES_MASTER_TOKEN") + kubernetes_port = kwargs.get("KUBERNETES_MASTER_PORT") + storage_uri = kwargs.get("EMP_STORAGE_URI") + username = kwargs.get("KUBERNETES_USERNAME") + namespace = kwargs.get('K8S_NAMESPACE') + if base_url is not None and base_url != "": + self.k8s_connector = KubernetesConnector( + ip=self.kubernetes_host, + port=kubernetes_port, + token=kubernetes_token, + username=username, + namespace=namespace + ) + if storage_uri is not None: + self.connector_db = ConnectorDB(storage_uri) + + def onboard_app(self, app_manifest: AppManifest) -> Dict: + print(f"Submitting application: {app_manifest}") + logging.info("Extracting variables from payload...") + app_id = app_manifest.get("appId") + app_name = app_manifest.get("name") + image = app_manifest.get("appRepo").get("imagePath") + package_type = app_manifest.get("packageType") + network_interfaces = app_manifest.get("componentSpec")[0].get( + "networkInterfaces" + ) + ports = [] + for ni in network_interfaces: + ports.append(ni.get("port")) + insert_doc = ServiceFunctionRegistrationRequest( + service_function_id=app_id, + service_function_image=image, + service_function_name=app_name, + service_function_type=package_type, + application_ports=ports, + ) + result = self.connector_db.insert_document_service_function( + insert_doc.to_dict() + ) + if type(result) is str: + return result + return {"appId": str(result.inserted_id)} + + def get_all_onboarded_apps(self) -> List[Dict]: + logging.info("Retrieving all registered apps from database...") + db_list = self.connector_db.get_documents_from_collection( + collection_input="service_functions" + ) + app_list = [] + for sf in db_list: + app_list.append(self.__transform_to_camara(sf)) + return app_list + # return [{"appId": "1234-5678", "name": "TestApp"}] + + def get_onboarded_app(self, app_id: str) -> Dict: + logging.info( + "Searching for registered app with ID: " + app_id + " in database..." + ) + app = self.connector_db.get_documents_from_collection( + "service_functions", input_type="_id", input_value=app_id + ) + if len(app) > 0: + return self.__transform_to_camara(app[0]) + else: + return [] + + def delete_onboarded_app(self, app_id: str) -> None: + result, code = self.connector_db.delete_document_service_function(_id=app_id) + print(f"Removing application metadata: {app_id}") + return code + + def deploy_app(self, body: dict) -> Dict: + logging.info( + "Searching for registered app with ID: " + + body.get("appId") + + " in database..." + ) + app = self.connector_db.get_documents_from_collection( + "service_functions", input_type="_id", input_value=body.get("appId") + ) + # success_response = [] + result = None + response = None + if len(app) < 1: + return "Application with ID: " + body.get("appId") + " not found", 404 + if app is not None: + sf = DeployServiceFunction( + service_function_name=app[0].get("name"), + service_function_instance_name=body.get("name"), + # location=body.get('edgeCloudZoneId'), + ) + result = deploy_service_function( + service_function=sf, + connector_db=self.connector_db, + kubernetes_connector=self.k8s_connector, + ) + if type(result) is V1Deployment: + response = {} + response["name"] = body.get("name") + response["appId"] = app[0].get("_id") + response["appInstanceId"] = result.metadata.uid + response["appProvider"] = app[0].get("appProvider") + response["status"] = "unknown" + response["componentEndpointInfo"] = {} + response["kubernetesClusterRef"] = "" + response["edgeCloudZoneId"] = body.get("edgeCloudZoneId") + else: + response = {"Error": result} + return response + + def get_all_deployed_apps( + self, + app_id: Optional[str] = None, + app_instance_id: Optional[str] = None, + region: Optional[str] = None, + ) -> List[Dict]: + logging.info("Retrieving all deployed apps in the edge cloud platform") + deployments = self.k8s_connector.get_deployed_service_functions( + self.connector_db + ) + response = [] + for deployment in deployments: + item = {} + item["name"] = deployment.get("service_function_catalogue_name") + item["appId"] = deployment.get("appId") + item["appProvider"] = deployment.get("appProvider") + item["appInstanceId"] = deployment.get("appInstanceId") + item["status"] = deployment.get("status") + interfaces = [] + for port in deployment.get("ports"): + access_point = {"port": port} + interfaces.append({"interfaceId": "", "accessPoints": access_point}) + item["componentEndpointInfo"] = interfaces + item["kubernetesClusterRef"] = "" + item["edgeCloudZoneId"] = {} + response.append(item) + return response + # return [{"appInstanceId": "abcd-efgh", "status": "ready"}] + + def undeploy_app(self, app_instance_id: str) -> None: + logging.info( + "Searching for deployed app with ID: " + app_instance_id + " in database..." + ) + print(f"Deleting app instance: {app_instance_id}") + sfs = self.k8s_connector.get_deployed_service_functions(self.connector_db) + response = "App instance with ID [" + app_instance_id + "] not found" + for service_fun in sfs: + if service_fun["appInstanceId"] == app_instance_id: + self.k8s_connector.delete_service_function( + self.connector_db, service_fun["service_function_instance_name"] + ) + response = ( + "App instance with ID [" + + app_instance_id + + "] successfully removed" + ) + break + return response + + def get_edge_cloud_zones( + self, region: Optional[str] = None, status: Optional[str] = None + ) -> List[Dict]: + + nodes_response = self.k8s_connector.get_PoPs() + zone_list = [] + + for node in nodes_response: + zone = {} + zone["edgeCloudZoneId"] = node.get("uid") + zone["edgeCloudZoneName"] = node.get("name") + zone["edgeCloudZoneStatus"] = node.get("status") + zone["edgeCloudProvider"] = self.edge_cloud_provider + zone["edgeCloudRegion"] = node.get("location") + zone_list.append(zone) + return zone_list + + def get_edge_cloud_zones_details( + self, zone_id: str, flavour_id: Optional[str] = None + ) -> Dict: + nodes = self.k8s_connector.get_node_details() + node_details = None + for item in nodes.get("items"): + if item.get("metadata").get("uid") == zone_id: + node_details = item + break + labels = node_details.get("metadata").get("labels") + status = node_details.get("status") + arch_type = labels.get("beta.kubernetes.io/arch") + computeResourceQuotaLimits = [ + { + "cpuArchType": arch_type, + "numCPU": status.get("capacity").get("cpu"), + "memory": status.get("capacity").get("memory"), + # "memory": int(status.get("capacity").get("memory")) / (1024 * 1024), + } + ] + reservedComputeResources = [ + { + "cpuArchType": arch_type, + "numCPU": status.get("allocatable").get("cpu"), + "memory": status.get("allocatable").get("memory"), + # "memory": int(status.get("allocatable").get("memory")) / (1024 * 1024), + } + ] + flavoursSupported = [] + node_details["computeResourceQuotaLimits"] = computeResourceQuotaLimits + node_details["reservedComputeResources"] = reservedComputeResources + node_details["flavoursSupported"] = flavoursSupported + node_details["zoneId"] = zone_id + return node_details + + def __transform_to_camara(self, app_data): + app = {} + app["appId"] = app_data.get("_id") + app["name"] = app_data.get("name") + app["packageType"] = app_data.get("type") + appRepo = {"imagePath": app_data.get("image")} + app["appRepo"] = appRepo + networkInterfaces = [] + for port in app_data.get("application_ports"): + port_spec = {"protocol": "TCP", "port": port} + networkInterfaces.append(port_spec) + app["componentSpec"] = [ + { + "componentName": app_data.get("name"), + "networkInterfaces": networkInterfaces, + } + ] + return app diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/core/__init__.py b/service-resource-manager-implementation/src/adapters/edgecloud/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/core/edgecloud_interface.py b/service-resource-manager-implementation/src/adapters/edgecloud/core/edgecloud_interface.py new file mode 100644 index 0000000..7dd2c6f --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/edgecloud/core/edgecloud_interface.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +## +# Copyright 2025-present by Software Networks Area, i2CAT. +# All rights reserved. +# +# This file is part of the Open SDK +# +# Contributors: +# - Adrián Pino Martínez (adrian.pino@i2cat.net) +## +from abc import ABC, abstractmethod +from typing import Dict, List, Optional + + +class EdgeCloudManagementInterface(ABC): + """ + Abstract Base Class for Edge Application Management. + """ + + @abstractmethod + def onboard_app(self, app_manifest: Dict) -> Dict: + """ + Onboards an app, submitting application metadata + to the Edge Cloud Provider. + + :param app_manifest: Application metadata in dictionary format. + :return: Dictionary containing created application details. + """ + pass + + @abstractmethod + def get_all_onboarded_apps(self) -> List[Dict]: + """ + Retrieves a list of onboarded applications. + + :return: List of application metadata dictionaries. + """ + pass + + @abstractmethod + def get_onboarded_app(self, app_id: str) -> Dict: + """ + Retrieves information of a specific onboarded application. + + :param app_id: Unique identifier of the application. + :return: Dictionary with application details. + """ + pass + + @abstractmethod + def delete_onboarded_app(self, app_id: str) -> None: + """ + Deletes an application onboarded from the Edge Cloud Provider. + + :param app_id: Unique identifier of the application. + """ + pass + + @abstractmethod + def deploy_app(self, app_id: str, app_zones: List[Dict]) -> Dict: + """ + Requests the instantiation of an application instance. + + :param app_id: Unique identifier of the application. + :param app_zones: List of Edge Cloud Zones where the app should be + instantiated. + :return: Dictionary with instance details. + """ + pass + + @abstractmethod + def get_all_deployed_apps( + self, + app_id: Optional[str] = None, + app_instance_id: Optional[str] = None, + region: Optional[str] = None, + ) -> List[Dict]: + """ + Retrieves information of application instances. + + :param app_id: Filter by application ID. + :param app_instance_id: Filter by instance ID. + :param region: Filter by Edge Cloud region. + :return: List of application instance details. + """ + pass + + @abstractmethod + def undeploy_app(self, app_instance_id: str) -> None: + """ + Terminates a specific application instance. + + :param app_instance_id: Unique identifier of the application instance. + """ + pass + + @abstractmethod + def get_edge_cloud_zones( + self, region: Optional[str] = None, status: Optional[str] = None + ) -> List[Dict]: + """ + Retrieves a list of available Edge Cloud Zones. + + :param region: Filter by geographical region. + :param status: Filter by status (active, inactive, unknown). + :return: List of Edge Cloud Zones. + """ + pass + + @abstractmethod + def get_edge_cloud_zones_details( + self, federation_context_id: str, zone_id: str + ) -> Dict: + """ + Retrieves details of a specific Edge Cloud Zone reserved + for the specified zone by the partner OP. + + :param federation_context_id: Identifier of the federation context. + :param zone_id: Unique identifier of the Edge Cloud Zone. + :return: Dictionary with Edge Cloud Zone details. + """ + pass diff --git a/service-resource-manager-implementation/src/adapters/logger.py b/service-resource-manager-implementation/src/adapters/logger.py new file mode 100644 index 0000000..14c3f6b --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/logger.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +## +# Copyright 2025-present by Software Networks Area, i2CAT. +# All rights reserved. +# +# This file is part of the Open SDK +# +# Contributors: +# - Sergio Giménez (sergio.gimenez@i2cat.net) +## +import logging +import sys +from pathlib import Path + +from colorlog import ColoredFormatter + +APP_LOGGER_NAME = "edgecloud" +COLORED_FORMATERR = ( + "%(log_color)s%(levelname)s%(reset)s | " + "[%(log_color)s%(name)s%(reset)s:%(log_color)s%(lineno)d%(reset)s] " + "%(log_color)s%(message)s%(reset)s" +) +FILE_FORMATTER = "[%(asctime)s] {%(name)s: %(lineno)d} %(levelname)s - %(message)s" + + +def setup_logger(logger_name=APP_LOGGER_NAME, is_debug=True, file_name=None): + logger = logging.getLogger(logger_name) + logger.setLevel(logging.DEBUG if is_debug else logging.INFO) + + colored_formatter = ColoredFormatter(COLORED_FORMATERR) + sh = logging.StreamHandler(sys.stdout) + sh.setFormatter(colored_formatter) + logger.handlers.clear() + logger.addHandler(sh) + + if file_name: + log_path = Path(file_name) + log_path.parent.mkdir(parents=True, exist_ok=True) + fh = logging.FileHandler(file_name) + fh.setFormatter(logging.Formatter(FILE_FORMATTER)) + logger.addHandler(fh) + + return logger + + +def get_logger(module_name): + return logging.getLogger(APP_LOGGER_NAME).getChild(module_name) diff --git a/service-resource-manager-implementation/src/adapters/network/__init__.py b/service-resource-manager-implementation/src/adapters/network/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service-resource-manager-implementation/src/adapters/network/adapters/__init__.py b/service-resource-manager-implementation/src/adapters/network/adapters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service-resource-manager-implementation/src/adapters/network/adapters/errors.py b/service-resource-manager-implementation/src/adapters/network/adapters/errors.py new file mode 100644 index 0000000..6497bd8 --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/network/adapters/errors.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +class NetworkPlatformError(Exception): + pass diff --git a/service-resource-manager-implementation/src/adapters/network/adapters/oai/__init__.py b/service-resource-manager-implementation/src/adapters/network/adapters/oai/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service-resource-manager-implementation/src/adapters/network/adapters/oai/client.py b/service-resource-manager-implementation/src/adapters/network/adapters/oai/client.py new file mode 100644 index 0000000..f0f6552 --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/network/adapters/oai/client.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +## +# Copyright (c) 2025 Netsoft Group, EURECOM. +# All rights reserved. +# +# This file is part of the Open SDK +# +# Contributors: +# - Giulio Carota (giulio.carota@eurecom.fr) +## +from src.adapters import logger +from src.adapters.network.core.base_network_client import BaseNetworkClient +from src.adapters.network.core.schemas import ( + AsSessionWithQoSSubscription, + CreateSession, + CreateTrafficInfluence, + FlowInfo, + MonitoringEventSubscriptionRequest, + RetrievalLocationRequest, + Snssai, + TrafficInfluSub, +) + +log = logger.get_logger(__name__) +supportedQos = ["qos-e", "qos-s", "qos-m", "qos-l"] + + +class NetworkManager(BaseNetworkClient): + def __init__(self, base_url: str, scs_as_id: str = None): + """ + Initialize Network Client for OAI Core Network + The currently supported features are: + - QoD + - Traffic Influence + """ + try: + super().__init__() + self.base_url = base_url + self.scs_as_id = scs_as_id + log.info( + f"Initialized OaiNefClient with base_url: {self.base_url} and scs_as_id: {self.scs_as_id}" + ) + + except Exception as e: + log.error(f"Failed to initialize OaiNefClient: {e}") + raise e + + def core_specific_qod_validation(self, session_info: CreateSession): + """ + Validates core-specific parameters for the session creation. + + args: + session_info: The session information to validate. + + raises: + ValidationError: If the session information does not meet core-specific requirements. + """ + if session_info.qosProfile.root not in supportedQos: + raise OaiValidationError( + f"QoS profile {session_info.qosProfile} not supported by OAI, supported profiles are {supportedQos}" + ) + + if session_info.device is None or session_info.device.ipv4Address is None: + raise OaiValidationError("OAI requires UE IPv4 Address to activate QoS") + + if session_info.applicationServer.ipv4Address is None: + raise OaiValidationError("OAI requires App IPv4 Address to activate QoS") + return + + def add_core_specific_qod_parameters( + self, + session_info: CreateSession, + subscription: AsSessionWithQoSSubscription, + ) -> None: + device_ip = _retrieve_ue_ipv4(session_info) + server_ip = _retrieve_app_ipv4(session_info) + + # build flow descriptor in oai format using device ip and server ip + flow_descriptor = f"permit out ip from {device_ip}/32 to {server_ip}/32" + _add_qod_flow_descriptor(subscription, flow_descriptor) + _add_qod_snssai(subscription, 1, "FFFFFF") + subscription.dnn = "oai" + + def add_core_specific_ti_parameters( + self, + traffic_influence_info: CreateTrafficInfluence, + subscription: TrafficInfluSub, + ): + # todo oai add dnn, ssnai, afServiceId + subscription.dnn = "oai" + subscription.add_snssai(1, "FFFFFF") + subscription.afServiceId = self.scs_as_id + + def core_specific_traffic_influence_validation( + self, traffic_influence_info: CreateTrafficInfluence + ) -> None: + """ + Validates core-specific parameters for the session creation. + + args: + session_info: The session information to validate. + + raises: + ValidationError: If the session information does not meet core-specific requirements. + """ + # Placeholder for core-specific validation logic + # This method should be overridden by subclasses if needed + + if ( + traffic_influence_info.device is None + or traffic_influence_info.device.ipv4Address is None + ): + raise OaiValidationError( + "OAI requires UE IPv4 Address to activate Traffic Influence" + ) + + def core_specific_monitoring_event_validation( + self, retrieve_location_request: RetrievalLocationRequest + ) -> None: + raise NotImplementedError( + "core_specific_monitoring_event_validation not implemented for OAI" + ) + + def add_core_specific_location_parameters( + self, retrieve_location_request: RetrievalLocationRequest + ) -> MonitoringEventSubscriptionRequest: + raise NotImplementedError( + "add_core_specific_location_parameters not implemented for OAI" + ) + + +def _retrieve_ue_ipv4(session_info: CreateSession): + return session_info.device.ipv4Address.root.privateAddress + + +def _retrieve_app_ipv4(session_info: CreateSession): + return session_info.applicationServer.ipv4Address + + +def _add_qod_flow_descriptor( + qos_sub: AsSessionWithQoSSubscription, flow_desriptor: str +): + qos_sub.flowInfo = list() + qos_sub.flowInfo.append( + FlowInfo(flowId=len(qos_sub.flowInfo) + 1, flowDescriptions=[flow_desriptor]) + ) + + +def _add_qod_snssai(qos_sub: AsSessionWithQoSSubscription, sst: int, sd: str = None): + qos_sub.snssai = Snssai(sst=sst, sd=sd) + + +class OaiValidationError(Exception): + pass diff --git a/service-resource-manager-implementation/src/adapters/network/adapters/open5gcore/__init__.py b/service-resource-manager-implementation/src/adapters/network/adapters/open5gcore/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service-resource-manager-implementation/src/adapters/network/adapters/open5gcore/client.py b/service-resource-manager-implementation/src/adapters/network/adapters/open5gcore/client.py new file mode 100644 index 0000000..3069c91 --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/network/adapters/open5gcore/client.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +from pydantic import ValidationError + +from src.adapters import logger +from src.adapters.network.core.base_network_client import ( + BaseNetworkClient, + build_flows, +) + +from ...core import schemas + +log = logger.get_logger("Open5GCore") # Usage of brand name + +qos_support_map = { + "qos-e": 1, # ToDo + "qos-s": 5, + "qos-m": 9, + "qos-l": 9, # ToDo not yet available in Nokia RAN +} + + +class NetworkManager(BaseNetworkClient): + def __init__(self, base_url: str, scs_as_id: str): + if not base_url: + raise ValueError("base_url is required and cannot be empty.") + if not scs_as_id: + raise ValueError("scs_as_id is required and cannot be empty.") + + self.base_url = base_url + self.scs_as_id = scs_as_id + + def core_specific_qod_validation(self, session_info: schemas.CreateSession): + qos_key = session_info.qosProfile.root.strip().lower() + + if qos_key not in qos_support_map: + supported = ", ".join(qos_support_map.keys()) + raise ValidationError( + f"Unsupported QoS profile '{session_info.qosProfile.root}'. " + f"Supported profiles for Open5GCore are: {supported}" + ) + + def add_core_specific_qod_parameters( + self, + session_info: schemas.CreateSession, + subscription: schemas.AsSessionWithQoSSubscription, + ) -> None: + flow_id = qos_support_map[session_info.qosProfile.root] + subscription.flowInfo = build_flows(flow_id, session_info) + subscription.ueIpv4Addr = "192.168.6.1" # ToDo + + def add_core_specific_ti_parameters( + self, + traffic_influence_info: schemas.CreateTrafficInfluence, + subscription: schemas.TrafficInfluSub, + ): + raise NotImplementedError( + "add_core_specific_ti_parameters not implemented for Open5GCore" + ) + + def core_specific_traffic_influence_validation( + self, traffic_influence_info: schemas.CreateTrafficInfluence + ) -> None: + raise NotImplementedError( + "core_specific_traffic_influence_validation not implemented for Open5GCore" + ) + + def core_specific_monitoring_event_validation( + self, retrieve_location_request: schemas.RetrievalLocationRequest + ) -> None: + raise NotImplementedError( + "core_specific_monitoring_event_validation not implemented for Open5GCore" + ) + + def add_core_specific_location_parameters( + self, retrieve_location_request: schemas.RetrievalLocationRequest + ) -> schemas.MonitoringEventSubscriptionRequest: + raise NotImplementedError( + "add_core_specific_location_parameters not implemented for Open5GCore" + ) diff --git a/service-resource-manager-implementation/src/adapters/network/adapters/open5gs/__init__.py b/service-resource-manager-implementation/src/adapters/network/adapters/open5gs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service-resource-manager-implementation/src/adapters/network/adapters/open5gs/client.py b/service-resource-manager-implementation/src/adapters/network/adapters/open5gs/client.py new file mode 100644 index 0000000..035822f --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/network/adapters/open5gs/client.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- + +# Contributors: +# - Ferran Cañellas (ferran.canellas@i2cat.net) +# - Panagiotis Pavlidis (p.pavlidis@iit.demokritos.gr) +## +from pydantic import ValidationError + +from src.adapters import logger +from src.adapters.network.core.base_network_client import ( + BaseNetworkClient, + build_flows, +) + +from ...core import schemas + +log = logger.get_logger(__name__) + +flow_id_mapping = {"qos-e": 3, "qos-s": 4, "qos-m": 5, "qos-l": 6} + + +class NetworkManager(BaseNetworkClient): + """ + This client implements the BaseNetworkClient and translates the + CAMARA APIs into specific HTTP requests understandable by the Open5GS NEF API. + + Invloved partners and their roles in this implementation: + - I2CAT: Responsible for the CAMARA QoD API and its mapping to the + 3GPP AsSessionWithQoS API exposed by Open5GS NEF. + - NCSRD: Responsible for the CAMARA Location API and its mapping to the + 3GPP Monitoring Event API exposed Open5GS NEF. + """ + + def __init__(self, base_url: str, scs_as_id): + """ + Initializes the Open5GS Client. + """ + try: + self.base_url = base_url + self.scs_as_id = scs_as_id + log.info( + f"Initialized Open5GSClient with base_url: {self.base_url} " + f"and scs_as_id: {self.scs_as_id}" + ) + except Exception as e: + log.error(f"Failed to initialize Open5GSClient: {e}") + raise e + + def core_specific_qod_validation(self, session_info: schemas.CreateSession): + if session_info.qosProfile.root not in flow_id_mapping.keys(): + raise ValidationError( + f"Open5Gs only supports these qos-profiles: {', '.join(flow_id_mapping.keys())}" + ) + + def add_core_specific_qod_parameters( + self, + session_info: schemas.CreateSession, + subscription: schemas.AsSessionWithQoSSubscription, + ) -> None: + subscription.supportedFeatures = schemas.SupportedFeatures("003C") + flow_id = flow_id_mapping[session_info.qosProfile.root] + subscription.flowInfo = build_flows(flow_id, session_info) + + def core_specific_monitoring_event_validation( + self, retrieve_location_request: schemas.RetrievalLocationRequest + ) -> None: + """Check core specific elements that required for location retrieval in NEF.""" + if retrieve_location_request.device is None: + raise ValidationError( + "Open5GS requires a device to be specified for location retrieval in NEF." + ) + + def add_core_specific_location_parameters( + self, retrieve_location_request: schemas.RetrievalLocationRequest + ) -> schemas.MonitoringEventSubscriptionRequest: + """Add core specific location parameters to support location retrieval scenario in NEF.""" + return schemas.MonitoringEventSubscriptionRequest( + msisdn=retrieve_location_request.device.phoneNumber.root.lstrip("+"), + notificationDestination="http://127.0.0.1:8001", + monitoringType=schemas.MonitoringType.LOCATION_REPORTING, + locationType=schemas.LocationType.LAST_KNOWN, + ) + # subscription.msisdn = retrieve_location_request.device.phoneNumber.root.lstrip('+') + # monitoringType = schemas.MonitoringType.LOCATION_REPORTING + # locationType = schemas.LocationType.LAST_KNOWN + # locationType = schemas.LocationType.CURRENT_LOCATION + # maximumNumberOfReports = 1 + # repPeriod = schemas.DurationSec(root=20) + + +# Note: +# As this class is inheriting from BaseNetworkClient, it is +# expected to implement all the abstract methods defined in that interface. +# +# In case this network adapter doesn't support a specific method, it should +# be marked as NotImplementedError. diff --git a/service-resource-manager-implementation/src/adapters/network/clients/__init__.py b/service-resource-manager-implementation/src/adapters/network/clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service-resource-manager-implementation/src/adapters/network/clients/errors.py b/service-resource-manager-implementation/src/adapters/network/clients/errors.py new file mode 100644 index 0000000..6497bd8 --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/network/clients/errors.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +class NetworkPlatformError(Exception): + pass diff --git a/service-resource-manager-implementation/src/adapters/network/clients/oai/__init__.py b/service-resource-manager-implementation/src/adapters/network/clients/oai/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service-resource-manager-implementation/src/adapters/network/clients/oai/client.py b/service-resource-manager-implementation/src/adapters/network/clients/oai/client.py new file mode 100644 index 0000000..cd4791c --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/network/clients/oai/client.py @@ -0,0 +1,139 @@ +## +# Copyright (c) 2025 Netsoft Group, EURECOM. +# All rights reserved. +# +# This file is part of the Open SDK +# +# Contributors: +# - Giulio Carota (giulio.carota@eurecom.fr) +## + + +from src.clients import logger +from src.clients.network.core.network_interface import NetworkManagementInterface +from src.clients.network.core.schemas import ( + AsSessionWithQoSSubscription, + CreateSession, + CreateTrafficInfluence, + FlowInfo, + Snssai, + TrafficInfluSub, +) + +log = logger.get_logger(__name__) +supportedQos = ["qos-e", "qos-s", "qos-m", "qos-l"] + + +class NetworkManager(NetworkManagementInterface): + def __init__(self, base_url: str, scs_as_id: str = None): + """ + Initialize Network Client for OAI Core Network + The currently supported features are: + - QoD + - Traffic Influence + """ + try: + super().__init__() + self.base_url = base_url + self.scs_as_id = scs_as_id + log.info( + f"Initialized OaiNefClient with base_url: {self.base_url} and scs_as_id: {self.scs_as_id}" + ) + + except Exception as e: + log.error(f"Failed to initialize OaiNefClient: {e}") + raise e + + def core_specific_qod_validation(self, session_info: CreateSession): + """ + Validates core-specific parameters for the session creation. + + args: + session_info: The session information to validate. + + raises: + ValidationError: If the session information does not meet core-specific requirements. + """ + if session_info.qosProfile.root not in supportedQos: + raise OaiValidationError( + f"QoS profile {session_info.qosProfile} not supported by OAI, supported profiles are {supportedQos}" + ) + + if session_info.device is None or session_info.device.ipv4Address is None: + raise OaiValidationError("OAI requires UE IPv4 Address to activate QoS") + + if session_info.applicationServer.ipv4Address is None: + raise OaiValidationError("OAI requires App IPv4 Address to activate QoS") + return + + def add_core_specific_qod_parameters( + self, + session_info: CreateSession, + subscription: AsSessionWithQoSSubscription, + ) -> None: + device_ip = _retrieve_ue_ipv4(session_info) + server_ip = _retrieve_app_ipv4(session_info) + + # build flow descriptor in oai format using device ip and server ip + flow_descriptor = f"permit out ip from {device_ip}/32 to {server_ip}/32" + _add_qod_flow_descriptor(subscription, flow_descriptor) + _add_qod_snssai(subscription, 1, "FFFFFF") + subscription.dnn = "oai" + + def add_core_specific_ti_parameters( + self, + traffic_influence_info: CreateTrafficInfluence, + subscription: TrafficInfluSub, + ): + # todo oai add dnn, ssnai, afServiceId + subscription.dnn = "oai" + subscription.add_snssai(1, "FFFFFF") + subscription.afServiceId = self.scs_as_id + + def core_specific_traffic_influence_validation( + self, traffic_influence_info: CreateTrafficInfluence + ) -> None: + """ + Validates core-specific parameters for the session creation. + + args: + session_info: The session information to validate. + + raises: + ValidationError: If the session information does not meet core-specific requirements. + """ + # Placeholder for core-specific validation logic + # This method should be overridden by subclasses if needed + + if ( + traffic_influence_info.device is None + or traffic_influence_info.device.ipv4Address is None + ): + raise OaiValidationError( + "OAI requires UE IPv4 Address to activate Traffic Influence" + ) + + +def _retrieve_ue_ipv4(session_info: CreateSession): + return session_info.device.ipv4Address.root.privateAddress + + +def _retrieve_app_ipv4(session_info: CreateSession): + return session_info.applicationServer.ipv4Address + + +def _add_qod_flow_descriptor( + qos_sub: AsSessionWithQoSSubscription, flow_desriptor: str +): + qos_sub.flowInfo = list() + qos_sub.flowInfo.append( + FlowInfo(flowId=len(qos_sub.flowInfo) + 1, flowDescriptions=[flow_desriptor]) + ) + + +def _add_qod_snssai(qos_sub: AsSessionWithQoSSubscription, sst: int, sd: str = None): + qos_sub.snssai = Snssai(sst=sst, sd=sd) + + +class OaiValidationError(Exception): + pass diff --git a/service-resource-manager-implementation/src/adapters/network/clients/open5gcore/__init__.py b/service-resource-manager-implementation/src/adapters/network/clients/open5gcore/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service-resource-manager-implementation/src/adapters/network/clients/open5gcore/client.py b/service-resource-manager-implementation/src/adapters/network/clients/open5gcore/client.py new file mode 100644 index 0000000..ef88210 --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/network/clients/open5gcore/client.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +from pydantic import ValidationError + +from src.adapters import logger +from src.adapters.network.core.network_interface import ( + NetworkManagementInterface, + build_flows, +) + +from ...core import schemas + +log = logger.get_logger("Open5GCore") # Usage of brand name + +qos_support_map = { + "qos-e": 1, # ToDo + "qos-s": 5, + "qos-m": 9, + "qos-l": 9, # ToDo not yet available in Nokia RAN +} + + +class NetworkManager(NetworkManagementInterface): + def __init__(self, base_url: str, scs_as_id: str): + if not base_url: + raise ValueError("base_url is required and cannot be empty.") + if not scs_as_id: + raise ValueError("scs_as_id is required and cannot be empty.") + + self.base_url = base_url + self.scs_as_id = scs_as_id + + def core_specific_qod_validation(self, session_info: schemas.CreateSession): + qos_key = session_info.qosProfile.root.strip().lower() + + if qos_key not in qos_support_map: + supported = ", ".join(qos_support_map.keys()) + raise ValidationError( + f"Unsupported QoS profile '{session_info.qosProfile.root}'. " + f"Supported profiles for Open5GCore are: {supported}" + ) + + def add_core_specific_qod_parameters( + self, + session_info: schemas.CreateSession, + subscription: schemas.AsSessionWithQoSSubscription, + ) -> None: + flow_id = qos_support_map[session_info.qosProfile.root] + subscription.flowInfo = build_flows(flow_id, session_info) + subscription.ueIpv4Addr = "192.168.6.1" # ToDo diff --git a/service-resource-manager-implementation/src/adapters/network/clients/open5gs/__init__.py b/service-resource-manager-implementation/src/adapters/network/clients/open5gs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service-resource-manager-implementation/src/adapters/network/clients/open5gs/client.py b/service-resource-manager-implementation/src/adapters/network/clients/open5gs/client.py new file mode 100644 index 0000000..ecdb743 --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/network/clients/open5gs/client.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +from pydantic import ValidationError + +from src.clients import logger +from src.clients.network.core.network_interface import ( + NetworkManagementInterface, + build_flows, +) + +from ...core import schemas + +log = logger.get_logger(__name__) + +flow_id_mapping = {"qos-e": 3, "qos-s": 4, "qos-m": 5, "qos-l": 6} + + +class NetworkManager(NetworkManagementInterface): + """ + This client implements the NetworkManagementInterface and translates the + CAMARA APIs into specific HTTP requests understandable by the Open5GS NEF API. + + Invloved partners and their roles in this implementation: + - I2CAT: Responsible for the CAMARA QoD API and its mapping to the + 3GPP AsSessionWithQoS API exposed by Open5GS NEF. + - NCSRD: Responsible for the CAMARA Location API and its mapping to the + 3GPP Monitoring Event API exposed Open5GS NEF. + """ + + def __init__(self, base_url: str, scs_as_id): + """ + Initializes the Open5GS Client. + """ + try: + self.base_url = base_url + self.scs_as_id = scs_as_id + log.info( + f"Initialized Open5GSClient with base_url: {self.base_url} " + f"and scs_as_id: {self.scs_as_id}" + ) + except Exception as e: + log.error(f"Failed to initialize Open5GSClient: {e}") + raise e + + def core_specific_qod_validation(self, session_info: schemas.CreateSession): + if session_info.qosProfile.root not in flow_id_mapping.keys(): + raise ValidationError( + f"Open5Gs only supports these qos-profiles: {', '.join(flow_id_mapping.keys())}" + ) + + def add_core_specific_qod_parameters( + self, + session_info: schemas.CreateSession, + subscription: schemas.AsSessionWithQoSSubscription, + ) -> None: + subscription.supportedFeatures = schemas.SupportedFeatures("003C") + flow_id = flow_id_mapping[session_info.qosProfile.root] + subscription.flowInfo = build_flows(flow_id, session_info) + + +# Note: +# As this class is inheriting from NetworkManagementInterface, it is +# expected to implement all the abstract methods defined in that interface. +# +# In case this network adapter doesn't support a specific method, it should +# be marked as NotImplementedError. diff --git a/service-resource-manager-implementation/src/adapters/network/core/__init__.py b/service-resource-manager-implementation/src/adapters/network/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service-resource-manager-implementation/src/adapters/network/core/base_network_client.py b/service-resource-manager-implementation/src/adapters/network/core/base_network_client.py new file mode 100644 index 0000000..321718a --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/network/core/base_network_client.py @@ -0,0 +1,469 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +## +# This file is part of the Open SDK +# +# Contributors: +# - Reza Mosahebfard (reza.mosahebfard@i2cat.net) +# - Ferran Cañellas (ferran.canellas@i2cat.net) +# - Giulio Carota (giulio.carota@eurecom.fr) +# - Panagiotis Pavlidis (p.pavlidis@iit.demokritos.gr) +## +import uuid +from datetime import datetime, timedelta, timezone +from itertools import product +from typing import Dict + +from src.adapters import logger +from src.adapters.network.adapters.errors import NetworkPlatformError +from src.adapters.network.core import common, schemas + +log = logger.get_logger(__name__) + + +def flatten_port_spec(ports_spec: schemas.PortsSpec | None) -> list[str]: + has_ports = False + has_ranges = False + flat_ports = [] + if ports_spec and ports_spec.ports: + has_ports = True + flat_ports.extend([str(port) for port in ports_spec.ports]) + if ports_spec and ports_spec.ranges: + has_ranges = True + flat_ports.extend( + [f"{range.from_.root}-{range.to.root}" for range in ports_spec.ranges] + ) + if not has_ports and not has_ranges: + flat_ports.append("0-65535") + return flat_ports + + +def build_flows( + flow_id: int, + session_info: schemas.CreateSession, +) -> list[schemas.FlowInfo]: + device_ports = flatten_port_spec(session_info.devicePorts) + server_ports = flatten_port_spec(session_info.applicationServerPorts) + ports_combis = list(product(device_ports, server_ports)) + + device_ip = session_info.device.ipv4Address or session_info.device.ipv6Address + if isinstance(device_ip, schemas.DeviceIpv6Address): + device_ip = device_ip.root + else: # IPv4 + device_ip = ( + device_ip.root.publicAddress.root or device_ip.root.privateAddress.root + ) + device_ip = str(device_ip) + server_ip = ( + session_info.applicationServer.ipv4Address + or session_info.applicationServer.ipv6Address + ) + server_ip = server_ip.root + flow_descrs = [] + for device_port, server_port in ports_combis: + flow_descrs.append( + f"permit in ip from {device_ip} {device_port} to {server_ip} {server_port}" + ) + flow_descrs.append( + f"permit out ip from {server_ip} {server_port} to {device_ip} {device_port}" + ) + flows = [ + schemas.FlowInfo(flowId=flow_id, flowDescriptions=[", ".join(flow_descrs)]) + ] + return flows + + +class BaseNetworkClient: + """ + Class for Network Resource Management. + + This class provides shared logic and extension points for different + Network 5G Cores (e.g., Open5GS, OAI, Open5GCore) interacting with + NEF-like platforms using CAMARA APIs. + """ + + base_url: str + scs_as_id: str + + def add_core_specific_qod_parameters( + self, + session_info: schemas.CreateSession, + subscription: schemas.AsSessionWithQoSSubscription, + ): + """ + Placeholder for adding core-specific parameters to the subscription. + This method should be overridden by subclasses to implement specific logic. + """ + pass + + def add_core_specific_ti_parameters( + self, + traffic_influence_info: schemas.CreateTrafficInfluence, + subscription: schemas.TrafficInfluSub, + ): + """ + Placeholder for adding core-specific parameters to the subscription. + This method should be overridden by subclasses to implement specific logic. + """ + pass + + def add_core_specific_location_parameters( + self, retrieve_location_request: schemas.RetrievalLocationRequest + ) -> schemas.MonitoringEventSubscriptionRequest: + """ + Placeholder for adding core-specific parameters to the location subscription. + This method should be overridden by subclasses to implement specific logic. + """ + pass + + def core_specific_qod_validation(self, session_info: schemas.CreateSession) -> None: + """ + Validates core-specific parameters for the session creation. + + args: + session_info: The session information to validate. + + raises: + ValidationError: If the session information does not meet core-specific requirements. + """ + # Placeholder for core-specific validation logic + # This method should be overridden by subclasses if needed + pass + + def core_specific_traffic_influence_validation( + self, traffic_influence_info: schemas.CreateTrafficInfluence + ) -> None: + """ + Validates core-specific parameters for the session creation. + + args: + session_info: The session information to validate. + + raises: + ValidationError: If the session information does not meet core-specific requirements. + """ + # Placeholder for core-specific validation logic + # This method should be overridden by subclasses if needed + pass + + def core_specific_monitoring_event_validation( + self, retrieve_location_request: schemas.RetrievalLocationRequest + ) -> None: + """ + Validates core-specific parameters for the monitoring event subscription. + + args: + retrieve_location_request: The request information to validate. + + raises: + ValidationError: If the request information does not meet core-specific requirements. + """ + # Placeholder for core-specific validation logic + # This method should be overwritten by subclasses if needed + pass + + def _build_qod_subscription( + self, session_info: Dict + ) -> schemas.AsSessionWithQoSSubscription: + valid_session_info = schemas.CreateSession.model_validate(session_info) + device_ipv4 = None + if valid_session_info.device.ipv4Address: + device_ipv4 = valid_session_info.device.ipv4Address.root.publicAddress.root + + self.core_specific_qod_validation(valid_session_info) + subscription = schemas.AsSessionWithQoSSubscription( + notificationDestination=str(valid_session_info.sink), + qosReference=valid_session_info.qosProfile.root, + ueIpv4Addr=device_ipv4, + ueIpv6Addr=valid_session_info.device.ipv6Address, + usageThreshold=schemas.UsageThreshold(duration=valid_session_info.duration), + ) + self.add_core_specific_qod_parameters(valid_session_info, subscription) + return subscription + + def _build_ti_subscription(self, traffic_influence_info: Dict): + traffic_influence_data = schemas.CreateTrafficInfluence.model_validate( + traffic_influence_info + ) + self.core_specific_traffic_influence_validation(traffic_influence_data) + + device_ip = traffic_influence_data.retrieve_ue_ipv4() + server_ip = ( + traffic_influence_data.appInstanceId + ) # assume that the instance id corresponds to its IPv4 address + sink_url = traffic_influence_data.notificationUri + edge_zone = traffic_influence_data.edgeCloudZoneId + + # build flow descriptor in oai format using device ip and server ip + flow_descriptor = f"permit out ip from {device_ip}/32 to {server_ip}/32" + + subscription = schemas.TrafficInfluSub( + afAppId=traffic_influence_data.appId, + ipv4Addr=str(device_ip), + notificationDestination=sink_url, + ) + subscription.add_flow_descriptor(flow_descriptor=flow_descriptor) + subscription.add_traffic_route(dnai=edge_zone) + + self.add_core_specific_ti_parameters(traffic_influence_data, subscription) + return subscription + + def _build_camara_ti(self, trafficInflSub: Dict): + traffic_influence_data = schemas.TrafficInfluSub.model_validate(trafficInflSub) + + flowDesc = traffic_influence_data.trafficFilters[0].flowDescriptions[0] + serverIp = flowDesc.split("to ")[1].split("/32")[0] + edgeId = traffic_influence_data.trafficRoutes[0].dnai + + camara_ti = schemas.CreateTrafficInfluence( + appId=traffic_influence_data.afAppId, + appInstanceId=serverIp, + edgeCloudZoneId=edgeId, + notificationUri=traffic_influence_data.notificationDestination, + device=schemas.Device( + ipv4Address=schemas.DeviceIpv4Addr1( + publicAddress=traffic_influence_data.ipv4Addr, + privateAddress=traffic_influence_data.ipv4Addr, + ) + ), + ) + return camara_ti + + def _build_monitoring_event_subscription( + self, retrieve_location_request: schemas.RetrievalLocationRequest + ) -> schemas.MonitoringEventSubscriptionRequest: + self.core_specific_monitoring_event_validation(retrieve_location_request) + subscription_3gpp = self.add_core_specific_location_parameters( + retrieve_location_request + ) + device = retrieve_location_request.device + subscription_3gpp.externalId = device.networkAccessIdentifier + subscription_3gpp.ipv4Addr = device.ipv4Address + subscription_3gpp.ipv6Addr = device.ipv6Address + # subscription.msisdn = device.phoneNumber.root.lstrip('+') + # subscription.notificationDestination = "http://127.0.0.1:8001" + + return subscription_3gpp + + def _compute_camara_last_location_time( + self, event_time: datetime, age_of_location_info_min: int = None + ) -> datetime: + """ + Computes the last location time based on the event time and age of location info. + + args: + event_time: ISO 8601 datetime, e.g. "2025-06-18T12:30:00Z" + age_of_location_info_min: unsigned int, age of location info in minutes + + returns: + datetime object representing the last location time in UTC. + """ + if age_of_location_info_min is not None: + last_location_time = event_time - timedelta( + minutes=age_of_location_info_min + ) + return last_location_time.replace(tzinfo=timezone.utc) + else: + return event_time.replace(tzinfo=timezone.utc) + + def create_monitoring_event_subscription( + self, retrieve_location_request: schemas.RetrievalLocationRequest + ) -> schemas.Location: + """ + Creates a Monitoring Event subscription based on CAMARA Location API input. + + args: + retrieve_location_request: Dictionary containing location retrieval details conforming to + the CAMARA Location API parameters. + + returns: + dictionary containing the created subscription details, including its ID. + """ + subscription = self._build_monitoring_event_subscription( + retrieve_location_request + ) + response = common.monitoring_event_post( + self.base_url, self.scs_as_id, subscription + ) + + monitoring_event_report = schemas.MonitoringEventReport(**response) + if monitoring_event_report.locationInfo is None: + log.error( + "Failed to retrieve location information from monitoring event report" + ) + raise NetworkPlatformError( + "Location information not found in monitoring event report" + ) + geo_area = monitoring_event_report.locationInfo.geographicArea + report_event_time = monitoring_event_report.eventTime + age_of_location_info = None + if monitoring_event_report.locationInfo.ageOfLocationInfo is not None: + age_of_location_info = ( + monitoring_event_report.locationInfo.ageOfLocationInfo.duration + ) + last_location_time = self._compute_camara_last_location_time( + report_event_time, age_of_location_info + ) + log.debug(f"Last Location time is {last_location_time}") + camara_point_list: list[schemas.Point] = [] + for point in geo_area.polygon.point_list.geographical_coords: + camara_point_list.append( + schemas.Point(latitude=point.lat, longitude=point.lon) + ) + camara_polygon = schemas.Polygon( + areaType=schemas.AreaType.polygon, + boundary=schemas.PointList(camara_point_list), + ) + + camara_location = schemas.Location( + area=camara_polygon, lastLocationTime=last_location_time + ) + + return camara_location + + def create_qod_session(self, session_info: Dict) -> Dict: + """ + Creates a QoS session based on CAMARA QoD API input. + + args: + session_info: Dictionary containing session details conforming to + the CAMARA QoD session creation parameters. + + returns: + dictionary containing the created session details, including its ID. + """ + subscription = self._build_qod_subscription(session_info) + response = common.as_session_with_qos_post( + self.base_url, self.scs_as_id, subscription + ) + subscription_info: schemas.AsSessionWithQoSSubscription = ( + schemas.AsSessionWithQoSSubscription(**response) + ) + + session_info = schemas.SessionInfo( + sessionId=schemas.SessionId(uuid.UUID(subscription_info.subscription_id)), + qosStatus=schemas.QosStatus.REQUESTED, + **session_info, + ) + return session_info.model_dump() + + def get_qod_session(self, session_id: str) -> Dict: + """ + Retrieves details of a specific Quality on Demand (QoS) session. + + args: + session_id: The unique identifier of the QoS session. + + returns: + Dictionary containing the details of the requested QoS session. + """ + response = common.as_session_with_qos_get( + self.base_url, self.scs_as_id, session_id=session_id + ) + subscription_info = schemas.AsSessionWithQoSSubscription(**response) + flowDesc = subscription_info.flowInfo[0].flowDescriptions[0] + serverIp = flowDesc.split("to ")[1].split("/")[0] + session_info = schemas.SessionInfo( + sessionId=schemas.SessionId(uuid.UUID(subscription_info.subscription_id)), + duration=subscription_info.usageThreshold.duration, + sink=subscription_info.notificationDestination, + qosProfile=subscription_info.qosReference, + device=schemas.Device( + ipv4Address=schemas.DeviceIpv4Addr1( + publicAddress=subscription_info.ueIpv4Addr, + privateAddress=subscription_info.ueIpv4Addr, + ), + ), + applicationServer=schemas.ApplicationServer( + ipv4Address=schemas.ApplicationServerIpv4Address(serverIp) + ), + ) + return session_info.model_dump() + + def delete_qod_session(self, session_id: str) -> None: + """ + Deletes a specific Quality on Demand (QoS) session. + + args: + session_id: The unique identifier of the QoS session to delete. + + returns: + None + """ + common.as_session_with_qos_delete( + self.base_url, self.scs_as_id, session_id=session_id + ) + log.info(f"QoD session deleted successfully [id={session_id}]") + + def create_traffic_influence_resource(self, traffic_influence_info: Dict) -> Dict: + """ + Creates a Traffic Influence resource based on CAMARA TI API input. + + args: + traffic_influence_info: Dictionary containing traffic influence details conforming to + the CAMARA TI resource creation parameters. + + returns: + dictionary containing the created traffic influence resource details, including its ID. + """ + + subscription = self._build_ti_subscription(traffic_influence_info) + response = common.traffic_influence_post( + self.base_url, self.scs_as_id, subscription + ) + + # retrieve the NEF resource id + if "self" in response.keys(): + subscription_id = response["self"] + else: + subscription_id = None + + traffic_influence_info["trafficInfluenceID"] = subscription_id + return traffic_influence_info + + def put_traffic_influence_resource( + self, resource_id: str, traffic_influence_info: Dict + ) -> Dict: + """ + Retrieves details of a specific Traffic Influence resource. + + args: + resource_id: The unique identifier of the Traffic Influence resource. + + returns: + Dictionary containing the details of the requested Traffic Influence resource. + """ + subscription = self._build_ti_subscription(traffic_influence_info) + common.traffic_influence_put( + self.base_url, self.scs_as_id, resource_id, subscription + ) + + traffic_influence_info["trafficInfluenceID"] = resource_id + return traffic_influence_info + + def delete_traffic_influence_resource(self, resource_id: str) -> None: + """ + Deletes a specific Traffic Influence resource. + + args: + resource_id: The unique identifier of the Traffic Influence resource to delete. + + returns: + None + """ + common.traffic_influence_delete(self.base_url, self.scs_as_id, resource_id) + return + + def get_individual_traffic_influence_resource(self, resource_id: str) -> Dict: + nef_response = common.traffic_influence_get( + self.base_url, self.scs_as_id, resource_id + ) + camara_ti = self._build_camara_ti(nef_response) + return camara_ti + + def get_all_traffic_influence_resource(self) -> list[Dict]: + r = common.traffic_influence_get(self.base_url, self.scs_as_id) + return [self._build_camara_ti(item) for item in r] + + +# Placeholder for other CAMARA APIs diff --git a/service-resource-manager-implementation/src/adapters/network/core/common.py b/service-resource-manager-implementation/src/adapters/network/core/common.py new file mode 100644 index 0000000..9f15c5a --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/network/core/common.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- + +import requests +from pydantic import BaseModel + +from src.adapters import logger + +log = logger.get_logger(__name__) + + +def _make_request(method: str, url: str, data=None): + try: + headers = None + if method == "POST" or method == "PUT": + headers = { + "Content-Type": "application/json", + "accept": "application/json", + } + elif method == "GET": + headers = { + "accept": "application/json", + } + response = requests.request(method, url, headers=headers, data=data) + response.raise_for_status() + if response.content: + return response.json() + except requests.exceptions.HTTPError as e: + raise CoreHttpError(e) from e + except requests.exceptions.ConnectionError as e: + raise CoreHttpError("connection error") from e + + +# Monitoring Event Methods +def monitoring_event_post( + base_url: str, scs_as_id: str, model_payload: BaseModel +) -> dict: + data = model_payload.model_dump_json(exclude_none=True, by_alias=True) + url = monitoring_event_build_url(base_url, scs_as_id) + return _make_request("POST", url, data=data) + + +def monitoring_event_build_url(base_url: str, scs_as_id: str, session_id: str = None): + url = f"{base_url}/3gpp-monitoring-event/v1/{scs_as_id}/subscriptions" + if session_id is not None and len(session_id) > 0: + return f"{url}/{session_id}" + else: + return url + + +# QoD methods +def as_session_with_qos_post( + base_url: str, scs_as_id: str, model_payload: BaseModel +) -> dict: + data = model_payload.model_dump_json(exclude_none=True, by_alias=True) + url = as_session_with_qos_build_url(base_url, scs_as_id) + return _make_request("POST", url, data=data) + + +def as_session_with_qos_get(base_url: str, scs_as_id: str, session_id: str) -> dict: + url = as_session_with_qos_build_url(base_url, scs_as_id, session_id) + return _make_request("GET", url) + + +def as_session_with_qos_delete(base_url: str, scs_as_id: str, session_id: str): + url = as_session_with_qos_build_url(base_url, scs_as_id, session_id) + return _make_request("DELETE", url) + + +def as_session_with_qos_build_url( + base_url: str, scs_as_id: str, session_id: str = None +): + url = f"{base_url}/3gpp-as-session-with-qos/v1/{scs_as_id}/subscriptions" + if session_id is not None and len(session_id) > 0: + return f"{url}/{session_id}" + else: + return url + + +# Traffic Influence Methods +def traffic_influence_post( + base_url: str, scs_as_id: str, model_payload: BaseModel +) -> dict: + data = model_payload.model_dump_json(exclude_none=True) + url = traffic_influence_build_url(base_url, scs_as_id) + return _make_request("POST", url, data=data) + + +def traffic_influence_delete(base_url: str, scs_as_id: str, session_id: str): + url = traffic_influence_build_url(base_url, scs_as_id, session_id) + return _make_request("DELETE", url) + + +def traffic_influence_put( + base_url: str, scs_as_id: str, session_id: str, model_payload: BaseModel +) -> dict: + data = model_payload.model_dump_json(exclude_none=True) + url = traffic_influence_build_url(base_url, scs_as_id, session_id) + return _make_request("PUT", url, data=data) + + +def traffic_influence_get(base_url: str, scs_as_id: str, sessionId: str = None) -> dict: + url = traffic_influence_build_url(base_url, scs_as_id, sessionId) + return _make_request("GET", url) + + +def traffic_influence_get_all( + base_url: str, scs_as_id: str, sessionId: str = None +) -> list[dict]: + url = traffic_influence_build_url(base_url, scs_as_id) + return _make_request("GET", url) + + +def traffic_influence_build_url(base_url: str, scs_as_id: str, session_id: str = None): + url = f"{base_url}/3gpp-traffic-influence/v1/{scs_as_id}/subscriptions" + if session_id is not None and len(session_id) > 0: + return f"{url}/{session_id}" + else: + return url + + +class CoreHttpError(Exception): + pass diff --git a/service-resource-manager-implementation/src/adapters/network/core/network_interface.py b/service-resource-manager-implementation/src/adapters/network/core/network_interface.py new file mode 100644 index 0000000..078dc40 --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/network/core/network_interface.py @@ -0,0 +1,311 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +## +# Copyright 2025-present by Software Networks Area, i2CAT. +# All rights reserved. +# +# This file is part of the Open SDK +# +# Contributors: +# - Reza Mosahebfard (reza.mosahebfard@i2cat.net) +# - Ferran Cañellas (ferran.canellas@i2cat.net) +## +import uuid +from abc import ABC +from itertools import product +from typing import Dict + +from src.adapters import logger +from src.adapters.network.clients.errors import NetworkPlatformError +from src.adapters.network.core import common, schemas + +log = logger.get_logger(__name__) + + +def flatten_port_spec(ports_spec: schemas.PortsSpec | None) -> list[str]: + has_ports = False + has_ranges = False + flat_ports = [] + if ports_spec and ports_spec.ports: + has_ports = True + flat_ports.extend([str(port) for port in ports_spec.ports]) + if ports_spec and ports_spec.ranges: + has_ranges = True + flat_ports.extend( + [f"{range.from_.root}-{range.to.root}" for range in ports_spec.ranges] + ) + if not has_ports and not has_ranges: + flat_ports.append("0-65535") + return flat_ports + + +def build_flows( + flow_id: int, + session_info: schemas.CreateSession, +) -> list[schemas.FlowInfo]: + device_ports = flatten_port_spec(session_info.devicePorts) + server_ports = flatten_port_spec(session_info.applicationServerPorts) + ports_combis = list(product(device_ports, server_ports)) + + device_ip = session_info.device.ipv4Address or session_info.device.ipv6Address + if isinstance(device_ip, schemas.DeviceIpv6Address): + device_ip = device_ip.root + else: # IPv4 + device_ip = ( + device_ip.root.publicAddress.root or device_ip.root.privateAddress.root + ) + device_ip = str(device_ip) + server_ip = ( + session_info.applicationServer.ipv4Address + or session_info.applicationServer.ipv6Address + ) + server_ip = server_ip.root + flow_descrs = [] + for device_port, server_port in ports_combis: + flow_descrs.append( + f"permit in ip from {device_ip} {device_port} to {server_ip} {server_port}" + ) + flow_descrs.append( + f"permit out ip from {server_ip} {server_port} to {device_ip} {device_port}" + ) + flows = [ + schemas.FlowInfo(flowId=flow_id, flowDescriptions=[", ".join(flow_descrs)]) + ] + return flows + + +class NetworkManagementInterface(ABC): + """ + Abstract Base Class for Network Resource Management. + + This interface defines the standard methods that all + Network Clients (Open5GS, OAI, Open5GCore) must implement. + + Partners implementing a new network client should inherit from this class + and provide concrete implementations for all abstract methods relevant + to their specific NEF capabilities. + """ + + base_url: str + scs_as_id: str + + def add_core_specific_qod_parameters( + self, + session_info: schemas.CreateSession, + subscription: schemas.AsSessionWithQoSSubscription, + ): + """ + Placeholder for adding core-specific parameters to the subscription. + This method should be overridden by subclasses to implement specific logic. + """ + pass + + def add_core_specific_ti_parameters( + self, + traffic_influence_info: schemas.CreateTrafficInfluence, + subscription: schemas.TrafficInfluSub, + ): + """ + Placeholder for adding core-specific parameters to the subscription. + This method should be overridden by subclasses to implement specific logic. + """ + pass + + def core_specific_qod_validation(self, session_info: schemas.CreateSession) -> None: + """ + Validates core-specific parameters for the session creation. + + args: + session_info: The session information to validate. + + raises: + ValidationError: If the session information does not meet core-specific requirements. + """ + # Placeholder for core-specific validation logic + # This method should be overridden by subclasses if needed + pass + + def core_specific_traffic_influence_validation( + self, traffic_influence_info: schemas.CreateTrafficInfluence + ) -> None: + """ + Validates core-specific parameters for the session creation. + + args: + session_info: The session information to validate. + + raises: + ValidationError: If the session information does not meet core-specific requirements. + """ + # Placeholder for core-specific validation logic + # This method should be overridden by subclasses if needed + pass + + def _build_qod_subscription( + self, session_info: Dict + ) -> schemas.AsSessionWithQoSSubscription: + valid_session_info = schemas.CreateSession.model_validate(session_info) + device_ipv4 = None + if valid_session_info.device.ipv4Address: + device_ipv4 = valid_session_info.device.ipv4Address.root.publicAddress.root + + self.core_specific_qod_validation(valid_session_info) + subscription = schemas.AsSessionWithQoSSubscription( + notificationDestination=str(valid_session_info.sink), + qosReference=valid_session_info.qosProfile.root, + ueIpv4Addr=device_ipv4, + ueIpv6Addr=valid_session_info.device.ipv6Address, + usageThreshold=schemas.UsageThreshold(duration=valid_session_info.duration), + ) + self.add_core_specific_qod_parameters(valid_session_info, subscription) + return subscription + + def _build_ti_subscription(self, traffic_influence_info: Dict): + traffic_influence_data = schemas.CreateTrafficInfluence.model_validate( + traffic_influence_info + ) + self.core_specific_traffic_influence_validation(traffic_influence_data) + + device_ip = traffic_influence_data.retrieve_ue_ipv4() + server_ip = ( + traffic_influence_data.appInstanceId + ) # assume that the instance id corresponds to its IPv4 address + sink_url = traffic_influence_data.notificationUri + edge_zone = traffic_influence_data.edgeCloudZoneId + + # build flow descriptor in oai format using device ip and server ip + flow_descriptor = f"permit out ip from {device_ip}/32 to {server_ip}/32" + + subscription = schemas.TrafficInfluSub( + afAppId=traffic_influence_data.appId, + ipv4Addr=str(device_ip), + notificationDestination=sink_url, + ) + subscription.add_flow_descriptor(flow_desriptor=flow_descriptor) + subscription.add_traffic_route(dnai=edge_zone) + + self.add_core_specific_ti_parameters(traffic_influence_data, subscription) + return subscription + + def create_qod_session(self, session_info: Dict) -> Dict: + """ + Creates a QoS session based on CAMARA QoD API input. + + args: + session_info: Dictionary containing session details conforming to + the CAMARA QoD session creation parameters. + + returns: + dictionary containing the created session details, including its ID. + """ + subscription = self._build_qod_subscription(session_info) + response = common.as_session_with_qos_post( + self.base_url, self.scs_as_id, subscription + ) + subscription_info: schemas.AsSessionWithQoSSubscription = ( + schemas.AsSessionWithQoSSubscription(**response) + ) + subscription_url = subscription_info.self_.root + subscription_id = subscription_url.split("/")[-1] if subscription_url else None + if not subscription_id: + log.error("Failed to retrieve QoS session ID from response") + raise NetworkPlatformError("QoS session ID not found in response") + session_info = schemas.SessionInfo( + sessionId=schemas.SessionId(uuid.UUID(subscription_id)), + qosStatus=schemas.QosStatus.REQUESTED, + **session_info, + ) + return session_info.model_dump() + + def get_qod_session(self, session_id: str) -> Dict: + """ + Retrieves details of a specific Quality on Demand (QoS) session. + + args: + session_id: The unique identifier of the QoS session. + + returns: + Dictionary containing the details of the requested QoS session. + """ + session = common.as_session_with_qos_get( + self.base_url, self.scs_as_id, session_id=session_id + ) + log.info(f"QoD session retrived successfully [id={session_id}]") + return session + + def delete_qod_session(self, session_id: str) -> None: + """ + Deletes a specific Quality on Demand (QoS) session. + + args: + session_id: The unique identifier of the QoS session to delete. + + returns: + None + """ + common.as_session_with_qos_delete( + self.base_url, self.scs_as_id, session_id=session_id + ) + log.info(f"QoD session deleted successfully [id={session_id}]") + + def create_traffic_influence_resource(self, traffic_influence_info: Dict) -> Dict: + """ + Creates a Traffic Influence resource based on CAMARA TI API input. + + args: + traffic_influence_info: Dictionary containing traffic influence details conforming to + the CAMARA TI resource creation parameters. + + returns: + dictionary containing the created traffic influence resource details, including its ID. + """ + + subscription = self._build_ti_subscription(traffic_influence_info) + response = common.traffic_influence_post( + self.base_url, self.scs_as_id, subscription + ) + + # retrieve the NEF resource id + if "self" in response.keys(): + subscription_id = response["self"] + else: + subscription_id = None + + traffic_influence_info["trafficInfluenceID"] = subscription_id + return traffic_influence_info + + def put_traffic_influence_resource( + self, resource_id: str, traffic_influence_info: Dict + ) -> Dict: + """ + Retrieves details of a specific Traffic Influence resource. + + args: + resource_id: The unique identifier of the Traffic Influence resource. + + returns: + Dictionary containing the details of the requested Traffic Influence resource. + """ + subscription = self._build_ti_subscription(traffic_influence_info) + common.traffic_influence_put( + self.base_url, self.scs_as_id, resource_id, subscription + ) + + traffic_influence_info["trafficInfluenceID"] = resource_id + return traffic_influence_info + + def delete_traffic_influence_resource(self, resource_id: str) -> None: + """ + Deletes a specific Traffic Influence resource. + + args: + resource_id: The unique identifier of the Traffic Influence resource to delete. + + returns: + None + """ + common.traffic_influence_delete(self.base_url, self.scs_as_id, resource_id) + return + + # Placeholder for other CAMARA APIs (e.g., Traffic Influence, + # Location-retrieval, etc.) diff --git a/service-resource-manager-implementation/src/adapters/network/core/schemas.py b/service-resource-manager-implementation/src/adapters/network/core/schemas.py new file mode 100644 index 0000000..43e48c3 --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/network/core/schemas.py @@ -0,0 +1,798 @@ +# -*- coding: utf-8 -*- +# This file defines the Pydantic models that represent the data structures (schemas) +# for the requests sent to and responses received from the Open5GS NEF API, +# specifically focusing on the APIs needed to support CAMARA QoD. + +import ipaddress +from datetime import datetime +from enum import Enum +from ipaddress import IPv4Address, IPv6Address +from typing import Annotated, Literal +from uuid import UUID + +from pydantic import ( + AnyHttpUrl, + AnyUrl, + BaseModel, + ConfigDict, + Field, + NonNegativeInt, + RootModel, +) +from pydantic_extra_types.mac_address import MacAddress + +from src.adapters.logger import setup_logger +from src.adapters.network.adapters.errors import NetworkPlatformError + +log = setup_logger(__name__) + + +class FlowDirection(Enum): + """ + DOWNLINK: The corresponding filter applies for traffic to the UE. + UPLINK: The corresponding filter applies for traffic from the UE. + BIDIRECTIONAL: The corresponding filter applies for traffic both to and from the UE. + UNSPECIFIED: The corresponding filter applies for traffic to the UE (downlink), but + has no specific direction declared. The service data flow detection shall apply the + filter for uplink traffic as if the filter was bidirectional. The PCF shall not use + the value UNSPECIFIED in filters created by the network in NW-initiated procedures. + The PCF shall only include the value UNSPECIFIED in filters in UE-initiated + procedures if the same value is received from the SMF. + """ + + DOWNLINK = "DOWNLINK" + UPLINK = "UPLINK" + BIDIRECTIONAL = "BIDIRECTIONAL" + UNSPECIFIED = "UNSPECIFIED" + + +class RequestedQosMonitoringParameter(Enum): + DOWNLINK = "DOWNLINK" + UPLINK = "UPLINK" + ROUND_TRIP = "ROUND_TRIP" + + +class ReportingFrequency(Enum): + EVENT_TRIGGERED = "EVENT_TRIGGERED" + PERIODIC = "PERIODIC" + SESSION_RELEASE = "SESSION_RELEASE" + + +Uinteger = Annotated[int, Field(ge=0)] + + +class DurationSec(RootModel[NonNegativeInt]): + root: NonNegativeInt = Field( + ..., + description="Unsigned integer identifying a period of time in units of \ + seconds.", + ) + + +class Volume(RootModel[NonNegativeInt]): + root: NonNegativeInt = Field( + ..., description="Unsigned integer identifying a volume in units of bytes." + ) + + +class SupportedFeatures(RootModel[str]): + root: str = Field( + ..., + pattern=r"^[A-Fa-f0-9]*$", + description="Hexadecimal string representing supported features.", + ) + + +class Link(RootModel[str]): + root: str = Field( + ..., + description="String formatted according to IETF RFC 3986 identifying a \ + referenced resource.", + ) + + +class FlowDescriptionModel(RootModel[str]): + root: str = Field(..., description="Defines a packet filter of an IP flow.") + + +class EthFlowDescription(BaseModel): + destMacAddr: MacAddress | None = None + ethType: str + fDesc: FlowDescriptionModel | None = None + fDir: FlowDirection | None = None + sourceMacAddr: MacAddress | None = None + vlanTags: list[str] | None = Field(None, max_length=2, min_length=1) + srcMacAddrEnd: MacAddress | None = None + destMacAddrEnd: MacAddress | None = None + + +class UsageThreshold(BaseModel): + duration: DurationSec | None = None + totalVolume: Volume | None = None + downlinkVolume: Volume | None = None + uplinkVolume: Volume | None = None + + +class SponsorInformation(BaseModel): + sponsorId: str = Field(..., description="It indicates Sponsor ID.") + aspId: str = Field(..., description="It indicates Application Service Provider ID.") + + +class QosMonitoringInformationModel(BaseModel): + reqQosMonParams: list[RequestedQosMonitoringParameter] | None = Field( + None, min_length=1 + ) + repFreqs: list[ReportingFrequency] | None = Field(None, min_length=1) + repThreshDl: Uinteger | None = None + repThreshUl: Uinteger | None = None + repThreshRp: Uinteger | None = None + waitTime: int | None = None + repPeriod: int | None = None + + +class FlowInfo(BaseModel): + flowId: int = Field(..., description="Indicates the IP flow.") + flowDescriptions: list[str] | None = Field( + None, + description="Indicates the packet filters of the IP flow. Refer to subclause \ + 5.3.8 of 3GPP TS 29.214 for encoding. It shall contain UL and/or DL IP \ + flow description.", + max_length=2, + min_length=1, + ) + + +class Snssai(BaseModel): + sst: int = Field(default=1) + sd: str = Field(default="FFFFFF") + + +class AsSessionWithQoSSubscription(BaseModel): + model_config = ConfigDict(serialize_by_alias=True) + self_: Link | None = Field(None, alias="self") + supportedFeatures: SupportedFeatures | None = None + notificationDestination: Link + flowInfo: list[FlowInfo] | None = Field( + None, description="Describe the data flow which requires QoS.", min_length=1 + ) + ethFlowInfo: list[EthFlowDescription] | None = Field( + None, description="Identifies Ethernet packet flows.", min_length=1 + ) + qosReference: str | None = Field( + None, description="Identifies a pre-defined QoS information" + ) + altQoSReferences: list[str] | None = Field( + None, + description="Identifies an ordered list of pre-defined QoS information. The \ + lower the index of the array for a given entry, the higher the priority.", + min_length=1, + ) + ueIpv4Addr: ipaddress.IPv4Address | None = None + ueIpv6Addr: ipaddress.IPv6Address | None = None + macAddr: MacAddress | None = None + snssai: Snssai | None = None + dnn: str | None = None + usageThreshold: UsageThreshold | None = None + sponsorInfo: SponsorInformation | None = None + qosMonInfo: QosMonitoringInformationModel | None = None + + @property + def subscription_id(self) -> str: + """ + Returns the subscription ID, which is the same as the self link. + """ + subscription_id = self.self_.root.split("/")[-1] if self.self_.root else None + if not subscription_id: + log.error("Failed to retrieve QoS session ID from response") + raise NetworkPlatformError("QoS session ID not found in response") + + +class SourceTrafficFilters(BaseModel): + sourcePort: int + + +class DestinationTrafficFilters(BaseModel): + destinationPort: int + destinationProtocol: str + + +class TrafficRoute(BaseModel): + dnai: str + + +class TrafficInfluSub(BaseModel): # Replace with a meaningful name + afServiceId: str | None = None + afAppId: str + dnn: str | None = None + snssai: Snssai | None = None + trafficFilters: list[FlowInfo] | None = Field( + None, + description="Describe the data flow which requires Traffic Influence.", + min_length=1, + ) + ipv4Addr: str | None = None + ipv6Addr: str | None = None + + notificationDestination: str + trafficRoutes: list[TrafficRoute] | None = Field( + None, + description="Describe the list of DNAIs to reach the destination", + min_length=1, + ) + suppFeat: str | None = None + + def add_flow_descriptor(self, flow_descriptor: str): + self.trafficFilters = list() + self.trafficFilters.append( + FlowInfo( + flowId=len(self.trafficFilters) + 1, flowDescriptions=[flow_descriptor] + ) + ) + + def add_traffic_route(self, dnai: str): + self.trafficRoutes = list() + self.trafficRoutes.append(TrafficRoute(dnai=dnai)) + + def add_snssai(self, sst: int, sd: str = None): + self.snssai = Snssai(sst=sst, sd=sd) + + +# Monitoring Event API + + +class DurationMin(BaseModel): + duration: int = Field( + 0, + description="Unsigned integer identifying a period of time in units of minutes", + ge=0, + ) + + +class PlmnId(BaseModel): + mcc: str = Field( + ..., + description="String encoding a Mobile Country Code, comprising of 3 digits.", + ) + mnc: str = Field( + ..., + description="String encoding a Mobile Network Code, comprising of 2 or 3 digits.", + ) + + +# The enumeration Accuracy represents a desired granularity of accuracy of the requested location information. +class Accuracy(str, Enum): + cgi_ecgi = ( + "CGI_ECGI" # The AF requests to be notified using cell level location accuracy. + ) + ta_ra = ( + "TA_RA" # The AF requests to be notified using TA/RA level location accuracy. + ) + geo_area = "GEO_AREA" # The AF requests to be notified using the geographical area accuracy. + civic_addr = "CIVIC_ADDR" # The AF requests to be notified using the civic address accuracy. #EDGEAPP + + +# If locationType set to "LAST_KNOWN_LOCATION", the monitoring event request from AF shall be only for one-time monitoring request +class LocationType(str, Enum): + CURRENT_LOCATION = ( + "CURRENT_LOCATION" # The AF requests to be notified for current location. + ) + LAST_KNOWN = ( + "LAST_KNOWN_LOCATION" # The AF requests to be notified for last known location. + ) + + +# This data type represents a monitoring event type. +class MonitoringType(str, Enum): + LOCATION_REPORTING = "LOCATION_REPORTING" + + +class LocationFailureCause(str, Enum): + position_denied = "POSITIONING_DENIED" # Positioning is denied. + unsupported_by_ue = "UNSUPPORTED_BY_UE" # Positioning is not supported by UE. + not_registered_ue = "NOT_REGISTERED_UE" # UE is not registered. + unspecified = "UNSPECIFIED" # Unspecified cause. + + +class GeographicalCoordinates(BaseModel): + lon: float = Field(..., description="Longitude coordinate.") + lat: float = Field(..., description="Latitude coordinate.") + + +class PointListNef(BaseModel): + geographical_coords: list[GeographicalCoordinates] = Field( + ..., + description="List of geographical coordinates defining the points.", + min_length=3, + max_length=15, + ) + + +class NefPolygon(BaseModel): + point_list: PointListNef = Field( + ..., description="List of points defining the polygon." + ) + + +class GeographicArea(BaseModel): + polygon: NefPolygon | None = Field( + None, description="Identifies a polygonal geographic area." + ) + + +# This data type represents the user location information which is sent from the NEF to the AF. +class LocationInfo(BaseModel): + ageOfLocationInfo: DurationMin | None = Field( + None, + description="Indicates the elapsed time since the last network contact of the UE.", + ) + cellId: str | None = Field(None, description="Cell ID where the UE is located.") + trackingAreaId: str | None = Field( + None, description="TrackingArea ID where the UE is located." + ) + enodeBId: str | None = Field(None, description="eNodeB ID where the UE is located.") + routingAreaId: str | None = Field( + None, description="Routing Area ID where the UE is located" + ) + plmnId: PlmnId | None = Field(None, description="PLMN ID where the UE is located.") + twanId: str | None = Field(None, description="TWAN ID where the UE is located.") + geographicArea: GeographicArea | None = Field( + None, + description="Identifies a geographic area of the user where the UE is located.", + ) + + +class MonitoringEventSubscriptionRequest(BaseModel): + accuracy: Accuracy | None = Field( + None, + description="Accuracy represents a desired granularity of accuracy of the requested location information.", + ) + externalId: str | None = Field( + None, description="Identifies a user clause 4.6.2 TS 23.682 (optional)" + ) + msisdn: str | None = Field( + None, + description="Identifies the MS internal PSTN/ISDN number allocated for a UE.", + ) + ipv4Addr: IPv4Address | None = Field( + None, description="Identifies the Ipv4 address." + ) + ipv6Addr: IPv6Address | None = Field( + None, description="Identifies the Ipv6 address." + ) + notificationDestination: AnyHttpUrl = Field( + ..., + description="URI of a notification destination that the T8 message shall be delivered to.", + ) + monitoringType: MonitoringType = Field( + ..., description="Enumeration of monitoring type. Refer to clause 5.3.2.4.3." + ) + maximumNumberOfReports: int | None = Field( + None, + description="Identifies the maximum number of event reports to be generated by the AMF to the NEF and then the AF.", + ) + monitorExpireTime: datetime | None = Field( + None, + description="Identifies the absolute time at which the related monitoring event request is considered to expire.", + ) + locationType: LocationType | None = Field( + None, + description="Indicates whether the request is for Current Location, Initial Location, or Last Known Location.", + ) + repPeriod: DurationSec | None = Field( + None, description="Identifies the periodic time for the event reports." + ) + minimumReportInterval: DurationSec | None = Field( + None, + description="identifies a minimum time interval between Location Reporting notifications", + ) + + +# This data type represents a monitoring event notification which is sent from the NEF to the AF. +class MonitoringEventReport(BaseModel): + externalId: str | None = Field( + None, description="Identifies a user, clause 4.6.2 TS 23.682" + ) + msisdn: str | None = Field( + None, + description="Identifies the MS internal PSTN/ISDN number allocated for a UE.", + ) + locationInfo: LocationInfo | None = Field( + None, description="Indicates the user location related information." + ) + locFailureCause: LocationFailureCause | None = Field( + None, description="Indicates the location positioning failure cause." + ) + monitoringType: MonitoringType = Field( + ..., + description="Identifies the type of monitoring as defined in clause 5.3.2.4.3.", + ) + eventTime: datetime | None = Field( + None, + description="Identifies when the event is detected or received. Shall be included for each group of UEs.", + ) + + +# This data type represents a monitoring notification which is sent from the NEF to the AF. +class MonitoringNotification(BaseModel): + subscription: AnyHttpUrl = Field( + ..., + description="Link to the subscription resource to which this notification is related.", + ) + monitoringEventReports: list[MonitoringEventReport] | None = Field( + None, + description="Each element identifies a monitoring event report (optional).", + ) + cancelInd: bool | None = Field( + False, + description="Indicates whether to request to cancel the corresponding monitoring subscription. Set to false or omitted otherwise.", + ) + + +############################################################### +############################################################### +# CAMARA Models + + +class PhoneNumber(RootModel[str]): + root: Annotated[ + str, + Field( + description="A public identifier addressing a telephone subscription. In mobile networks it corresponds to the MSISDN (Mobile Station International Subscriber Directory Number). In order to be globally unique it has to be formatted in international format, according to E.164 standard, prefixed with '+'.", + examples=["+123456789"], + pattern="^\\+[1-9][0-9]{4,14}$", + ), + ] + + +class NetworkAccessIdentifier(RootModel[str]): + root: Annotated[ + str, + Field( + description="A public identifier addressing a subscription in a mobile network. In 3GPP terminology, it corresponds to the GPSI formatted with the External Identifier ({Local Identifier}@{Domain Identifier}). Unlike the telephone number, the network access identifier is not subjected to portability ruling in force, and is individually managed by each operator.", + examples=["123456789@domain.com"], + ), + ] + + +class SingleIpv4Addr(RootModel[IPv4Address]): + root: Annotated[ + IPv4Address, + Field( + description="A single IPv4 address with no subnet mask", + examples=["203.0.113.0"], + ), + ] + + +class Port(RootModel[int]): + root: Annotated[int, Field(description="TCP or UDP port number", ge=0, le=65535)] + + +class DeviceIpv4Addr1(BaseModel): + publicAddress: SingleIpv4Addr + privateAddress: SingleIpv4Addr + publicPort: Port | None = None + + +class DeviceIpv4Addr2(BaseModel): + publicAddress: SingleIpv4Addr + privateAddress: SingleIpv4Addr | None = None + publicPort: Port + + +class DeviceIpv4Addr(RootModel[DeviceIpv4Addr1 | DeviceIpv4Addr2]): + root: Annotated[ + DeviceIpv4Addr1 | DeviceIpv4Addr2, + Field( + description="The device should be identified by either the public (observed) IP address and port as seen by the application server, or the private (local) and any public (observed) IP addresses in use by the device (this information can be obtained by various means, for example from some DNS servers).\n\nIf the allocated and observed IP addresses are the same (i.e. NAT is not in use) then the same address should be specified for both publicAddress and privateAddress.\n\nIf NAT64 is in use, the device should be identified by its publicAddress and publicPort, or separately by its allocated IPv6 address (field ipv6Address of the Device object)\n\nIn all cases, publicAddress must be specified, along with at least one of either privateAddress or publicPort, dependent upon which is known. In general, mobile devices cannot be identified by their public IPv4 address alone.\n", + examples=[{"publicAddress": "203.0.113.0", "publicPort": 59765}], + ), + ] + + +class DeviceIpv6Address(RootModel[IPv6Address]): + root: Annotated[ + IPv6Address, + Field( + description="The device should be identified by the observed IPv6 address, or by any single IPv6 address from within the subnet allocated to the device (e.g. adding ::0 to the /64 prefix).\n\nThe session shall apply to all IP flows between the device subnet and the specified application server, unless further restricted by the optional parameters devicePorts or applicationServerPorts.\n", + examples=["2001:db8:85a3:8d3:1319:8a2e:370:7344"], + ), + ] + + +class Device(BaseModel): + phoneNumber: PhoneNumber | None = None + networkAccessIdentifier: NetworkAccessIdentifier | None = None + ipv4Address: DeviceIpv4Addr | None = None + ipv6Address: DeviceIpv6Address | None = None + + +class RetrievalLocationRequest(BaseModel): + """ + Request to retrieve the location of a device. Device is not required when using a 3-legged access token. + """ + + device: Annotated[ + Device | None, + Field(None, description="End-user device able to connect to a mobile network."), + ] + maxAge: Annotated[ + int | None, + Field( + None, + description="Maximum age of the location information which is accepted for the location retrieval (in seconds).", + ), + ] + maxSurface: Annotated[ + int | None, + Field( + None, + description="Maximum surface in square meters which is accepted by the client for the location retrieval.", + ge=1, + examples=[1000000], + ), + ] + + +class AreaType(str, Enum): + circle = "CIRCLE" # The area is defined as a circle. + polygon = "POLYGON" # The area is defined as a polygon. + + +class Point(BaseModel): + latitude: Annotated[ + float, + Field( + description="Latitude component of a location.", + examples=["50.735851"], + ge=-90, + le=90, + ), + ] + longitude: Annotated[ + float, + Field( + ..., + description="Longitude component of location.", + examples=["7.10066"], + ge=-180, + le=180, + ), + ] + + +class PointList( + RootModel[ + Annotated[ + list[Point], + Field( + min_length=3, + max_length=15, + description="List of points defining the area.", + ), + ] + ] +): + pass + + +class Circle(BaseModel): + areaType: Literal[AreaType.circle] + center: Annotated[Point, Field(description="Center point of the circle.")] + radius: Annotated[float, Field(description="Radius of the circle.", ge=1)] + + +class Polygon(BaseModel): + areaType: Literal[AreaType.polygon] + boundary: Annotated[ + PointList, Field(description="List of points defining the polygon.") + ] + + +Area = Annotated[Circle | Polygon, Field(discriminator="areaType")] + + +class LastLocationTime( + RootModel[ + Annotated[ + datetime, + Field( + description="Last date and time when the device was localized.", + examples="2023-09-07T10:40:52Z", + ), + ] + ] +): + pass + + +class Location(BaseModel): + lastLocationTime: Annotated[ + LastLocationTime, Field(description="Last known location time.") + ] + area: Annotated[Area, Field(description="Geographical area of the location.")] + + +class ApplicationServerIpv4Address(RootModel[str]): + root: Annotated[ + str, + Field( + description="IPv4 address may be specified in form
as:\n - address - an IPv4 number in dotted-quad form 1.2.3.4. Only this exact IP number will match the flow control rule.\n - address/mask - an IP number as above with a mask width of the form 1.2.3.4/24.\n In this case, all IP numbers from 1.2.3.0 to 1.2.3.255 will match. The bit width MUST be valid for the IP version.\n", + examples=["198.51.100.0/24"], + ), + ] + + +class ApplicationServerIpv6Address(RootModel[str]): + root: Annotated[ + str, + Field( + description="IPv6 address may be specified in form
as:\n - address - The /128 subnet is optional for single addresses:\n - 2001:db8:85a3:8d3:1319:8a2e:370:7344\n - 2001:db8:85a3:8d3:1319:8a2e:370:7344/128\n - address/mask - an IP v6 number with a mask:\n - 2001:db8:85a3:8d3::0/64\n - 2001:db8:85a3:8d3::/64\n", + examples=["2001:db8:85a3:8d3:1319:8a2e:370:7344"], + ), + ] + + +class ApplicationServer(BaseModel): + ipv4Address: ApplicationServerIpv4Address | None = None + ipv6Address: ApplicationServerIpv6Address | None = None + + +class Range(BaseModel): + from_: Annotated[Port, Field(alias="from")] + to: Port + + +class PortsSpec(BaseModel): + ranges: Annotated[ + list[Range] | None, Field(description="Range of TCP or UDP ports", min_length=1) + ] = None + ports: Annotated[ + list[Port] | None, Field(description="Array of TCP or UDP ports", min_length=1) + ] = None + + +class QosProfileName(RootModel[str]): + root: Annotated[ + str, + Field( + description="A unique name for identifying a specific QoS profile.\nThis may follow different formats depending on the API provider implementation.\nSome options addresses:\n - A UUID style string\n - Support for predefined profiles QOS_S, QOS_M, QOS_L, and QOS_E\n - A searchable descriptive name\nThe set of QoS Profiles that an API provider is offering may be retrieved by means of the QoS Profile API (qos-profile) or agreed on onboarding time.\n", + examples=["voice"], + max_length=256, + min_length=3, + pattern="^[a-zA-Z0-9_.-]+$", + ), + ] + + +class CredentialType(Enum): + PLAIN = "PLAIN" + ACCESSTOKEN = "ACCESSTOKEN" + REFRESHTOKEN = "REFRESHTOKEN" + + +class SinkCredential(BaseModel): + credentialType: Annotated[ + CredentialType, + Field( + description="The type of the credential.\nNote: Type of the credential - MUST be set to ACCESSTOKEN for now\n" + ), + ] + + +class NotificationSink(BaseModel): + sink: str | None + sinkCredential: SinkCredential | None + + +class BaseSessionInfo(BaseModel): + device: Device | None = None + applicationServer: ApplicationServer + devicePorts: Annotated[ + PortsSpec | None, + Field( + description="The ports used locally by the device for flows to which the requested QoS profile should apply. If omitted, then the qosProfile will apply to all flows between the device and the specified application server address and ports" + ), + ] = None + applicationServerPorts: Annotated[ + PortsSpec | None, + Field( + description="A list of single ports or port ranges on the application server" + ), + ] = None + qosProfile: QosProfileName + sink: Annotated[ + AnyUrl | None, + Field( + description="The address to which events about all status changes of the session (e.g. session termination) shall be delivered using the selected protocol.", + examples=["https://endpoint.example.com/sink"], + ), + ] = None + sinkCredential: Annotated[ + SinkCredential | None, + Field( + description="A sink credential provides authentication or authorization information necessary to enable delivery of events to a target." + ), + ] = None + + +class CreateSession(BaseSessionInfo): + duration: Annotated[ + int, + Field( + description="Requested session duration in seconds. Value may be explicitly limited for the QoS profile, as specified in the Qos Profile (see qos-profile API). Implementations can grant the requested session duration or set a different duration, based on network policies or conditions.\n", + examples=[3600], + ge=1, + ), + ] + + +class SessionId(RootModel[UUID]): + root: Annotated[UUID, Field(description="Session ID in UUID format")] + + +class QosStatus(Enum): + REQUESTED = "REQUESTED" + AVAILABLE = "AVAILABLE" + UNAVAILABLE = "UNAVAILABLE" + + +class StatusInfo(Enum): + DURATION_EXPIRED = "DURATION_EXPIRED" + NETWORK_TERMINATED = "NETWORK_TERMINATED" + DELETE_REQUESTED = "DELETE_REQUESTED" + + +class SessionInfo(BaseSessionInfo): + sessionId: SessionId + duration: Annotated[ + int, + Field( + description='Session duration in seconds. Implementations can grant the requested session duration or set a different duration, based on network policies or conditions.\n- When `qosStatus` is "REQUESTED", the value is the duration to be scheduled, granted by the implementation.\n- When `qosStatus` is AVAILABLE", the value is the overall duration since `startedAt. When the session is extended, the value is the new overall duration of the session.\n- When `qosStatus` is "UNAVAILABLE", the value is the overall effective duration since `startedAt` until the session was terminated.\n', + examples=[3600], + ge=1, + ), + ] + startedAt: Annotated[ + datetime | None, + Field( + description='Date and time when the QoS status became "AVAILABLE". Not to be returned when `qosStatus` is "REQUESTED". Format must follow RFC 3339 and must indicate time zone (UTC or local).', + examples=["2024-06-01T12:00:00Z"], + ), + ] = None + expiresAt: Annotated[ + datetime | None, + Field( + description='Date and time of the QoS session expiration. Format must follow RFC 3339 and must indicate time zone (UTC or local).\n- When `qosStatus` is "AVAILABLE", it is the limit time when the session is scheduled to finnish, if not terminated by other means.\n- When `qosStatus` is "UNAVAILABLE", it is the time when the session was terminated.\n- Not to be returned when `qosStatus` is "REQUESTED".\nWhen the session is extended, the value is the new expiration time of the session.\n', + examples=["2024-06-01T13:00:00Z"], + ), + ] = None + qosStatus: QosStatus + statusInfo: StatusInfo | None = None + + +class CreateTrafficInfluence(BaseModel): + trafficInfluenceID: str | None = None + apiConsumerId: str | None = None + appId: str + appInstanceId: str + edgeCloudRegion: str | None = None + edgeCloudZoneId: str | None = None + sourceTrafficFilters: SourceTrafficFilters | None = None + destinationTrafficFilters: DestinationTrafficFilters | None = None + notificationUri: str | None = None + notificationAuthToken: str | None = None + device: Device + notificationSink: NotificationSink | None = None + + def retrieve_ue_ipv4(self): + if self.device is not None and self.device.ipv4Address is not None: + return self.device.ipv4Address.root.privateAddress.root + else: + raise KeyError("device.ipv4Address.publicAddress") + + def add_ue_ipv4(self, ipv4: str): + if self.device is None: + self.device = Device() + if self.device.ipv4Address is None: + self.device.ipv4Address = DeviceIpv4Addr(publicAddress=ipv4) diff --git a/service-resource-manager-implementation/src/adapters/o-ran/__init__.py b/service-resource-manager-implementation/src/adapters/o-ran/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service-resource-manager-implementation/src/adapters/o-ran/clients/__init__.py b/service-resource-manager-implementation/src/adapters/o-ran/clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service-resource-manager-implementation/src/adapters/o-ran/clients/juniper-ric/__init__.py b/service-resource-manager-implementation/src/adapters/o-ran/clients/juniper-ric/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service-resource-manager-implementation/src/adapters/o-ran/clients/juniper-ric/client.py b/service-resource-manager-implementation/src/adapters/o-ran/clients/juniper-ric/client.py new file mode 100644 index 0000000..e69de29 diff --git a/service-resource-manager-implementation/src/adapters/o-ran/core/__init__.py b/service-resource-manager-implementation/src/adapters/o-ran/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service-resource-manager-implementation/src/adapters/o-ran/core/o-ran_interface.py b/service-resource-manager-implementation/src/adapters/o-ran/core/o-ran_interface.py new file mode 100644 index 0000000..4640904 --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/o-ran/core/o-ran_interface.py @@ -0,0 +1 @@ +# TODO diff --git a/service-resource-manager-implementation/src/adapters/oran/__init__.py b/service-resource-manager-implementation/src/adapters/oran/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service-resource-manager-implementation/src/adapters/oran/clients/__init__.py b/service-resource-manager-implementation/src/adapters/oran/clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service-resource-manager-implementation/src/adapters/oran/clients/juniper_ric/__init__.py b/service-resource-manager-implementation/src/adapters/oran/clients/juniper_ric/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service-resource-manager-implementation/src/adapters/oran/clients/juniper_ric/client.py b/service-resource-manager-implementation/src/adapters/oran/clients/juniper_ric/client.py new file mode 100644 index 0000000..e69de29 diff --git a/service-resource-manager-implementation/src/adapters/oran/core/__init__.py b/service-resource-manager-implementation/src/adapters/oran/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service-resource-manager-implementation/src/adapters/oran/core/oran_interface.py b/service-resource-manager-implementation/src/adapters/oran/core/oran_interface.py new file mode 100644 index 0000000..4640904 --- /dev/null +++ b/service-resource-manager-implementation/src/adapters/oran/core/oran_interface.py @@ -0,0 +1 @@ +# TODO diff --git a/service-resource-manager-implementation/src/controllers/app_instance_controller.py b/service-resource-manager-implementation/src/controllers/app_instance_controller.py index fac5855..40ccb00 100644 --- a/service-resource-manager-implementation/src/controllers/app_instance_controller.py +++ b/service-resource-manager-implementation/src/controllers/app_instance_controller.py @@ -1,6 +1,5 @@ from os import environ import logging -from src.adapters.kubernetes_adapter import submit_helm_chart, app_deploy import connexion logger = logging.getLogger(__name__) diff --git a/service-resource-manager-implementation/src/controllers/edge_cloud_management_controller.py b/service-resource-manager-implementation/src/controllers/edge_cloud_management_controller.py index 70f7f15..fccc81c 100644 --- a/service-resource-manager-implementation/src/controllers/edge_cloud_management_controller.py +++ b/service-resource-manager-implementation/src/controllers/edge_cloud_management_controller.py @@ -10,13 +10,13 @@ adapter_base_url = os.environ['ADAPTER_BASE_URL'] adapter = None if adapter_name=='aeros': - from src.clients.edgecloud.clients.aeros.client import EdgeApplicationManager + from src.adapters.edgecloud.adapters.aeros.client import EdgeApplicationManager adapter = EdgeApplicationManager(base_url=adapter_base_url) elif adapter_name=='i2edge': - from src.clients.edgecloud.clients.i2edge.client import EdgeApplicationManager + from src.adapters.edgecloud.adapters.i2edge.client import EdgeApplicationManager adapter = EdgeApplicationManager(base_url=adapter_base_url) elif adapter_name=='kubernetes': - from src.clients.edgecloud.clients.piedge.client import EdgeApplicationManager + from src.adapters.edgecloud.adapters.kubernetes.client import EdgeApplicationManager adapter = EdgeApplicationManager(base_url=adapter_base_url, **os.environ) def deregister_service_function(service_function_id: str): # noqa: E501 @@ -32,8 +32,8 @@ def deregister_service_function(service_function_id: str): # noqa: E501 """ try: - status_deregistration, code = adapter.delete_onboarded_app(service_function_id) - return status_deregistration, code + code = adapter.delete_onboarded_app(service_function_id) + return code except Exception as ce_: raise Exception("An exception occurred :", ce_) @@ -130,7 +130,7 @@ def deploy_service_function(): # noqa: E501 try: # body = DeployApp.from_dict(connexion.request.get_json()) body = connexion.request.get_json() - response = adapter.deploy_app(app_id=body.get("appId"), app_zones=body.get("appZones")) + response = adapter.deploy_app(body) # body = DeployServiceFunction.from_dict(connexion.request.get_json()) # response = piedge_encoder.deploy_service_function(body) return response diff --git a/service-resource-manager-implementation/src/controllers/network_functions_controller.py b/service-resource-manager-implementation/src/controllers/network_functions_controller.py index 57a441f..e8e3982 100644 --- a/service-resource-manager-implementation/src/controllers/network_functions_controller.py +++ b/service-resource-manager-implementation/src/controllers/network_functions_controller.py @@ -11,17 +11,18 @@ adapter = None if network_client is not None: if network_client=='oai': - from src.clients.network.clients.oai.client import NetworkManager + from src.adapters.network.clients.oai.client import NetworkManager adapter = NetworkManager() elif network_client=='open5gcore': - from src.clients.network.clients.open5gcore.client import NetworkManager + from src.adapters.network.clients.open5gcore.client import NetworkManager adapter = NetworkManager() else: - from src.clients.network.clients.open5gs.client import NetworkManager + from src.adapters.network.clients.open5gs.client import NetworkManager adapter = NetworkManager() -def create_qod_session(body): +def create_qod_session(body: dict): + if connexion.request.is_json: try: response = adapter.create_qod_session(body) @@ -33,6 +34,7 @@ def create_qod_session(body): return 'ERROR: Could not read JSON payload.', 400 def get_qod_session(id: str): + try: response = adapter.get_qod_session(id) return response @@ -41,21 +43,26 @@ def get_qod_session(id: str): return ce_ def delete_qod_session(id: str): + try: response = adapter.delete_qod_session(id) - return 'QoD successfully removed' + return response except Exception as ce_: logger.error(ce_) return ce_ def create_traffic_influence_resource(body): + #TODO pass def delete_traffic_influence_resource(id: str): + #TODO pass def get_traffic_influence_resource(id: str): + #TODO pass def get_all_traffic_influence_resources(): + #TODO pass \ No newline at end of file diff --git a/service-resource-manager-implementation/src/controllers/nodes_controller.py b/service-resource-manager-implementation/src/controllers/nodes_controller.py index 0e1d81e..c7713c8 100644 --- a/service-resource-manager-implementation/src/controllers/nodes_controller.py +++ b/service-resource-manager-implementation/src/controllers/nodes_controller.py @@ -1,9 +1,4 @@ -import connexion -import six -import time -from src.core import piedge_encoder import logging -# from src.__main__ import driver import os @@ -13,13 +8,13 @@ adapter_name = os.environ['EDGE_CLOUD_ADAPTER_NAME'] adapter = None if adapter_name=='aeros': - from src.clients.edgecloud.clients.aeros.client import EdgeApplicationManager + from src.adapters.edgecloud.adapters.aeros.client import EdgeApplicationManager adapter = EdgeApplicationManager() elif adapter_name=='i2edge': - from src.clients.edgecloud.clients.i2edge.client import EdgeApplicationManager + from src.adapters.edgecloud.adapters.i2edge.client import EdgeApplicationManager adapter = EdgeApplicationManager() elif adapter_name=='piedge': - from src.clients.edgecloud.clients.piedge.client import EdgeApplicationManager + from src.adapters.edgecloud.adapters.kubernetes.client import EdgeApplicationManager adapter = EdgeApplicationManager() @@ -35,3 +30,16 @@ def get_nodes(): # noqa: E501 return response except Exception as ce_: logger.info(ce_) + +def node_details(node_id: str): # noqa: E501 + """Returns the edge nodes status. + + # noqa: E501 + + :rtype: NodesResponse + """ + try: + response = adapter.get_edge_cloud_zones_details(node_id=node_id) + return response + except Exception as ce_: + logger.info(ce_) diff --git a/service-resource-manager-implementation/src/controllers/operations_controller.py b/service-resource-manager-implementation/src/controllers/operations_controller.py index aec2bf6..7073582 100644 --- a/service-resource-manager-implementation/src/controllers/operations_controller.py +++ b/service-resource-manager-implementation/src/controllers/operations_controller.py @@ -4,39 +4,41 @@ from src.models.helm_install_model import HelmInstall from os import environ import connexion -master_node_password=environ["KUBERNETES_MASTER_PASSWORD"].strip() -master_node_hostname=environ["KUBERNETES_MASTER_HOSTNAME"].strip() -master_node_ip=environ["KUBERNETES_MASTER_IP"].strip() -master_node_port=environ["KUBERNETES_MASTER_PORT"].strip() +# master_node_password=environ.get("KUBERNETES_MASTER_PASSWORD").strip() +# master_node_hostname=environ.get("KUBERNETES_MASTER_HOSTNAME").strip() +# master_node_ip=environ.get("KUBERNETES_MASTER_IP").strip() +# master_node_port=environ.get("KUBERNETES_MASTER_PORT").strip() def install_helm_chart(helm=None): - logging.info('Installing helm chart') - if connexion.request.is_json: - try: - # logging.info(connexion.request.get_json()) - helm =connexion.request.get_json() - ssh=paramiko.SSHClient() - ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - ssh.connect(master_node_ip,22, username='dlaskaratos', password=master_node_password) - creds_string='' - if helm.get('repo_password') is not None and helm.get('repo_username') is not None: - creds_string=' --username '+helm['repo_username']+' --password '+helm['repo_password'] - stdin, stdout, stderr= ssh.exec_command('echo | sudo helm install '+helm['deployment_name']+' '+helm['uri']+creds_string) - stdout.channel.set_combine_stderr(True) - lines=stdout.readlines() - return lines - except Exception as e: - logging.error(e) - return e.__cause__ - else: - return 'Error installing helm chart' + pass +# logging.info('Installing helm chart') +# if connexion.request.is_json: +# try: +# # logging.info(connexion.request.get_json()) +# helm =connexion.request.get_json() +# ssh=paramiko.SSHClient() +# ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +# ssh.connect(master_node_ip,22, username='dlaskaratos', password=master_node_password) +# creds_string='' +# if helm.get('repo_password') is not None and helm.get('repo_username') is not None: +# creds_string=' --username '+helm['repo_username']+' --password '+helm['repo_password'] +# stdin, stdout, stderr= ssh.exec_command('echo | sudo helm install '+helm['deployment_name']+' '+helm['uri']+creds_string) +# stdout.channel.set_combine_stderr(True) +# lines=stdout.readlines() +# return lines +# except Exception as e: +# logging.error(e) +# return e.__cause__ +# else: +# return 'Error installing helm chart' def uninstall_helm_chart(name: str): - logging.info('Uninstalling helm chart') - ssh=paramiko.SSHClient() - ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - ssh.connect(master_node_ip,22, master_node_hostname,master_node_password) - stdin, stdout, stderr= ssh.exec_command('echo | sudo helm uninstall '+name) - stdout.channel.set_combine_stderr(True) - lines=stdout.readlines() - return lines + pass +# logging.info('Uninstalling helm chart') +# ssh=paramiko.SSHClient() +# ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +# ssh.connect(master_node_ip,22, master_node_hostname,master_node_password) +# stdin, stdout, stderr= ssh.exec_command('echo | sudo helm uninstall '+name) +# stdout.channel.set_combine_stderr(True) +# lines=stdout.readlines() +# return lines diff --git a/service-resource-manager-implementation/src/swagger/swagger.yaml b/service-resource-manager-implementation/src/swagger/swagger.yaml index 18edf2a..f05078a 100644 --- a/service-resource-manager-implementation/src/swagger/swagger.yaml +++ b/service-resource-manager-implementation/src/swagger/swagger.yaml @@ -2,7 +2,7 @@ openapi: 3.0.0 info: title: SRM Controller API description: | - API exposed by SRM for "PaaS" - based interaction with NFV MANO. + API exposed by SRM for for CAMARA-deefined operations. termsOfService: http://swagger.io/terms/ contact: email: dlaskaratos@intracom-telecom.com @@ -14,26 +14,31 @@ externalDocs: description: Find out more about Swagger url: http://swagger.io servers: -- url: http://vitrualserver:8080/piedge-connector/2.0.0 +- url: http://vitrualserver:8080/srm/1.0.0 paths: -# /authentication: -# post: -# tags: -# - Login -# summary: Login with a username and password. -# operationId: authentication_login -# requestBody: -# description: Registration method to login -# content: -# application/json: -# schema: -# $ref: '#/components/schemas/LoginRegistrationRequest' -# responses: -# "200": -# description: A JSON Web Token (JWT). -# "401": -# description: Incorrect username or password. -# x-openapi-router-controller: src.controllers.login_controller + /zone-details/{zone_id}: + get: + tags: + - Nodes + summary: Get Node details by Node identifier + operationId: node_details + parameters: + - name: zone_id + in: path + description: Gets node details + required: true + style: simple + explode: false + schema: + type: string + responses: + "200": + description: Node details retrieved + "405": + description: Method not allowed + "404": + description: Node does not exist + x-openapi-router-controller: src.controllers.nodes_controller /helm: post: tags: diff --git a/srm-deployment.yaml b/srm-deployment.yaml index c71dba6..f86f59f 100644 --- a/srm-deployment.yaml +++ b/srm-deployment.yaml @@ -1,3 +1,9 @@ +kind: Namespace +apiVersion: v1 +metadata: + name: sunrise6g + labels: + name: sunrise6g --- apiVersion: apps/v1 kind: Deployment @@ -26,24 +32,18 @@ spec: spec: containers: - env: - - name: KUBERNETES_MASTER_HOSTNAME - value: k3d-sunriseop-server-0 - - name: KUBERNETES_MASTER_PASSWORD - value: ameThyst23! - name: KUBERNETES_MASTER_IP value: k3d-sunriseop-server-0 - name: KUBERNETES_MASTER_PORT value: "6443" - name: KUBERNETES_USERNAME value: cluster-admin - - name: KUBE_CONFIG_PATH - value: ../.kube/config + - name: K8S_NAMESPACE + value: sunrise6g - name: EMP_STORAGE_URI - value: mongodb://mongopiedge:27017 + value: mongodb://mongosrm:27017 - name: KUBERNETES_MASTER_TOKEN value: eyJhbGciOiJSUzI1NiIsImtpZCI6IkRRS3VMNktkc1BOYk5ZeDhfSnFvVmJQdkJ6em1FODhPeHNIMHFya3JEQzgifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6InNybS1zZWNyZXQiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiY2x1c3Rlci1hZG1pbiIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6ImU1MjUxZjhiLWY2ODItNDU0Ni1hOTgxLWNlNTk0YTg2NmZiNCIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmNsdXN0ZXItYWRtaW4ifQ.rnZyHFEE1ywceWqcio0UKQrp5GdfVGQOCXxx3RJpb_vvDj65GvNwN0VgA_anOlzj8kKJ9JQjWrA7an2k-5w0ycjeu8Ei_5Z0dvgRSpvKc4O5kCHddOB1kJl480hKWtZqgL0Vi6YbOziFGqvPd8hxHSTquxUgXEN2BStqII8MpVEK8z8iU2pJE5CNIaukGBozjlgc1Vb6HiEU4_UhlqG61uO6ReRVrzaYa4T1j4Zvvx1JN8t2HYcuv50QlHPrEAfW2F3ed0SBbb_X8AT0pGJrVas_uqZgMcN1j5BLO51RNmCY27ADHwCbj8HWuiHhyuLKQxYw8yKB-iMNQmq2fk3ezw - - name: DRIVER - value: kubernetes - name: ARTIFACT_MANAGER_ADDRESS value: http://artefact-manager-service:8000 - name: EDGE_CLOUD_ADAPTER_NAME @@ -83,32 +83,13 @@ status: loadBalancer: {} --- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: srm-ingress - annotations: - #traefik.ingress.kubernetes.io/router.entrypoints: web -spec: - ingressClassName: nginx - rules: - - http: - paths: - - path: /srm - pathType: Prefix - backend: - service: - name: srm - port: - number: 8080 ---- kind: PersistentVolume apiVersion: v1 metadata: name: mongodb-pv-volume # Sets PV's name labels: type: local # Sets PV's type to local - app: mongopiedge + app: mongosrm spec: storageClassName: manual capacity: @@ -142,13 +123,13 @@ metadata: kompose.version: 1.26.0 (40646f47) creationTimestamp: null labels: - io.kompose.service: mongopiedge - name: mongopiedge + io.kompose.service: mongosrm + name: mongosrm spec: replicas: 1 selector: matchLabels: - io.kompose.service: mongopiedge + io.kompose.service: mongosrm strategy: type: Recreate template: @@ -159,11 +140,11 @@ spec: creationTimestamp: null labels: #io.kompose.network/netEMPkub: "true" - io.kompose.service: mongopiedge + io.kompose.service: mongosrm spec: containers: - image: mongo - name: mongopiedge + name: mongosrm ports: - containerPort: 27017 resources: {} @@ -185,8 +166,8 @@ metadata: kompose.version: 1.26.0 (40646f47) creationTimestamp: null labels: - io.kompose.service: mongopiedge - name: mongopiedge + io.kompose.service: mongosrm + name: mongosrm spec: type: ClusterIP ports: @@ -194,454 +175,6 @@ spec: port: 27017 targetPort: 27017 selector: - io.kompose.service: mongopiedge -status: - loadBalancer: {} ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - annotations: - kompose.cmd: kompose convert - kompose.version: 1.26.0 (40646f47) - creationTimestamp: null - labels: - io.kompose.service: oegcontroller - name: oegcontroller -spec: - replicas: 1 - selector: - matchLabels: - io.kompose.service: oegcontroller - strategy: {} - template: - metadata: - annotations: - kompose.cmd: kompose convert - kompose.version: 1.26.0 (40646f47) - creationTimestamp: null - labels: - io.kompose.service: oegcontroller - spec: - containers: - - env: - - name: MONGO_URI - value: mongodb://oegmongo/sample_db?authSource=admin - - name: SRM_HOST - value: http://srm:8080/piedge-connector/2.0.0 - - name: FEDERATION_MANAGER_HOST - value: http://federation-manager:8989 - #- name: PI_EDGE_USERNAME - # value: username - #- name: PI_EDGE_PASSWORD - # value: password - #- name: HTTP_PROXY - # value: http://proxy - image: ghcr.io/sunriseopenoperatorplatform/oeg/oeg - name: oegcontroller - ports: - - containerPort: 8080 - resources: {} - imagePullPolicy: Always - restartPolicy: Always - -status: {} ---- -apiVersion: v1 -kind: Service -metadata: - annotations: - kompose.cmd: kompose convert - kompose.version: 1.26.0 (40646f47) - creationTimestamp: null - labels: - io.kompose.service: oeg - name: oeg -spec: - type: NodePort - ports: - - name: "8080" - nodePort: 32414 - port: 8080 - targetPort: 8080 - selector: - io.kompose.service: oegcontroller -status: - loadBalancer: {} - ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: oeg-ingress - annotations: - #traefik.ingress.kubernetes.io/router.entrypoints: web -spec: - ingressClassName: nginx - rules: - - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: oeg - port: - number: 8080 ---- -kind: PersistentVolume -apiVersion: v1 -metadata: - name: oegmongodb-pv-volume # Sets PV's name - labels: - type: local # Sets PV's type to local - app: oegmongo -spec: - storageClassName: manual - capacity: - storage: 200Mi # Sets PV Volume - accessModes: - - ReadWriteOnce - hostPath: - path: "/mnt/data/mongodb_oeg" ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - creationTimestamp: null - labels: - io.kompose.service: oegmongo - name: oeg-mongo-db -spec: - storageClassName: manual - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 200Mi -status: {} ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - annotations: - kompose.cmd: kompose convert - kompose.version: 1.26.0 (40646f47) - creationTimestamp: null - labels: - io.kompose.service: oegmongo - name: oegmongo -spec: - replicas: 1 - selector: - matchLabels: - io.kompose.service: oegmongo - strategy: - type: Recreate - template: - metadata: - annotations: - kompose.cmd: kompose convert - kompose.version: 1.26.0 (40646f47) - creationTimestamp: null - labels: - #io.kompose.network/netEMPkub: "true" - io.kompose.service: oegmongo - spec: - containers: - - image: mongo - name: oegmongo - ports: - - containerPort: 27017 - resources: {} - volumeMounts: - - mountPath: /data/db - name: mongo-db - restartPolicy: Always - volumes: - - name: mongo-db - persistentVolumeClaim: - claimName: oeg-mongo-db -status: {} ---- -apiVersion: v1 -kind: Service -metadata: - annotations: - kompose.cmd: kompose convert - kompose.version: 1.26.0 (40646f47) - creationTimestamp: null - labels: - io.kompose.service: oegmongo - name: oegmongo -spec: - type: ClusterIP - ports: - - name: "27018" - port: 27018 - targetPort: 27017 - selector: - io.kompose.service: oegmongo + io.kompose.service: mongosrm status: loadBalancer: {} ---- ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: artefact-manager -spec: - replicas: 1 - selector: - matchLabels: - app: artefact-manager - template: - metadata: - labels: - app: artefact-manager - spec: - containers: - - name: artefact-manager - image: ghcr.io/sunriseopenoperatorplatform/artefactmanager:0.5 - ports: - - containerPort: 8000 - env: - - name: PYTHONPATH - value: "/app" ---- -apiVersion: v1 -kind: Service -metadata: - name: artefact-manager-service -spec: - type: ClusterIP - selector: - app: artefact-manager - ports: - - protocol: TCP - port: 8000 - targetPort: 8000 ---- -kind: Secret -apiVersion: v1 -metadata: - name: federation-manager-config -data: - config.cfg: >- - W2tleWNsb2FrXQpjbGllbnQxX2lkID0gb3JpZ2luYXRpbmctb3AtMQpjbGllbnQxX3NlY3JldCA9IGRkN3ZOd0Zxak5wWXdhZ2hsRXdNYncxMGcwa2xXREhiCmNsaWVudDJfaWQgPSBvcmlnaW5hdGluZy1vcC0yCmNsaWVudDJfc2VjcmV0ID0gMm1oem5FUmZXY2xMRHVWb2pZNzdMcDRRZDJyNGU4TXMKc2NvcGUgPSBmZWQtbWdtdAoKW3NlcnZlcl0KaG9zdCA9IDEyNy4wLjAuMQpwb3J0ID0gODk4OQpwcmVmaXggPSBhcGkKdmVyc2lvbiA9IHYxLjAKcHJvdG9jb2wgPSBodHRwCgpbbW9uZ29kYl0KaG9zdCA9IG1vbmdvZGIubW9uZ29kYi5zdmMuY2x1c3Rlci5sb2NhbApwb3J0ID0gMjcwMTcKCltpMmVkZ2VdCmhvc3QgPSAxOTIuMTY4LjEyMy4yMzcKcG9ydCA9IDMwNzYwCgpbb3BfZGF0YV0KcGFydG5lck9QRmVkZXJhdGlvbklkID0gaTJjYXQKcGFydG5lck9QQ291bnRyeUNvZGUgPSBFUwpwYXJ0bmVyT1BNb2JpbGVOZXR3b3JrQ29kZV9NQ0MgPSAwMDEKcGFydG5lck9QTW9iaWxlTmV0d29ya0NvZGVfTU5DID0gMDEKcGFydG5lck9QRml4ZWROZXR3b3JrQ29kZSA9IDM0CnBsYXRmb3JtQ2FwcyA9IGhvbWVSb3V0aW5nCmVkZ2VEaXNjb3ZlcnlTZXJ2aWNlRW5kUG9pbnRfcG9ydCA9CmVkZ2VEaXNjb3ZlcnlTZXJ2aWNlRW5kUG9pbnRfZnFkbiA9IGRpc2NvdmVyeS5vcGVyYXRvcjEuY29tCmVkZ2VEaXNjb3ZlcnlTZXJ2aWNlRW5kUG9pbnRfaXB2NEFkZHJlc3NlcyA9CmVkZ2VEaXNjb3ZlcnlTZXJ2aWNlRW5kUG9pbnRfaXB2NkFkZHJlc3NlcyA9CmxjbVNlcnZpY2VFbmRQb2ludF9wb3J0ID0gODk4OQpsY21TZXJ2aWNlRW5kUG9pbnRfZnFkbiA9CmxjbVNlcnZpY2VFbmRQb2ludF9pcHY0QWRkcmVzc2VzID0gMTI3LjAuMC4xCmxjbVNlcnZpY2VFbmRQb2ludF9pcHY2QWRkcmVzc2VzID0KCltwYXJ0bmVyX29wXQojIERlZmluZXMgdGhlIHJvbGUgb2YgdGhlIEZlZGVyYXRpb24gTWFuYWdlcgpwYXJ0bmVyX29wX2hvc3QgPSAxMjcuMC4wLjEKcGFydG5lcl9vcF9zZXJ2ZXIgPSAvb3BlcmF0b3JwbGF0Zm9ybS9mZWRlcmF0aW9uL3YxCnBhcnRuZXJfb3BfcG9ydCA9IDg5OTAKI3JvbGUgPSBvcmlnaW5hdGluZ19vcApyb2xlID0gcGFydG5lcl9vcA== -type: Opaque ---- -kind: Deployment -apiVersion: apps/v1 -metadata: - labels: - app: federation-manager - name: federation-manager -spec: - replicas: 1 - selector: - matchLabels: - app: federation-manager - template: - metadata: - labels: - app: federation-manager - spec: - containers: - - name: federation-manager - image: ghcr.io/sunriseopenoperatorplatform/federation-manager:0.0.1 - imagePullPolicy: Always - volumeMounts: - - name: config - readOnly: false - mountPath: /usr/app/src/conf/ - ports: - - containerPort: 8989 - protocol: TCP - resources: - requests: - cpu: "2" - memory: "4Gi" - limits: - cpu: "4" - memory: "6Gi" - imagePullSecrets: - - name: federation-manager-regcred - volumes: - - name: config - secret: - secretName: federation-manager-config - defaultMode: 420 ---- -kind: Service -apiVersion: v1 -metadata: - labels: - app: federation-manager - name: federation-manager -spec: - type: ClusterIP - ports: - - name: http - port: 8989 - protocol: TCP - targetPort: 8989 - selector: - app: federation-manager ---- -kind: ConfigMap -apiVersion: v1 -metadata: - name: keycloak-config -data: - realm-import.json: | - { - "realm": "federation", - "enabled": true, - "clientScopes" : [ - { - "id" : "439d9c71-8a8a-469c-9280-058016000cc2", - "name" : "fed-mgmt", - "protocol": "openid-connect", - "description" : "fed-mgmt" - } - ], - "clients": [ - { - "clientId": "originating-op-1", - "enabled": true, - "clientAuthenticatorType": "client-secret", - "secret": "dd7vNwFqjNpYwaghlEwMbw10g0klWDHb", - "redirectUris": ["http://localhost:8080/*"], - "publicClient": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": true, - "defaultClientScopes": ["fed-mgmt"], - "webOrigins": ["*"] - } - ] - } ---- -kind: Deployment -apiVersion: apps/v1 -metadata: - name: keycloak -spec: - replicas: 1 - selector: - matchLabels: - app: keycloak - template: - metadata: - labels: - app: keycloak - spec: - containers: - - name: keycloak - image: quay.io/keycloak/keycloak:26.1.4 - ports: - - containerPort: 8080 - args: [ "start-dev", "--import-realm" ] - env: - - name: KC_BOOTSTRAP_ADMIN_USERNAME - value: admin - - name: KC_BOOTSTRAP_ADMIN_PASSWORD - value: admin - - name: KC_IMPORT - value: /opt/keycloak/data/import/realm-import.json - volumeMounts: - - name: realm-import - mountPath: /opt/keycloak/data/import/ - volumes: - - name: realm-import - configMap: - name: keycloak-config ---- -kind: Service -apiVersion: v1 -metadata: - name: keycloak -spec: - type: NodePort - ports: - - protocol: TCP - port: 8080 - targetPort: 8080 - nodePort: 30081 - selector: - app: keycloak ---- -kind: PersistentVolume -apiVersion: v1 -metadata: - name: mongodb -spec: - capacity: - storage: 1Gi - hostPath: - path: /tmp/db - accessModes: - - ReadWriteOnce - persistentVolumeReclaimPolicy: Retain ---- -kind: PersistentVolumeClaim -apiVersion: v1 -metadata: - name: mongodb -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 1Gi - volumeName: mongodb ---- -kind: Deployment -apiVersion: apps/v1 -metadata: - name: mongodb -spec: - replicas: 1 - selector: - matchLabels: - app: mongodb - template: - metadata: - labels: - app: mongodb - spec: - volumes: - - name: storage - persistentVolumeClaim: - claimName: mongodb - containers: - - name: mongodb - image: 'mongo:7.0' - ports: - - containerPort: 27017 - protocol: TCP - env: - - name: MONGO_INITDB_DATABASE - value: federation-manager - - name: MONGODB_DATA_DIR - value: /data/db - - name: MONDODB_LOG_DIR - value: /dev/null - volumeMounts: - - name: storage - mountPath: /data/db - imagePullPolicy: IfNotPresent ---- -kind: Service -apiVersion: v1 -metadata: - name: mongodb -spec: - type: NodePort - ports: - - protocol: TCP - port: 27017 - targetPort: 27017 - nodePort: 30017 - selector: - app: mongodb \ No newline at end of file diff --git a/sunrise6g-deployment.yaml b/sunrise6g-deployment.yaml new file mode 100644 index 0000000..27d8a87 --- /dev/null +++ b/sunrise6g-deployment.yaml @@ -0,0 +1,193 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.cmd: kompose convert + kompose.version: 1.26.0 (40646f47) + creationTimestamp: null + labels: + io.kompose.service: srmcontroller + name: srmcontroller +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: srmcontroller + strategy: {} + template: + metadata: + annotations: + kompose.cmd: kompose convert + kompose.version: 1.26.0 (40646f47) + creationTimestamp: null + labels: + io.kompose.service: srmcontroller + spec: + containers: + - env: + - name: KUBERNETES_MASTER_IP + value: k3d-sunriseop-server-0 + - name: KUBERNETES_MASTER_PORT + value: "6443" + - name: KUBERNETES_USERNAME + value: cluster-admin + - name: K8S_NAMESPACE + value: sunrise6g + - name: EMP_STORAGE_URI + value: mongodb://mongopiedge:27017 + - name: KUBERNETES_MASTER_TOKEN + value: eyJhbGciOiJSUzI1NiIsImtpZCI6IkRRS3VMNktkc1BOYk5ZeDhfSnFvVmJQdkJ6em1FODhPeHNIMHFya3JEQzgifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6InNybS1zZWNyZXQiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiY2x1c3Rlci1hZG1pbiIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6ImU1MjUxZjhiLWY2ODItNDU0Ni1hOTgxLWNlNTk0YTg2NmZiNCIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmNsdXN0ZXItYWRtaW4ifQ.rnZyHFEE1ywceWqcio0UKQrp5GdfVGQOCXxx3RJpb_vvDj65GvNwN0VgA_anOlzj8kKJ9JQjWrA7an2k-5w0ycjeu8Ei_5Z0dvgRSpvKc4O5kCHddOB1kJl480hKWtZqgL0Vi6YbOziFGqvPd8hxHSTquxUgXEN2BStqII8MpVEK8z8iU2pJE5CNIaukGBozjlgc1Vb6HiEU4_UhlqG61uO6ReRVrzaYa4T1j4Zvvx1JN8t2HYcuv50QlHPrEAfW2F3ed0SBbb_X8AT0pGJrVas_uqZgMcN1j5BLO51RNmCY27ADHwCbj8HWuiHhyuLKQxYw8yKB-iMNQmq2fk3ezw + - name: ARTIFACT_MANAGER_ADDRESS + value: http://artefact-manager-service:8000 + - name: EDGE_CLOUD_ADAPTER_NAME + value: kubernetes + - name: PLATFORM_PROVIDER + value: ISI + image: ghcr.io/sunriseopenoperatorplatform/srm/srm:1.0.0 + name: srmcontroller + ports: + - containerPort: 8080 + resources: {} + imagePullPolicy: Always + restartPolicy: Always +status: {} + +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.cmd: kompose convert + kompose.version: 1.26.0 (40646f47) + creationTimestamp: null + labels: + io.kompose.service: srm + name: srm +spec: + type: NodePort + ports: + - name: "8080" + nodePort: 32415 + port: 8080 + targetPort: 8080 + selector: + io.kompose.service: srmcontroller +status: + loadBalancer: {} + +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: srm-ingress + annotations: + #traefik.ingress.kubernetes.io/router.entrypoints: web +spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: /srm + pathType: Prefix + backend: + service: + name: srm + port: + number: 8080 +--- +kind: PersistentVolume +apiVersion: v1 +metadata: + name: mongodb-pv-volume # Sets PV's name + labels: + type: local # Sets PV's type to local + app: mongopiedge +spec: + storageClassName: manual + capacity: + storage: 200Mi # Sets PV Volume + accessModes: + - ReadWriteOnce + hostPath: + path: "/mnt/data/mongodb_srm" +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + creationTimestamp: null + labels: + io.kompose.service: mongo-db + name: mongo-db +spec: + storageClassName: manual + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 200Mi +status: {} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.cmd: kompose convert + kompose.version: 1.26.0 (40646f47) + creationTimestamp: null + labels: + io.kompose.service: mongopiedge + name: mongopiedge +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: mongopiedge + strategy: + type: Recreate + template: + metadata: + annotations: + kompose.cmd: kompose convert + kompose.version: 1.26.0 (40646f47) + creationTimestamp: null + labels: + #io.kompose.network/netEMPkub: "true" + io.kompose.service: mongopiedge + spec: + containers: + - image: mongo + name: mongopiedge + ports: + - containerPort: 27017 + resources: {} + volumeMounts: + - mountPath: /data/db + name: mongo-db + restartPolicy: Always + volumes: + - name: mongo-db + persistentVolumeClaim: + claimName: mongo-db +status: {} +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.cmd: kompose convert + kompose.version: 1.26.0 (40646f47) + creationTimestamp: null + labels: + io.kompose.service: mongopiedge + name: mongopiedge +spec: + type: ClusterIP + ports: + - name: "27017" + port: 27017 + targetPort: 27017 + selector: + io.kompose.service: mongopiedge +status: + loadBalancer: {} -- GitLab From 4c0091c1fe899351bb4231bba15abda992de80f1 Mon Sep 17 00:00:00 2001 From: Laskaratos Dimitris Date: Mon, 30 Jun 2025 10:54:35 +0300 Subject: [PATCH 2/7] Fixed deployment file --- srm-deployment.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/srm-deployment.yaml b/srm-deployment.yaml index f86f59f..6e28ca2 100644 --- a/srm-deployment.yaml +++ b/srm-deployment.yaml @@ -47,6 +47,8 @@ spec: - name: ARTIFACT_MANAGER_ADDRESS value: http://artefact-manager-service:8000 - name: EDGE_CLOUD_ADAPTER_NAME + - name: ADAPTER_BASE_URL + value: k3d-sunriseop-server-0 value: kubernetes - name: PLATFORM_PROVIDER value: ISI -- GitLab From 59ae37f2d057a887e0ccdd9205b44d06c3be0035 Mon Sep 17 00:00:00 2001 From: Laskaratos Dimitris Date: Mon, 30 Jun 2025 10:55:19 +0300 Subject: [PATCH 3/7] Fixed deployment file --- srm-deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/srm-deployment.yaml b/srm-deployment.yaml index 6e28ca2..0fd5434 100644 --- a/srm-deployment.yaml +++ b/srm-deployment.yaml @@ -46,9 +46,9 @@ spec: value: eyJhbGciOiJSUzI1NiIsImtpZCI6IkRRS3VMNktkc1BOYk5ZeDhfSnFvVmJQdkJ6em1FODhPeHNIMHFya3JEQzgifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6InNybS1zZWNyZXQiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiY2x1c3Rlci1hZG1pbiIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6ImU1MjUxZjhiLWY2ODItNDU0Ni1hOTgxLWNlNTk0YTg2NmZiNCIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmNsdXN0ZXItYWRtaW4ifQ.rnZyHFEE1ywceWqcio0UKQrp5GdfVGQOCXxx3RJpb_vvDj65GvNwN0VgA_anOlzj8kKJ9JQjWrA7an2k-5w0ycjeu8Ei_5Z0dvgRSpvKc4O5kCHddOB1kJl480hKWtZqgL0Vi6YbOziFGqvPd8hxHSTquxUgXEN2BStqII8MpVEK8z8iU2pJE5CNIaukGBozjlgc1Vb6HiEU4_UhlqG61uO6ReRVrzaYa4T1j4Zvvx1JN8t2HYcuv50QlHPrEAfW2F3ed0SBbb_X8AT0pGJrVas_uqZgMcN1j5BLO51RNmCY27ADHwCbj8HWuiHhyuLKQxYw8yKB-iMNQmq2fk3ezw - name: ARTIFACT_MANAGER_ADDRESS value: http://artefact-manager-service:8000 - - name: EDGE_CLOUD_ADAPTER_NAME - name: ADAPTER_BASE_URL value: k3d-sunriseop-server-0 + - name: EDGE_CLOUD_ADAPTER_NAME value: kubernetes - name: PLATFORM_PROVIDER value: ISI -- GitLab From 523765b1434e3f30466dcbcaebc2a5232b2b424c Mon Sep 17 00:00:00 2001 From: Laskaratos Dimitris Date: Tue, 1 Jul 2025 12:37:23 +0300 Subject: [PATCH 4/7] Added sunrise6g as an sdk --- README.md | 79 +------------------ .../requirements.txt | 36 ++------- .../edge_cloud_management_controller.py | 14 ++-- .../network_functions_controller.py | 33 ++++---- .../src/swagger/swagger.yaml | 6 +- srm-deployment.yaml | 4 +- 6 files changed, 34 insertions(+), 138 deletions(-) diff --git a/README.md b/README.md index 86dfea1..f359a2a 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,6 @@ The Service Resource Manager facilitates the North-South Bound Interface (NSBI) SRM supports the following CAMARA functions: -NOTE: NEF APIs are not yet implemented -
| Edge Cloud Management API | Network Exposure API (QoD & Traffic Influence)| @@ -33,79 +31,4 @@ SRM can be deployed in a Kubernetes cluster by executing the file _srm-deploymen | KUBERNETES_MASTER_TOKEN | Token with which all access to K8s apiserver will authenticated | | ARTIFACT_MANAGER_ADDRESS | Address of the Artefact Manager | | EDGE_CLOUD_ADAPTER_NAME | The adapter SRM is going to use throughout its lifecycle. For direct access to K8s just type 'kubernetes' | -|PLATFORM_PROVIDER| The Edge Cloud infrastructure provider| - -## Usage - -Assuming an instance of Open Exposure Gateway (OEG) is running so that CAMARA APIs are accessible, here are a few request examples with responses, all CAMARA compatible: - -### Get all registered apps - -_curl -X GET http://[OEG_root_url]/apps_ - -Example response: - -_[ - { - "appId": "68503f9fe81dc7441fdaae94", - "appRepo": { - "imagePath": "mongo:4.4.18" - }, - "componentSpec": [ - { - "componentName": "mongodb", - "networkInterfaces": [ - { - "port": 27017, - "protocol": "TCP" - } - ] - } - ], - "name": "mongodb", - "packageType": "QCOW2" - }, - { - "appId": "685122aa8fff437507ec8932", - "appRepo": { - "imagePath": "nginx" - }, - "componentSpec": [ - { - "componentName": "nginx", - "networkInterfaces": [ - { - "port": 80, - "protocol": "TCP" - }, - { - "port": 443, - "protocol": "TCP" - } - ] - } - ], - "name": "nginx", - "packageType": "QCOW2" - } -]_ - -### Register app metadata - -_curl -X POST http://[OEG_root_url]/apps --data '{"name": "nginx", "version": "1", "packageType": "QCOW2", "appRepo": {"imagePath": "nginx", "type": "PRIVATEREPO"} -, "componentSpec": [{"componentName": "nginx", "networkInterfaces": [{"protocol": "TCP", "port": 80, "interfaceId": "Uj6qThvzkegxa3L4b88", "visibilityType": "VISIBILITY_EXTERNAL"}, {"protoco -l": "TCP", "port": 443, "interfaceId": "Uj6qThvzkegxa3L4b88", "visibilityType": "VISIBILITY_EXTERNAL"}]}]}' -H "Content-Type: application/json"_ - -Example Response: - -_{ - "appId": "685bdc7dc2db24cc0e8927dc" -}_ - -### Instantiate registered app - -_curl -X POST http://[OEG_root_url]/appinstances --data '{"appId": "685bdc7dc2db24cc0e8927dc", "name": "nginx-test", "appZones": [{"EdgeCloudZone":{"edgeCloudZoneI -d": "f39c5ea3-f4e3-472f-b080-2f3b81c39995", "edgeCloudZoneName": "k3d-sunriseop-agent-2", "edgeCloudProvider": "ISI"}}]}' -H "Content-Type: application/json"_ - -Example Response: - +|PLATFORM_PROVIDER| The Edge Cloud infrastructure provider| \ No newline at end of file diff --git a/service-resource-manager-implementation/requirements.txt b/service-resource-manager-implementation/requirements.txt index 235e5e2..5df83b4 100644 --- a/service-resource-manager-implementation/requirements.txt +++ b/service-resource-manager-implementation/requirements.txt @@ -1,39 +1,15 @@ -#connexion >= 2.6.0 -#connexion[swagger-ui] >= 2.6.0 -#python_dateutil == 2.6.0 -#setuptools >= 21.0.0 -#swagger-ui-bundle >= 0.0.2 -#requests -#pymongo -#logging -#traceback -#pprint - -#connexion >= 2.6.0 -#connexion == 2.5.0 used in python 3.5 running server for dev -#connexion #for test +connexion<3.0.0 connexion[swagger-ui] python_dateutil == 2.6.0 setuptools >= 21.0.0 -#setuptools==50.3.2 pymongo==3.12.0 -#git+https://github.com/kubernetes-client/python.git -requests==2.25.1 -#kubernetes==17.17.0 +requests==2.32.4 kubernetes==18.20.0 - -python-jose[cryptography] cffi==1.15.1 -#bcrypt -#bcrypt==3.1.7 #used in python 3.5 running server for dev - psycopg2-binary -#psycopg2==2.7.7 #used in python 3.5 running server for dev - -#not used! -#pandas==0.24.2 paramiko>=2.12.0 urllib3 -colorlog==6.8.2 -pydantic==2.10.6 -pydantic-extra-types==2.10.3 \ No newline at end of file + +pydantic==2.11.3 +pydantic-extra-types==2.10.3 +sunrise6g_opensdk \ No newline at end of file diff --git a/service-resource-manager-implementation/src/controllers/edge_cloud_management_controller.py b/service-resource-manager-implementation/src/controllers/edge_cloud_management_controller.py index fccc81c..b7c49e1 100644 --- a/service-resource-manager-implementation/src/controllers/edge_cloud_management_controller.py +++ b/service-resource-manager-implementation/src/controllers/edge_cloud_management_controller.py @@ -1,7 +1,9 @@ import connexion -import six import logging import os +from sunrise6g_opensdk.edgecloud.adapters.aeros.client import EdgeApplicationManager as AerOSClient +from sunrise6g_opensdk.edgecloud.adapters.i2edge.client import EdgeApplicationManager as I2EdgeClient +from sunrise6g_opensdk.edgecloud.adapters.kubernetes.client import EdgeApplicationManager as kubernetesClient logger=logging.getLogger(__name__) @@ -10,14 +12,12 @@ adapter_base_url = os.environ['ADAPTER_BASE_URL'] adapter = None if adapter_name=='aeros': - from src.adapters.edgecloud.adapters.aeros.client import EdgeApplicationManager - adapter = EdgeApplicationManager(base_url=adapter_base_url) + + adapter = AerOSClient(base_url=adapter_base_url) elif adapter_name=='i2edge': - from src.adapters.edgecloud.adapters.i2edge.client import EdgeApplicationManager - adapter = EdgeApplicationManager(base_url=adapter_base_url) + adapter = I2EdgeClient(base_url=adapter_base_url) elif adapter_name=='kubernetes': - from src.adapters.edgecloud.adapters.kubernetes.client import EdgeApplicationManager - adapter = EdgeApplicationManager(base_url=adapter_base_url, **os.environ) + adapter = kubernetesClient(base_url=adapter_base_url, **os.environ) def deregister_service_function(service_function_id: str): # noqa: E501 """Deregister service. diff --git a/service-resource-manager-implementation/src/controllers/network_functions_controller.py b/service-resource-manager-implementation/src/controllers/network_functions_controller.py index e8e3982..6838b75 100644 --- a/service-resource-manager-implementation/src/controllers/network_functions_controller.py +++ b/service-resource-manager-implementation/src/controllers/network_functions_controller.py @@ -1,55 +1,54 @@ from os import environ import logging import connexion +from sunrise6g_opensdk.network.adapters.oai.client import NetworkManager as OAIClient +from sunrise6g_opensdk.network.adapters.open5gcore.client import NetworkManager as Open5GCoreClient +from sunrise6g_opensdk.network.adapters.open5gs.client import NetworkManager as Open5GSClient logger = logging.getLogger(__name__) logging.basicConfig(level=logging.DEBUG) - network_client = environ.get('NETWORK_CLIENT') adapter = None if network_client is not None: if network_client=='oai': - from src.adapters.network.clients.oai.client import NetworkManager - adapter = NetworkManager() + adapter = OAIClient() elif network_client=='open5gcore': - from src.adapters.network.clients.open5gcore.client import NetworkManager - adapter = NetworkManager() + adapter = Open5GCoreClient() else: - from src.adapters.network.clients.open5gs.client import NetworkManager - adapter = NetworkManager() + adapter = Open5GSClient() -def create_qod_session(body: dict): +def create_qod_session(): if connexion.request.is_json: try: - response = adapter.create_qod_session(body) + response = adapter.create_qod_session(connexion.request.get_json()) return response except Exception as ce_: logger.error(ce_) - return ce_ + return ce_ else: return 'ERROR: Could not read JSON payload.', 400 -def get_qod_session(id: str): +def get_qod_session(session_id: str): try: response = adapter.get_qod_session(id) - return response + return {'status': 200, 'session': response.json()} except Exception as ce_: - logger.error(ce_) - return ce_ + logger.error(ce_) + return ce_ -def delete_qod_session(id: str): +def delete_qod_session(session_id: str): try: response = adapter.delete_qod_session(id) return response except Exception as ce_: - logger.error(ce_) - return ce_ + logger.error(ce_) + return ce_ def create_traffic_influence_resource(body): #TODO diff --git a/service-resource-manager-implementation/src/swagger/swagger.yaml b/service-resource-manager-implementation/src/swagger/swagger.yaml index f05078a..f69163b 100644 --- a/service-resource-manager-implementation/src/swagger/swagger.yaml +++ b/service-resource-manager-implementation/src/swagger/swagger.yaml @@ -316,14 +316,14 @@ paths: "200": description: Session created. x-openapi-router-controller: src.controllers.network_functions_controller - /sessions/{id}: + /sessions/{sessionId}: get: tags: - Quality on Demand Functions summary: Retrieve details of a QoD Session operationId: get_qod_session parameters: - - name: id + - name: sessionId in: path description: Represents a QoD Session. required: true @@ -345,7 +345,7 @@ paths: summary: Remove QoD Session operationId: delete_qod_session parameters: - - name: id + - name: sessionId in: path description: Represents a QoD Session. required: true diff --git a/srm-deployment.yaml b/srm-deployment.yaml index 0fd5434..f86f59f 100644 --- a/srm-deployment.yaml +++ b/srm-deployment.yaml @@ -46,9 +46,7 @@ spec: value: eyJhbGciOiJSUzI1NiIsImtpZCI6IkRRS3VMNktkc1BOYk5ZeDhfSnFvVmJQdkJ6em1FODhPeHNIMHFya3JEQzgifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6InNybS1zZWNyZXQiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiY2x1c3Rlci1hZG1pbiIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6ImU1MjUxZjhiLWY2ODItNDU0Ni1hOTgxLWNlNTk0YTg2NmZiNCIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmNsdXN0ZXItYWRtaW4ifQ.rnZyHFEE1ywceWqcio0UKQrp5GdfVGQOCXxx3RJpb_vvDj65GvNwN0VgA_anOlzj8kKJ9JQjWrA7an2k-5w0ycjeu8Ei_5Z0dvgRSpvKc4O5kCHddOB1kJl480hKWtZqgL0Vi6YbOziFGqvPd8hxHSTquxUgXEN2BStqII8MpVEK8z8iU2pJE5CNIaukGBozjlgc1Vb6HiEU4_UhlqG61uO6ReRVrzaYa4T1j4Zvvx1JN8t2HYcuv50QlHPrEAfW2F3ed0SBbb_X8AT0pGJrVas_uqZgMcN1j5BLO51RNmCY27ADHwCbj8HWuiHhyuLKQxYw8yKB-iMNQmq2fk3ezw - name: ARTIFACT_MANAGER_ADDRESS value: http://artefact-manager-service:8000 - - name: ADAPTER_BASE_URL - value: k3d-sunriseop-server-0 - - name: EDGE_CLOUD_ADAPTER_NAME + - name: EDGE_CLOUD_ADAPTER_NAME value: kubernetes - name: PLATFORM_PROVIDER value: ISI -- GitLab From e5ec8539b6782d332a0b02cdfd1b8e7cb1befa2b Mon Sep 17 00:00:00 2001 From: Laskaratos Dimitris Date: Tue, 1 Jul 2025 12:40:22 +0300 Subject: [PATCH 5/7] Added sunrise6g as an sdk --- srm-deployment.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/srm-deployment.yaml b/srm-deployment.yaml index f86f59f..674a893 100644 --- a/srm-deployment.yaml +++ b/srm-deployment.yaml @@ -48,6 +48,8 @@ spec: value: http://artefact-manager-service:8000 - name: EDGE_CLOUD_ADAPTER_NAME value: kubernetes + - name: ADAPTER_BASE_URL + value: k3d-sunriseop-server-0 - name: PLATFORM_PROVIDER value: ISI image: ghcr.io/sunriseopenoperatorplatform/srm/srm:1.0.0 -- GitLab From 1175247f2329dbb7b37cd8ae114b09b2648eb20f Mon Sep 17 00:00:00 2001 From: Laskaratos Dimitris Date: Tue, 1 Jul 2025 16:26:51 +0300 Subject: [PATCH 6/7] Mino change on network controller --- .../Dockerfile | 26 +------------------ .../requirements.txt | 6 ++--- .../edge_cloud_management_controller.py | 1 - .../network_functions_controller.py | 11 +++++--- srm-deployment.yaml | 8 +++++- 5 files changed, 18 insertions(+), 34 deletions(-) diff --git a/service-resource-manager-implementation/Dockerfile b/service-resource-manager-implementation/Dockerfile index fd4129c..25ff369 100644 --- a/service-resource-manager-implementation/Dockerfile +++ b/service-resource-manager-implementation/Dockerfile @@ -1,33 +1,10 @@ -FROM python:3.9-alpine - -#RUN apk add git +FROM python:3.12-alpine RUN mkdir -p /usr/src/app WORKDIR /usr/src/app -#RUN apk add --no-cache --virtual .build-deps gcc musl-dev - -#RUN apk update && apk add python3-dev \ -# gcc \ -# libc-dev - - -#THIS SOLVED THE ISSUE WITH CFFI: building wheel for cffi (setup.py) finished with status 'error'! -#RUN apk add --no-cache libffi-dev build-base -# COPY requirements.txt /usr/src/app/ -#RUN pip3 install connexion - -#ENV EMP_STORAGE_DRIVER mongo -#ENV EMP_STORAGE_URI mongodb://203.0.113.8:27017 -# -#ENV PIP_ROOT_USER_ACTION=ignore -# ENV PYTHONUNBUFFERED=1 -#RUN apk add --update --no-cache python3 && ln -sf python3 /usr/bin/python -#RUN python3 -m ensurepip - -#RUN pip3 install --no-cache --upgrade pip setuptools RUN python3 -m venv .venv RUN source .venv/bin/activate @@ -36,7 +13,6 @@ RUN pip3 install --upgrade pip RUN pip3 install wheel -#RUN pip3 install --no-cache --upgrade setuptools RUN pip3 install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host=files.pythonhosted.org --no-cache-dir -r requirements.txt COPY . /usr/src/app diff --git a/service-resource-manager-implementation/requirements.txt b/service-resource-manager-implementation/requirements.txt index 5df83b4..41e740e 100644 --- a/service-resource-manager-implementation/requirements.txt +++ b/service-resource-manager-implementation/requirements.txt @@ -4,12 +4,12 @@ python_dateutil == 2.6.0 setuptools >= 21.0.0 pymongo==3.12.0 requests==2.32.4 -kubernetes==18.20.0 -cffi==1.15.1 +kubernetes==33.1.0 +#cffi==1.15.1 psycopg2-binary paramiko>=2.12.0 urllib3 pydantic==2.11.3 pydantic-extra-types==2.10.3 -sunrise6g_opensdk \ No newline at end of file +sunrise6g_opensdk==1.0.2.post2 \ No newline at end of file diff --git a/service-resource-manager-implementation/src/controllers/edge_cloud_management_controller.py b/service-resource-manager-implementation/src/controllers/edge_cloud_management_controller.py index b7c49e1..4873f29 100644 --- a/service-resource-manager-implementation/src/controllers/edge_cloud_management_controller.py +++ b/service-resource-manager-implementation/src/controllers/edge_cloud_management_controller.py @@ -12,7 +12,6 @@ adapter_base_url = os.environ['ADAPTER_BASE_URL'] adapter = None if adapter_name=='aeros': - adapter = AerOSClient(base_url=adapter_base_url) elif adapter_name=='i2edge': adapter = I2EdgeClient(base_url=adapter_base_url) diff --git a/service-resource-manager-implementation/src/controllers/network_functions_controller.py b/service-resource-manager-implementation/src/controllers/network_functions_controller.py index 6838b75..dc332ef 100644 --- a/service-resource-manager-implementation/src/controllers/network_functions_controller.py +++ b/service-resource-manager-implementation/src/controllers/network_functions_controller.py @@ -8,16 +8,19 @@ from sunrise6g_opensdk.network.adapters.open5gs.client import NetworkManager as logger = logging.getLogger(__name__) logging.basicConfig(level=logging.DEBUG) -network_client = environ.get('NETWORK_CLIENT') +network_client = environ.get('NETWORK_ADAPTER_NAME') +base_url = environ.get('NETWORK_ADAPTER_BASE_URL') +scs_as_id = environ.get('SCS_AS_ID') + adapter = None if network_client is not None: if network_client=='oai': - adapter = OAIClient() + adapter = OAIClient(base_url=base_url, scs_as_id=scs_as_id) elif network_client=='open5gcore': - adapter = Open5GCoreClient() + adapter = Open5GCoreClient(base_url=base_url, scs_as_id=scs_as_id) else: - adapter = Open5GSClient() + adapter = Open5GSClient(base_url=base_url, scs_as_id=scs_as_id) def create_qod_session(): diff --git a/srm-deployment.yaml b/srm-deployment.yaml index 674a893..3424da5 100644 --- a/srm-deployment.yaml +++ b/srm-deployment.yaml @@ -49,7 +49,13 @@ spec: - name: EDGE_CLOUD_ADAPTER_NAME value: kubernetes - name: ADAPTER_BASE_URL - value: k3d-sunriseop-server-0 + value: k3d-sunriseop-server-0 + - name: NETWORK_ADAPTER_NAME + value: open5gs + - name: NETWORK_ADAPTER_BASE_URL + value: http://192.168.124.233:8002/ + - name: SCS_AS_ID + value: scs - name: PLATFORM_PROVIDER value: ISI image: ghcr.io/sunriseopenoperatorplatform/srm/srm:1.0.0 -- GitLab From 548d9d148bcc64db4c8374307c6f74eb2b5c2b95 Mon Sep 17 00:00:00 2001 From: Laskaratos Dimitris Date: Tue, 8 Jul 2025 12:27:31 +0300 Subject: [PATCH 7/7] Added adapters as SDK --- .../Dockerfile | 2 +- .../requirements.txt | 11 +- .../src/__main__.py | 11 +- .../src/adapters/__init__.py | 2 - .../src/adapters/common/__init__.py | 0 .../src/adapters/common/adapters_factory.py | 95 --- .../src/adapters/common/sdk.py | 71 -- .../src/adapters/common/sdk_factory.py | 90 -- .../src/adapters/edgecloud/.env | 7 - .../src/adapters/edgecloud/__init__.py | 0 .../adapters/edgecloud/adapters/__init__.py | 0 .../edgecloud/adapters/aeros/__init__.py | 23 - .../edgecloud/adapters/aeros/client.py | 433 ---------- .../edgecloud/adapters/aeros/config.py | 27 - .../adapters/aeros/continuum_client.py | 196 ----- .../edgecloud/adapters/aeros/utils.py | 43 - .../src/adapters/edgecloud/adapters/errors.py | 5 - .../edgecloud/adapters/i2edge/__init__.py | 0 .../edgecloud/adapters/i2edge/client.py | 243 ------ .../edgecloud/adapters/i2edge/common.py | 100 --- .../edgecloud/adapters/i2edge/schemas.py | 167 ---- .../edgecloud/adapters/i2edge/utils.py | 152 ---- .../edgecloud/adapters/kubernetes/__init__.py | 0 .../edgecloud/adapters/kubernetes/client.py | 263 ------ .../src/adapters/edgecloud/core/__init__.py | 0 .../edgecloud/core/edgecloud_interface.py | 123 --- .../src/adapters/logger.py | 48 -- .../src/adapters/network/__init__.py | 0 .../src/adapters/network/adapters/__init__.py | 0 .../src/adapters/network/adapters/errors.py | 3 - .../adapters/network/adapters/oai/__init__.py | 0 .../adapters/network/adapters/oai/client.py | 155 ---- .../network/adapters/open5gcore/__init__.py | 0 .../network/adapters/open5gcore/client.py | 79 -- .../network/adapters/open5gs/__init__.py | 0 .../network/adapters/open5gs/client.py | 96 --- .../src/adapters/network/clients/__init__.py | 0 .../src/adapters/network/clients/errors.py | 3 - .../adapters/network/clients/oai/__init__.py | 0 .../adapters/network/clients/oai/client.py | 139 --- .../network/clients/open5gcore/__init__.py | 0 .../network/clients/open5gcore/client.py | 49 -- .../network/clients/open5gs/__init__.py | 0 .../network/clients/open5gs/client.py | 65 -- .../src/adapters/network/core/__init__.py | 0 .../network/core/base_network_client.py | 469 ---------- .../src/adapters/network/core/common.py | 122 --- .../network/core/network_interface.py | 311 ------- .../src/adapters/network/core/schemas.py | 798 ------------------ .../src/adapters/o-ran/__init__.py | 0 .../src/adapters/o-ran/clients/__init__.py | 0 .../o-ran/clients/juniper-ric/__init__.py | 0 .../o-ran/clients/juniper-ric/client.py | 0 .../src/adapters/o-ran/core/__init__.py | 0 .../adapters/o-ran/core/o-ran_interface.py | 1 - .../src/adapters/oran/__init__.py | 0 .../src/adapters/oran/clients/__init__.py | 0 .../oran/clients/juniper_ric/__init__.py | 0 .../oran/clients/juniper_ric/client.py | 0 .../src/adapters/oran/core/__init__.py | 0 .../src/adapters/oran/core/oran_interface.py | 1 - .../src/clients/__init__.py | 0 .../src/clients/common/__init__.py | 0 .../src/clients/common/sdk.py | 76 -- .../src/clients/common/sdk_factory.py | 78 -- .../src/clients/edgecloud/.env | 7 - .../src/clients/edgecloud/__init__.py | 0 .../src/clients/edgecloud/clients/__init__.py | 0 .../edgecloud/clients/aeros/__init__.py | 23 - .../clients/edgecloud/clients/aeros/client.py | 263 ------ .../clients/edgecloud/clients/aeros/config.py | 27 - .../clients/aeros/continuum_client.py | 170 ---- .../clients/edgecloud/clients/aeros/utils.py | 43 - .../src/clients/edgecloud/clients/errors.py | 5 - .../edgecloud/clients/i2edge/__init__.py | 0 .../edgecloud/clients/i2edge/client.py | 241 ------ .../edgecloud/clients/i2edge/common.py | 100 --- .../edgecloud/clients/i2edge/schemas.py | 167 ---- .../clients/edgecloud/clients/i2edge/utils.py | 151 ---- .../edgecloud/clients/piedge/__init__.py | 0 .../edgecloud/clients/piedge/client.py | 158 ---- .../src/clients/edgecloud/core/__init__.py | 0 .../edgecloud/core/edgecloud_interface.py | 123 --- .../src/clients/logger.py | 48 -- .../src/clients/network/__init__.py | 0 .../src/clients/network/clients/__init__.py | 0 .../src/clients/network/clients/errors.py | 3 - .../clients/network/clients/oai/__init__.py | 0 .../src/clients/network/clients/oai/client.py | 139 --- .../network/clients/open5gcore/__init__.py | 0 .../network/clients/open5gcore/client.py | 14 - .../network/clients/open5gs/__init__.py | 0 .../clients/network/clients/open5gs/client.py | 62 -- .../src/clients/network/core/__init__.py | 0 .../src/clients/network/core/common.py | 94 --- .../clients/network/core/network_interface.py | 295 ------- .../src/clients/network/core/schemas.py | 430 ---------- .../src/clients/o-ran/__init__.py | 0 .../src/clients/o-ran/clients/__init__.py | 0 .../o-ran/clients/juniper-ric/__init__.py | 0 .../o-ran/clients/juniper-ric/client.py | 0 .../src/clients/o-ran/core/__init__.py | 0 .../src/clients/o-ran/core/o-ran_interface.py | 1 - .../edge_cloud_management_controller.py | 104 +-- .../network_functions_controller.py | 37 +- .../src/controllers/operations_controller.py | 2 +- .../src/core/__init__.py | 0 .../src/core/piedge_encoder.py | 325 ------- .../src/encoder.py | 7 +- .../src/swagger/swagger.yaml | 6 +- srm-deployment.yaml | 146 +++- 111 files changed, 205 insertions(+), 7543 deletions(-) delete mode 100644 service-resource-manager-implementation/src/adapters/__init__.py delete mode 100644 service-resource-manager-implementation/src/adapters/common/__init__.py delete mode 100644 service-resource-manager-implementation/src/adapters/common/adapters_factory.py delete mode 100644 service-resource-manager-implementation/src/adapters/common/sdk.py delete mode 100644 service-resource-manager-implementation/src/adapters/common/sdk_factory.py delete mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/.env delete mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/__init__.py delete mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/adapters/__init__.py delete mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/__init__.py delete mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/client.py delete mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/config.py delete mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/continuum_client.py delete mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/utils.py delete mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/adapters/errors.py delete mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/__init__.py delete mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/client.py delete mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/common.py delete mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/schemas.py delete mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/utils.py delete mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/adapters/kubernetes/__init__.py delete mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/adapters/kubernetes/client.py delete mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/core/__init__.py delete mode 100644 service-resource-manager-implementation/src/adapters/edgecloud/core/edgecloud_interface.py delete mode 100644 service-resource-manager-implementation/src/adapters/logger.py delete mode 100644 service-resource-manager-implementation/src/adapters/network/__init__.py delete mode 100644 service-resource-manager-implementation/src/adapters/network/adapters/__init__.py delete mode 100644 service-resource-manager-implementation/src/adapters/network/adapters/errors.py delete mode 100644 service-resource-manager-implementation/src/adapters/network/adapters/oai/__init__.py delete mode 100644 service-resource-manager-implementation/src/adapters/network/adapters/oai/client.py delete mode 100644 service-resource-manager-implementation/src/adapters/network/adapters/open5gcore/__init__.py delete mode 100644 service-resource-manager-implementation/src/adapters/network/adapters/open5gcore/client.py delete mode 100644 service-resource-manager-implementation/src/adapters/network/adapters/open5gs/__init__.py delete mode 100644 service-resource-manager-implementation/src/adapters/network/adapters/open5gs/client.py delete mode 100644 service-resource-manager-implementation/src/adapters/network/clients/__init__.py delete mode 100644 service-resource-manager-implementation/src/adapters/network/clients/errors.py delete mode 100644 service-resource-manager-implementation/src/adapters/network/clients/oai/__init__.py delete mode 100644 service-resource-manager-implementation/src/adapters/network/clients/oai/client.py delete mode 100644 service-resource-manager-implementation/src/adapters/network/clients/open5gcore/__init__.py delete mode 100644 service-resource-manager-implementation/src/adapters/network/clients/open5gcore/client.py delete mode 100644 service-resource-manager-implementation/src/adapters/network/clients/open5gs/__init__.py delete mode 100644 service-resource-manager-implementation/src/adapters/network/clients/open5gs/client.py delete mode 100644 service-resource-manager-implementation/src/adapters/network/core/__init__.py delete mode 100644 service-resource-manager-implementation/src/adapters/network/core/base_network_client.py delete mode 100644 service-resource-manager-implementation/src/adapters/network/core/common.py delete mode 100644 service-resource-manager-implementation/src/adapters/network/core/network_interface.py delete mode 100644 service-resource-manager-implementation/src/adapters/network/core/schemas.py delete mode 100644 service-resource-manager-implementation/src/adapters/o-ran/__init__.py delete mode 100644 service-resource-manager-implementation/src/adapters/o-ran/clients/__init__.py delete mode 100644 service-resource-manager-implementation/src/adapters/o-ran/clients/juniper-ric/__init__.py delete mode 100644 service-resource-manager-implementation/src/adapters/o-ran/clients/juniper-ric/client.py delete mode 100644 service-resource-manager-implementation/src/adapters/o-ran/core/__init__.py delete mode 100644 service-resource-manager-implementation/src/adapters/o-ran/core/o-ran_interface.py delete mode 100644 service-resource-manager-implementation/src/adapters/oran/__init__.py delete mode 100644 service-resource-manager-implementation/src/adapters/oran/clients/__init__.py delete mode 100644 service-resource-manager-implementation/src/adapters/oran/clients/juniper_ric/__init__.py delete mode 100644 service-resource-manager-implementation/src/adapters/oran/clients/juniper_ric/client.py delete mode 100644 service-resource-manager-implementation/src/adapters/oran/core/__init__.py delete mode 100644 service-resource-manager-implementation/src/adapters/oran/core/oran_interface.py delete mode 100644 service-resource-manager-implementation/src/clients/__init__.py delete mode 100644 service-resource-manager-implementation/src/clients/common/__init__.py delete mode 100644 service-resource-manager-implementation/src/clients/common/sdk.py delete mode 100644 service-resource-manager-implementation/src/clients/common/sdk_factory.py delete mode 100644 service-resource-manager-implementation/src/clients/edgecloud/.env delete mode 100644 service-resource-manager-implementation/src/clients/edgecloud/__init__.py delete mode 100644 service-resource-manager-implementation/src/clients/edgecloud/clients/__init__.py delete mode 100644 service-resource-manager-implementation/src/clients/edgecloud/clients/aeros/__init__.py delete mode 100644 service-resource-manager-implementation/src/clients/edgecloud/clients/aeros/client.py delete mode 100644 service-resource-manager-implementation/src/clients/edgecloud/clients/aeros/config.py delete mode 100644 service-resource-manager-implementation/src/clients/edgecloud/clients/aeros/continuum_client.py delete mode 100644 service-resource-manager-implementation/src/clients/edgecloud/clients/aeros/utils.py delete mode 100644 service-resource-manager-implementation/src/clients/edgecloud/clients/errors.py delete mode 100644 service-resource-manager-implementation/src/clients/edgecloud/clients/i2edge/__init__.py delete mode 100644 service-resource-manager-implementation/src/clients/edgecloud/clients/i2edge/client.py delete mode 100644 service-resource-manager-implementation/src/clients/edgecloud/clients/i2edge/common.py delete mode 100644 service-resource-manager-implementation/src/clients/edgecloud/clients/i2edge/schemas.py delete mode 100644 service-resource-manager-implementation/src/clients/edgecloud/clients/i2edge/utils.py delete mode 100644 service-resource-manager-implementation/src/clients/edgecloud/clients/piedge/__init__.py delete mode 100644 service-resource-manager-implementation/src/clients/edgecloud/clients/piedge/client.py delete mode 100644 service-resource-manager-implementation/src/clients/edgecloud/core/__init__.py delete mode 100644 service-resource-manager-implementation/src/clients/edgecloud/core/edgecloud_interface.py delete mode 100644 service-resource-manager-implementation/src/clients/logger.py delete mode 100644 service-resource-manager-implementation/src/clients/network/__init__.py delete mode 100644 service-resource-manager-implementation/src/clients/network/clients/__init__.py delete mode 100644 service-resource-manager-implementation/src/clients/network/clients/errors.py delete mode 100644 service-resource-manager-implementation/src/clients/network/clients/oai/__init__.py delete mode 100644 service-resource-manager-implementation/src/clients/network/clients/oai/client.py delete mode 100644 service-resource-manager-implementation/src/clients/network/clients/open5gcore/__init__.py delete mode 100644 service-resource-manager-implementation/src/clients/network/clients/open5gcore/client.py delete mode 100644 service-resource-manager-implementation/src/clients/network/clients/open5gs/__init__.py delete mode 100644 service-resource-manager-implementation/src/clients/network/clients/open5gs/client.py delete mode 100644 service-resource-manager-implementation/src/clients/network/core/__init__.py delete mode 100644 service-resource-manager-implementation/src/clients/network/core/common.py delete mode 100644 service-resource-manager-implementation/src/clients/network/core/network_interface.py delete mode 100644 service-resource-manager-implementation/src/clients/network/core/schemas.py delete mode 100644 service-resource-manager-implementation/src/clients/o-ran/__init__.py delete mode 100644 service-resource-manager-implementation/src/clients/o-ran/clients/__init__.py delete mode 100644 service-resource-manager-implementation/src/clients/o-ran/clients/juniper-ric/__init__.py delete mode 100644 service-resource-manager-implementation/src/clients/o-ran/clients/juniper-ric/client.py delete mode 100644 service-resource-manager-implementation/src/clients/o-ran/core/__init__.py delete mode 100644 service-resource-manager-implementation/src/clients/o-ran/core/o-ran_interface.py delete mode 100644 service-resource-manager-implementation/src/core/__init__.py delete mode 100644 service-resource-manager-implementation/src/core/piedge_encoder.py diff --git a/service-resource-manager-implementation/Dockerfile b/service-resource-manager-implementation/Dockerfile index 25ff369..3c60ffd 100644 --- a/service-resource-manager-implementation/Dockerfile +++ b/service-resource-manager-implementation/Dockerfile @@ -11,7 +11,7 @@ RUN source .venv/bin/activate RUN pip3 install --upgrade pip -RUN pip3 install wheel +RUN pip3 install wheel --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host=files.pythonhosted.org RUN pip3 install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host=files.pythonhosted.org --no-cache-dir -r requirements.txt diff --git a/service-resource-manager-implementation/requirements.txt b/service-resource-manager-implementation/requirements.txt index 41e740e..9ea2d85 100644 --- a/service-resource-manager-implementation/requirements.txt +++ b/service-resource-manager-implementation/requirements.txt @@ -1,15 +1,8 @@ connexion<3.0.0 connexion[swagger-ui] -python_dateutil == 2.6.0 setuptools >= 21.0.0 -pymongo==3.12.0 requests==2.32.4 -kubernetes==33.1.0 -#cffi==1.15.1 psycopg2-binary -paramiko>=2.12.0 -urllib3 - -pydantic==2.11.3 +urllib3 pydantic-extra-types==2.10.3 -sunrise6g_opensdk==1.0.2.post2 \ No newline at end of file +sunrise6g-opensdk==1.0.2.post3 \ No newline at end of file diff --git a/service-resource-manager-implementation/src/__main__.py b/service-resource-manager-implementation/src/__main__.py index 2b50d2b..828513a 100644 --- a/service-resource-manager-implementation/src/__main__.py +++ b/service-resource-manager-implementation/src/__main__.py @@ -1,21 +1,18 @@ #!/usr/bin/env python3 import connexion - import logging -import sys -import os +import src.encoder as encoder +from json import JSONEncoder -from src import encoder import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) def main(): - global driver logging.basicConfig(level=logging.INFO) app = connexion.App(__name__, specification_dir='./swagger/') - app.app.json_encoder = encoder.JSONEncoder - app.add_api('swagger.yaml', strict_validation=True, arguments={'title': 'π-edge Controller API'}, pythonic_params=True) + app.app.json_encoder = JSONEncoder + app.add_api('swagger.yaml', strict_validation=True, arguments={'title': 'Service Resource Manager Controller API'}, pythonic_params=True) app.run(port=8080) diff --git a/service-resource-manager-implementation/src/adapters/__init__.py b/service-resource-manager-implementation/src/adapters/__init__.py deleted file mode 100644 index c0e4bd9..0000000 --- a/service-resource-manager-implementation/src/adapters/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# -*- coding: utf-8 -*- -from .common.sdk import Sdk diff --git a/service-resource-manager-implementation/src/adapters/common/__init__.py b/service-resource-manager-implementation/src/adapters/common/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/adapters/common/adapters_factory.py b/service-resource-manager-implementation/src/adapters/common/adapters_factory.py deleted file mode 100644 index 42af9d2..0000000 --- a/service-resource-manager-implementation/src/adapters/common/adapters_factory.py +++ /dev/null @@ -1,95 +0,0 @@ -# -*- coding: utf-8 -*- -## -# Copyright 2025-present by Software Networks Area, i2CAT. -# All rights reserved. -# -# This file is part of the Open SDK -# -# Contributors: -# - Adrián Pino Martínez (adrian.pino@i2cat.net) -## - -from src.adapters.edgecloud.adapters.aeros.client import ( - EdgeApplicationManager as AerosClient, -) -from src.adapters.edgecloud.adapters.i2edge.client import ( - EdgeApplicationManager as I2EdgeClient, -) -from src.adapters.edgecloud.adapters.kubernetes.client import ( - EdgeApplicationManager as kubernetesClient, -) -# from src.adapters.network.adapters.oai.client import ( -# NetworkManager as OaiCoreClient, -# ) -# from src.adapters.network.adapters.open5gcore.client import ( -# NetworkManager as Open5GCoreClient, -# ) -# from src.adapters.network.adapters.open5gs.client import ( -# NetworkManager as Open5GSClient, -# ) - - -def _edgecloud_adapters_factory(client_name: str, base_url: str, **kwargs): - if client_name == "i2edge": - if "flavour_id" not in kwargs: - raise ValueError("Missing required 'flavour_id' for i2edge client.") - - edge_cloud_factory = { - "aeros": lambda url, **kw: AerosClient(base_url=url, **kw), - "i2edge": lambda url, **kw: I2EdgeClient(base_url=url, **kw), - "kubernetes": lambda url, **kw: kubernetesClient(base_url=url, **kw), - } - try: - return edge_cloud_factory[client_name](base_url, **kwargs) - except KeyError: - raise ValueError( - f"Invalid edgecloud client '{client_name}'. Available: {list(edge_cloud_factory)}" - ) - - -# def _network_adapters_factory(client_name: str, base_url: str, **kwargs): -# if "scs_as_id" not in kwargs: -# raise ValueError("Missing required 'scs_as_id' for network adapters.") -# scs_as_id = kwargs.pop("scs_as_id") - -# network_factory = { -# "open5gs": lambda url, scs_id, **kw: Open5GSClient( -# base_url=url, scs_as_id=scs_id, **kw -# ), -# "oai": lambda url, scs_id, **kw: OaiCoreClient( -# base_url=url, scs_as_id=scs_id, **kw -# ), -# "open5gcore": lambda url, scs_id, **kw: Open5GCoreClient( -# base_url=url, scs_as_id=scs_id, **kw -# ), -# } -# try: -# return network_factory[client_name](base_url, scs_as_id, **kwargs) -# except KeyError: -# raise ValueError( -# f"Invalid network client '{client_name}'. Available: {list(network_factory)}" -# ) - - -# def _oran_adapters_factory(client_name: str, base_url: str): -# # TODO - - -class AdaptersFactory: - _domain_factories = { - "edgecloud": _edgecloud_adapters_factory, - # "network": _network_adapters_factory, - # "oran": _oran_adapters_factory, - } - - @classmethod - def instantiate_and_retrieve_adapters( - cls, domain: str, client_name: str, base_url: str, **kwargs - ): - try: - catalog = cls._domain_factories[domain] - except KeyError: - raise ValueError( - f"Unsupported domain '{domain}'. Supported: {list(cls._domain_factories)}" - ) - return catalog(client_name, base_url, **kwargs) \ No newline at end of file diff --git a/service-resource-manager-implementation/src/adapters/common/sdk.py b/service-resource-manager-implementation/src/adapters/common/sdk.py deleted file mode 100644 index 44ac013..0000000 --- a/service-resource-manager-implementation/src/adapters/common/sdk.py +++ /dev/null @@ -1,71 +0,0 @@ -# -*- coding: utf-8 -*- -## -# Copyright 2025-present by Software Networks Area, i2CAT. -# All rights reserved. -# -# This file is part of the Open SDK -# -# Contributors: -# - Adrián Pino Martínez (adrian.pino@i2cat.net) -## -from typing import Dict - -from src.adapters.common.adapters_factory import AdaptersFactory - - -class Sdk: - @staticmethod - def create_adapters_from( - adapter_specs: Dict[str, Dict[str, str]], - ) -> Dict[str, object]: - """ - Create and return a dictionary of instantiated edgecloud/network/oran adapters - based on the provided specifications. - - Args: - adapter_specs (dict): A dictionary where each key is the client's domain (e.g., 'edgecloud', 'network'), - and each value is a dictionary containing: - - 'client_name' (str): The specific name of the client (e.g., 'i2edge', 'open5gs'). - - 'base_url' (str): The base URL for the client's API. - Additional parameters like 'scs_as_id' may also be included. - - Returns: - dict: A dictionary where keys are the 'client_name' (str) and values are - the instantiated client objects. - - # TODO: Update it - # Example: - >>> from src.common.universal_client_catalog import UniversalCatalogClient - >>> - >>> adapter_specs_example = { - >>> 'edgecloud': { - >>> 'client_name': 'i2edge', - >>> 'base_url': 'http://ip_edge_cloud:port', - >>> 'additionalEdgeCloudParamater1': 'example' - >>> }, - >>> 'network': { - >>> 'client_name': 'open5gs', - >>> 'base_url': 'http://ip_network:port', - >>> 'additionalNetworkParamater1': 'example' - >>> } - >>> } - >>> - """ - sdk_client = AdaptersFactory() - adapters = {} - - for domain, config in adapter_specs.items(): - client_name = config["client_name"] - base_url = config["base_url"] - - # Support of additional paramaters for specific adapters - kwargs = { - k: v for k, v in config.items() if k not in ("client_name", "base_url") - } - - client = sdk_client.instantiate_and_retrieve_adapters( - domain, client_name, base_url, **kwargs - ) - adapters[domain] = client - - return adapters diff --git a/service-resource-manager-implementation/src/adapters/common/sdk_factory.py b/service-resource-manager-implementation/src/adapters/common/sdk_factory.py deleted file mode 100644 index 0eaf792..0000000 --- a/service-resource-manager-implementation/src/adapters/common/sdk_factory.py +++ /dev/null @@ -1,90 +0,0 @@ -# -*- coding: utf-8 -*- -## -# Copyright 2025-present by Software Networks Area, i2CAT. -# All rights reserved. -# -# This file is part of the Open SDK -# -# Contributors: -# - Adrián Pino Martínez (adrian.pino@i2cat.net) -## -from src.adapters.edgecloud.adapters.aeros.client import ( - EdgeApplicationManager as AerosClient, -) -from src.adapters.edgecloud.adapters.i2edge.client import ( - EdgeApplicationManager as I2EdgeClient, -) -from src.adapters.edgecloud.adapters.kubernetes.client import ( - EdgeApplicationManager as KubernetesClient, -) -# from src.clients.network.clients.oai.client import NetworkManager as OaiCoreClient -# from src.clients.network.clients.open5gcore.client import ( -# NetworkManager as Open5GCoreClient, -# ) -# from src.clients.network.clients.open5gs.client import ( -# NetworkManager as Open5GSClient, -# ) - -# - - -def _edgecloud_factory(client_name: str, base_url: str, **kwargs): - edge_cloud_factory = { - "aeros": lambda url, **kw: AerosClient(base_url=url, **kw), - "i2edge": lambda url: I2EdgeClient(base_url=url), - "kubernetes": lambda url, **kw: KubernetesClient(base_url=url, **kw), - } - try: - return edge_cloud_factory[client_name](base_url, **kwargs) - except KeyError: - raise ValueError( - f"Invalid edgecloud client '{client_name}'. Available: {list(edge_cloud_factory)}" - ) - - -# def _network_factory(client_name: str, base_url: str, **kwargs): -# if "scs_as_id" not in kwargs: -# raise ValueError("Missing required 'scs_as_id' for network clients.") -# scs_as_id = kwargs.pop("scs_as_id") - -# network_factory = { -# "open5gs": lambda url, scs_id, **kw: Open5GSClient( -# base_url=url, scs_as_id=scs_id, **kw -# ), -# "oai": lambda url, scs_id, **kw: OaiCoreClient( -# base_url=url, scs_as_id=scs_id, **kw -# ), -# "open5gcore": lambda url, scs_id, **kw: Open5GCoreClient( -# base_url=url, scs_as_id=scs_id, **kw -# ), -# } -# try: -# return network_factory[client_name](base_url, scs_as_id, **kwargs) -# except KeyError: -# raise ValueError( -# f"Invalid network client '{client_name}'. Available: {list(network_factory)}" -# ) - - -# def _oran_factory(client_name: str, base_url: str): -# # TODO - - -class SdkFactory: - _domain_factories = { - "edgecloud": _edgecloud_factory, - # "network": _network_factory, - # "oran": _oran_factory, - } - - @classmethod - def instantiate_and_retrieve_clients( - cls, domain: str, client_name: str, base_url: str, **kwargs - ): - try: - catalog = cls._domain_factories[domain] - except KeyError: - raise ValueError( - f"Unsupported domain '{domain}'. Supported: {list(cls._domain_factories)}" - ) - return catalog(client_name, base_url, **kwargs) diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/.env b/service-resource-manager-implementation/src/adapters/edgecloud/.env deleted file mode 100644 index 8bcda46..0000000 --- a/service-resource-manager-implementation/src/adapters/edgecloud/.env +++ /dev/null @@ -1,7 +0,0 @@ -# #### Logging #### -# LOG_LEVEL="debug" -# LOG_FILE="edgecloud.log" - -#### EdgeCloud #### -# EDGE_CLOUD="i2edge" -EDGE_CLOUD_URL=http://192.168.123.86:30769 diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/__init__.py b/service-resource-manager-implementation/src/adapters/edgecloud/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/__init__.py b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/__init__.py b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/__init__.py deleted file mode 100644 index 47625e8..0000000 --- a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -""" -aerOS client - This module provides a client for interacting with the aerOS REST API. - It includes methods for onboarding/deploying applications, - and querying aerOS continuum entities - aerOS domain is exposed as zones - aerOS services and service components are exposed as applications - Client is initialized with a base URL for the aerOS API - and an access token for authentication. -""" - -from src.adapters.edgecloud.adapters.aeros import config -from src.adapters.logger import setup_logger - -logger = setup_logger(__name__, is_debug=True, file_name=config.LOG_FILE) - -# TODO: The following should only appear in case aerOS client is used -# Currently even if another client is used, the logs appear -# logger.info("aerOS client initialized") -# logger.debug("aerOS API URL: %s", config.aerOS_API_URL) -# logger.debug("aerOS access token: %s", config.aerOS_ACCESS_TOKEN) -# logger.debug("aerOS debug mode: %s", config.DEBUG) -# logger.debug("aerOS log file: %s", config.LOG_FILE) diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/client.py b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/client.py deleted file mode 100644 index 5d05af2..0000000 --- a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/client.py +++ /dev/null @@ -1,433 +0,0 @@ -## -# This file is part of the Open SDK -# -# Contributors: -# - Vasilis Pitsilis (vpitsilis@dat.demokritos.gr, vpitsilis@iit.demokritos.gr) -# - Andreas Sakellaropoulos (asakellaropoulos@iit.demokritos.gr) -## -import uuid -from typing import Any, Dict, List, Optional - -import yaml - -from src.adapters.edgecloud.adapters.aeros import config -from src.adapters.edgecloud.adapters.aeros.continuum_client import ContinuumClient -from src.adapters.edgecloud.adapters.errors import EdgeCloudPlatformError -from src.adapters.edgecloud.core.edgecloud_interface import ( - EdgeCloudManagementInterface, -) -from src.adapters.logger import setup_logger - - -class EdgeApplicationManager(EdgeCloudManagementInterface): - """ - aerOS Continuum Client - FIXME: Handle None responses from continuum client - """ - - def __init__(self, base_url: str, **kwargs): - self.base_url = base_url - self.logger = setup_logger(__name__, is_debug=True, file_name=config.LOG_FILE) - self._app_store: Dict[str, Dict] = {} - self._deployed_services: Dict[str, List[str]] = {} - self._stopped_services: Dict[str, List[str]] = {} - - # Overwrite config values if provided via kwargs - if "aerOS_API_URL" in kwargs: - config.aerOS_API_URL = kwargs["aerOS_API_URL"] - if "aerOS_ACCESS_TOKEN" in kwargs: - config.aerOS_ACCESS_TOKEN = kwargs["aerOS_ACCESS_TOKEN"] - if "aerOS_HLO_TOKEN" in kwargs: - config.aerOS_HLO_TOKEN = kwargs["aerOS_HLO_TOKEN"] - - if not config.aerOS_API_URL: - raise ValueError("Missing 'aerOS_API_URL'") - if not config.aerOS_ACCESS_TOKEN: - raise ValueError("Missing 'aerOS_ACCESS_TOKEN'") - if not config.aerOS_HLO_TOKEN: - raise ValueError("Missing 'aerOS_HLO_TOKEN'") - - def onboard_app(self, app_manifest: Dict) -> Dict: - app_id = app_manifest.get("appId") - if not app_id: - raise EdgeCloudPlatformError("Missing 'appId' in app manifest") - - if app_id in self._app_store: - raise EdgeCloudPlatformError( - f"Application with id '{app_id}' already exists" - ) - - self._app_store[app_id] = app_manifest - self.logger.debug("Onboarded application with id: %s", app_id) - return {"appId": app_id} - - def get_all_onboarded_apps(self) -> List[Dict]: - self.logger.debug("Onboarded applications: %s", list(self._app_store.keys())) - return list(self._app_store.values()) - - def get_onboarded_app(self, app_id: str) -> Dict: - if app_id not in self._app_store: - raise EdgeCloudPlatformError( - f"Application with id '{app_id}' does not exist" - ) - self.logger.debug("Retrieved application with id: %s", app_id) - return self._app_store[app_id] - - def delete_onboarded_app(self, app_id: str) -> None: - if app_id not in self._app_store: - raise EdgeCloudPlatformError( - f"Application with id '{app_id}' does not exist" - ) - service_instances = self._stopped_services.get(app_id, []) - self.logger.debug( - "Deleting application with id: %s and instances: %s", - app_id, - service_instances, - ) - for service_instance in service_instances: - self._purge_deployed_app_from_continuum(service_instance) - self.logger.debug( - "successfully purged service instance: %s", service_instance - ) - del self._stopped_services[app_id] # Clean up stopped services - del self._app_store[app_id] # Remove from onboarded apps - - def _generate_service_id(self, app_id: str) -> str: - return f"urn:ngsi-ld:Service:{app_id}-{uuid.uuid4().hex[:4]}" - - def _generate_tosca_yaml_dict( - self, app_manifest: Dict, app_zones: List[Dict] - ) -> Dict: - component = app_manifest.get("componentSpec", [{}])[0] - component_name = component.get("componentName", "application") - - image_path = app_manifest.get("appRepo", {}).get("imagePath", "") - image_file = image_path.split("/")[-1] - repository_url = ( - "/".join(image_path.split("/")[:-1]) if "/" in image_path else "docker_hub" - ) - zone_id = ( - app_zones[0].get("EdgeCloudZone", {}).get("edgeCloudZoneId", "default-zone") - ) - - # Extract minNodeMemory - min_node_memory = ( - app_manifest.get("requiredResources", {}) - .get("applicationResources", {}) - .get("cpuPool", {}) - .get("topology", {}) - .get("minNodeMemory", 1024) - ) - - ports = {} - for iface in component.get("networkInterfaces", []): - interface_id = iface.get("interfaceId", "default") - protocol = iface.get("protocol", "TCP").lower() - port = iface.get("port", 8080) - ports[interface_id] = { - "properties": {"protocol": [protocol], "source": port} - } - - expose_ports = any( - iface.get("visibilityType") == "VISIBILITY_EXTERNAL" - for iface in component.get("networkInterfaces", []) - ) - - yaml_dict = { - "tosca_definitions_version": "tosca_simple_yaml_1_3", - "description": f"TOSCA for {app_manifest.get('name', 'application')}", - "serviceOverlay": False, - "node_templates": { - component_name: { - "type": "tosca.nodes.Container.Application", - "isJob": False, - "requirements": [ - { - "network": { - "properties": { - "ports": ports, - "exposePorts": expose_ports, - } - } - }, - { - "host": { - "node_filter": { - "capabilities": [ - { - "host": { - "properties": { - "cpu_arch": {"equal": "x64"}, - "realtime": {"equal": False}, - "cpu_usage": { - "less_or_equal": "0.1" - }, - "mem_size": { - "greater_or_equal": str( - min_node_memory - ) - }, - "domain_id": {"equal": zone_id}, - } - } - } - ], - "properties": None, - } - } - }, - ], - "artifacts": { - "application_image": { - "file": image_file, - "type": "tosca.artifacts.Deployment.Image.Container.Docker", - "is_private": False, - "repository": repository_url, - } - }, - "interfaces": { - "Standard": { - "create": { - "implementation": "application_image", - "inputs": {"cliArgs": [], "envVars": []}, - } - } - }, - } - }, - } - - return yaml_dict - - def deploy_app(self, app_id: str, app_zones: List[Dict]) -> Dict: - # 1. Get app CAMARA manifest - app_manifest = self._app_store.get(app_id) - if not app_manifest: - raise EdgeCloudPlatformError( - f"Application with id '{app_id}' does not exist" - ) - - # 2. Generate unique service ID - service_id = self._generate_service_id(app_id) - - # 3. Convert dict to YAML string - yaml_dict = self._generate_tosca_yaml_dict(app_manifest, app_zones) - tosca_yaml = yaml.dump(yaml_dict, sort_keys=False) - self.logger.info("Generated TOSCA YAML:") - self.logger.info(tosca_yaml) - - # 4. Instantiate client and call continuum to deploy service - aeros_client = ContinuumClient(self.base_url) - response = aeros_client.onboard_and_deploy_service(service_id, tosca_yaml) - - if "serviceId" not in response: - raise EdgeCloudPlatformError( - "Invalid response from onboard_service: missing 'serviceId'" - ) - - # 5. Track deployment - if app_id not in self._deployed_services: - self._deployed_services[app_id] = [] - self._deployed_services[app_id].append(service_id) - - # 6. Return expected format - return {"appInstanceId": response["serviceId"]} - - def get_all_deployed_apps( - self, - app_id: Optional[str] = None, - app_instance_id: Optional[str] = None, - region: Optional[str] = None, - ) -> List[Dict]: - deployed = [] - for stored_app_id, instance_ids in self._deployed_services.items(): - for instance_id in instance_ids: - deployed.append({"appId": stored_app_id, "appInstanceId": instance_id}) - return deployed - - def _purge_deployed_app_from_continuum(self, app_id: str) -> None: - aeros_client = ContinuumClient(self.base_url) - response = aeros_client.purge_service(app_id) - if response: - self.logger.debug("Purged deployed application with id: %s", app_id) - else: - raise EdgeCloudPlatformError( - f"Failed to purg service with id from the continuum '{app_id}'" - ) - - def undeploy_app(self, app_instance_id: str) -> None: - # 1. Locate app_id corresponding to this instance - found_app_id = None - for app_id, instances in self._deployed_services.items(): - if app_instance_id in instances: - found_app_id = app_id - break - - if not found_app_id: - raise EdgeCloudPlatformError( - f"No deployed app instance with ID '{app_instance_id}' found" - ) - - # 2. Call the external undeploy_service - aeros_client = ContinuumClient(self.base_url) - try: - aeros_client.undeploy_service(app_instance_id) - except Exception as e: - raise EdgeCloudPlatformError( - f"Failed to undeploy app instance '{app_instance_id}': {str(e)}" - ) from e - - # We could do it here with a little wait but better all instances in the same app are purged at once - # 3. Purge the deployed app from continuum - # self._purge_deployed_app_from_continuum(app_instance_id) - - # 4. Clean up internal tracking - self._deployed_services[found_app_id].remove(app_instance_id) - # Add instance to _stopped_services to purge it later - if found_app_id not in self._stopped_services: - self._stopped_services[found_app_id] = [] - self._stopped_services[found_app_id].append(app_instance_id) - # If app has no instances left, remove it from deployed services - if not self._deployed_services[found_app_id]: - del self._deployed_services[found_app_id] - - def get_edge_cloud_zones( - self, region: Optional[str] = None, status: Optional[str] = None - ) -> List[Dict]: - aeros_client = ContinuumClient(self.base_url) - ngsild_params = "type=Domain&format=simplified" - aeros_domains = aeros_client.query_entities(ngsild_params) - return [ - { - "zoneId": domain["id"], - "status": domain["domainStatus"].split(":")[-1].lower(), - "geographyDetails": "NOT_USED", - } - for domain in aeros_domains - ] - - def get_edge_cloud_zones_details( - self, zone_id: str, flavour_id: Optional[str] = None - ) -> Dict: - """ - Get details of a specific edge cloud zone. - :param zone_id: The ID of the edge cloud zone - :param flavour_id: Optional flavour ID to filter the results - :return: Details of the edge cloud zone - """ - # Minimal mocked response based on required fields of 'ZoneRegisteredData' in GSMA OPG E/WBI API - # return { - # "zoneId": - # zone_id, - # "reservedComputeResources": [{ - # "cpuArchType": "ISA_X86_64", - # "numCPU": "4", - # "memory": 8192, - # }], - # "computeResourceQuotaLimits": [{ - # "cpuArchType": "ISA_X86_64", - # "numCPU": "8", - # "memory": 16384, - # }], - # "flavoursSupported": [{ - # "flavourId": - # "medium-x86", - # "cpuArchType": - # "ISA_X86_64", - # "supportedOSTypes": [{ - # "architecture": "x86_64", - # "distribution": "UBUNTU", - # "version": "OS_VERSION_UBUNTU_2204_LTS", - # "license": "OS_LICENSE_TYPE_FREE", - # }], - # "numCPU": - # 4, - # "memorySize": - # 8192, - # "storageSize": - # 100, - # }], - # # - # } - aeros_client = ContinuumClient(self.base_url) - ngsild_params = ( - f'format=simplified&type=InfrastructureElement&q=domain=="{zone_id}"' - ) - self.logger.debug( - "Querying infrastructure elements for zone %s with params: %s", - zone_id, - ngsild_params, - ) - # Query the infrastructure elements for the specified zonese - aeros_domain_ies = aeros_client.query_entities(ngsild_params) - # Transform the infrastructure elements into the required format - # and return the details of the edge cloud zone - response = self.transform_infrastructure_elements( - domain_ies=aeros_domain_ies, domain=zone_id - ) - self.logger.debug("Transformed response: %s", response) - # Return the transformed response - return response - - def transform_infrastructure_elements( - self, domain_ies: List[Dict[str, Any]], domain: str - ) -> Dict[str, Any]: - """ - Transform the infrastructure elements into a format suitable for the - edge cloud zone details. - :param domain_ies: List of infrastructure elements - :param domain: The ID of the edge cloud zone - :return: Transformed details of the edge cloud zone - """ - total_cpu = 0 - total_ram = 0 - total_disk = 0 - total_available_ram = 0 - total_available_disk = 0 - - flavours_supported = [] - - for element in domain_ies: - total_cpu += element.get("cpuCores", 0) - total_ram += element.get("ramCapacity", 0) - total_available_ram += element.get("availableRam", 0) - total_disk += element.get("diskCapacity", 0) - total_available_disk += element.get("availableDisk", 0) - - # Create a flavour per machine - flavour = { - "flavourId": f"{element.get('hostname')}-{element.get('containerTechnology')}", - "cpuArchType": f"{element.get('cpuArchitecture')}", - "supportedOSTypes": [ - { - "architecture": f"{element.get('cpuArchitecture')}", - "distribution": f"{element.get('operatingSystem')}", # assume - "version": "OS_VERSION_UBUNTU_2204_LTS", - "license": "OS_LICENSE_TYPE_FREE", - } - ], - "numCPU": element.get("cpuCores", 0), - "memorySize": element.get("ramCapacity", 0), - "storageSize": element.get("diskCapacity", 0), - } - flavours_supported.append(flavour) - - result = { - "zoneId": domain, - "reservedComputeResources": [ - { - "cpuArchType": "ISA_X86_64", - "numCPU": str(total_cpu), - "memory": total_ram, - } - ], - "computeResourceQuotaLimits": [ - { - "cpuArchType": "ISA_X86_64", - "numCPU": str(total_cpu * 2), # Assume quota is 2x total? - "memory": total_ram * 2, - } - ], - "flavoursSupported": flavours_supported, - } - return result diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/config.py b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/config.py deleted file mode 100644 index 794cba5..0000000 --- a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/config.py +++ /dev/null @@ -1,27 +0,0 @@ -## -# This file is part of the Open SDK -# -# Contributors: -# - Vasilis Pitsilis (vpitsilis@dat.demokritos.gr, vpitsilis@iit.demokritos.gr) -# - Andreas Sakellaropoulos (asakellaropoulos@iit.demokritos.gr) -## -""" -aerOS access configuration -Access tokens need to be provided in environment variables. -""" -# import os - -# aerOS_API_URL = os.environ.get("aerOS_API_URL") -aerOS_API_URL = "harcoded_api" -if not aerOS_API_URL: - raise ValueError("Environment variable 'aerOS_API_URL' is not set.") -# aerOS_ACCESS_TOKEN = os.environ.get("aerOS_ACCESS_TOKEN") -aerOS_ACCESS_TOKEN = "harcoded_access_token" -if not aerOS_ACCESS_TOKEN: - raise ValueError("Environment variable 'aerOS_ACCESS_TOKEN' is not set.") -# aerOS_HLO_TOKEN = os.environ.get("aerOS_HLO_TOKEN") -aerOS_HLO_TOKEN = "harcoded_hlo_token" -if not aerOS_HLO_TOKEN: - raise ValueError("Environment variable 'aerOS_HLO_TOKEN' is not set.") -DEBUG = False -LOG_FILE = ".log/aeros_client.log" diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/continuum_client.py b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/continuum_client.py deleted file mode 100644 index 6f8b7c5..0000000 --- a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/continuum_client.py +++ /dev/null @@ -1,196 +0,0 @@ -## -# This file is part of the Open SDK -# -# Contributors: -# - Vasilis Pitsilis (vpitsilis@dat.demokritos.gr, vpitsilis@iit.demokritos.gr) -# - Andreas Sakellaropoulos (asakellaropoulos@iit.demokritos.gr) -## -""" -aerOS REST API Client - This client is used to interact with the aerOS REST API. -""" - -import requests - -from src.adapters.edgecloud.adapters.aeros import config -from src.adapters.edgecloud.adapters.aeros.utils import catch_requests_exceptions -from src.adapters.logger import setup_logger - - -class ContinuumClient: - """ - Client to aerOS ngsi-ld based continuum exposure - """ - - def __init__(self, base_url: str = None): - """ - :param base_url: the base url of the aerOS API - """ - if base_url is None: - self.api_url = config.aerOS_API_URL - else: - self.api_url = base_url - self.logger = setup_logger(__name__, is_debug=True, file_name=config.LOG_FILE) - self.m2m_cb_token = config.aerOS_ACCESS_TOKEN - self.hlo_token = config.aerOS_HLO_TOKEN - self.headers = { - "Content-Type": "application/json", - "Accept": "application/json", - "aerOS": "true", - "Authorization": f"Bearer {self.m2m_cb_token}", - } - self.hlo_headers = { - "Content-Type": "application/json", - "Accept": "application/json", - "aerOS": "true", - "Authorization": f"Bearer {self.hlo_token}", - } - self.hlo_onboard_headers = { - "Content-Type": "application/yaml", - "Authorization": f"Bearer {self.hlo_token}", - } - - @catch_requests_exceptions - def query_entity(self, entity_id, ngsild_params) -> dict: - """ - Query entity with ngsi-ld params - :input - @param entity_id: the id of the queried entity - @param ngsi-ld: the query params - :output - ngsi-ld object - """ - entity_url = f"{self.api_url}/entities/{entity_id}?{ngsild_params}" - response = requests.get(entity_url, headers=self.headers, timeout=15) - if response is None: - return None - else: - if config.DEBUG: - self.logger.debug("Query entity URL: %s", entity_url) - self.logger.debug( - "Query entity response: %s %s", response.status_code, response.text - ) - return response.json() - - @catch_requests_exceptions - def query_entities(self, ngsild_params): - """ - Query entities with ngsi-ld params - :input - @param ngsi-ld: the query params - :output - ngsi-ld object - """ - entities_url = f"{self.api_url}/entities?{ngsild_params}" - response = requests.get(entities_url, headers=self.headers, timeout=15) - if response is None: - return None - # else: - # if config.DEBUG: - # self.logger.debug("Query entities URL: %s", entities_url) - # self.logger.debug("Query entities response: %s %s", - # response.status_code, response.text) - return response.json() - - @catch_requests_exceptions - def deploy_service(self, service_id: str) -> dict: - """ - Re-allocate (deploy) service on aerOS continuum - :input - @param service_id: the id of the service to be re-allocated - :output - the re-allocated service json object - """ - re_allocate_url = f"{self.api_url}/hlo_fe/services/{service_id}" - response = requests.put(re_allocate_url, headers=self.hlo_headers, timeout=15) - if response is None: - return None - else: - if config.DEBUG: - self.logger.debug("Re-allocate service URL: %s", re_allocate_url) - self.logger.debug( - "Re-allocate service response: %s %s", - response.status_code, - response.text, - ) - return response.json() - - @catch_requests_exceptions - def undeploy_service(self, service_id: str) -> dict: - """ - Undeploy service - :input - @param service_id: the id of the service to be undeployed - :output - the undeployed service json object - """ - undeploy_url = f"{self.api_url}/hlo_fe/services/{service_id}" - response = requests.delete(undeploy_url, headers=self.hlo_headers, timeout=15) - if response is None: - return None - else: - if config.DEBUG: - self.logger.debug("Re-allocate service URL: %s", undeploy_url) - self.logger.debug( - "Undeploy service response: %s %s", - response.status_code, - response.text, - ) - return response.json() - - @catch_requests_exceptions - def onboard_and_deploy_service(self, service_id: str, tosca_str: str) -> dict: - """ - Onboard (& deploy) service on aerOS continuum - :input - @param service_id: the id of the service to onboarded (& deployed) - @param tosca_str: the tosca whith all orchestration information - :output - the allocated service json object - """ - onboard_url = f"{self.api_url}/hlo_fe/services/{service_id}" - if config.DEBUG: - self.logger.debug("Onboard service URL: %s", onboard_url) - self.logger.debug( - "Onboard service request body (TOSCA-YAML): %s", tosca_str - ) - response = requests.post( - onboard_url, data=tosca_str, headers=self.hlo_onboard_headers, timeout=15 - ) - if response is None: - return None - else: - if config.DEBUG: - self.logger.debug("Onboard service URL: %s", onboard_url) - self.logger.debug( - "Onboard service response: %s %s", - response.status_code, - response.text, - ) - return response.json() - - @catch_requests_exceptions - def purge_service(self, service_id: str) -> bool: - """ - Purge service from aerOS continuum - :input - @param service_id: the id of the service to be purged - :output - the purge result message from aerOS continuum - """ - purge_url = f"{self.api_url}/hlo_fe/services/{service_id}/purge" - response = requests.delete(purge_url, headers=self.hlo_headers, timeout=15) - if response is None: - return False - else: - if config.DEBUG: - self.logger.debug("Purge service URL: %s", purge_url) - self.logger.debug( - "Purge service response: %s %s", - response.status_code, - response.text, - ) - if response.status_code != 200: - self.logger.error("Failed to purge service: %s", response.text) - return False - return True diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/utils.py b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/utils.py deleted file mode 100644 index b552b37..0000000 --- a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/aeros/utils.py +++ /dev/null @@ -1,43 +0,0 @@ -## -# This file is part of the Open SDK -# -# Contributors: -# - Vasilis Pitsilis (vpitsilis@dat.demokritos.gr, vpitsilis@iit.demokritos.gr) -# - Andreas Sakellaropoulos (asakellaropoulos@iit.demokritos.gr) -## -""" -Docstring -""" -from requests.exceptions import HTTPError, RequestException, Timeout - -import src.adapters.edgecloud.adapters.aeros.config as config -from src.adapters.logger import setup_logger - - -def catch_requests_exceptions(func): - """ - Docstring - """ - logger = setup_logger(__name__, is_debug=True, file_name=config.LOG_FILE) - - def wrapper(*args, **kwargs): - try: - result = func(*args, **kwargs) - return result - except HTTPError as e: - logger.info("4xx or 5xx: %s \n", {e}) - return None # raise our custom exception or log, etc. - except ConnectionError as e: - logger.info( - "Raised for connection-related issues (e.g., DNS resolution failure, network issues): %s \n", - {e}, - ) - return None # raise our custom exception or log, etc. - except Timeout as e: - logger.info("Timeout occured: %s \n", {e}) - return None # raise our custom exception or log, etc. - except RequestException as e: - logger.info("Request failed: %s \n", {e}) - return None # raise our custom exception or log, etc. - - return wrapper diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/errors.py b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/errors.py deleted file mode 100644 index 97b14bc..0000000 --- a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/errors.py +++ /dev/null @@ -1,5 +0,0 @@ -# -*- coding: utf-8 -*- - - -class EdgeCloudPlatformError(Exception): - pass diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/__init__.py b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/client.py b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/client.py deleted file mode 100644 index 0ee3aa9..0000000 --- a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/client.py +++ /dev/null @@ -1,243 +0,0 @@ -# -*- coding: utf-8 -*- -## -# Copyright 2025-present by Software Networks Area, i2CAT. -# All rights reserved. -# -# This file is part of the Open SDK -# -# Contributors: -# - Adrián Pino Martínez (adrian.pino@i2cat.net) -# - Sergio Giménez (sergio.gimenez@i2cat.net) -## -from typing import Dict, List, Optional - -from src.adapters import logger -from src.adapters.edgecloud.core.edgecloud_interface import ( - EdgeCloudManagementInterface, -) - -from ...adapters.i2edge import schemas -from .common import ( - I2EdgeError, - i2edge_delete, - i2edge_get, - i2edge_post, - i2edge_post_multiform_data, -) - -log = logger.get_logger(__name__) - - -class EdgeApplicationManager(EdgeCloudManagementInterface): - """ - i2Edge Client - """ - - def __init__(self, base_url: str, flavour_id: str): - self.base_url = base_url - self.flavour_id = flavour_id - - def get_edge_cloud_zones( - self, region: Optional[str] = None, status: Optional[str] = None - ) -> list[dict]: - url = "{}/zones/list".format(self.base_url) - params = {} - try: - response = i2edge_get(url, params=params) - log.info("Availability zones retrieved successfully") - return response - except I2EdgeError as e: - raise e - - def get_edge_cloud_zones_details( - self, zone_id: str, flavour_id: Optional[str] = None - ) -> Dict: - url = "{}zone/{}".format(self.base_url, zone_id) - params = {} - try: - response = i2edge_get(url, params=params) - log.info("Availability zone details retrieved successfully") - return response - except I2EdgeError as e: - raise e - - def _create_artefact( - self, - artefact_id: str, - artefact_name: str, - repo_name: str, - repo_type: str, - repo_url: str, - password: Optional[str] = None, - token: Optional[str] = None, - user_name: Optional[str] = None, - ): - repo_type = schemas.RepoType(repo_type) - url = "{}/artefact".format(self.base_url) - payload = schemas.ArtefactOnboarding( - artefact_id=artefact_id, - name=artefact_name, - repo_password=password, - repo_name=repo_name, - repo_type=repo_type, - repo_url=repo_url, - repo_token=token, - repo_user_name=user_name, - ) - try: - i2edge_post_multiform_data(url, payload) - log.info("Artifact added successfully") - except I2EdgeError as e: - raise e - - def _get_artefact(self, artefact_id: str) -> Dict: - url = "{}/artefact/{}".format(self.base_url, artefact_id) - try: - response = i2edge_get(url, artefact_id) - log.info("Artifact retrieved successfully") - return response - except I2EdgeError as e: - raise e - - def _get_all_artefacts(self) -> List[Dict]: - url = "{}/artefact".format(self.base_url) - try: - response = i2edge_get(url, {}) - log.info("Artifacts retrieved successfully") - return response - except I2EdgeError as e: - raise e - - def _delete_artefact(self, artefact_id: str): - url = "{}/artefact".format(self.base_url) - try: - i2edge_delete(url, artefact_id) - log.info("Artifact deleted successfully") - except I2EdgeError as e: - raise e - - def onboard_app(self, app_manifest: Dict) -> Dict: - try: - app_id = app_manifest["appId"] - artefact_id = app_id - - app_component_spec = schemas.AppComponentSpec(artefactId=artefact_id) - data = schemas.ApplicationOnboardingData( - app_id=app_id, appComponentSpecs=[app_component_spec] - ) - payload = schemas.ApplicationOnboardingRequest(profile_data=data) - url = "{}/application/onboarding".format(self.base_url) - i2edge_post(url, payload) - except I2EdgeError as e: - raise e - except KeyError as e: - raise I2EdgeError("Missing required field in app_manifest: {}".format(e)) - - def delete_onboarded_app(self, app_id: str) -> None: - url = "{}/application/onboarding".format(self.base_url) - try: - i2edge_delete(url, app_id) - except I2EdgeError as e: - raise e - - def get_onboarded_app(self, app_id: str) -> Dict: - url = "{}/application/onboarding/{}".format(self.base_url, app_id) - try: - response = i2edge_get(url, app_id) - return response - except I2EdgeError as e: - raise e - - def get_all_onboarded_apps(self) -> List[Dict]: - url = "{}/applications/onboarding".format(self.base_url) - params = {} - try: - response = i2edge_get(url, params) - return response - except I2EdgeError as e: - raise e - - # def _select_best_flavour_for_app(self, zone_id) -> str: - # # list_of_flavours = self.get_edge_cloud_zones_details(zone_id) - # # - # return flavourId - - def deploy_app(self, app_id: str, app_zones: List[Dict]) -> Dict: - appId = app_id - app = self.get_onboarded_app(appId) - profile_data = app["profile_data"] - appProviderId = profile_data["appProviderId"] - appVersion = profile_data["appMetaData"]["version"] - zone_info = app_zones[0]["EdgeCloudZone"] - zone_id = zone_info["edgeCloudZoneId"] - # TODO: atm the flavour id is specified as an input parameter - # flavourId = self._select_best_flavour_for_app(zone_id=zone_id) - app_deploy_data = schemas.AppDeployData( - appId=appId, - appProviderId=appProviderId, - appVersion=appVersion, - zoneInfo=schemas.ZoneInfo(flavourId=self.flavour_id, zoneId=zone_id), - ) - url = "{}/app/".format(self.base_url) - payload = schemas.AppDeploy(app_deploy_data=app_deploy_data) - try: - response = i2edge_post(url, payload) - log.info("App deployed successfully") - print(response) - return response - except I2EdgeError as e: - raise e - - def get_all_deployed_apps(self) -> List[Dict]: - url = "{}/app/".format(self.base_url) - params = {} - try: - response = i2edge_get(url, params=params) - log.info("All app instances retrieved successfully") - return response - except I2EdgeError as e: - raise e - - def get_deployed_app(self, app_id, zone_id) -> List[Dict]: - # Logic: Get all onboarded apps and filter the one where release_name == artifact name - - # Step 1) Extract "app_name" from the onboarded app using the "app_id" - onboarded_app = self.get_onboarded_app(app_id) - if not onboarded_app: - raise ValueError(f"No onboarded app found with ID: {app_id}") - - try: - app_name = onboarded_app["profile_data"]["appMetaData"]["appName"] - except KeyError as e: - raise ValueError(f"Onboarded app missing required field: {e}") - - # Step 2) Retrieve all deployed apps and filter the one(s) where release_name == app_name - deployed_apps = self.get_all_deployed_apps() - if not deployed_apps: - return [] - - # Filter apps where release_name matches our app_name and zone matches - for app_instance_name in deployed_apps: - if ( - app_instance_name.get("release_name") == app_name - and app_instance_name.get("zone_id") == zone_id - ): - return app_instance_name - return None - - url = "{}/app/{}/{}".format(self.base_url, zone_id, app_instance_name) - params = {} - try: - response = i2edge_get(url, params=params) - log.info("App instance retrieved successfully") - return response - except I2EdgeError as e: - raise e - - def undeploy_app(self, app_instance_id: str) -> None: - url = "{}/app".format(self.base_url) - try: - i2edge_delete(url, app_instance_id) - log.info("App instance deleted successfully") - except I2EdgeError as e: - raise e diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/common.py b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/common.py deleted file mode 100644 index 33c2d12..0000000 --- a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/common.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -## -# Copyright 2025-present by Software Networks Area, i2CAT. -# All rights reserved. -# -# This file is part of the Open SDK -# -# Contributors: -# - Sergio Giménez (sergio.gimenez@i2cat.net) -## -import json -from typing import Optional - -import requests -from pydantic import BaseModel - -from src.adapters import logger -from src.adapters.edgecloud.adapters.errors import EdgeCloudPlatformError - -log = logger.get_logger(__name__) - - -class I2EdgeError(EdgeCloudPlatformError): - pass - - -class I2EdgeErrorResponse(BaseModel): - message: str - detail: dict - - -def get_error_message_from(response: requests.Response) -> str: - try: - error_response = I2EdgeErrorResponse(**response.json()) - return error_response.message - except Exception as e: - log.error("Failed to parse error response from i2edge: {}".format(e)) - return response.text - - -def i2edge_post(url: str, model_payload: BaseModel) -> dict: - headers = { - "Content-Type": "application/json", - "accept": "application/json", - } - json_payload = json.dumps(model_payload.model_dump(mode="json")) - try: - response = requests.post(url, data=json_payload, headers=headers) - response.raise_for_status() - return response.json() - except requests.exceptions.HTTPError as e: - i2edge_err_msg = get_error_message_from(response) - err_msg = "Failed to deploy app: {}. Detail: {}".format(i2edge_err_msg, e) - log.error(err_msg) - raise I2EdgeError(err_msg) - - -def i2edge_post_multiform_data(url: str, model_payload: BaseModel) -> dict: - headers = { - "accept": "application/json", - } - payload_dict = model_payload.model_dump(mode="json") - payload_in_str = {k: str(v) for k, v in payload_dict.items()} - try: - response = requests.post(url, data=payload_in_str, headers=headers) - response.raise_for_status() - return response.json() - except requests.exceptions.HTTPError as e: - i2edge_err_msg = get_error_message_from(response) - err_msg = "Failed to deploy app: {}. Detail: {}".format(i2edge_err_msg, e) - log.error(err_msg) - raise I2EdgeError(err_msg) - - -def i2edge_delete(url: str, id: str) -> dict: - headers = {"accept": "application/json"} - try: - query = "{}/{}".format(url, id) - response = requests.delete(query, headers=headers) - response.raise_for_status() - return response.json() - except requests.exceptions.HTTPError as e: - i2edge_err_msg = get_error_message_from(response) - err_msg = "Failed to undeploy app: {}. Detail: {}".format(i2edge_err_msg, e) - log.error(err_msg) - raise I2EdgeError(err_msg) - - -def i2edge_get(url: str, params: Optional[dict]): - headers = {"accept": "application/json"} - try: - response = requests.get(url, params=params, headers=headers) - response.raise_for_status() - return response.json() - except requests.exceptions.HTTPError as e: - i2edge_err_msg = get_error_message_from(response) - err_msg = "Failed to get apps: {}. Detail: {}".format(i2edge_err_msg, e) - log.error(err_msg) - raise I2EdgeError(err_msg) diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/schemas.py b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/schemas.py deleted file mode 100644 index c0d522f..0000000 --- a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/schemas.py +++ /dev/null @@ -1,167 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -## -# Copyright 2025-present by Software Networks Area, i2CAT. -# All rights reserved. -# -# This file is part of the Open SDK -# -# Contributors: -# - Sergio Giménez (sergio.gimenez@i2cat.net) -# - César Cajas (cesar.cajas@i2cat.net) -## -from enum import Enum -from typing import List, Optional - -from pydantic import BaseModel, ConfigDict, Field, field_validator - - -class ZoneInfo(BaseModel): - flavourId: str - zoneId: str - - -class AppParameters(BaseModel): - namespace: Optional[str] = None - - -class AppDeployData(BaseModel): - appId: str - appProviderId: str - appVersion: str - zoneInfo: ZoneInfo - - -class AppDeploy(BaseModel): - app_deploy_data: AppDeployData - app_parameters: Optional[AppParameters] = Field(default=AppParameters()) - - -# Artefact - - -class RepoType(str, Enum): - UPLOAD = "UPLOAD" - PUBLICREPO = "PUBLICREPO" - PRIVATEREPO = "PRIVATEREPO" - - -class ArtefactOnboarding(BaseModel): - artefact_id: str - name: str - # chart: Optional[bytes] = Field(default=None) # XXX AFAIK not supported by CAMARA. - repo_password: Optional[str] = None - repo_name: Optional[str] = None - repo_type: RepoType - repo_url: Optional[str] = None - repo_token: Optional[str] = None - repo_user_name: Optional[str] = None - model_config = ConfigDict(use_enum_values=True) - - -# Application Onboarding - -# XXX Leaving default values since i2edge only cares about appid and artifactid, at least for now. - - -class AppComponentSpec(BaseModel): - artefactId: str - componentName: str = Field(default="default_component") - serviceNameEW: str = Field(default="default_ew_service") - serviceNameNB: str = Field(default="default_nb_service") - - -class AppMetaData(BaseModel): - appDescription: str = Field(default="Default app description") - appName: str = Field(default="Default App") - category: str = Field(default="DEFAULT") - mobilitySupport: bool = Field(default=False) - version: str = Field(default="1.0") - - -class AppQoSProfile(BaseModel): - appProvisioning: bool = Field(default=True) - bandwidthRequired: int = Field(default=1) - latencyConstraints: str = Field(default="NONE") - multiUserClients: str = Field(default="APP_TYPE_SINGLE_USER") - noOfUsersPerAppInst: int = Field(default=1) - - -class ApplicationOnboardingData(BaseModel): - appComponentSpecs: List[AppComponentSpec] - appDeploymentZones: List[str] = Field(default=["default_zone"]) - app_id: str - appMetaData: AppMetaData = Field(default_factory=AppMetaData) - appProviderId: str = Field(default="default_provider") - appQoSProfile: AppQoSProfile = Field(default_factory=AppQoSProfile) - appStatusCallbackLink: Optional[str] = None - - -class ApplicationOnboardingRequest(BaseModel): - profile_data: ApplicationOnboardingData - - -# Flavour - - -class GPU(BaseModel): - gpuMemory: int = Field(default=0, description="GPU memory in MB") - gpuModeName: str = Field(default="", description="GPU mode name") - gpuVendorType: str = Field( - default="GPU_PROVIDER_NVIDIA", description="GPU vendor type" - ) - numGPU: int = Field(..., description="Number of GPUs") - - -class Hugepages(BaseModel): - number: int = Field(default=0, description="Number of hugepages") - pageSize: str = Field(default="2MB", description="Size of hugepages") - - -class SupportedOSTypes(BaseModel): - architecture: str = Field(default="x86_64", description="OS architecture") - distribution: str = Field(default="RHEL", description="OS distribution") - license: str = Field(default="OS_LICENSE_TYPE_FREE", description="OS license type") - version: str = Field(default="OS_VERSION_UBUNTU_2204_LTS", description="OS version") - - -class FlavourSupported(BaseModel): - cpuArchType: str = Field(default="ISA_X86", description="CPU architecture type") - cpuExclusivity: bool = Field(default=True, description="CPU exclusivity") - fpga: int = Field(default=0, description="Number of FPGAs") - gpu: Optional[List[GPU]] = Field(default=None, description="List of GPUs") - hugepages: List[Hugepages] = Field( - default_factory=lambda: [Hugepages()], description="List of hugepages" - ) - memorySize: str = Field(..., description="Memory size (e.g., '1024MB' or '2GB')") - numCPU: int = Field(..., description="Number of CPUs") - storageSize: int = Field(default=0, description="Storage size in GB") - supportedOSTypes: List[SupportedOSTypes] = Field( - default_factory=lambda: [SupportedOSTypes()], - description="List of supported OS types", - ) - vpu: int = Field(default=0, description="Number of VPUs") - - @field_validator("memorySize") - @classmethod - def validate_memory_size(cls, v): - if not (v.endswith("MB") or v.endswith("GB")): - raise ValueError("memorySize must end with MB or GB") - try: - int(v[:-2]) - except ValueError: - raise ValueError("memorySize must be a number followed by MB or GB") - return v - - -class Flavour(BaseModel): - flavour_supported: FlavourSupported - - -# EdgeCloud Zones - - -class Zone(BaseModel): - geographyDetails: str - geolocation: str - zoneId: str diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/utils.py b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/utils.py deleted file mode 100644 index c02b79d..0000000 --- a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/i2edge/utils.py +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -## -# Copyright 2025-present by Software Networks Area, i2CAT. -# All rights reserved. -# -# This file is part of the Open SDK -# -# Contributors: -# - Sergio Giménez (sergio.gimenez@i2cat.net) -# - César Cajas (cesar.cajas@i2cat.net) -## -import uuid -from typing import Optional, Union -from uuid import UUID - -from src.edgecloud import logger - -from src.adapters.edgecloud.api.routers.lcm.schemas import RequiredResources -from src.adapters.edgecloud.core import utils as core_utils - -from .client import I2EdgeClient -from .common import I2EdgeError - -log = logger.get_logger(__name__) - - -def generate_namespace_name_from(app_id: str, app_instance_id: str) -> str: - max_length = 63 - combined_name = "{}-{}".format(app_id, app_instance_id) - if len(combined_name) > max_length: - combined_name = combined_name[:max_length] - return combined_name - - -def generate_unique_id() -> UUID: - return uuid.uuid4() - - -def instantiate_app_with( - camara_app_id: UUID, - zone_id: str, - required_resources: RequiredResources, - i2edge: I2EdgeClient, -) -> tuple[str, str]: - memory_size_str = "{}GB".format(required_resources.memory + 1) - num_gpus = core_utils.get_num_gpus_from(required_resources) - try: - flavour_id = i2edge.create_flavour( - zone_id=zone_id, - memory_size=memory_size_str, - num_cpu=required_resources.numCPU, - num_gpus=num_gpus, - ) - i2edge_instance_id = generate_unique_id() - application_k8s_namespace = generate_namespace_name_from( - str(camara_app_id), str(i2edge_instance_id) - ) - i2edge.deploy_app( - appId=str(camara_app_id), - zoneId=zone_id, - flavourId=flavour_id, - namespace=application_k8s_namespace, - ) - return flavour_id, application_k8s_namespace - except I2EdgeError as e: - err_msg = "Error instantiating app {} in zone {}".format(camara_app_id, zone_id) - log.error("{}. Detailed error: {}".format(err_msg, e)) - raise e - - -def onboard_app_with( - application_id: UUID, - artefact_id: UUID, - app_name: str, - app_version: Optional[str], # TODO pass this to i2edge - repo_type: str, - app_repo: str, - user_name: Optional[str], - password: Optional[str], - token: Optional[str], - i2edge: I2EdgeClient, -): - try: - # TODO Come back to handle errors when onboarding and perform rollbacks - i2edge.create_artefact( - artefact_id=str(artefact_id), - artefact_name=app_name, - repo_name=app_name, - repo_type=repo_type, - repo_url=app_repo, - user_name=user_name, - password=password, - token=token, - ) - - i2edge.onboard_app(app_id=str(application_id), artefact_id=str(application_id)) - except I2EdgeError as e: - err_msg = "Error onboarding app {} in i2edge".format(app_name) - log.error("{}. Detailed error: {}".format(err_msg, e)) - raise e - - -def delete_app_instance_by( - namespace: str, flavour_id: str, zone_id: str, i2edge: I2EdgeClient -): - i2edge_app_instance_name = get_app_name_from(namespace, i2edge) - if i2edge_app_instance_name is None: - err_msg = "Couldn't retrieve app instance from I2Edge." - log.error(err_msg) - raise I2EdgeError(err_msg) - i2edge.undeploy_app(i2edge_app_instance_name) - i2edge.delete_flavour(flavour_id=str(flavour_id), zone_id=zone_id) - - -def get_app_name_from(namespace: str, i2edge: I2EdgeClient) -> Union[str, None]: - try: - response = i2edge.get_all_deployed_apps() - for deployment in response: - if deployment.get("bodytosend", {}).get("namespace") == namespace: - return deployment.get("name") - return None - except I2EdgeError as e: - err_msg = "Error getting app name for namespace {}".format(namespace) - log.error("{}. Detailed error: {}".format(err_msg, e)) - raise e - - -def delete_app_by(app_id: UUID, artefact_id: UUID, i2edge: I2EdgeClient): - try: - i2edge.delete_onboarded_app(app_id=str(app_id)) - i2edge.delete_artefact(artefact_id=str(artefact_id)) - except I2EdgeError as e: - err_msg = "Error deleting app {}".format(app_id) - log.error("{}. Detailed error: {}".format(err_msg, e)) - raise e - - -def get_edgecloud_zones(i2edge: I2EdgeClient) -> list[str]: - try: - zone_ids = [] - response = i2edge.get_zones_list() - for zone in response: - zone_id = zone.get("zoneId") - if zone_id is not None: - zone_ids.append(zone_id) - return zone_ids - - except I2EdgeError as e: - err_msg = "Error getting zones from i2edge" - log.error("{}. Detailed error: {}".format(err_msg, e)) - raise e diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/kubernetes/__init__.py b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/kubernetes/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/kubernetes/client.py b/service-resource-manager-implementation/src/adapters/edgecloud/adapters/kubernetes/client.py deleted file mode 100644 index a5207c4..0000000 --- a/service-resource-manager-implementation/src/adapters/edgecloud/adapters/kubernetes/client.py +++ /dev/null @@ -1,263 +0,0 @@ -# Mocked API for testing purposes -import logging -from typing import Dict, List, Optional - -from kubernetes.client import V1Deployment - -from src.adapters.edgecloud.adapters.kubernetes.lib.core.piedge_encoder import ( - deploy_service_function, -) -from src.adapters.edgecloud.adapters.kubernetes.lib.models.app_manifest import ( - AppManifest, -) -from src.adapters.edgecloud.adapters.kubernetes.lib.models.deploy_service_function import ( - DeployServiceFunction, -) -from src.adapters.edgecloud.adapters.kubernetes.lib.models.service_function_registration_request import ( - ServiceFunctionRegistrationRequest, -) -from src.adapters.edgecloud.adapters.kubernetes.lib.utils.connector_db import ( - ConnectorDB, -) -from src.adapters.edgecloud.adapters.kubernetes.lib.utils.kubernetes_connector import ( - KubernetesConnector, -) -from src.adapters.edgecloud.core.edgecloud_interface import ( - EdgeCloudManagementInterface, -) - - -class EdgeApplicationManager(EdgeCloudManagementInterface): - - def __init__(self, base_url: str, **kwargs): - self.kubernetes_host = base_url - self.edge_cloud_provider = kwargs.get("PLATFORM_PROVIDER") - kubernetes_token = kwargs.get("KUBERNETES_MASTER_TOKEN") - kubernetes_port = kwargs.get("KUBERNETES_MASTER_PORT") - storage_uri = kwargs.get("EMP_STORAGE_URI") - username = kwargs.get("KUBERNETES_USERNAME") - namespace = kwargs.get('K8S_NAMESPACE') - if base_url is not None and base_url != "": - self.k8s_connector = KubernetesConnector( - ip=self.kubernetes_host, - port=kubernetes_port, - token=kubernetes_token, - username=username, - namespace=namespace - ) - if storage_uri is not None: - self.connector_db = ConnectorDB(storage_uri) - - def onboard_app(self, app_manifest: AppManifest) -> Dict: - print(f"Submitting application: {app_manifest}") - logging.info("Extracting variables from payload...") - app_id = app_manifest.get("appId") - app_name = app_manifest.get("name") - image = app_manifest.get("appRepo").get("imagePath") - package_type = app_manifest.get("packageType") - network_interfaces = app_manifest.get("componentSpec")[0].get( - "networkInterfaces" - ) - ports = [] - for ni in network_interfaces: - ports.append(ni.get("port")) - insert_doc = ServiceFunctionRegistrationRequest( - service_function_id=app_id, - service_function_image=image, - service_function_name=app_name, - service_function_type=package_type, - application_ports=ports, - ) - result = self.connector_db.insert_document_service_function( - insert_doc.to_dict() - ) - if type(result) is str: - return result - return {"appId": str(result.inserted_id)} - - def get_all_onboarded_apps(self) -> List[Dict]: - logging.info("Retrieving all registered apps from database...") - db_list = self.connector_db.get_documents_from_collection( - collection_input="service_functions" - ) - app_list = [] - for sf in db_list: - app_list.append(self.__transform_to_camara(sf)) - return app_list - # return [{"appId": "1234-5678", "name": "TestApp"}] - - def get_onboarded_app(self, app_id: str) -> Dict: - logging.info( - "Searching for registered app with ID: " + app_id + " in database..." - ) - app = self.connector_db.get_documents_from_collection( - "service_functions", input_type="_id", input_value=app_id - ) - if len(app) > 0: - return self.__transform_to_camara(app[0]) - else: - return [] - - def delete_onboarded_app(self, app_id: str) -> None: - result, code = self.connector_db.delete_document_service_function(_id=app_id) - print(f"Removing application metadata: {app_id}") - return code - - def deploy_app(self, body: dict) -> Dict: - logging.info( - "Searching for registered app with ID: " - + body.get("appId") - + " in database..." - ) - app = self.connector_db.get_documents_from_collection( - "service_functions", input_type="_id", input_value=body.get("appId") - ) - # success_response = [] - result = None - response = None - if len(app) < 1: - return "Application with ID: " + body.get("appId") + " not found", 404 - if app is not None: - sf = DeployServiceFunction( - service_function_name=app[0].get("name"), - service_function_instance_name=body.get("name"), - # location=body.get('edgeCloudZoneId'), - ) - result = deploy_service_function( - service_function=sf, - connector_db=self.connector_db, - kubernetes_connector=self.k8s_connector, - ) - if type(result) is V1Deployment: - response = {} - response["name"] = body.get("name") - response["appId"] = app[0].get("_id") - response["appInstanceId"] = result.metadata.uid - response["appProvider"] = app[0].get("appProvider") - response["status"] = "unknown" - response["componentEndpointInfo"] = {} - response["kubernetesClusterRef"] = "" - response["edgeCloudZoneId"] = body.get("edgeCloudZoneId") - else: - response = {"Error": result} - return response - - def get_all_deployed_apps( - self, - app_id: Optional[str] = None, - app_instance_id: Optional[str] = None, - region: Optional[str] = None, - ) -> List[Dict]: - logging.info("Retrieving all deployed apps in the edge cloud platform") - deployments = self.k8s_connector.get_deployed_service_functions( - self.connector_db - ) - response = [] - for deployment in deployments: - item = {} - item["name"] = deployment.get("service_function_catalogue_name") - item["appId"] = deployment.get("appId") - item["appProvider"] = deployment.get("appProvider") - item["appInstanceId"] = deployment.get("appInstanceId") - item["status"] = deployment.get("status") - interfaces = [] - for port in deployment.get("ports"): - access_point = {"port": port} - interfaces.append({"interfaceId": "", "accessPoints": access_point}) - item["componentEndpointInfo"] = interfaces - item["kubernetesClusterRef"] = "" - item["edgeCloudZoneId"] = {} - response.append(item) - return response - # return [{"appInstanceId": "abcd-efgh", "status": "ready"}] - - def undeploy_app(self, app_instance_id: str) -> None: - logging.info( - "Searching for deployed app with ID: " + app_instance_id + " in database..." - ) - print(f"Deleting app instance: {app_instance_id}") - sfs = self.k8s_connector.get_deployed_service_functions(self.connector_db) - response = "App instance with ID [" + app_instance_id + "] not found" - for service_fun in sfs: - if service_fun["appInstanceId"] == app_instance_id: - self.k8s_connector.delete_service_function( - self.connector_db, service_fun["service_function_instance_name"] - ) - response = ( - "App instance with ID [" - + app_instance_id - + "] successfully removed" - ) - break - return response - - def get_edge_cloud_zones( - self, region: Optional[str] = None, status: Optional[str] = None - ) -> List[Dict]: - - nodes_response = self.k8s_connector.get_PoPs() - zone_list = [] - - for node in nodes_response: - zone = {} - zone["edgeCloudZoneId"] = node.get("uid") - zone["edgeCloudZoneName"] = node.get("name") - zone["edgeCloudZoneStatus"] = node.get("status") - zone["edgeCloudProvider"] = self.edge_cloud_provider - zone["edgeCloudRegion"] = node.get("location") - zone_list.append(zone) - return zone_list - - def get_edge_cloud_zones_details( - self, zone_id: str, flavour_id: Optional[str] = None - ) -> Dict: - nodes = self.k8s_connector.get_node_details() - node_details = None - for item in nodes.get("items"): - if item.get("metadata").get("uid") == zone_id: - node_details = item - break - labels = node_details.get("metadata").get("labels") - status = node_details.get("status") - arch_type = labels.get("beta.kubernetes.io/arch") - computeResourceQuotaLimits = [ - { - "cpuArchType": arch_type, - "numCPU": status.get("capacity").get("cpu"), - "memory": status.get("capacity").get("memory"), - # "memory": int(status.get("capacity").get("memory")) / (1024 * 1024), - } - ] - reservedComputeResources = [ - { - "cpuArchType": arch_type, - "numCPU": status.get("allocatable").get("cpu"), - "memory": status.get("allocatable").get("memory"), - # "memory": int(status.get("allocatable").get("memory")) / (1024 * 1024), - } - ] - flavoursSupported = [] - node_details["computeResourceQuotaLimits"] = computeResourceQuotaLimits - node_details["reservedComputeResources"] = reservedComputeResources - node_details["flavoursSupported"] = flavoursSupported - node_details["zoneId"] = zone_id - return node_details - - def __transform_to_camara(self, app_data): - app = {} - app["appId"] = app_data.get("_id") - app["name"] = app_data.get("name") - app["packageType"] = app_data.get("type") - appRepo = {"imagePath": app_data.get("image")} - app["appRepo"] = appRepo - networkInterfaces = [] - for port in app_data.get("application_ports"): - port_spec = {"protocol": "TCP", "port": port} - networkInterfaces.append(port_spec) - app["componentSpec"] = [ - { - "componentName": app_data.get("name"), - "networkInterfaces": networkInterfaces, - } - ] - return app diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/core/__init__.py b/service-resource-manager-implementation/src/adapters/edgecloud/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/adapters/edgecloud/core/edgecloud_interface.py b/service-resource-manager-implementation/src/adapters/edgecloud/core/edgecloud_interface.py deleted file mode 100644 index 7dd2c6f..0000000 --- a/service-resource-manager-implementation/src/adapters/edgecloud/core/edgecloud_interface.py +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -## -# Copyright 2025-present by Software Networks Area, i2CAT. -# All rights reserved. -# -# This file is part of the Open SDK -# -# Contributors: -# - Adrián Pino Martínez (adrian.pino@i2cat.net) -## -from abc import ABC, abstractmethod -from typing import Dict, List, Optional - - -class EdgeCloudManagementInterface(ABC): - """ - Abstract Base Class for Edge Application Management. - """ - - @abstractmethod - def onboard_app(self, app_manifest: Dict) -> Dict: - """ - Onboards an app, submitting application metadata - to the Edge Cloud Provider. - - :param app_manifest: Application metadata in dictionary format. - :return: Dictionary containing created application details. - """ - pass - - @abstractmethod - def get_all_onboarded_apps(self) -> List[Dict]: - """ - Retrieves a list of onboarded applications. - - :return: List of application metadata dictionaries. - """ - pass - - @abstractmethod - def get_onboarded_app(self, app_id: str) -> Dict: - """ - Retrieves information of a specific onboarded application. - - :param app_id: Unique identifier of the application. - :return: Dictionary with application details. - """ - pass - - @abstractmethod - def delete_onboarded_app(self, app_id: str) -> None: - """ - Deletes an application onboarded from the Edge Cloud Provider. - - :param app_id: Unique identifier of the application. - """ - pass - - @abstractmethod - def deploy_app(self, app_id: str, app_zones: List[Dict]) -> Dict: - """ - Requests the instantiation of an application instance. - - :param app_id: Unique identifier of the application. - :param app_zones: List of Edge Cloud Zones where the app should be - instantiated. - :return: Dictionary with instance details. - """ - pass - - @abstractmethod - def get_all_deployed_apps( - self, - app_id: Optional[str] = None, - app_instance_id: Optional[str] = None, - region: Optional[str] = None, - ) -> List[Dict]: - """ - Retrieves information of application instances. - - :param app_id: Filter by application ID. - :param app_instance_id: Filter by instance ID. - :param region: Filter by Edge Cloud region. - :return: List of application instance details. - """ - pass - - @abstractmethod - def undeploy_app(self, app_instance_id: str) -> None: - """ - Terminates a specific application instance. - - :param app_instance_id: Unique identifier of the application instance. - """ - pass - - @abstractmethod - def get_edge_cloud_zones( - self, region: Optional[str] = None, status: Optional[str] = None - ) -> List[Dict]: - """ - Retrieves a list of available Edge Cloud Zones. - - :param region: Filter by geographical region. - :param status: Filter by status (active, inactive, unknown). - :return: List of Edge Cloud Zones. - """ - pass - - @abstractmethod - def get_edge_cloud_zones_details( - self, federation_context_id: str, zone_id: str - ) -> Dict: - """ - Retrieves details of a specific Edge Cloud Zone reserved - for the specified zone by the partner OP. - - :param federation_context_id: Identifier of the federation context. - :param zone_id: Unique identifier of the Edge Cloud Zone. - :return: Dictionary with Edge Cloud Zone details. - """ - pass diff --git a/service-resource-manager-implementation/src/adapters/logger.py b/service-resource-manager-implementation/src/adapters/logger.py deleted file mode 100644 index 14c3f6b..0000000 --- a/service-resource-manager-implementation/src/adapters/logger.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -## -# Copyright 2025-present by Software Networks Area, i2CAT. -# All rights reserved. -# -# This file is part of the Open SDK -# -# Contributors: -# - Sergio Giménez (sergio.gimenez@i2cat.net) -## -import logging -import sys -from pathlib import Path - -from colorlog import ColoredFormatter - -APP_LOGGER_NAME = "edgecloud" -COLORED_FORMATERR = ( - "%(log_color)s%(levelname)s%(reset)s | " - "[%(log_color)s%(name)s%(reset)s:%(log_color)s%(lineno)d%(reset)s] " - "%(log_color)s%(message)s%(reset)s" -) -FILE_FORMATTER = "[%(asctime)s] {%(name)s: %(lineno)d} %(levelname)s - %(message)s" - - -def setup_logger(logger_name=APP_LOGGER_NAME, is_debug=True, file_name=None): - logger = logging.getLogger(logger_name) - logger.setLevel(logging.DEBUG if is_debug else logging.INFO) - - colored_formatter = ColoredFormatter(COLORED_FORMATERR) - sh = logging.StreamHandler(sys.stdout) - sh.setFormatter(colored_formatter) - logger.handlers.clear() - logger.addHandler(sh) - - if file_name: - log_path = Path(file_name) - log_path.parent.mkdir(parents=True, exist_ok=True) - fh = logging.FileHandler(file_name) - fh.setFormatter(logging.Formatter(FILE_FORMATTER)) - logger.addHandler(fh) - - return logger - - -def get_logger(module_name): - return logging.getLogger(APP_LOGGER_NAME).getChild(module_name) diff --git a/service-resource-manager-implementation/src/adapters/network/__init__.py b/service-resource-manager-implementation/src/adapters/network/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/adapters/network/adapters/__init__.py b/service-resource-manager-implementation/src/adapters/network/adapters/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/adapters/network/adapters/errors.py b/service-resource-manager-implementation/src/adapters/network/adapters/errors.py deleted file mode 100644 index 6497bd8..0000000 --- a/service-resource-manager-implementation/src/adapters/network/adapters/errors.py +++ /dev/null @@ -1,3 +0,0 @@ -# -*- coding: utf-8 -*- -class NetworkPlatformError(Exception): - pass diff --git a/service-resource-manager-implementation/src/adapters/network/adapters/oai/__init__.py b/service-resource-manager-implementation/src/adapters/network/adapters/oai/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/adapters/network/adapters/oai/client.py b/service-resource-manager-implementation/src/adapters/network/adapters/oai/client.py deleted file mode 100644 index f0f6552..0000000 --- a/service-resource-manager-implementation/src/adapters/network/adapters/oai/client.py +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -## -# Copyright (c) 2025 Netsoft Group, EURECOM. -# All rights reserved. -# -# This file is part of the Open SDK -# -# Contributors: -# - Giulio Carota (giulio.carota@eurecom.fr) -## -from src.adapters import logger -from src.adapters.network.core.base_network_client import BaseNetworkClient -from src.adapters.network.core.schemas import ( - AsSessionWithQoSSubscription, - CreateSession, - CreateTrafficInfluence, - FlowInfo, - MonitoringEventSubscriptionRequest, - RetrievalLocationRequest, - Snssai, - TrafficInfluSub, -) - -log = logger.get_logger(__name__) -supportedQos = ["qos-e", "qos-s", "qos-m", "qos-l"] - - -class NetworkManager(BaseNetworkClient): - def __init__(self, base_url: str, scs_as_id: str = None): - """ - Initialize Network Client for OAI Core Network - The currently supported features are: - - QoD - - Traffic Influence - """ - try: - super().__init__() - self.base_url = base_url - self.scs_as_id = scs_as_id - log.info( - f"Initialized OaiNefClient with base_url: {self.base_url} and scs_as_id: {self.scs_as_id}" - ) - - except Exception as e: - log.error(f"Failed to initialize OaiNefClient: {e}") - raise e - - def core_specific_qod_validation(self, session_info: CreateSession): - """ - Validates core-specific parameters for the session creation. - - args: - session_info: The session information to validate. - - raises: - ValidationError: If the session information does not meet core-specific requirements. - """ - if session_info.qosProfile.root not in supportedQos: - raise OaiValidationError( - f"QoS profile {session_info.qosProfile} not supported by OAI, supported profiles are {supportedQos}" - ) - - if session_info.device is None or session_info.device.ipv4Address is None: - raise OaiValidationError("OAI requires UE IPv4 Address to activate QoS") - - if session_info.applicationServer.ipv4Address is None: - raise OaiValidationError("OAI requires App IPv4 Address to activate QoS") - return - - def add_core_specific_qod_parameters( - self, - session_info: CreateSession, - subscription: AsSessionWithQoSSubscription, - ) -> None: - device_ip = _retrieve_ue_ipv4(session_info) - server_ip = _retrieve_app_ipv4(session_info) - - # build flow descriptor in oai format using device ip and server ip - flow_descriptor = f"permit out ip from {device_ip}/32 to {server_ip}/32" - _add_qod_flow_descriptor(subscription, flow_descriptor) - _add_qod_snssai(subscription, 1, "FFFFFF") - subscription.dnn = "oai" - - def add_core_specific_ti_parameters( - self, - traffic_influence_info: CreateTrafficInfluence, - subscription: TrafficInfluSub, - ): - # todo oai add dnn, ssnai, afServiceId - subscription.dnn = "oai" - subscription.add_snssai(1, "FFFFFF") - subscription.afServiceId = self.scs_as_id - - def core_specific_traffic_influence_validation( - self, traffic_influence_info: CreateTrafficInfluence - ) -> None: - """ - Validates core-specific parameters for the session creation. - - args: - session_info: The session information to validate. - - raises: - ValidationError: If the session information does not meet core-specific requirements. - """ - # Placeholder for core-specific validation logic - # This method should be overridden by subclasses if needed - - if ( - traffic_influence_info.device is None - or traffic_influence_info.device.ipv4Address is None - ): - raise OaiValidationError( - "OAI requires UE IPv4 Address to activate Traffic Influence" - ) - - def core_specific_monitoring_event_validation( - self, retrieve_location_request: RetrievalLocationRequest - ) -> None: - raise NotImplementedError( - "core_specific_monitoring_event_validation not implemented for OAI" - ) - - def add_core_specific_location_parameters( - self, retrieve_location_request: RetrievalLocationRequest - ) -> MonitoringEventSubscriptionRequest: - raise NotImplementedError( - "add_core_specific_location_parameters not implemented for OAI" - ) - - -def _retrieve_ue_ipv4(session_info: CreateSession): - return session_info.device.ipv4Address.root.privateAddress - - -def _retrieve_app_ipv4(session_info: CreateSession): - return session_info.applicationServer.ipv4Address - - -def _add_qod_flow_descriptor( - qos_sub: AsSessionWithQoSSubscription, flow_desriptor: str -): - qos_sub.flowInfo = list() - qos_sub.flowInfo.append( - FlowInfo(flowId=len(qos_sub.flowInfo) + 1, flowDescriptions=[flow_desriptor]) - ) - - -def _add_qod_snssai(qos_sub: AsSessionWithQoSSubscription, sst: int, sd: str = None): - qos_sub.snssai = Snssai(sst=sst, sd=sd) - - -class OaiValidationError(Exception): - pass diff --git a/service-resource-manager-implementation/src/adapters/network/adapters/open5gcore/__init__.py b/service-resource-manager-implementation/src/adapters/network/adapters/open5gcore/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/adapters/network/adapters/open5gcore/client.py b/service-resource-manager-implementation/src/adapters/network/adapters/open5gcore/client.py deleted file mode 100644 index 3069c91..0000000 --- a/service-resource-manager-implementation/src/adapters/network/adapters/open5gcore/client.py +++ /dev/null @@ -1,79 +0,0 @@ -# -*- coding: utf-8 -*- -from pydantic import ValidationError - -from src.adapters import logger -from src.adapters.network.core.base_network_client import ( - BaseNetworkClient, - build_flows, -) - -from ...core import schemas - -log = logger.get_logger("Open5GCore") # Usage of brand name - -qos_support_map = { - "qos-e": 1, # ToDo - "qos-s": 5, - "qos-m": 9, - "qos-l": 9, # ToDo not yet available in Nokia RAN -} - - -class NetworkManager(BaseNetworkClient): - def __init__(self, base_url: str, scs_as_id: str): - if not base_url: - raise ValueError("base_url is required and cannot be empty.") - if not scs_as_id: - raise ValueError("scs_as_id is required and cannot be empty.") - - self.base_url = base_url - self.scs_as_id = scs_as_id - - def core_specific_qod_validation(self, session_info: schemas.CreateSession): - qos_key = session_info.qosProfile.root.strip().lower() - - if qos_key not in qos_support_map: - supported = ", ".join(qos_support_map.keys()) - raise ValidationError( - f"Unsupported QoS profile '{session_info.qosProfile.root}'. " - f"Supported profiles for Open5GCore are: {supported}" - ) - - def add_core_specific_qod_parameters( - self, - session_info: schemas.CreateSession, - subscription: schemas.AsSessionWithQoSSubscription, - ) -> None: - flow_id = qos_support_map[session_info.qosProfile.root] - subscription.flowInfo = build_flows(flow_id, session_info) - subscription.ueIpv4Addr = "192.168.6.1" # ToDo - - def add_core_specific_ti_parameters( - self, - traffic_influence_info: schemas.CreateTrafficInfluence, - subscription: schemas.TrafficInfluSub, - ): - raise NotImplementedError( - "add_core_specific_ti_parameters not implemented for Open5GCore" - ) - - def core_specific_traffic_influence_validation( - self, traffic_influence_info: schemas.CreateTrafficInfluence - ) -> None: - raise NotImplementedError( - "core_specific_traffic_influence_validation not implemented for Open5GCore" - ) - - def core_specific_monitoring_event_validation( - self, retrieve_location_request: schemas.RetrievalLocationRequest - ) -> None: - raise NotImplementedError( - "core_specific_monitoring_event_validation not implemented for Open5GCore" - ) - - def add_core_specific_location_parameters( - self, retrieve_location_request: schemas.RetrievalLocationRequest - ) -> schemas.MonitoringEventSubscriptionRequest: - raise NotImplementedError( - "add_core_specific_location_parameters not implemented for Open5GCore" - ) diff --git a/service-resource-manager-implementation/src/adapters/network/adapters/open5gs/__init__.py b/service-resource-manager-implementation/src/adapters/network/adapters/open5gs/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/adapters/network/adapters/open5gs/client.py b/service-resource-manager-implementation/src/adapters/network/adapters/open5gs/client.py deleted file mode 100644 index 035822f..0000000 --- a/service-resource-manager-implementation/src/adapters/network/adapters/open5gs/client.py +++ /dev/null @@ -1,96 +0,0 @@ -# -*- coding: utf-8 -*- - -# Contributors: -# - Ferran Cañellas (ferran.canellas@i2cat.net) -# - Panagiotis Pavlidis (p.pavlidis@iit.demokritos.gr) -## -from pydantic import ValidationError - -from src.adapters import logger -from src.adapters.network.core.base_network_client import ( - BaseNetworkClient, - build_flows, -) - -from ...core import schemas - -log = logger.get_logger(__name__) - -flow_id_mapping = {"qos-e": 3, "qos-s": 4, "qos-m": 5, "qos-l": 6} - - -class NetworkManager(BaseNetworkClient): - """ - This client implements the BaseNetworkClient and translates the - CAMARA APIs into specific HTTP requests understandable by the Open5GS NEF API. - - Invloved partners and their roles in this implementation: - - I2CAT: Responsible for the CAMARA QoD API and its mapping to the - 3GPP AsSessionWithQoS API exposed by Open5GS NEF. - - NCSRD: Responsible for the CAMARA Location API and its mapping to the - 3GPP Monitoring Event API exposed Open5GS NEF. - """ - - def __init__(self, base_url: str, scs_as_id): - """ - Initializes the Open5GS Client. - """ - try: - self.base_url = base_url - self.scs_as_id = scs_as_id - log.info( - f"Initialized Open5GSClient with base_url: {self.base_url} " - f"and scs_as_id: {self.scs_as_id}" - ) - except Exception as e: - log.error(f"Failed to initialize Open5GSClient: {e}") - raise e - - def core_specific_qod_validation(self, session_info: schemas.CreateSession): - if session_info.qosProfile.root not in flow_id_mapping.keys(): - raise ValidationError( - f"Open5Gs only supports these qos-profiles: {', '.join(flow_id_mapping.keys())}" - ) - - def add_core_specific_qod_parameters( - self, - session_info: schemas.CreateSession, - subscription: schemas.AsSessionWithQoSSubscription, - ) -> None: - subscription.supportedFeatures = schemas.SupportedFeatures("003C") - flow_id = flow_id_mapping[session_info.qosProfile.root] - subscription.flowInfo = build_flows(flow_id, session_info) - - def core_specific_monitoring_event_validation( - self, retrieve_location_request: schemas.RetrievalLocationRequest - ) -> None: - """Check core specific elements that required for location retrieval in NEF.""" - if retrieve_location_request.device is None: - raise ValidationError( - "Open5GS requires a device to be specified for location retrieval in NEF." - ) - - def add_core_specific_location_parameters( - self, retrieve_location_request: schemas.RetrievalLocationRequest - ) -> schemas.MonitoringEventSubscriptionRequest: - """Add core specific location parameters to support location retrieval scenario in NEF.""" - return schemas.MonitoringEventSubscriptionRequest( - msisdn=retrieve_location_request.device.phoneNumber.root.lstrip("+"), - notificationDestination="http://127.0.0.1:8001", - monitoringType=schemas.MonitoringType.LOCATION_REPORTING, - locationType=schemas.LocationType.LAST_KNOWN, - ) - # subscription.msisdn = retrieve_location_request.device.phoneNumber.root.lstrip('+') - # monitoringType = schemas.MonitoringType.LOCATION_REPORTING - # locationType = schemas.LocationType.LAST_KNOWN - # locationType = schemas.LocationType.CURRENT_LOCATION - # maximumNumberOfReports = 1 - # repPeriod = schemas.DurationSec(root=20) - - -# Note: -# As this class is inheriting from BaseNetworkClient, it is -# expected to implement all the abstract methods defined in that interface. -# -# In case this network adapter doesn't support a specific method, it should -# be marked as NotImplementedError. diff --git a/service-resource-manager-implementation/src/adapters/network/clients/__init__.py b/service-resource-manager-implementation/src/adapters/network/clients/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/adapters/network/clients/errors.py b/service-resource-manager-implementation/src/adapters/network/clients/errors.py deleted file mode 100644 index 6497bd8..0000000 --- a/service-resource-manager-implementation/src/adapters/network/clients/errors.py +++ /dev/null @@ -1,3 +0,0 @@ -# -*- coding: utf-8 -*- -class NetworkPlatformError(Exception): - pass diff --git a/service-resource-manager-implementation/src/adapters/network/clients/oai/__init__.py b/service-resource-manager-implementation/src/adapters/network/clients/oai/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/adapters/network/clients/oai/client.py b/service-resource-manager-implementation/src/adapters/network/clients/oai/client.py deleted file mode 100644 index cd4791c..0000000 --- a/service-resource-manager-implementation/src/adapters/network/clients/oai/client.py +++ /dev/null @@ -1,139 +0,0 @@ -## -# Copyright (c) 2025 Netsoft Group, EURECOM. -# All rights reserved. -# -# This file is part of the Open SDK -# -# Contributors: -# - Giulio Carota (giulio.carota@eurecom.fr) -## - - -from src.clients import logger -from src.clients.network.core.network_interface import NetworkManagementInterface -from src.clients.network.core.schemas import ( - AsSessionWithQoSSubscription, - CreateSession, - CreateTrafficInfluence, - FlowInfo, - Snssai, - TrafficInfluSub, -) - -log = logger.get_logger(__name__) -supportedQos = ["qos-e", "qos-s", "qos-m", "qos-l"] - - -class NetworkManager(NetworkManagementInterface): - def __init__(self, base_url: str, scs_as_id: str = None): - """ - Initialize Network Client for OAI Core Network - The currently supported features are: - - QoD - - Traffic Influence - """ - try: - super().__init__() - self.base_url = base_url - self.scs_as_id = scs_as_id - log.info( - f"Initialized OaiNefClient with base_url: {self.base_url} and scs_as_id: {self.scs_as_id}" - ) - - except Exception as e: - log.error(f"Failed to initialize OaiNefClient: {e}") - raise e - - def core_specific_qod_validation(self, session_info: CreateSession): - """ - Validates core-specific parameters for the session creation. - - args: - session_info: The session information to validate. - - raises: - ValidationError: If the session information does not meet core-specific requirements. - """ - if session_info.qosProfile.root not in supportedQos: - raise OaiValidationError( - f"QoS profile {session_info.qosProfile} not supported by OAI, supported profiles are {supportedQos}" - ) - - if session_info.device is None or session_info.device.ipv4Address is None: - raise OaiValidationError("OAI requires UE IPv4 Address to activate QoS") - - if session_info.applicationServer.ipv4Address is None: - raise OaiValidationError("OAI requires App IPv4 Address to activate QoS") - return - - def add_core_specific_qod_parameters( - self, - session_info: CreateSession, - subscription: AsSessionWithQoSSubscription, - ) -> None: - device_ip = _retrieve_ue_ipv4(session_info) - server_ip = _retrieve_app_ipv4(session_info) - - # build flow descriptor in oai format using device ip and server ip - flow_descriptor = f"permit out ip from {device_ip}/32 to {server_ip}/32" - _add_qod_flow_descriptor(subscription, flow_descriptor) - _add_qod_snssai(subscription, 1, "FFFFFF") - subscription.dnn = "oai" - - def add_core_specific_ti_parameters( - self, - traffic_influence_info: CreateTrafficInfluence, - subscription: TrafficInfluSub, - ): - # todo oai add dnn, ssnai, afServiceId - subscription.dnn = "oai" - subscription.add_snssai(1, "FFFFFF") - subscription.afServiceId = self.scs_as_id - - def core_specific_traffic_influence_validation( - self, traffic_influence_info: CreateTrafficInfluence - ) -> None: - """ - Validates core-specific parameters for the session creation. - - args: - session_info: The session information to validate. - - raises: - ValidationError: If the session information does not meet core-specific requirements. - """ - # Placeholder for core-specific validation logic - # This method should be overridden by subclasses if needed - - if ( - traffic_influence_info.device is None - or traffic_influence_info.device.ipv4Address is None - ): - raise OaiValidationError( - "OAI requires UE IPv4 Address to activate Traffic Influence" - ) - - -def _retrieve_ue_ipv4(session_info: CreateSession): - return session_info.device.ipv4Address.root.privateAddress - - -def _retrieve_app_ipv4(session_info: CreateSession): - return session_info.applicationServer.ipv4Address - - -def _add_qod_flow_descriptor( - qos_sub: AsSessionWithQoSSubscription, flow_desriptor: str -): - qos_sub.flowInfo = list() - qos_sub.flowInfo.append( - FlowInfo(flowId=len(qos_sub.flowInfo) + 1, flowDescriptions=[flow_desriptor]) - ) - - -def _add_qod_snssai(qos_sub: AsSessionWithQoSSubscription, sst: int, sd: str = None): - qos_sub.snssai = Snssai(sst=sst, sd=sd) - - -class OaiValidationError(Exception): - pass diff --git a/service-resource-manager-implementation/src/adapters/network/clients/open5gcore/__init__.py b/service-resource-manager-implementation/src/adapters/network/clients/open5gcore/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/adapters/network/clients/open5gcore/client.py b/service-resource-manager-implementation/src/adapters/network/clients/open5gcore/client.py deleted file mode 100644 index ef88210..0000000 --- a/service-resource-manager-implementation/src/adapters/network/clients/open5gcore/client.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- coding: utf-8 -*- -from pydantic import ValidationError - -from src.adapters import logger -from src.adapters.network.core.network_interface import ( - NetworkManagementInterface, - build_flows, -) - -from ...core import schemas - -log = logger.get_logger("Open5GCore") # Usage of brand name - -qos_support_map = { - "qos-e": 1, # ToDo - "qos-s": 5, - "qos-m": 9, - "qos-l": 9, # ToDo not yet available in Nokia RAN -} - - -class NetworkManager(NetworkManagementInterface): - def __init__(self, base_url: str, scs_as_id: str): - if not base_url: - raise ValueError("base_url is required and cannot be empty.") - if not scs_as_id: - raise ValueError("scs_as_id is required and cannot be empty.") - - self.base_url = base_url - self.scs_as_id = scs_as_id - - def core_specific_qod_validation(self, session_info: schemas.CreateSession): - qos_key = session_info.qosProfile.root.strip().lower() - - if qos_key not in qos_support_map: - supported = ", ".join(qos_support_map.keys()) - raise ValidationError( - f"Unsupported QoS profile '{session_info.qosProfile.root}'. " - f"Supported profiles for Open5GCore are: {supported}" - ) - - def add_core_specific_qod_parameters( - self, - session_info: schemas.CreateSession, - subscription: schemas.AsSessionWithQoSSubscription, - ) -> None: - flow_id = qos_support_map[session_info.qosProfile.root] - subscription.flowInfo = build_flows(flow_id, session_info) - subscription.ueIpv4Addr = "192.168.6.1" # ToDo diff --git a/service-resource-manager-implementation/src/adapters/network/clients/open5gs/__init__.py b/service-resource-manager-implementation/src/adapters/network/clients/open5gs/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/adapters/network/clients/open5gs/client.py b/service-resource-manager-implementation/src/adapters/network/clients/open5gs/client.py deleted file mode 100644 index ecdb743..0000000 --- a/service-resource-manager-implementation/src/adapters/network/clients/open5gs/client.py +++ /dev/null @@ -1,65 +0,0 @@ -# -*- coding: utf-8 -*- -from pydantic import ValidationError - -from src.clients import logger -from src.clients.network.core.network_interface import ( - NetworkManagementInterface, - build_flows, -) - -from ...core import schemas - -log = logger.get_logger(__name__) - -flow_id_mapping = {"qos-e": 3, "qos-s": 4, "qos-m": 5, "qos-l": 6} - - -class NetworkManager(NetworkManagementInterface): - """ - This client implements the NetworkManagementInterface and translates the - CAMARA APIs into specific HTTP requests understandable by the Open5GS NEF API. - - Invloved partners and their roles in this implementation: - - I2CAT: Responsible for the CAMARA QoD API and its mapping to the - 3GPP AsSessionWithQoS API exposed by Open5GS NEF. - - NCSRD: Responsible for the CAMARA Location API and its mapping to the - 3GPP Monitoring Event API exposed Open5GS NEF. - """ - - def __init__(self, base_url: str, scs_as_id): - """ - Initializes the Open5GS Client. - """ - try: - self.base_url = base_url - self.scs_as_id = scs_as_id - log.info( - f"Initialized Open5GSClient with base_url: {self.base_url} " - f"and scs_as_id: {self.scs_as_id}" - ) - except Exception as e: - log.error(f"Failed to initialize Open5GSClient: {e}") - raise e - - def core_specific_qod_validation(self, session_info: schemas.CreateSession): - if session_info.qosProfile.root not in flow_id_mapping.keys(): - raise ValidationError( - f"Open5Gs only supports these qos-profiles: {', '.join(flow_id_mapping.keys())}" - ) - - def add_core_specific_qod_parameters( - self, - session_info: schemas.CreateSession, - subscription: schemas.AsSessionWithQoSSubscription, - ) -> None: - subscription.supportedFeatures = schemas.SupportedFeatures("003C") - flow_id = flow_id_mapping[session_info.qosProfile.root] - subscription.flowInfo = build_flows(flow_id, session_info) - - -# Note: -# As this class is inheriting from NetworkManagementInterface, it is -# expected to implement all the abstract methods defined in that interface. -# -# In case this network adapter doesn't support a specific method, it should -# be marked as NotImplementedError. diff --git a/service-resource-manager-implementation/src/adapters/network/core/__init__.py b/service-resource-manager-implementation/src/adapters/network/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/adapters/network/core/base_network_client.py b/service-resource-manager-implementation/src/adapters/network/core/base_network_client.py deleted file mode 100644 index 321718a..0000000 --- a/service-resource-manager-implementation/src/adapters/network/core/base_network_client.py +++ /dev/null @@ -1,469 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -## -# This file is part of the Open SDK -# -# Contributors: -# - Reza Mosahebfard (reza.mosahebfard@i2cat.net) -# - Ferran Cañellas (ferran.canellas@i2cat.net) -# - Giulio Carota (giulio.carota@eurecom.fr) -# - Panagiotis Pavlidis (p.pavlidis@iit.demokritos.gr) -## -import uuid -from datetime import datetime, timedelta, timezone -from itertools import product -from typing import Dict - -from src.adapters import logger -from src.adapters.network.adapters.errors import NetworkPlatformError -from src.adapters.network.core import common, schemas - -log = logger.get_logger(__name__) - - -def flatten_port_spec(ports_spec: schemas.PortsSpec | None) -> list[str]: - has_ports = False - has_ranges = False - flat_ports = [] - if ports_spec and ports_spec.ports: - has_ports = True - flat_ports.extend([str(port) for port in ports_spec.ports]) - if ports_spec and ports_spec.ranges: - has_ranges = True - flat_ports.extend( - [f"{range.from_.root}-{range.to.root}" for range in ports_spec.ranges] - ) - if not has_ports and not has_ranges: - flat_ports.append("0-65535") - return flat_ports - - -def build_flows( - flow_id: int, - session_info: schemas.CreateSession, -) -> list[schemas.FlowInfo]: - device_ports = flatten_port_spec(session_info.devicePorts) - server_ports = flatten_port_spec(session_info.applicationServerPorts) - ports_combis = list(product(device_ports, server_ports)) - - device_ip = session_info.device.ipv4Address or session_info.device.ipv6Address - if isinstance(device_ip, schemas.DeviceIpv6Address): - device_ip = device_ip.root - else: # IPv4 - device_ip = ( - device_ip.root.publicAddress.root or device_ip.root.privateAddress.root - ) - device_ip = str(device_ip) - server_ip = ( - session_info.applicationServer.ipv4Address - or session_info.applicationServer.ipv6Address - ) - server_ip = server_ip.root - flow_descrs = [] - for device_port, server_port in ports_combis: - flow_descrs.append( - f"permit in ip from {device_ip} {device_port} to {server_ip} {server_port}" - ) - flow_descrs.append( - f"permit out ip from {server_ip} {server_port} to {device_ip} {device_port}" - ) - flows = [ - schemas.FlowInfo(flowId=flow_id, flowDescriptions=[", ".join(flow_descrs)]) - ] - return flows - - -class BaseNetworkClient: - """ - Class for Network Resource Management. - - This class provides shared logic and extension points for different - Network 5G Cores (e.g., Open5GS, OAI, Open5GCore) interacting with - NEF-like platforms using CAMARA APIs. - """ - - base_url: str - scs_as_id: str - - def add_core_specific_qod_parameters( - self, - session_info: schemas.CreateSession, - subscription: schemas.AsSessionWithQoSSubscription, - ): - """ - Placeholder for adding core-specific parameters to the subscription. - This method should be overridden by subclasses to implement specific logic. - """ - pass - - def add_core_specific_ti_parameters( - self, - traffic_influence_info: schemas.CreateTrafficInfluence, - subscription: schemas.TrafficInfluSub, - ): - """ - Placeholder for adding core-specific parameters to the subscription. - This method should be overridden by subclasses to implement specific logic. - """ - pass - - def add_core_specific_location_parameters( - self, retrieve_location_request: schemas.RetrievalLocationRequest - ) -> schemas.MonitoringEventSubscriptionRequest: - """ - Placeholder for adding core-specific parameters to the location subscription. - This method should be overridden by subclasses to implement specific logic. - """ - pass - - def core_specific_qod_validation(self, session_info: schemas.CreateSession) -> None: - """ - Validates core-specific parameters for the session creation. - - args: - session_info: The session information to validate. - - raises: - ValidationError: If the session information does not meet core-specific requirements. - """ - # Placeholder for core-specific validation logic - # This method should be overridden by subclasses if needed - pass - - def core_specific_traffic_influence_validation( - self, traffic_influence_info: schemas.CreateTrafficInfluence - ) -> None: - """ - Validates core-specific parameters for the session creation. - - args: - session_info: The session information to validate. - - raises: - ValidationError: If the session information does not meet core-specific requirements. - """ - # Placeholder for core-specific validation logic - # This method should be overridden by subclasses if needed - pass - - def core_specific_monitoring_event_validation( - self, retrieve_location_request: schemas.RetrievalLocationRequest - ) -> None: - """ - Validates core-specific parameters for the monitoring event subscription. - - args: - retrieve_location_request: The request information to validate. - - raises: - ValidationError: If the request information does not meet core-specific requirements. - """ - # Placeholder for core-specific validation logic - # This method should be overwritten by subclasses if needed - pass - - def _build_qod_subscription( - self, session_info: Dict - ) -> schemas.AsSessionWithQoSSubscription: - valid_session_info = schemas.CreateSession.model_validate(session_info) - device_ipv4 = None - if valid_session_info.device.ipv4Address: - device_ipv4 = valid_session_info.device.ipv4Address.root.publicAddress.root - - self.core_specific_qod_validation(valid_session_info) - subscription = schemas.AsSessionWithQoSSubscription( - notificationDestination=str(valid_session_info.sink), - qosReference=valid_session_info.qosProfile.root, - ueIpv4Addr=device_ipv4, - ueIpv6Addr=valid_session_info.device.ipv6Address, - usageThreshold=schemas.UsageThreshold(duration=valid_session_info.duration), - ) - self.add_core_specific_qod_parameters(valid_session_info, subscription) - return subscription - - def _build_ti_subscription(self, traffic_influence_info: Dict): - traffic_influence_data = schemas.CreateTrafficInfluence.model_validate( - traffic_influence_info - ) - self.core_specific_traffic_influence_validation(traffic_influence_data) - - device_ip = traffic_influence_data.retrieve_ue_ipv4() - server_ip = ( - traffic_influence_data.appInstanceId - ) # assume that the instance id corresponds to its IPv4 address - sink_url = traffic_influence_data.notificationUri - edge_zone = traffic_influence_data.edgeCloudZoneId - - # build flow descriptor in oai format using device ip and server ip - flow_descriptor = f"permit out ip from {device_ip}/32 to {server_ip}/32" - - subscription = schemas.TrafficInfluSub( - afAppId=traffic_influence_data.appId, - ipv4Addr=str(device_ip), - notificationDestination=sink_url, - ) - subscription.add_flow_descriptor(flow_descriptor=flow_descriptor) - subscription.add_traffic_route(dnai=edge_zone) - - self.add_core_specific_ti_parameters(traffic_influence_data, subscription) - return subscription - - def _build_camara_ti(self, trafficInflSub: Dict): - traffic_influence_data = schemas.TrafficInfluSub.model_validate(trafficInflSub) - - flowDesc = traffic_influence_data.trafficFilters[0].flowDescriptions[0] - serverIp = flowDesc.split("to ")[1].split("/32")[0] - edgeId = traffic_influence_data.trafficRoutes[0].dnai - - camara_ti = schemas.CreateTrafficInfluence( - appId=traffic_influence_data.afAppId, - appInstanceId=serverIp, - edgeCloudZoneId=edgeId, - notificationUri=traffic_influence_data.notificationDestination, - device=schemas.Device( - ipv4Address=schemas.DeviceIpv4Addr1( - publicAddress=traffic_influence_data.ipv4Addr, - privateAddress=traffic_influence_data.ipv4Addr, - ) - ), - ) - return camara_ti - - def _build_monitoring_event_subscription( - self, retrieve_location_request: schemas.RetrievalLocationRequest - ) -> schemas.MonitoringEventSubscriptionRequest: - self.core_specific_monitoring_event_validation(retrieve_location_request) - subscription_3gpp = self.add_core_specific_location_parameters( - retrieve_location_request - ) - device = retrieve_location_request.device - subscription_3gpp.externalId = device.networkAccessIdentifier - subscription_3gpp.ipv4Addr = device.ipv4Address - subscription_3gpp.ipv6Addr = device.ipv6Address - # subscription.msisdn = device.phoneNumber.root.lstrip('+') - # subscription.notificationDestination = "http://127.0.0.1:8001" - - return subscription_3gpp - - def _compute_camara_last_location_time( - self, event_time: datetime, age_of_location_info_min: int = None - ) -> datetime: - """ - Computes the last location time based on the event time and age of location info. - - args: - event_time: ISO 8601 datetime, e.g. "2025-06-18T12:30:00Z" - age_of_location_info_min: unsigned int, age of location info in minutes - - returns: - datetime object representing the last location time in UTC. - """ - if age_of_location_info_min is not None: - last_location_time = event_time - timedelta( - minutes=age_of_location_info_min - ) - return last_location_time.replace(tzinfo=timezone.utc) - else: - return event_time.replace(tzinfo=timezone.utc) - - def create_monitoring_event_subscription( - self, retrieve_location_request: schemas.RetrievalLocationRequest - ) -> schemas.Location: - """ - Creates a Monitoring Event subscription based on CAMARA Location API input. - - args: - retrieve_location_request: Dictionary containing location retrieval details conforming to - the CAMARA Location API parameters. - - returns: - dictionary containing the created subscription details, including its ID. - """ - subscription = self._build_monitoring_event_subscription( - retrieve_location_request - ) - response = common.monitoring_event_post( - self.base_url, self.scs_as_id, subscription - ) - - monitoring_event_report = schemas.MonitoringEventReport(**response) - if monitoring_event_report.locationInfo is None: - log.error( - "Failed to retrieve location information from monitoring event report" - ) - raise NetworkPlatformError( - "Location information not found in monitoring event report" - ) - geo_area = monitoring_event_report.locationInfo.geographicArea - report_event_time = monitoring_event_report.eventTime - age_of_location_info = None - if monitoring_event_report.locationInfo.ageOfLocationInfo is not None: - age_of_location_info = ( - monitoring_event_report.locationInfo.ageOfLocationInfo.duration - ) - last_location_time = self._compute_camara_last_location_time( - report_event_time, age_of_location_info - ) - log.debug(f"Last Location time is {last_location_time}") - camara_point_list: list[schemas.Point] = [] - for point in geo_area.polygon.point_list.geographical_coords: - camara_point_list.append( - schemas.Point(latitude=point.lat, longitude=point.lon) - ) - camara_polygon = schemas.Polygon( - areaType=schemas.AreaType.polygon, - boundary=schemas.PointList(camara_point_list), - ) - - camara_location = schemas.Location( - area=camara_polygon, lastLocationTime=last_location_time - ) - - return camara_location - - def create_qod_session(self, session_info: Dict) -> Dict: - """ - Creates a QoS session based on CAMARA QoD API input. - - args: - session_info: Dictionary containing session details conforming to - the CAMARA QoD session creation parameters. - - returns: - dictionary containing the created session details, including its ID. - """ - subscription = self._build_qod_subscription(session_info) - response = common.as_session_with_qos_post( - self.base_url, self.scs_as_id, subscription - ) - subscription_info: schemas.AsSessionWithQoSSubscription = ( - schemas.AsSessionWithQoSSubscription(**response) - ) - - session_info = schemas.SessionInfo( - sessionId=schemas.SessionId(uuid.UUID(subscription_info.subscription_id)), - qosStatus=schemas.QosStatus.REQUESTED, - **session_info, - ) - return session_info.model_dump() - - def get_qod_session(self, session_id: str) -> Dict: - """ - Retrieves details of a specific Quality on Demand (QoS) session. - - args: - session_id: The unique identifier of the QoS session. - - returns: - Dictionary containing the details of the requested QoS session. - """ - response = common.as_session_with_qos_get( - self.base_url, self.scs_as_id, session_id=session_id - ) - subscription_info = schemas.AsSessionWithQoSSubscription(**response) - flowDesc = subscription_info.flowInfo[0].flowDescriptions[0] - serverIp = flowDesc.split("to ")[1].split("/")[0] - session_info = schemas.SessionInfo( - sessionId=schemas.SessionId(uuid.UUID(subscription_info.subscription_id)), - duration=subscription_info.usageThreshold.duration, - sink=subscription_info.notificationDestination, - qosProfile=subscription_info.qosReference, - device=schemas.Device( - ipv4Address=schemas.DeviceIpv4Addr1( - publicAddress=subscription_info.ueIpv4Addr, - privateAddress=subscription_info.ueIpv4Addr, - ), - ), - applicationServer=schemas.ApplicationServer( - ipv4Address=schemas.ApplicationServerIpv4Address(serverIp) - ), - ) - return session_info.model_dump() - - def delete_qod_session(self, session_id: str) -> None: - """ - Deletes a specific Quality on Demand (QoS) session. - - args: - session_id: The unique identifier of the QoS session to delete. - - returns: - None - """ - common.as_session_with_qos_delete( - self.base_url, self.scs_as_id, session_id=session_id - ) - log.info(f"QoD session deleted successfully [id={session_id}]") - - def create_traffic_influence_resource(self, traffic_influence_info: Dict) -> Dict: - """ - Creates a Traffic Influence resource based on CAMARA TI API input. - - args: - traffic_influence_info: Dictionary containing traffic influence details conforming to - the CAMARA TI resource creation parameters. - - returns: - dictionary containing the created traffic influence resource details, including its ID. - """ - - subscription = self._build_ti_subscription(traffic_influence_info) - response = common.traffic_influence_post( - self.base_url, self.scs_as_id, subscription - ) - - # retrieve the NEF resource id - if "self" in response.keys(): - subscription_id = response["self"] - else: - subscription_id = None - - traffic_influence_info["trafficInfluenceID"] = subscription_id - return traffic_influence_info - - def put_traffic_influence_resource( - self, resource_id: str, traffic_influence_info: Dict - ) -> Dict: - """ - Retrieves details of a specific Traffic Influence resource. - - args: - resource_id: The unique identifier of the Traffic Influence resource. - - returns: - Dictionary containing the details of the requested Traffic Influence resource. - """ - subscription = self._build_ti_subscription(traffic_influence_info) - common.traffic_influence_put( - self.base_url, self.scs_as_id, resource_id, subscription - ) - - traffic_influence_info["trafficInfluenceID"] = resource_id - return traffic_influence_info - - def delete_traffic_influence_resource(self, resource_id: str) -> None: - """ - Deletes a specific Traffic Influence resource. - - args: - resource_id: The unique identifier of the Traffic Influence resource to delete. - - returns: - None - """ - common.traffic_influence_delete(self.base_url, self.scs_as_id, resource_id) - return - - def get_individual_traffic_influence_resource(self, resource_id: str) -> Dict: - nef_response = common.traffic_influence_get( - self.base_url, self.scs_as_id, resource_id - ) - camara_ti = self._build_camara_ti(nef_response) - return camara_ti - - def get_all_traffic_influence_resource(self) -> list[Dict]: - r = common.traffic_influence_get(self.base_url, self.scs_as_id) - return [self._build_camara_ti(item) for item in r] - - -# Placeholder for other CAMARA APIs diff --git a/service-resource-manager-implementation/src/adapters/network/core/common.py b/service-resource-manager-implementation/src/adapters/network/core/common.py deleted file mode 100644 index 9f15c5a..0000000 --- a/service-resource-manager-implementation/src/adapters/network/core/common.py +++ /dev/null @@ -1,122 +0,0 @@ -# -*- coding: utf-8 -*- - -import requests -from pydantic import BaseModel - -from src.adapters import logger - -log = logger.get_logger(__name__) - - -def _make_request(method: str, url: str, data=None): - try: - headers = None - if method == "POST" or method == "PUT": - headers = { - "Content-Type": "application/json", - "accept": "application/json", - } - elif method == "GET": - headers = { - "accept": "application/json", - } - response = requests.request(method, url, headers=headers, data=data) - response.raise_for_status() - if response.content: - return response.json() - except requests.exceptions.HTTPError as e: - raise CoreHttpError(e) from e - except requests.exceptions.ConnectionError as e: - raise CoreHttpError("connection error") from e - - -# Monitoring Event Methods -def monitoring_event_post( - base_url: str, scs_as_id: str, model_payload: BaseModel -) -> dict: - data = model_payload.model_dump_json(exclude_none=True, by_alias=True) - url = monitoring_event_build_url(base_url, scs_as_id) - return _make_request("POST", url, data=data) - - -def monitoring_event_build_url(base_url: str, scs_as_id: str, session_id: str = None): - url = f"{base_url}/3gpp-monitoring-event/v1/{scs_as_id}/subscriptions" - if session_id is not None and len(session_id) > 0: - return f"{url}/{session_id}" - else: - return url - - -# QoD methods -def as_session_with_qos_post( - base_url: str, scs_as_id: str, model_payload: BaseModel -) -> dict: - data = model_payload.model_dump_json(exclude_none=True, by_alias=True) - url = as_session_with_qos_build_url(base_url, scs_as_id) - return _make_request("POST", url, data=data) - - -def as_session_with_qos_get(base_url: str, scs_as_id: str, session_id: str) -> dict: - url = as_session_with_qos_build_url(base_url, scs_as_id, session_id) - return _make_request("GET", url) - - -def as_session_with_qos_delete(base_url: str, scs_as_id: str, session_id: str): - url = as_session_with_qos_build_url(base_url, scs_as_id, session_id) - return _make_request("DELETE", url) - - -def as_session_with_qos_build_url( - base_url: str, scs_as_id: str, session_id: str = None -): - url = f"{base_url}/3gpp-as-session-with-qos/v1/{scs_as_id}/subscriptions" - if session_id is not None and len(session_id) > 0: - return f"{url}/{session_id}" - else: - return url - - -# Traffic Influence Methods -def traffic_influence_post( - base_url: str, scs_as_id: str, model_payload: BaseModel -) -> dict: - data = model_payload.model_dump_json(exclude_none=True) - url = traffic_influence_build_url(base_url, scs_as_id) - return _make_request("POST", url, data=data) - - -def traffic_influence_delete(base_url: str, scs_as_id: str, session_id: str): - url = traffic_influence_build_url(base_url, scs_as_id, session_id) - return _make_request("DELETE", url) - - -def traffic_influence_put( - base_url: str, scs_as_id: str, session_id: str, model_payload: BaseModel -) -> dict: - data = model_payload.model_dump_json(exclude_none=True) - url = traffic_influence_build_url(base_url, scs_as_id, session_id) - return _make_request("PUT", url, data=data) - - -def traffic_influence_get(base_url: str, scs_as_id: str, sessionId: str = None) -> dict: - url = traffic_influence_build_url(base_url, scs_as_id, sessionId) - return _make_request("GET", url) - - -def traffic_influence_get_all( - base_url: str, scs_as_id: str, sessionId: str = None -) -> list[dict]: - url = traffic_influence_build_url(base_url, scs_as_id) - return _make_request("GET", url) - - -def traffic_influence_build_url(base_url: str, scs_as_id: str, session_id: str = None): - url = f"{base_url}/3gpp-traffic-influence/v1/{scs_as_id}/subscriptions" - if session_id is not None and len(session_id) > 0: - return f"{url}/{session_id}" - else: - return url - - -class CoreHttpError(Exception): - pass diff --git a/service-resource-manager-implementation/src/adapters/network/core/network_interface.py b/service-resource-manager-implementation/src/adapters/network/core/network_interface.py deleted file mode 100644 index 078dc40..0000000 --- a/service-resource-manager-implementation/src/adapters/network/core/network_interface.py +++ /dev/null @@ -1,311 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -## -# Copyright 2025-present by Software Networks Area, i2CAT. -# All rights reserved. -# -# This file is part of the Open SDK -# -# Contributors: -# - Reza Mosahebfard (reza.mosahebfard@i2cat.net) -# - Ferran Cañellas (ferran.canellas@i2cat.net) -## -import uuid -from abc import ABC -from itertools import product -from typing import Dict - -from src.adapters import logger -from src.adapters.network.clients.errors import NetworkPlatformError -from src.adapters.network.core import common, schemas - -log = logger.get_logger(__name__) - - -def flatten_port_spec(ports_spec: schemas.PortsSpec | None) -> list[str]: - has_ports = False - has_ranges = False - flat_ports = [] - if ports_spec and ports_spec.ports: - has_ports = True - flat_ports.extend([str(port) for port in ports_spec.ports]) - if ports_spec and ports_spec.ranges: - has_ranges = True - flat_ports.extend( - [f"{range.from_.root}-{range.to.root}" for range in ports_spec.ranges] - ) - if not has_ports and not has_ranges: - flat_ports.append("0-65535") - return flat_ports - - -def build_flows( - flow_id: int, - session_info: schemas.CreateSession, -) -> list[schemas.FlowInfo]: - device_ports = flatten_port_spec(session_info.devicePorts) - server_ports = flatten_port_spec(session_info.applicationServerPorts) - ports_combis = list(product(device_ports, server_ports)) - - device_ip = session_info.device.ipv4Address or session_info.device.ipv6Address - if isinstance(device_ip, schemas.DeviceIpv6Address): - device_ip = device_ip.root - else: # IPv4 - device_ip = ( - device_ip.root.publicAddress.root or device_ip.root.privateAddress.root - ) - device_ip = str(device_ip) - server_ip = ( - session_info.applicationServer.ipv4Address - or session_info.applicationServer.ipv6Address - ) - server_ip = server_ip.root - flow_descrs = [] - for device_port, server_port in ports_combis: - flow_descrs.append( - f"permit in ip from {device_ip} {device_port} to {server_ip} {server_port}" - ) - flow_descrs.append( - f"permit out ip from {server_ip} {server_port} to {device_ip} {device_port}" - ) - flows = [ - schemas.FlowInfo(flowId=flow_id, flowDescriptions=[", ".join(flow_descrs)]) - ] - return flows - - -class NetworkManagementInterface(ABC): - """ - Abstract Base Class for Network Resource Management. - - This interface defines the standard methods that all - Network Clients (Open5GS, OAI, Open5GCore) must implement. - - Partners implementing a new network client should inherit from this class - and provide concrete implementations for all abstract methods relevant - to their specific NEF capabilities. - """ - - base_url: str - scs_as_id: str - - def add_core_specific_qod_parameters( - self, - session_info: schemas.CreateSession, - subscription: schemas.AsSessionWithQoSSubscription, - ): - """ - Placeholder for adding core-specific parameters to the subscription. - This method should be overridden by subclasses to implement specific logic. - """ - pass - - def add_core_specific_ti_parameters( - self, - traffic_influence_info: schemas.CreateTrafficInfluence, - subscription: schemas.TrafficInfluSub, - ): - """ - Placeholder for adding core-specific parameters to the subscription. - This method should be overridden by subclasses to implement specific logic. - """ - pass - - def core_specific_qod_validation(self, session_info: schemas.CreateSession) -> None: - """ - Validates core-specific parameters for the session creation. - - args: - session_info: The session information to validate. - - raises: - ValidationError: If the session information does not meet core-specific requirements. - """ - # Placeholder for core-specific validation logic - # This method should be overridden by subclasses if needed - pass - - def core_specific_traffic_influence_validation( - self, traffic_influence_info: schemas.CreateTrafficInfluence - ) -> None: - """ - Validates core-specific parameters for the session creation. - - args: - session_info: The session information to validate. - - raises: - ValidationError: If the session information does not meet core-specific requirements. - """ - # Placeholder for core-specific validation logic - # This method should be overridden by subclasses if needed - pass - - def _build_qod_subscription( - self, session_info: Dict - ) -> schemas.AsSessionWithQoSSubscription: - valid_session_info = schemas.CreateSession.model_validate(session_info) - device_ipv4 = None - if valid_session_info.device.ipv4Address: - device_ipv4 = valid_session_info.device.ipv4Address.root.publicAddress.root - - self.core_specific_qod_validation(valid_session_info) - subscription = schemas.AsSessionWithQoSSubscription( - notificationDestination=str(valid_session_info.sink), - qosReference=valid_session_info.qosProfile.root, - ueIpv4Addr=device_ipv4, - ueIpv6Addr=valid_session_info.device.ipv6Address, - usageThreshold=schemas.UsageThreshold(duration=valid_session_info.duration), - ) - self.add_core_specific_qod_parameters(valid_session_info, subscription) - return subscription - - def _build_ti_subscription(self, traffic_influence_info: Dict): - traffic_influence_data = schemas.CreateTrafficInfluence.model_validate( - traffic_influence_info - ) - self.core_specific_traffic_influence_validation(traffic_influence_data) - - device_ip = traffic_influence_data.retrieve_ue_ipv4() - server_ip = ( - traffic_influence_data.appInstanceId - ) # assume that the instance id corresponds to its IPv4 address - sink_url = traffic_influence_data.notificationUri - edge_zone = traffic_influence_data.edgeCloudZoneId - - # build flow descriptor in oai format using device ip and server ip - flow_descriptor = f"permit out ip from {device_ip}/32 to {server_ip}/32" - - subscription = schemas.TrafficInfluSub( - afAppId=traffic_influence_data.appId, - ipv4Addr=str(device_ip), - notificationDestination=sink_url, - ) - subscription.add_flow_descriptor(flow_desriptor=flow_descriptor) - subscription.add_traffic_route(dnai=edge_zone) - - self.add_core_specific_ti_parameters(traffic_influence_data, subscription) - return subscription - - def create_qod_session(self, session_info: Dict) -> Dict: - """ - Creates a QoS session based on CAMARA QoD API input. - - args: - session_info: Dictionary containing session details conforming to - the CAMARA QoD session creation parameters. - - returns: - dictionary containing the created session details, including its ID. - """ - subscription = self._build_qod_subscription(session_info) - response = common.as_session_with_qos_post( - self.base_url, self.scs_as_id, subscription - ) - subscription_info: schemas.AsSessionWithQoSSubscription = ( - schemas.AsSessionWithQoSSubscription(**response) - ) - subscription_url = subscription_info.self_.root - subscription_id = subscription_url.split("/")[-1] if subscription_url else None - if not subscription_id: - log.error("Failed to retrieve QoS session ID from response") - raise NetworkPlatformError("QoS session ID not found in response") - session_info = schemas.SessionInfo( - sessionId=schemas.SessionId(uuid.UUID(subscription_id)), - qosStatus=schemas.QosStatus.REQUESTED, - **session_info, - ) - return session_info.model_dump() - - def get_qod_session(self, session_id: str) -> Dict: - """ - Retrieves details of a specific Quality on Demand (QoS) session. - - args: - session_id: The unique identifier of the QoS session. - - returns: - Dictionary containing the details of the requested QoS session. - """ - session = common.as_session_with_qos_get( - self.base_url, self.scs_as_id, session_id=session_id - ) - log.info(f"QoD session retrived successfully [id={session_id}]") - return session - - def delete_qod_session(self, session_id: str) -> None: - """ - Deletes a specific Quality on Demand (QoS) session. - - args: - session_id: The unique identifier of the QoS session to delete. - - returns: - None - """ - common.as_session_with_qos_delete( - self.base_url, self.scs_as_id, session_id=session_id - ) - log.info(f"QoD session deleted successfully [id={session_id}]") - - def create_traffic_influence_resource(self, traffic_influence_info: Dict) -> Dict: - """ - Creates a Traffic Influence resource based on CAMARA TI API input. - - args: - traffic_influence_info: Dictionary containing traffic influence details conforming to - the CAMARA TI resource creation parameters. - - returns: - dictionary containing the created traffic influence resource details, including its ID. - """ - - subscription = self._build_ti_subscription(traffic_influence_info) - response = common.traffic_influence_post( - self.base_url, self.scs_as_id, subscription - ) - - # retrieve the NEF resource id - if "self" in response.keys(): - subscription_id = response["self"] - else: - subscription_id = None - - traffic_influence_info["trafficInfluenceID"] = subscription_id - return traffic_influence_info - - def put_traffic_influence_resource( - self, resource_id: str, traffic_influence_info: Dict - ) -> Dict: - """ - Retrieves details of a specific Traffic Influence resource. - - args: - resource_id: The unique identifier of the Traffic Influence resource. - - returns: - Dictionary containing the details of the requested Traffic Influence resource. - """ - subscription = self._build_ti_subscription(traffic_influence_info) - common.traffic_influence_put( - self.base_url, self.scs_as_id, resource_id, subscription - ) - - traffic_influence_info["trafficInfluenceID"] = resource_id - return traffic_influence_info - - def delete_traffic_influence_resource(self, resource_id: str) -> None: - """ - Deletes a specific Traffic Influence resource. - - args: - resource_id: The unique identifier of the Traffic Influence resource to delete. - - returns: - None - """ - common.traffic_influence_delete(self.base_url, self.scs_as_id, resource_id) - return - - # Placeholder for other CAMARA APIs (e.g., Traffic Influence, - # Location-retrieval, etc.) diff --git a/service-resource-manager-implementation/src/adapters/network/core/schemas.py b/service-resource-manager-implementation/src/adapters/network/core/schemas.py deleted file mode 100644 index 43e48c3..0000000 --- a/service-resource-manager-implementation/src/adapters/network/core/schemas.py +++ /dev/null @@ -1,798 +0,0 @@ -# -*- coding: utf-8 -*- -# This file defines the Pydantic models that represent the data structures (schemas) -# for the requests sent to and responses received from the Open5GS NEF API, -# specifically focusing on the APIs needed to support CAMARA QoD. - -import ipaddress -from datetime import datetime -from enum import Enum -from ipaddress import IPv4Address, IPv6Address -from typing import Annotated, Literal -from uuid import UUID - -from pydantic import ( - AnyHttpUrl, - AnyUrl, - BaseModel, - ConfigDict, - Field, - NonNegativeInt, - RootModel, -) -from pydantic_extra_types.mac_address import MacAddress - -from src.adapters.logger import setup_logger -from src.adapters.network.adapters.errors import NetworkPlatformError - -log = setup_logger(__name__) - - -class FlowDirection(Enum): - """ - DOWNLINK: The corresponding filter applies for traffic to the UE. - UPLINK: The corresponding filter applies for traffic from the UE. - BIDIRECTIONAL: The corresponding filter applies for traffic both to and from the UE. - UNSPECIFIED: The corresponding filter applies for traffic to the UE (downlink), but - has no specific direction declared. The service data flow detection shall apply the - filter for uplink traffic as if the filter was bidirectional. The PCF shall not use - the value UNSPECIFIED in filters created by the network in NW-initiated procedures. - The PCF shall only include the value UNSPECIFIED in filters in UE-initiated - procedures if the same value is received from the SMF. - """ - - DOWNLINK = "DOWNLINK" - UPLINK = "UPLINK" - BIDIRECTIONAL = "BIDIRECTIONAL" - UNSPECIFIED = "UNSPECIFIED" - - -class RequestedQosMonitoringParameter(Enum): - DOWNLINK = "DOWNLINK" - UPLINK = "UPLINK" - ROUND_TRIP = "ROUND_TRIP" - - -class ReportingFrequency(Enum): - EVENT_TRIGGERED = "EVENT_TRIGGERED" - PERIODIC = "PERIODIC" - SESSION_RELEASE = "SESSION_RELEASE" - - -Uinteger = Annotated[int, Field(ge=0)] - - -class DurationSec(RootModel[NonNegativeInt]): - root: NonNegativeInt = Field( - ..., - description="Unsigned integer identifying a period of time in units of \ - seconds.", - ) - - -class Volume(RootModel[NonNegativeInt]): - root: NonNegativeInt = Field( - ..., description="Unsigned integer identifying a volume in units of bytes." - ) - - -class SupportedFeatures(RootModel[str]): - root: str = Field( - ..., - pattern=r"^[A-Fa-f0-9]*$", - description="Hexadecimal string representing supported features.", - ) - - -class Link(RootModel[str]): - root: str = Field( - ..., - description="String formatted according to IETF RFC 3986 identifying a \ - referenced resource.", - ) - - -class FlowDescriptionModel(RootModel[str]): - root: str = Field(..., description="Defines a packet filter of an IP flow.") - - -class EthFlowDescription(BaseModel): - destMacAddr: MacAddress | None = None - ethType: str - fDesc: FlowDescriptionModel | None = None - fDir: FlowDirection | None = None - sourceMacAddr: MacAddress | None = None - vlanTags: list[str] | None = Field(None, max_length=2, min_length=1) - srcMacAddrEnd: MacAddress | None = None - destMacAddrEnd: MacAddress | None = None - - -class UsageThreshold(BaseModel): - duration: DurationSec | None = None - totalVolume: Volume | None = None - downlinkVolume: Volume | None = None - uplinkVolume: Volume | None = None - - -class SponsorInformation(BaseModel): - sponsorId: str = Field(..., description="It indicates Sponsor ID.") - aspId: str = Field(..., description="It indicates Application Service Provider ID.") - - -class QosMonitoringInformationModel(BaseModel): - reqQosMonParams: list[RequestedQosMonitoringParameter] | None = Field( - None, min_length=1 - ) - repFreqs: list[ReportingFrequency] | None = Field(None, min_length=1) - repThreshDl: Uinteger | None = None - repThreshUl: Uinteger | None = None - repThreshRp: Uinteger | None = None - waitTime: int | None = None - repPeriod: int | None = None - - -class FlowInfo(BaseModel): - flowId: int = Field(..., description="Indicates the IP flow.") - flowDescriptions: list[str] | None = Field( - None, - description="Indicates the packet filters of the IP flow. Refer to subclause \ - 5.3.8 of 3GPP TS 29.214 for encoding. It shall contain UL and/or DL IP \ - flow description.", - max_length=2, - min_length=1, - ) - - -class Snssai(BaseModel): - sst: int = Field(default=1) - sd: str = Field(default="FFFFFF") - - -class AsSessionWithQoSSubscription(BaseModel): - model_config = ConfigDict(serialize_by_alias=True) - self_: Link | None = Field(None, alias="self") - supportedFeatures: SupportedFeatures | None = None - notificationDestination: Link - flowInfo: list[FlowInfo] | None = Field( - None, description="Describe the data flow which requires QoS.", min_length=1 - ) - ethFlowInfo: list[EthFlowDescription] | None = Field( - None, description="Identifies Ethernet packet flows.", min_length=1 - ) - qosReference: str | None = Field( - None, description="Identifies a pre-defined QoS information" - ) - altQoSReferences: list[str] | None = Field( - None, - description="Identifies an ordered list of pre-defined QoS information. The \ - lower the index of the array for a given entry, the higher the priority.", - min_length=1, - ) - ueIpv4Addr: ipaddress.IPv4Address | None = None - ueIpv6Addr: ipaddress.IPv6Address | None = None - macAddr: MacAddress | None = None - snssai: Snssai | None = None - dnn: str | None = None - usageThreshold: UsageThreshold | None = None - sponsorInfo: SponsorInformation | None = None - qosMonInfo: QosMonitoringInformationModel | None = None - - @property - def subscription_id(self) -> str: - """ - Returns the subscription ID, which is the same as the self link. - """ - subscription_id = self.self_.root.split("/")[-1] if self.self_.root else None - if not subscription_id: - log.error("Failed to retrieve QoS session ID from response") - raise NetworkPlatformError("QoS session ID not found in response") - - -class SourceTrafficFilters(BaseModel): - sourcePort: int - - -class DestinationTrafficFilters(BaseModel): - destinationPort: int - destinationProtocol: str - - -class TrafficRoute(BaseModel): - dnai: str - - -class TrafficInfluSub(BaseModel): # Replace with a meaningful name - afServiceId: str | None = None - afAppId: str - dnn: str | None = None - snssai: Snssai | None = None - trafficFilters: list[FlowInfo] | None = Field( - None, - description="Describe the data flow which requires Traffic Influence.", - min_length=1, - ) - ipv4Addr: str | None = None - ipv6Addr: str | None = None - - notificationDestination: str - trafficRoutes: list[TrafficRoute] | None = Field( - None, - description="Describe the list of DNAIs to reach the destination", - min_length=1, - ) - suppFeat: str | None = None - - def add_flow_descriptor(self, flow_descriptor: str): - self.trafficFilters = list() - self.trafficFilters.append( - FlowInfo( - flowId=len(self.trafficFilters) + 1, flowDescriptions=[flow_descriptor] - ) - ) - - def add_traffic_route(self, dnai: str): - self.trafficRoutes = list() - self.trafficRoutes.append(TrafficRoute(dnai=dnai)) - - def add_snssai(self, sst: int, sd: str = None): - self.snssai = Snssai(sst=sst, sd=sd) - - -# Monitoring Event API - - -class DurationMin(BaseModel): - duration: int = Field( - 0, - description="Unsigned integer identifying a period of time in units of minutes", - ge=0, - ) - - -class PlmnId(BaseModel): - mcc: str = Field( - ..., - description="String encoding a Mobile Country Code, comprising of 3 digits.", - ) - mnc: str = Field( - ..., - description="String encoding a Mobile Network Code, comprising of 2 or 3 digits.", - ) - - -# The enumeration Accuracy represents a desired granularity of accuracy of the requested location information. -class Accuracy(str, Enum): - cgi_ecgi = ( - "CGI_ECGI" # The AF requests to be notified using cell level location accuracy. - ) - ta_ra = ( - "TA_RA" # The AF requests to be notified using TA/RA level location accuracy. - ) - geo_area = "GEO_AREA" # The AF requests to be notified using the geographical area accuracy. - civic_addr = "CIVIC_ADDR" # The AF requests to be notified using the civic address accuracy. #EDGEAPP - - -# If locationType set to "LAST_KNOWN_LOCATION", the monitoring event request from AF shall be only for one-time monitoring request -class LocationType(str, Enum): - CURRENT_LOCATION = ( - "CURRENT_LOCATION" # The AF requests to be notified for current location. - ) - LAST_KNOWN = ( - "LAST_KNOWN_LOCATION" # The AF requests to be notified for last known location. - ) - - -# This data type represents a monitoring event type. -class MonitoringType(str, Enum): - LOCATION_REPORTING = "LOCATION_REPORTING" - - -class LocationFailureCause(str, Enum): - position_denied = "POSITIONING_DENIED" # Positioning is denied. - unsupported_by_ue = "UNSUPPORTED_BY_UE" # Positioning is not supported by UE. - not_registered_ue = "NOT_REGISTERED_UE" # UE is not registered. - unspecified = "UNSPECIFIED" # Unspecified cause. - - -class GeographicalCoordinates(BaseModel): - lon: float = Field(..., description="Longitude coordinate.") - lat: float = Field(..., description="Latitude coordinate.") - - -class PointListNef(BaseModel): - geographical_coords: list[GeographicalCoordinates] = Field( - ..., - description="List of geographical coordinates defining the points.", - min_length=3, - max_length=15, - ) - - -class NefPolygon(BaseModel): - point_list: PointListNef = Field( - ..., description="List of points defining the polygon." - ) - - -class GeographicArea(BaseModel): - polygon: NefPolygon | None = Field( - None, description="Identifies a polygonal geographic area." - ) - - -# This data type represents the user location information which is sent from the NEF to the AF. -class LocationInfo(BaseModel): - ageOfLocationInfo: DurationMin | None = Field( - None, - description="Indicates the elapsed time since the last network contact of the UE.", - ) - cellId: str | None = Field(None, description="Cell ID where the UE is located.") - trackingAreaId: str | None = Field( - None, description="TrackingArea ID where the UE is located." - ) - enodeBId: str | None = Field(None, description="eNodeB ID where the UE is located.") - routingAreaId: str | None = Field( - None, description="Routing Area ID where the UE is located" - ) - plmnId: PlmnId | None = Field(None, description="PLMN ID where the UE is located.") - twanId: str | None = Field(None, description="TWAN ID where the UE is located.") - geographicArea: GeographicArea | None = Field( - None, - description="Identifies a geographic area of the user where the UE is located.", - ) - - -class MonitoringEventSubscriptionRequest(BaseModel): - accuracy: Accuracy | None = Field( - None, - description="Accuracy represents a desired granularity of accuracy of the requested location information.", - ) - externalId: str | None = Field( - None, description="Identifies a user clause 4.6.2 TS 23.682 (optional)" - ) - msisdn: str | None = Field( - None, - description="Identifies the MS internal PSTN/ISDN number allocated for a UE.", - ) - ipv4Addr: IPv4Address | None = Field( - None, description="Identifies the Ipv4 address." - ) - ipv6Addr: IPv6Address | None = Field( - None, description="Identifies the Ipv6 address." - ) - notificationDestination: AnyHttpUrl = Field( - ..., - description="URI of a notification destination that the T8 message shall be delivered to.", - ) - monitoringType: MonitoringType = Field( - ..., description="Enumeration of monitoring type. Refer to clause 5.3.2.4.3." - ) - maximumNumberOfReports: int | None = Field( - None, - description="Identifies the maximum number of event reports to be generated by the AMF to the NEF and then the AF.", - ) - monitorExpireTime: datetime | None = Field( - None, - description="Identifies the absolute time at which the related monitoring event request is considered to expire.", - ) - locationType: LocationType | None = Field( - None, - description="Indicates whether the request is for Current Location, Initial Location, or Last Known Location.", - ) - repPeriod: DurationSec | None = Field( - None, description="Identifies the periodic time for the event reports." - ) - minimumReportInterval: DurationSec | None = Field( - None, - description="identifies a minimum time interval between Location Reporting notifications", - ) - - -# This data type represents a monitoring event notification which is sent from the NEF to the AF. -class MonitoringEventReport(BaseModel): - externalId: str | None = Field( - None, description="Identifies a user, clause 4.6.2 TS 23.682" - ) - msisdn: str | None = Field( - None, - description="Identifies the MS internal PSTN/ISDN number allocated for a UE.", - ) - locationInfo: LocationInfo | None = Field( - None, description="Indicates the user location related information." - ) - locFailureCause: LocationFailureCause | None = Field( - None, description="Indicates the location positioning failure cause." - ) - monitoringType: MonitoringType = Field( - ..., - description="Identifies the type of monitoring as defined in clause 5.3.2.4.3.", - ) - eventTime: datetime | None = Field( - None, - description="Identifies when the event is detected or received. Shall be included for each group of UEs.", - ) - - -# This data type represents a monitoring notification which is sent from the NEF to the AF. -class MonitoringNotification(BaseModel): - subscription: AnyHttpUrl = Field( - ..., - description="Link to the subscription resource to which this notification is related.", - ) - monitoringEventReports: list[MonitoringEventReport] | None = Field( - None, - description="Each element identifies a monitoring event report (optional).", - ) - cancelInd: bool | None = Field( - False, - description="Indicates whether to request to cancel the corresponding monitoring subscription. Set to false or omitted otherwise.", - ) - - -############################################################### -############################################################### -# CAMARA Models - - -class PhoneNumber(RootModel[str]): - root: Annotated[ - str, - Field( - description="A public identifier addressing a telephone subscription. In mobile networks it corresponds to the MSISDN (Mobile Station International Subscriber Directory Number). In order to be globally unique it has to be formatted in international format, according to E.164 standard, prefixed with '+'.", - examples=["+123456789"], - pattern="^\\+[1-9][0-9]{4,14}$", - ), - ] - - -class NetworkAccessIdentifier(RootModel[str]): - root: Annotated[ - str, - Field( - description="A public identifier addressing a subscription in a mobile network. In 3GPP terminology, it corresponds to the GPSI formatted with the External Identifier ({Local Identifier}@{Domain Identifier}). Unlike the telephone number, the network access identifier is not subjected to portability ruling in force, and is individually managed by each operator.", - examples=["123456789@domain.com"], - ), - ] - - -class SingleIpv4Addr(RootModel[IPv4Address]): - root: Annotated[ - IPv4Address, - Field( - description="A single IPv4 address with no subnet mask", - examples=["203.0.113.0"], - ), - ] - - -class Port(RootModel[int]): - root: Annotated[int, Field(description="TCP or UDP port number", ge=0, le=65535)] - - -class DeviceIpv4Addr1(BaseModel): - publicAddress: SingleIpv4Addr - privateAddress: SingleIpv4Addr - publicPort: Port | None = None - - -class DeviceIpv4Addr2(BaseModel): - publicAddress: SingleIpv4Addr - privateAddress: SingleIpv4Addr | None = None - publicPort: Port - - -class DeviceIpv4Addr(RootModel[DeviceIpv4Addr1 | DeviceIpv4Addr2]): - root: Annotated[ - DeviceIpv4Addr1 | DeviceIpv4Addr2, - Field( - description="The device should be identified by either the public (observed) IP address and port as seen by the application server, or the private (local) and any public (observed) IP addresses in use by the device (this information can be obtained by various means, for example from some DNS servers).\n\nIf the allocated and observed IP addresses are the same (i.e. NAT is not in use) then the same address should be specified for both publicAddress and privateAddress.\n\nIf NAT64 is in use, the device should be identified by its publicAddress and publicPort, or separately by its allocated IPv6 address (field ipv6Address of the Device object)\n\nIn all cases, publicAddress must be specified, along with at least one of either privateAddress or publicPort, dependent upon which is known. In general, mobile devices cannot be identified by their public IPv4 address alone.\n", - examples=[{"publicAddress": "203.0.113.0", "publicPort": 59765}], - ), - ] - - -class DeviceIpv6Address(RootModel[IPv6Address]): - root: Annotated[ - IPv6Address, - Field( - description="The device should be identified by the observed IPv6 address, or by any single IPv6 address from within the subnet allocated to the device (e.g. adding ::0 to the /64 prefix).\n\nThe session shall apply to all IP flows between the device subnet and the specified application server, unless further restricted by the optional parameters devicePorts or applicationServerPorts.\n", - examples=["2001:db8:85a3:8d3:1319:8a2e:370:7344"], - ), - ] - - -class Device(BaseModel): - phoneNumber: PhoneNumber | None = None - networkAccessIdentifier: NetworkAccessIdentifier | None = None - ipv4Address: DeviceIpv4Addr | None = None - ipv6Address: DeviceIpv6Address | None = None - - -class RetrievalLocationRequest(BaseModel): - """ - Request to retrieve the location of a device. Device is not required when using a 3-legged access token. - """ - - device: Annotated[ - Device | None, - Field(None, description="End-user device able to connect to a mobile network."), - ] - maxAge: Annotated[ - int | None, - Field( - None, - description="Maximum age of the location information which is accepted for the location retrieval (in seconds).", - ), - ] - maxSurface: Annotated[ - int | None, - Field( - None, - description="Maximum surface in square meters which is accepted by the client for the location retrieval.", - ge=1, - examples=[1000000], - ), - ] - - -class AreaType(str, Enum): - circle = "CIRCLE" # The area is defined as a circle. - polygon = "POLYGON" # The area is defined as a polygon. - - -class Point(BaseModel): - latitude: Annotated[ - float, - Field( - description="Latitude component of a location.", - examples=["50.735851"], - ge=-90, - le=90, - ), - ] - longitude: Annotated[ - float, - Field( - ..., - description="Longitude component of location.", - examples=["7.10066"], - ge=-180, - le=180, - ), - ] - - -class PointList( - RootModel[ - Annotated[ - list[Point], - Field( - min_length=3, - max_length=15, - description="List of points defining the area.", - ), - ] - ] -): - pass - - -class Circle(BaseModel): - areaType: Literal[AreaType.circle] - center: Annotated[Point, Field(description="Center point of the circle.")] - radius: Annotated[float, Field(description="Radius of the circle.", ge=1)] - - -class Polygon(BaseModel): - areaType: Literal[AreaType.polygon] - boundary: Annotated[ - PointList, Field(description="List of points defining the polygon.") - ] - - -Area = Annotated[Circle | Polygon, Field(discriminator="areaType")] - - -class LastLocationTime( - RootModel[ - Annotated[ - datetime, - Field( - description="Last date and time when the device was localized.", - examples="2023-09-07T10:40:52Z", - ), - ] - ] -): - pass - - -class Location(BaseModel): - lastLocationTime: Annotated[ - LastLocationTime, Field(description="Last known location time.") - ] - area: Annotated[Area, Field(description="Geographical area of the location.")] - - -class ApplicationServerIpv4Address(RootModel[str]): - root: Annotated[ - str, - Field( - description="IPv4 address may be specified in form
as:\n - address - an IPv4 number in dotted-quad form 1.2.3.4. Only this exact IP number will match the flow control rule.\n - address/mask - an IP number as above with a mask width of the form 1.2.3.4/24.\n In this case, all IP numbers from 1.2.3.0 to 1.2.3.255 will match. The bit width MUST be valid for the IP version.\n", - examples=["198.51.100.0/24"], - ), - ] - - -class ApplicationServerIpv6Address(RootModel[str]): - root: Annotated[ - str, - Field( - description="IPv6 address may be specified in form
as:\n - address - The /128 subnet is optional for single addresses:\n - 2001:db8:85a3:8d3:1319:8a2e:370:7344\n - 2001:db8:85a3:8d3:1319:8a2e:370:7344/128\n - address/mask - an IP v6 number with a mask:\n - 2001:db8:85a3:8d3::0/64\n - 2001:db8:85a3:8d3::/64\n", - examples=["2001:db8:85a3:8d3:1319:8a2e:370:7344"], - ), - ] - - -class ApplicationServer(BaseModel): - ipv4Address: ApplicationServerIpv4Address | None = None - ipv6Address: ApplicationServerIpv6Address | None = None - - -class Range(BaseModel): - from_: Annotated[Port, Field(alias="from")] - to: Port - - -class PortsSpec(BaseModel): - ranges: Annotated[ - list[Range] | None, Field(description="Range of TCP or UDP ports", min_length=1) - ] = None - ports: Annotated[ - list[Port] | None, Field(description="Array of TCP or UDP ports", min_length=1) - ] = None - - -class QosProfileName(RootModel[str]): - root: Annotated[ - str, - Field( - description="A unique name for identifying a specific QoS profile.\nThis may follow different formats depending on the API provider implementation.\nSome options addresses:\n - A UUID style string\n - Support for predefined profiles QOS_S, QOS_M, QOS_L, and QOS_E\n - A searchable descriptive name\nThe set of QoS Profiles that an API provider is offering may be retrieved by means of the QoS Profile API (qos-profile) or agreed on onboarding time.\n", - examples=["voice"], - max_length=256, - min_length=3, - pattern="^[a-zA-Z0-9_.-]+$", - ), - ] - - -class CredentialType(Enum): - PLAIN = "PLAIN" - ACCESSTOKEN = "ACCESSTOKEN" - REFRESHTOKEN = "REFRESHTOKEN" - - -class SinkCredential(BaseModel): - credentialType: Annotated[ - CredentialType, - Field( - description="The type of the credential.\nNote: Type of the credential - MUST be set to ACCESSTOKEN for now\n" - ), - ] - - -class NotificationSink(BaseModel): - sink: str | None - sinkCredential: SinkCredential | None - - -class BaseSessionInfo(BaseModel): - device: Device | None = None - applicationServer: ApplicationServer - devicePorts: Annotated[ - PortsSpec | None, - Field( - description="The ports used locally by the device for flows to which the requested QoS profile should apply. If omitted, then the qosProfile will apply to all flows between the device and the specified application server address and ports" - ), - ] = None - applicationServerPorts: Annotated[ - PortsSpec | None, - Field( - description="A list of single ports or port ranges on the application server" - ), - ] = None - qosProfile: QosProfileName - sink: Annotated[ - AnyUrl | None, - Field( - description="The address to which events about all status changes of the session (e.g. session termination) shall be delivered using the selected protocol.", - examples=["https://endpoint.example.com/sink"], - ), - ] = None - sinkCredential: Annotated[ - SinkCredential | None, - Field( - description="A sink credential provides authentication or authorization information necessary to enable delivery of events to a target." - ), - ] = None - - -class CreateSession(BaseSessionInfo): - duration: Annotated[ - int, - Field( - description="Requested session duration in seconds. Value may be explicitly limited for the QoS profile, as specified in the Qos Profile (see qos-profile API). Implementations can grant the requested session duration or set a different duration, based on network policies or conditions.\n", - examples=[3600], - ge=1, - ), - ] - - -class SessionId(RootModel[UUID]): - root: Annotated[UUID, Field(description="Session ID in UUID format")] - - -class QosStatus(Enum): - REQUESTED = "REQUESTED" - AVAILABLE = "AVAILABLE" - UNAVAILABLE = "UNAVAILABLE" - - -class StatusInfo(Enum): - DURATION_EXPIRED = "DURATION_EXPIRED" - NETWORK_TERMINATED = "NETWORK_TERMINATED" - DELETE_REQUESTED = "DELETE_REQUESTED" - - -class SessionInfo(BaseSessionInfo): - sessionId: SessionId - duration: Annotated[ - int, - Field( - description='Session duration in seconds. Implementations can grant the requested session duration or set a different duration, based on network policies or conditions.\n- When `qosStatus` is "REQUESTED", the value is the duration to be scheduled, granted by the implementation.\n- When `qosStatus` is AVAILABLE", the value is the overall duration since `startedAt. When the session is extended, the value is the new overall duration of the session.\n- When `qosStatus` is "UNAVAILABLE", the value is the overall effective duration since `startedAt` until the session was terminated.\n', - examples=[3600], - ge=1, - ), - ] - startedAt: Annotated[ - datetime | None, - Field( - description='Date and time when the QoS status became "AVAILABLE". Not to be returned when `qosStatus` is "REQUESTED". Format must follow RFC 3339 and must indicate time zone (UTC or local).', - examples=["2024-06-01T12:00:00Z"], - ), - ] = None - expiresAt: Annotated[ - datetime | None, - Field( - description='Date and time of the QoS session expiration. Format must follow RFC 3339 and must indicate time zone (UTC or local).\n- When `qosStatus` is "AVAILABLE", it is the limit time when the session is scheduled to finnish, if not terminated by other means.\n- When `qosStatus` is "UNAVAILABLE", it is the time when the session was terminated.\n- Not to be returned when `qosStatus` is "REQUESTED".\nWhen the session is extended, the value is the new expiration time of the session.\n', - examples=["2024-06-01T13:00:00Z"], - ), - ] = None - qosStatus: QosStatus - statusInfo: StatusInfo | None = None - - -class CreateTrafficInfluence(BaseModel): - trafficInfluenceID: str | None = None - apiConsumerId: str | None = None - appId: str - appInstanceId: str - edgeCloudRegion: str | None = None - edgeCloudZoneId: str | None = None - sourceTrafficFilters: SourceTrafficFilters | None = None - destinationTrafficFilters: DestinationTrafficFilters | None = None - notificationUri: str | None = None - notificationAuthToken: str | None = None - device: Device - notificationSink: NotificationSink | None = None - - def retrieve_ue_ipv4(self): - if self.device is not None and self.device.ipv4Address is not None: - return self.device.ipv4Address.root.privateAddress.root - else: - raise KeyError("device.ipv4Address.publicAddress") - - def add_ue_ipv4(self, ipv4: str): - if self.device is None: - self.device = Device() - if self.device.ipv4Address is None: - self.device.ipv4Address = DeviceIpv4Addr(publicAddress=ipv4) diff --git a/service-resource-manager-implementation/src/adapters/o-ran/__init__.py b/service-resource-manager-implementation/src/adapters/o-ran/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/adapters/o-ran/clients/__init__.py b/service-resource-manager-implementation/src/adapters/o-ran/clients/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/adapters/o-ran/clients/juniper-ric/__init__.py b/service-resource-manager-implementation/src/adapters/o-ran/clients/juniper-ric/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/adapters/o-ran/clients/juniper-ric/client.py b/service-resource-manager-implementation/src/adapters/o-ran/clients/juniper-ric/client.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/adapters/o-ran/core/__init__.py b/service-resource-manager-implementation/src/adapters/o-ran/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/adapters/o-ran/core/o-ran_interface.py b/service-resource-manager-implementation/src/adapters/o-ran/core/o-ran_interface.py deleted file mode 100644 index 4640904..0000000 --- a/service-resource-manager-implementation/src/adapters/o-ran/core/o-ran_interface.py +++ /dev/null @@ -1 +0,0 @@ -# TODO diff --git a/service-resource-manager-implementation/src/adapters/oran/__init__.py b/service-resource-manager-implementation/src/adapters/oran/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/adapters/oran/clients/__init__.py b/service-resource-manager-implementation/src/adapters/oran/clients/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/adapters/oran/clients/juniper_ric/__init__.py b/service-resource-manager-implementation/src/adapters/oran/clients/juniper_ric/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/adapters/oran/clients/juniper_ric/client.py b/service-resource-manager-implementation/src/adapters/oran/clients/juniper_ric/client.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/adapters/oran/core/__init__.py b/service-resource-manager-implementation/src/adapters/oran/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/adapters/oran/core/oran_interface.py b/service-resource-manager-implementation/src/adapters/oran/core/oran_interface.py deleted file mode 100644 index 4640904..0000000 --- a/service-resource-manager-implementation/src/adapters/oran/core/oran_interface.py +++ /dev/null @@ -1 +0,0 @@ -# TODO diff --git a/service-resource-manager-implementation/src/clients/__init__.py b/service-resource-manager-implementation/src/clients/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/clients/common/__init__.py b/service-resource-manager-implementation/src/clients/common/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/clients/common/sdk.py b/service-resource-manager-implementation/src/clients/common/sdk.py deleted file mode 100644 index 2070946..0000000 --- a/service-resource-manager-implementation/src/clients/common/sdk.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- coding: utf-8 -*- -## -# Copyright 2025-present by Software Networks Area, i2CAT. -# All rights reserved. -# -# This file is part of the Open SDK -# -# Contributors: -# - Adrián Pino Martínez (adrian.pino@i2cat.net) -## -from typing import Dict - -from src.common.sdk_factory import SdkFactory - - -class Sdk: - @staticmethod - def create_clients_from( - client_specs: Dict[str, Dict[str, str]], - ) -> Dict[str, object]: - """ - Create and return a dictionary of instantiated edgecloud/network/o-ran clients - based on the provided specifications. - - Args: - client_specs (dict): A dictionary where each key is the client's domain (e.g., 'edgecloud', 'network'), - and each value is a dictionary containing: - - 'client_name' (str): The specific name of the client (e.g., 'i2edge', 'open5gs'). - - 'base_url' (str): The base URL for the client's API. - Additional parameters like 'scs_as_id' may also be included. - - Returns: - dict: A dictionary where keys are the 'client_name' (str) and values are - the instantiated client objects. - - Example: - >>> from src.common.universal_client_catalog import UniversalCatalogClient - >>> - >>> client_specs_example = { - >>> 'edgecloud': { - >>> 'client_name': 'i2edge', - >>> 'base_url': 'http://ip_edge_cloud:port', - >>> 'additionalEdgeCloudParamater1': 'example' - >>> }, - >>> 'network': { - >>> 'client_name': 'open5gs', - >>> 'base_url': 'http://ip_network:port', - >>> 'additionalNetworkParamater1': 'example' - >>> } - >>> } - >>> - >>> clients = UniversalCatalogClient.create_clients(client_specs_example) - >>> edgecloud_client = clients.get("edgecloud") - >>> network_client = clients.get("network") - >>> - >>> edgecloud_client.get_edge_cloud_zones() - >>> network_client.get_qod_session(session_id="example_session_id") - """ - sdk_client = SdkFactory() - clients = {} - - for domain, config in client_specs.items(): - client_name = config["client_name"] - base_url = config["base_url"] - - # Support of additional paramaters for specific clients - kwargs = { - k: v for k, v in config.items() if k not in ("client_name", "base_url") - } - - client = sdk_client.instantiate_and_retrieve_clients( - domain, client_name, base_url, **kwargs - ) - clients[domain] = client - - return clients diff --git a/service-resource-manager-implementation/src/clients/common/sdk_factory.py b/service-resource-manager-implementation/src/clients/common/sdk_factory.py deleted file mode 100644 index f67bd08..0000000 --- a/service-resource-manager-implementation/src/clients/common/sdk_factory.py +++ /dev/null @@ -1,78 +0,0 @@ -# -*- coding: utf-8 -*- -## -# Copyright 2025-present by Software Networks Area, i2CAT. -# All rights reserved. -# -# This file is part of the Open SDK -# -# Contributors: -# - Adrián Pino Martínez (adrian.pino@i2cat.net) -## -from src.edgecloud.clients.aeros.client import EdgeApplicationManager as AerosClient -from src.edgecloud.clients.i2edge.client import EdgeApplicationManager as I2EdgeClient -from src.edgecloud.clients.piedge.client import EdgeApplicationManager as PiEdgeClient -# from src.network.clients.oai.client import NetworkManager as OaiCoreClient -# from src.network.clients.open5gcore.client import NetworkManager as Open5GCoreClient -# from src.network.clients.open5gs.client import NetworkManager as Open5GSClient - - -def _edgecloud_factory(client_name: str, base_url: str, **kwargs): - edge_cloud_factory = { - "aeros": lambda url, **kw: AerosClient(base_url=url, **kw), - "i2edge": lambda url: I2EdgeClient(base_url=url), - "piedge": lambda url, **kw: PiEdgeClient(base_url=url, **kw) - } - try: - return edge_cloud_factory[client_name](base_url, **kwargs) - except KeyError: - raise ValueError( - f"Invalid edgecloud client '{client_name}'. Available: {list(edge_cloud_factory)}" - ) - - -# def _network_factory(client_name: str, base_url: str, **kwargs): -# if "scs_as_id" not in kwargs: -# raise ValueError("Missing required 'scs_as_id' for network clients.") -# scs_as_id = kwargs.pop("scs_as_id") - -# network_factory = { -# "open5gs": lambda url, scs_id, **kw: Open5GSClient( -# base_url=url, scs_as_id=scs_id, **kw -# ), -# "oai": lambda url, scs_id, **kw: OaiCoreClient( -# base_url=url, scs_as_id=scs_id, **kw -# ), -# "open5gcore": lambda url, scs_id, **kw: Open5GCoreClient( -# base_url=url, scs_as_id=scs_id, **kw -# ), -# } -# try: -# return network_factory[client_name](base_url, scs_as_id, **kwargs) -# except KeyError: -# raise ValueError( -# f"Invalid network client '{client_name}'. Available: {list(network_factory)}" -# ) - - -# def _oran_factory(client_name: str, base_url: str): -# # TODO - - -class SdkFactory: - _domain_factories = { - "edgecloud": _edgecloud_factory - # "network": _network_factory, - # "oran": _oran_factory, - } - - @classmethod - def instantiate_and_retrieve_clients( - cls, domain: str, client_name: str, base_url: str, **kwargs - ): - try: - catalog = cls._domain_factories[domain] - except KeyError: - raise ValueError( - f"Unsupported domain '{domain}'. Supported: {list(cls._domain_factories)}" - ) - return catalog(client_name, base_url, **kwargs) diff --git a/service-resource-manager-implementation/src/clients/edgecloud/.env b/service-resource-manager-implementation/src/clients/edgecloud/.env deleted file mode 100644 index 8bcda46..0000000 --- a/service-resource-manager-implementation/src/clients/edgecloud/.env +++ /dev/null @@ -1,7 +0,0 @@ -# #### Logging #### -# LOG_LEVEL="debug" -# LOG_FILE="edgecloud.log" - -#### EdgeCloud #### -# EDGE_CLOUD="i2edge" -EDGE_CLOUD_URL=http://192.168.123.86:30769 diff --git a/service-resource-manager-implementation/src/clients/edgecloud/__init__.py b/service-resource-manager-implementation/src/clients/edgecloud/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/clients/edgecloud/clients/__init__.py b/service-resource-manager-implementation/src/clients/edgecloud/clients/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/clients/edgecloud/clients/aeros/__init__.py b/service-resource-manager-implementation/src/clients/edgecloud/clients/aeros/__init__.py deleted file mode 100644 index 0ea3493..0000000 --- a/service-resource-manager-implementation/src/clients/edgecloud/clients/aeros/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -""" -aerOS client - This module provides a client for interacting with the aerOS REST API. - It includes methods for onboarding/deploying applications, - and querying aerOS continuum entities - aerOS domain is exposed as zones - aerOS services and service components are exposed as applications - Client is initialized with a base URL for the aerOS API - and an access token for authentication. -""" - -from src.edgecloud.clients.aeros import config -from src.logger import setup_logger - -logger = setup_logger(__name__, is_debug=True, file_name=config.LOG_FILE) - -# TODO: The following should only appear in case aerOS client is used -# Currently even if another client is used, the logs appear -# logger.info("aerOS client initialized") -# logger.debug("aerOS API URL: %s", config.aerOS_API_URL) -# logger.debug("aerOS access token: %s", config.aerOS_ACCESS_TOKEN) -# logger.debug("aerOS debug mode: %s", config.DEBUG) -# logger.debug("aerOS log file: %s", config.LOG_FILE) diff --git a/service-resource-manager-implementation/src/clients/edgecloud/clients/aeros/client.py b/service-resource-manager-implementation/src/clients/edgecloud/clients/aeros/client.py deleted file mode 100644 index 1e8742c..0000000 --- a/service-resource-manager-implementation/src/clients/edgecloud/clients/aeros/client.py +++ /dev/null @@ -1,263 +0,0 @@ -## -# This file is part of the Open SDK -# -# Contributors: -# - Vasilis Pitsilis (vpitsilis@dat.demokritos.gr, vpitsilis@iit.demokritos.gr) -# - Andreas Sakellaropoulos (asakellaropoulos@iit.demokritos.gr) -## -from typing import Any, Dict, List, Optional - -from src.edgecloud.clients.aeros import config -from src.edgecloud.clients.aeros.continuum_client import ContinuumClient -from src.edgecloud.core.edgecloud_interface import EdgeCloudManagementInterface -from src.logger import setup_logger - - -class EdgeApplicationManager(EdgeCloudManagementInterface): - """ - aerOS Continuum Client - FIXME: Handle None responses from continuum client - """ - - def __init__(self, base_url: str, **kwargs): - self.base_url = base_url - self.logger = setup_logger(__name__, is_debug=True, file_name=config.LOG_FILE) - - # Overwrite config values if provided via kwargs - if "aerOS_API_URL" in kwargs: - config.aerOS_API_URL = kwargs["aerOS_API_URL"] - if "aerOS_ACCESS_TOKEN" in kwargs: - config.aerOS_ACCESS_TOKEN = kwargs["aerOS_ACCESS_TOKEN"] - if "aerOS_HLO_TOKEN" in kwargs: - config.aerOS_HLO_TOKEN = kwargs["aerOS_HLO_TOKEN"] - - if not config.aerOS_API_URL: - raise ValueError("Missing 'aerOS_API_URL'") - if not config.aerOS_ACCESS_TOKEN: - raise ValueError("Missing 'aerOS_ACCESS_TOKEN'") - if not config.aerOS_HLO_TOKEN: - raise ValueError("Missing 'aerOS_HLO_TOKEN'") - - def onboard_app(self, app_manifest: Dict) -> Dict: - # HLO-FE POST with TOSCA and app_id (service_id) - service_id = app_manifest.get("serviceId") - tosca_str = app_manifest.get("tosca") - aeros_client = ContinuumClient(self.base_url) - onboard_response = aeros_client.onboard_service( - service_id=service_id, tosca_str=tosca_str - ) - return {"appId": onboard_response["serviceId"]} - - def get_all_onboarded_apps(self) -> List[Dict]: - aeros_client = ContinuumClient(self.base_url) - ngsild_params = "type=Service&format=simplified" - aeros_apps = aeros_client.query_entities(ngsild_params) - return [ - {"appId": service["id"], "name": service["name"]} for service in aeros_apps - ] - # return [{"appId": "1234-5678", "name": "TestApp"}] - - def get_onboarded_app(self, app_id: str) -> Dict: - aeros_client = ContinuumClient(self.base_url) - ngsild_params = "format=simplified" - aeros_app = aeros_client.query_entity(app_id, ngsild_params) - return {"appId": aeros_app["id"], "name": aeros_app["name"]} - - def delete_onboarded_app(self, app_id: str) -> None: - print(f"Deleting application: {app_id}") - # TBD: Purge from continuum (make all ngsil-ld calls for servieId connected entities) - # Should check if undeployed first - - def deploy_app(self, app_id: str, app_zones: List[Dict]) -> Dict: - # HLO-FE PUT with app_id (service_id) - aeros_client = ContinuumClient(self.base_url) - deploy_response = aeros_client.deploy_service(app_id) - return {"appInstanceId": deploy_response["serviceId"]} - - def get_all_deployed_apps( - self, - app_id: Optional[str] = None, - app_instance_id: Optional[str] = None, - region: Optional[str] = None, - ) -> List[Dict]: - # FIXME: Get services in deployed state - aeros_client = ContinuumClient(self.base_url) - ngsild_params = 'type=Service&format=simplified&q=actionType=="DEPLOYED"' - if app_id: - ngsild_params += f'&q=service=="{app_id}"' - aeros_apps = aeros_client.query_entities(ngsild_params) - return [ - { - "appInstanceId": service["id"], - "status": - # scomponent["serviceComponentStatus"].split(":")[-1].lower() - service["actionType"], - } - for service in aeros_apps - ] - # return [{"appInstanceId": "abcd-efgh", "status": "ready"}] - - # def get_all_deployed_apps(self, - # app_id: Optional[str] = None, - # app_instance_id: Optional[str] = None, - # region: Optional[str] = None) -> List[Dict]: - # # FIXME: Get services in deployed state - # aeros_client = ContinuumClient(self.base_url) - # ngsild_params = "type=ServiceComponent&format=simplified" - # if app_id: - # ngsild_params += f'&q=service=="{app_id}"' - # aeros_apps = aeros_client.query_entities(ngsild_params) - # return [{ - # "appInstanceId": - # scomponent["id"], - # "status": - # scomponent["serviceComponentStatus"].split(":")[-1].lower() - # } for scomponent in aeros_apps] - # # return [{"appInstanceId": "abcd-efgh", "status": "ready"}] - - def undeploy_app(self, app_instance_id: str) -> None: - # HLO-FE DELETE with app_id (service_id) - aeros_client = ContinuumClient(self.base_url) - _ = aeros_client.undeploy_service(app_instance_id) - - def get_edge_cloud_zones( - self, region: Optional[str] = None, status: Optional[str] = None - ) -> List[Dict]: - aeros_client = ContinuumClient(self.base_url) - ngsild_params = "type=Domain&format=simplified" - aeros_domains = aeros_client.query_entities(ngsild_params) - return [ - { - "edgeCloudZoneId": domain["id"], - "status": domain["domainStatus"].split(":")[-1].lower(), - } - for domain in aeros_domains - ] - - # return [{"edgeCloudZoneId": "zone-1", "status": "active"}] - - def get_edge_cloud_zones_details( - self, zone_id: str, flavour_id: Optional[str] = None - ) -> Dict: - """ - Get details of a specific edge cloud zone. - :param zone_id: The ID of the edge cloud zone - :param flavour_id: Optional flavour ID to filter the results - :return: Details of the edge cloud zone - """ - # Minimal mocked response based on required fields of 'ZoneRegisteredData' in GSMA OPG E/WBI API - # return { - # "zoneId": - # zone_id, - # "reservedComputeResources": [{ - # "cpuArchType": "ISA_X86_64", - # "numCPU": "4", - # "memory": 8192, - # }], - # "computeResourceQuotaLimits": [{ - # "cpuArchType": "ISA_X86_64", - # "numCPU": "8", - # "memory": 16384, - # }], - # "flavoursSupported": [{ - # "flavourId": - # "medium-x86", - # "cpuArchType": - # "ISA_X86_64", - # "supportedOSTypes": [{ - # "architecture": "x86_64", - # "distribution": "UBUNTU", - # "version": "OS_VERSION_UBUNTU_2204_LTS", - # "license": "OS_LICENSE_TYPE_FREE", - # }], - # "numCPU": - # 4, - # "memorySize": - # 8192, - # "storageSize": - # 100, - # }], - # # - # } - aeros_client = ContinuumClient(self.base_url) - ngsild_params = ( - f'format=simplified&type=InfrastructureElement&q=domain=="{zone_id}"' - ) - self.logger.debug( - "Querying infrastructure elements for zone %s with params: %s", - zone_id, - ngsild_params, - ) - # Query the infrastructure elements for the specified zonese - aeros_domain_ies = aeros_client.query_entities(ngsild_params) - # Transform the infrastructure elements into the required format - # and return the details of the edge cloud zone - response = self.transform_infrastructure_elements( - domain_ies=aeros_domain_ies, domain=zone_id - ) - self.logger.debug("Transformed response: %s", response) - # Return the transformed response - return response - - def transform_infrastructure_elements( - self, domain_ies: List[Dict[str, Any]], domain: str - ) -> Dict[str, Any]: - """ - Transform the infrastructure elements into a format suitable for the - edge cloud zone details. - :param domain_ies: List of infrastructure elements - :param domain: The ID of the edge cloud zone - :return: Transformed details of the edge cloud zone - """ - total_cpu = 0 - total_ram = 0 - total_disk = 0 - total_available_ram = 0 - total_available_disk = 0 - - flavours_supported = [] - - for element in domain_ies: - total_cpu += element.get("cpuCores", 0) - total_ram += element.get("ramCapacity", 0) - total_available_ram += element.get("availableRam", 0) - total_disk += element.get("diskCapacity", 0) - total_available_disk += element.get("availableDisk", 0) - - # Create a flavour per machine - flavour = { - "flavourId": f"{element.get('hostname')}-{element.get('containerTechnology')}", - "cpuArchType": f"{element.get('cpuArchitecture')}", - "supportedOSTypes": [ - { - "architecture": f"{element.get('cpuArchitecture')}", - "distribution": f"{element.get('operatingSystem')}", # assume - "version": "OS_VERSION_UBUNTU_2204_LTS", - "license": "OS_LICENSE_TYPE_FREE", - } - ], - "numCPU": element.get("cpuCores", 0), - "memorySize": element.get("ramCapacity", 0), - "storageSize": element.get("diskCapacity", 0), - } - flavours_supported.append(flavour) - - result = { - "zoneId": domain, - "reservedComputeResources": [ - { - "cpuArchType": "ISA_X86_64", - "numCPU": str(total_cpu), - "memory": total_ram, - } - ], - "computeResourceQuotaLimits": [ - { - "cpuArchType": "ISA_X86_64", - "numCPU": str(total_cpu * 2), # Assume quota is 2x total? - "memory": total_ram * 2, - } - ], - "flavoursSupported": flavours_supported, - } - return result diff --git a/service-resource-manager-implementation/src/clients/edgecloud/clients/aeros/config.py b/service-resource-manager-implementation/src/clients/edgecloud/clients/aeros/config.py deleted file mode 100644 index 794cba5..0000000 --- a/service-resource-manager-implementation/src/clients/edgecloud/clients/aeros/config.py +++ /dev/null @@ -1,27 +0,0 @@ -## -# This file is part of the Open SDK -# -# Contributors: -# - Vasilis Pitsilis (vpitsilis@dat.demokritos.gr, vpitsilis@iit.demokritos.gr) -# - Andreas Sakellaropoulos (asakellaropoulos@iit.demokritos.gr) -## -""" -aerOS access configuration -Access tokens need to be provided in environment variables. -""" -# import os - -# aerOS_API_URL = os.environ.get("aerOS_API_URL") -aerOS_API_URL = "harcoded_api" -if not aerOS_API_URL: - raise ValueError("Environment variable 'aerOS_API_URL' is not set.") -# aerOS_ACCESS_TOKEN = os.environ.get("aerOS_ACCESS_TOKEN") -aerOS_ACCESS_TOKEN = "harcoded_access_token" -if not aerOS_ACCESS_TOKEN: - raise ValueError("Environment variable 'aerOS_ACCESS_TOKEN' is not set.") -# aerOS_HLO_TOKEN = os.environ.get("aerOS_HLO_TOKEN") -aerOS_HLO_TOKEN = "harcoded_hlo_token" -if not aerOS_HLO_TOKEN: - raise ValueError("Environment variable 'aerOS_HLO_TOKEN' is not set.") -DEBUG = False -LOG_FILE = ".log/aeros_client.log" diff --git a/service-resource-manager-implementation/src/clients/edgecloud/clients/aeros/continuum_client.py b/service-resource-manager-implementation/src/clients/edgecloud/clients/aeros/continuum_client.py deleted file mode 100644 index 064ad6e..0000000 --- a/service-resource-manager-implementation/src/clients/edgecloud/clients/aeros/continuum_client.py +++ /dev/null @@ -1,170 +0,0 @@ -## -# This file is part of the Open SDK -# -# Contributors: -# - Vasilis Pitsilis (vpitsilis@dat.demokritos.gr, vpitsilis@iit.demokritos.gr) -# - Andreas Sakellaropoulos (asakellaropoulos@iit.demokritos.gr) -## -""" -aerOS REST API Client - This client is used to interact with the aerOS REST API. -""" - -import requests - -from src.edgecloud.clients.aeros import config -from src.edgecloud.clients.aeros.utils import catch_requests_exceptions -from src.logger import setup_logger - - -class ContinuumClient: - """ - Client to aerOS ngsi-ld based continuum exposure - """ - - def __init__(self, base_url: str = None): - """ - :param base_url: the base url of the aerOS API - """ - if base_url is None: - self.api_url = config.aerOS_API_URL - else: - self.api_url = base_url - self.logger = setup_logger(__name__, is_debug=True, file_name=config.LOG_FILE) - self.m2m_cb_token = config.aerOS_ACCESS_TOKEN - self.hlo_token = config.aerOS_HLO_TOKEN - self.headers = { - "Content-Type": "application/json", - "Accept": "application/json", - "aerOS": "true", - "Authorization": f"Bearer {self.m2m_cb_token}", - } - self.hlo_headers = { - "Content-Type": "application/json", - "Accept": "application/json", - "aerOS": "true", - "Authorization": f"Bearer {self.hlo_token}", - } - self.hlo_onboard_headers = { - "Content-Type": "application/yaml", - "Authorization": f"Bearer {self.hlo_token}", - } - - @catch_requests_exceptions - def query_entity(self, entity_id, ngsild_params) -> dict: - """ - Query entity with ngsi-ld params - :input - @param entity_id: the id of the queried entity - @param ngsi-ld: the query params - :output - ngsi-ld object - """ - entity_url = f"{self.api_url}/entities/{entity_id}?{ngsild_params}" - response = requests.get(entity_url, headers=self.headers, timeout=15) - if response is None: - return None - else: - if config.DEBUG: - self.logger.debug("Query entity URL: %s", entity_url) - self.logger.debug( - "Query entity response: %s %s", response.status_code, response.text - ) - return response.json() - - @catch_requests_exceptions - def query_entities(self, ngsild_params): - """ - Query entities with ngsi-ld params - :input - @param ngsi-ld: the query params - :output - ngsi-ld object - """ - entities_url = f"{self.api_url}/entities?{ngsild_params}" - response = requests.get(entities_url, headers=self.headers, timeout=15) - if response is None: - return None - # else: - # if config.DEBUG: - # self.logger.debug("Query entities URL: %s", entities_url) - # self.logger.debug("Query entities response: %s %s", - # response.status_code, response.text) - return response.json() - - @catch_requests_exceptions - def deploy_service(self, service_id: str) -> dict: - """ - Re-allocate (deploy) service on aerOS continuum - :input - @param service_id: the id of the service to be re-allocated - :output - the re-allocated service json object - """ - re_allocate_url = f"{self.api_url}/hlo_fe/services/{service_id}" - response = requests.put(re_allocate_url, headers=self.hlo_headers, timeout=15) - if response is None: - return None - else: - if config.DEBUG: - self.logger.debug("Re-allocate service URL: %s", re_allocate_url) - self.logger.debug( - "Re-allocate service response: %s %s", - response.status_code, - response.text, - ) - return response.json() - - @catch_requests_exceptions - def undeploy_service(self, service_id: str) -> dict: - """ - Undeploy service - :input - @param service_id: the id of the service to be undeployed - :output - the undeployed service json object - """ - undeploy_url = f"{self.api_url}/hlo_fe/services/{service_id}" - response = requests.delete(undeploy_url, headers=self.hlo_headers, timeout=15) - if response is None: - return None - else: - if config.DEBUG: - self.logger.debug("Re-allocate service URL: %s", undeploy_url) - self.logger.debug( - "Undeploy service response: %s %s", - response.status_code, - response.text, - ) - return response.json() - - @catch_requests_exceptions - def onboard_service(self, service_id: str, tosca_str: str) -> dict: - """ - Onboard (& deploy) service on aerOS continuum - :input - @param service_id: the id of the service to onboarded (& deployed) - @param tosca_str: the tosca whith all orchestration information - :output - the allocated service json object - """ - onboard_url = f"{self.api_url}/hlo_fe/services/{service_id}" - if config.DEBUG: - self.logger.debug("Onboard service URL: %s", onboard_url) - self.logger.debug( - "Onboard service request body (TOSCA-YAML): %s", tosca_str - ) - response = requests.post( - onboard_url, data=tosca_str, headers=self.hlo_onboard_headers, timeout=15 - ) - if response is None: - return None - else: - if config.DEBUG: - self.logger.debug("Onboard service URL: %s", onboard_url) - self.logger.debug( - "Onboard service response: %s %s", - response.status_code, - response.text, - ) - return response.json() diff --git a/service-resource-manager-implementation/src/clients/edgecloud/clients/aeros/utils.py b/service-resource-manager-implementation/src/clients/edgecloud/clients/aeros/utils.py deleted file mode 100644 index d4f5cf5..0000000 --- a/service-resource-manager-implementation/src/clients/edgecloud/clients/aeros/utils.py +++ /dev/null @@ -1,43 +0,0 @@ -## -# This file is part of the Open SDK -# -# Contributors: -# - Vasilis Pitsilis (vpitsilis@dat.demokritos.gr, vpitsilis@iit.demokritos.gr) -# - Andreas Sakellaropoulos (asakellaropoulos@iit.demokritos.gr) -## -""" -Docstring -""" -from requests.exceptions import HTTPError, RequestException, Timeout - -import src.edgecloud.clients.aeros.config as config -from src.logger import setup_logger - - -def catch_requests_exceptions(func): - """ - Docstring - """ - logger = setup_logger(__name__, is_debug=True, file_name=config.LOG_FILE) - - def wrapper(*args, **kwargs): - try: - result = func(*args, **kwargs) - return result - except HTTPError as e: - logger.info("4xx or 5xx: %s \n", {e}) - return None # raise our custom exception or log, etc. - except ConnectionError as e: - logger.info( - "Raised for connection-related issues (e.g., DNS resolution failure, network issues): %s \n", - {e}, - ) - return None # raise our custom exception or log, etc. - except Timeout as e: - logger.info("Timeout occured: %s \n", {e}) - return None # raise our custom exception or log, etc. - except RequestException as e: - logger.info("Request failed: %s \n", {e}) - return None # raise our custom exception or log, etc. - - return wrapper diff --git a/service-resource-manager-implementation/src/clients/edgecloud/clients/errors.py b/service-resource-manager-implementation/src/clients/edgecloud/clients/errors.py deleted file mode 100644 index 97b14bc..0000000 --- a/service-resource-manager-implementation/src/clients/edgecloud/clients/errors.py +++ /dev/null @@ -1,5 +0,0 @@ -# -*- coding: utf-8 -*- - - -class EdgeCloudPlatformError(Exception): - pass diff --git a/service-resource-manager-implementation/src/clients/edgecloud/clients/i2edge/__init__.py b/service-resource-manager-implementation/src/clients/edgecloud/clients/i2edge/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/clients/edgecloud/clients/i2edge/client.py b/service-resource-manager-implementation/src/clients/edgecloud/clients/i2edge/client.py deleted file mode 100644 index 20807df..0000000 --- a/service-resource-manager-implementation/src/clients/edgecloud/clients/i2edge/client.py +++ /dev/null @@ -1,241 +0,0 @@ -# -*- coding: utf-8 -*- -## -# Copyright 2025-present by Software Networks Area, i2CAT. -# All rights reserved. -# -# This file is part of the Open SDK -# -# Contributors: -# - Adrián Pino Martínez (adrian.pino@i2cat.net) -# - Sergio Giménez (sergio.gimenez@i2cat.net) -## -from typing import Dict, List, Optional - -from src import logger -from src.edgecloud.core.edgecloud_interface import EdgeCloudManagementInterface - -from . import schemas -from .common import ( - I2EdgeError, - i2edge_delete, - i2edge_get, - i2edge_post, - i2edge_post_multiform_data, -) - -log = logger.get_logger(__name__) - - -class EdgeApplicationManager(EdgeCloudManagementInterface): - def __init__(self, base_url: str): - self.base_url = base_url - - def get_edge_cloud_zones( - self, region: Optional[str] = None, status: Optional[str] = None - ) -> List[dict]: - url = "{}/zones/list".format(self.base_url) - params = {} - try: - response = i2edge_get(url, params=params) - log.info("Availability zones retrieved successfully") - return response - except I2EdgeError as e: - raise e - - def get_edge_cloud_zones_details( - self, zone_id: str, flavour_id: Optional[str] = None - ) -> Dict: - url = "{}zone/{}".format(self.base_url, zone_id) - params = {} - try: - response = i2edge_get(url, params=params) - log.info("Availability zone details retrieved successfully") - return response - except I2EdgeError as e: - raise e - - def _create_artefact( - self, - artefact_id: str, - artefact_name: str, - repo_name: str, - repo_type: str, - repo_url: str, - password: Optional[str] = None, - token: Optional[str] = None, - user_name: Optional[str] = None, - ): - repo_type = schemas.RepoType(repo_type) - url = "{}/artefact".format(self.base_url) - payload = schemas.ArtefactOnboarding( - artefact_id=artefact_id, - name=artefact_name, - repo_password=password, - repo_name=repo_name, - repo_type=repo_type, - repo_url=repo_url, - repo_token=token, - repo_user_name=user_name, - ) - try: - i2edge_post_multiform_data(url, payload) - log.info("Artifact added successfully") - except I2EdgeError as e: - raise e - - def _get_artefact(self, artefact_id: str) -> Dict: - url = "{}/artefact/{}".format(self.base_url, artefact_id) - try: - response = i2edge_get(url, artefact_id) - log.info("Artifact retrieved successfully") - return response - except I2EdgeError as e: - raise e - - def _get_all_artefacts(self) -> List[Dict]: - url = "{}/artefact".format(self.base_url) - try: - response = i2edge_get(url, {}) - log.info("Artifacts retrieved successfully") - return response - except I2EdgeError as e: - raise e - - def _delete_artefact(self, artefact_id: str): - url = "{}/artefact".format(self.base_url) - try: - i2edge_delete(url, artefact_id) - log.info("Artifact deleted successfully") - except I2EdgeError as e: - raise e - - def onboard_app(self, app_manifest: Dict) -> Dict: - try: - app_id = app_manifest["appId"] - artefact_id = app_id - - app_component_spec = schemas.AppComponentSpec(artefactId=artefact_id) - data = schemas.ApplicationOnboardingData( - app_id=app_id, appComponentSpecs=[app_component_spec] - ) - payload = schemas.ApplicationOnboardingRequest(profile_data=data) - url = "{}/application/onboarding".format(self.base_url) - i2edge_post(url, payload) - except I2EdgeError as e: - raise e - except KeyError as e: - raise I2EdgeError("Missing required field in app_manifest: {}".format(e)) - - def delete_onboarded_app(self, app_id: str) -> None: - url = "{}/application/onboarding".format(self.base_url) - try: - i2edge_delete(url, app_id) - except I2EdgeError as e: - raise e - - def get_onboarded_app(self, app_id: str) -> Dict: - url = "{}/application/onboarding/{}".format(self.base_url, app_id) - try: - response = i2edge_get(url, app_id) - return response - except I2EdgeError as e: - raise e - - def get_all_onboarded_apps(self) -> List[Dict]: - url = "{}/applications/onboarding".format(self.base_url) - params = {} - try: - response = i2edge_get(url, params) - return response - except I2EdgeError as e: - raise e - - def _select_best_flavour_for_app(self, zone_id) -> str: - """ - Selects the best flavour for the specified app requirements in a given zone. - """ - # list_of_flavours = self.get_edge_cloud_zones_details(zone_id) - # - # TODO - Harcoded - flavourId = "67f3a0b0e3184a85952e174d" - return flavourId - - def deploy_app(self, app_id: str, app_zones: List[Dict]) -> Dict: - appId = app_id - app = self.get_onboarded_app(appId) - profile_data = app["profile_data"] - appProviderId = profile_data["appProviderId"] - appVersion = profile_data["appMetaData"]["version"] - # TODO: Iterate in the list; deploy the app in all zones - zone_info = app_zones[0]["EdgeCloudZone"] - zone_id = zone_info["edgeCloudZoneId"] - flavourId = self._select_best_flavour_for_app(zone_id=zone_id) - app_deploy_data = schemas.AppDeployData( - appId=appId, - appProviderId=appProviderId, - appVersion=appVersion, - zoneInfo=schemas.ZoneInfo(flavourId=flavourId, zoneId=zone_id), - ) - url = "{}/app/".format(self.base_url) - payload = schemas.AppDeploy(app_deploy_data=app_deploy_data) - try: - response = i2edge_post(url, payload) - log.info("App deployed successfully") - print(response) - return response - except I2EdgeError as e: - raise e - - def get_all_deployed_apps(self) -> List[Dict]: - url = "{}/app/".format(self.base_url) - params = {} - try: - response = i2edge_get(url, params=params) - log.info("All app instances retrieved successfully") - return response - except I2EdgeError as e: - raise e - - def get_deployed_app(self, app_id, zone_id) -> List[Dict]: - # Logic: Get all onboarded apps and filter the one where release_name == artifact name - - # Step 1) Extract "app_name" from the onboarded app using the "app_id" - onboarded_app = self.get_onboarded_app(app_id) - if not onboarded_app: - raise ValueError(f"No onboarded app found with ID: {app_id}") - - try: - app_name = onboarded_app["profile_data"]["appMetaData"]["appName"] - except KeyError as e: - raise ValueError(f"Onboarded app missing required field: {e}") - - # Step 2) Retrieve all deployed apps and filter the one(s) where release_name == app_name - deployed_apps = self.get_all_deployed_apps() - if not deployed_apps: - return [] - - # Filter apps where release_name matches our app_name and zone matches - for app_instance_name in deployed_apps: - if ( - app_instance_name.get("release_name") == app_name - and app_instance_name.get("zone_id") == zone_id - ): - return app_instance_name - return None - - url = "{}/app/{}/{}".format(self.base_url, zone_id, app_instance_name) - params = {} - try: - response = i2edge_get(url, params=params) - log.info("App instance retrieved successfully") - return response - except I2EdgeError as e: - raise e - - def undeploy_app(self, app_instance_id: str) -> None: - url = "{}/app".format(self.base_url) - try: - i2edge_delete(url, app_instance_id) - log.info("App instance deleted successfully") - except I2EdgeError as e: - raise e diff --git a/service-resource-manager-implementation/src/clients/edgecloud/clients/i2edge/common.py b/service-resource-manager-implementation/src/clients/edgecloud/clients/i2edge/common.py deleted file mode 100644 index d4cda49..0000000 --- a/service-resource-manager-implementation/src/clients/edgecloud/clients/i2edge/common.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -## -# Copyright 2025-present by Software Networks Area, i2CAT. -# All rights reserved. -# -# This file is part of the Open SDK -# -# Contributors: -# - Sergio Giménez (sergio.gimenez@i2cat.net) -## -import json -from typing import Optional - -import requests -from pydantic import BaseModel - -from src import logger -from src.edgecloud.clients.errors import EdgeCloudPlatformError - -log = logger.get_logger(__name__) - - -class I2EdgeError(EdgeCloudPlatformError): - pass - - -class I2EdgeErrorResponse(BaseModel): - message: str - detail: dict - - -def get_error_message_from(response: requests.Response) -> str: - try: - error_response = I2EdgeErrorResponse(**response.json()) - return error_response.message - except Exception as e: - log.error("Failed to parse error response from i2edge: {}".format(e)) - return response.text - - -def i2edge_post(url: str, model_payload: BaseModel) -> dict: - headers = { - "Content-Type": "application/json", - "accept": "application/json", - } - json_payload = json.dumps(model_payload.model_dump(mode="json")) - try: - response = requests.post(url, data=json_payload, headers=headers) - response.raise_for_status() - return response.json() - except requests.exceptions.HTTPError as e: - i2edge_err_msg = get_error_message_from(response) - err_msg = "Failed to deploy app: {}. Detail: {}".format(i2edge_err_msg, e) - log.error(err_msg) - raise I2EdgeError(err_msg) - - -def i2edge_post_multiform_data(url: str, model_payload: BaseModel) -> dict: - headers = { - "accept": "application/json", - } - payload_dict = model_payload.model_dump(mode="json") - payload_in_str = {k: str(v) for k, v in payload_dict.items()} - try: - response = requests.post(url, data=payload_in_str, headers=headers) - response.raise_for_status() - return response.json() - except requests.exceptions.HTTPError as e: - i2edge_err_msg = get_error_message_from(response) - err_msg = "Failed to deploy app: {}. Detail: {}".format(i2edge_err_msg, e) - log.error(err_msg) - raise I2EdgeError(err_msg) - - -def i2edge_delete(url: str, id: str) -> dict: - headers = {"accept": "application/json"} - try: - query = "{}/{}".format(url, id) - response = requests.delete(query, headers=headers) - response.raise_for_status() - return response.json() - except requests.exceptions.HTTPError as e: - i2edge_err_msg = get_error_message_from(response) - err_msg = "Failed to undeploy app: {}. Detail: {}".format(i2edge_err_msg, e) - log.error(err_msg) - raise I2EdgeError(err_msg) - - -def i2edge_get(url: str, params: Optional[dict]): - headers = {"accept": "application/json"} - try: - response = requests.get(url, params=params, headers=headers) - response.raise_for_status() - return response.json() - except requests.exceptions.HTTPError as e: - i2edge_err_msg = get_error_message_from(response) - err_msg = "Failed to get apps: {}. Detail: {}".format(i2edge_err_msg, e) - log.error(err_msg) - raise I2EdgeError(err_msg) diff --git a/service-resource-manager-implementation/src/clients/edgecloud/clients/i2edge/schemas.py b/service-resource-manager-implementation/src/clients/edgecloud/clients/i2edge/schemas.py deleted file mode 100644 index c0d522f..0000000 --- a/service-resource-manager-implementation/src/clients/edgecloud/clients/i2edge/schemas.py +++ /dev/null @@ -1,167 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -## -# Copyright 2025-present by Software Networks Area, i2CAT. -# All rights reserved. -# -# This file is part of the Open SDK -# -# Contributors: -# - Sergio Giménez (sergio.gimenez@i2cat.net) -# - César Cajas (cesar.cajas@i2cat.net) -## -from enum import Enum -from typing import List, Optional - -from pydantic import BaseModel, ConfigDict, Field, field_validator - - -class ZoneInfo(BaseModel): - flavourId: str - zoneId: str - - -class AppParameters(BaseModel): - namespace: Optional[str] = None - - -class AppDeployData(BaseModel): - appId: str - appProviderId: str - appVersion: str - zoneInfo: ZoneInfo - - -class AppDeploy(BaseModel): - app_deploy_data: AppDeployData - app_parameters: Optional[AppParameters] = Field(default=AppParameters()) - - -# Artefact - - -class RepoType(str, Enum): - UPLOAD = "UPLOAD" - PUBLICREPO = "PUBLICREPO" - PRIVATEREPO = "PRIVATEREPO" - - -class ArtefactOnboarding(BaseModel): - artefact_id: str - name: str - # chart: Optional[bytes] = Field(default=None) # XXX AFAIK not supported by CAMARA. - repo_password: Optional[str] = None - repo_name: Optional[str] = None - repo_type: RepoType - repo_url: Optional[str] = None - repo_token: Optional[str] = None - repo_user_name: Optional[str] = None - model_config = ConfigDict(use_enum_values=True) - - -# Application Onboarding - -# XXX Leaving default values since i2edge only cares about appid and artifactid, at least for now. - - -class AppComponentSpec(BaseModel): - artefactId: str - componentName: str = Field(default="default_component") - serviceNameEW: str = Field(default="default_ew_service") - serviceNameNB: str = Field(default="default_nb_service") - - -class AppMetaData(BaseModel): - appDescription: str = Field(default="Default app description") - appName: str = Field(default="Default App") - category: str = Field(default="DEFAULT") - mobilitySupport: bool = Field(default=False) - version: str = Field(default="1.0") - - -class AppQoSProfile(BaseModel): - appProvisioning: bool = Field(default=True) - bandwidthRequired: int = Field(default=1) - latencyConstraints: str = Field(default="NONE") - multiUserClients: str = Field(default="APP_TYPE_SINGLE_USER") - noOfUsersPerAppInst: int = Field(default=1) - - -class ApplicationOnboardingData(BaseModel): - appComponentSpecs: List[AppComponentSpec] - appDeploymentZones: List[str] = Field(default=["default_zone"]) - app_id: str - appMetaData: AppMetaData = Field(default_factory=AppMetaData) - appProviderId: str = Field(default="default_provider") - appQoSProfile: AppQoSProfile = Field(default_factory=AppQoSProfile) - appStatusCallbackLink: Optional[str] = None - - -class ApplicationOnboardingRequest(BaseModel): - profile_data: ApplicationOnboardingData - - -# Flavour - - -class GPU(BaseModel): - gpuMemory: int = Field(default=0, description="GPU memory in MB") - gpuModeName: str = Field(default="", description="GPU mode name") - gpuVendorType: str = Field( - default="GPU_PROVIDER_NVIDIA", description="GPU vendor type" - ) - numGPU: int = Field(..., description="Number of GPUs") - - -class Hugepages(BaseModel): - number: int = Field(default=0, description="Number of hugepages") - pageSize: str = Field(default="2MB", description="Size of hugepages") - - -class SupportedOSTypes(BaseModel): - architecture: str = Field(default="x86_64", description="OS architecture") - distribution: str = Field(default="RHEL", description="OS distribution") - license: str = Field(default="OS_LICENSE_TYPE_FREE", description="OS license type") - version: str = Field(default="OS_VERSION_UBUNTU_2204_LTS", description="OS version") - - -class FlavourSupported(BaseModel): - cpuArchType: str = Field(default="ISA_X86", description="CPU architecture type") - cpuExclusivity: bool = Field(default=True, description="CPU exclusivity") - fpga: int = Field(default=0, description="Number of FPGAs") - gpu: Optional[List[GPU]] = Field(default=None, description="List of GPUs") - hugepages: List[Hugepages] = Field( - default_factory=lambda: [Hugepages()], description="List of hugepages" - ) - memorySize: str = Field(..., description="Memory size (e.g., '1024MB' or '2GB')") - numCPU: int = Field(..., description="Number of CPUs") - storageSize: int = Field(default=0, description="Storage size in GB") - supportedOSTypes: List[SupportedOSTypes] = Field( - default_factory=lambda: [SupportedOSTypes()], - description="List of supported OS types", - ) - vpu: int = Field(default=0, description="Number of VPUs") - - @field_validator("memorySize") - @classmethod - def validate_memory_size(cls, v): - if not (v.endswith("MB") or v.endswith("GB")): - raise ValueError("memorySize must end with MB or GB") - try: - int(v[:-2]) - except ValueError: - raise ValueError("memorySize must be a number followed by MB or GB") - return v - - -class Flavour(BaseModel): - flavour_supported: FlavourSupported - - -# EdgeCloud Zones - - -class Zone(BaseModel): - geographyDetails: str - geolocation: str - zoneId: str diff --git a/service-resource-manager-implementation/src/clients/edgecloud/clients/i2edge/utils.py b/service-resource-manager-implementation/src/clients/edgecloud/clients/i2edge/utils.py deleted file mode 100644 index bee5e94..0000000 --- a/service-resource-manager-implementation/src/clients/edgecloud/clients/i2edge/utils.py +++ /dev/null @@ -1,151 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -## -# Copyright 2025-present by Software Networks Area, i2CAT. -# All rights reserved. -# -# This file is part of the Open SDK -# -# Contributors: -# - Sergio Giménez (sergio.gimenez@i2cat.net) -# - César Cajas (cesar.cajas@i2cat.net) -## -import uuid -from typing import Optional, Union -from uuid import UUID - -from src.edgecloud import logger -from src.edgecloud.api.routers.lcm.schemas import RequiredResources -from src.edgecloud.core import utils as core_utils - -from .client import I2EdgeClient -from .common import I2EdgeError - -log = logger.get_logger(__name__) - - -def generate_namespace_name_from(app_id: str, app_instance_id: str) -> str: - max_length = 63 - combined_name = "{}-{}".format(app_id, app_instance_id) - if len(combined_name) > max_length: - combined_name = combined_name[:max_length] - return combined_name - - -def generate_unique_id() -> UUID: - return uuid.uuid4() - - -def instantiate_app_with( - camara_app_id: UUID, - zone_id: str, - required_resources: RequiredResources, - i2edge: I2EdgeClient, -) -> tuple[str, str]: - memory_size_str = "{}GB".format(required_resources.memory + 1) - num_gpus = core_utils.get_num_gpus_from(required_resources) - try: - flavour_id = i2edge.create_flavour( - zone_id=zone_id, - memory_size=memory_size_str, - num_cpu=required_resources.numCPU, - num_gpus=num_gpus, - ) - i2edge_instance_id = generate_unique_id() - application_k8s_namespace = generate_namespace_name_from( - str(camara_app_id), str(i2edge_instance_id) - ) - i2edge.deploy_app( - appId=str(camara_app_id), - zoneId=zone_id, - flavourId=flavour_id, - namespace=application_k8s_namespace, - ) - return flavour_id, application_k8s_namespace - except I2EdgeError as e: - err_msg = "Error instantiating app {} in zone {}".format(camara_app_id, zone_id) - log.error("{}. Detailed error: {}".format(err_msg, e)) - raise e - - -def onboard_app_with( - application_id: UUID, - artefact_id: UUID, - app_name: str, - app_version: Optional[str], # TODO pass this to i2edge - repo_type: str, - app_repo: str, - user_name: Optional[str], - password: Optional[str], - token: Optional[str], - i2edge: I2EdgeClient, -): - try: - # TODO Come back to handle errors when onboarding and perform rollbacks - i2edge.create_artefact( - artefact_id=str(artefact_id), - artefact_name=app_name, - repo_name=app_name, - repo_type=repo_type, - repo_url=app_repo, - user_name=user_name, - password=password, - token=token, - ) - - i2edge.onboard_app(app_id=str(application_id), artefact_id=str(application_id)) - except I2EdgeError as e: - err_msg = "Error onboarding app {} in i2edge".format(app_name) - log.error("{}. Detailed error: {}".format(err_msg, e)) - raise e - - -def delete_app_instance_by( - namespace: str, flavour_id: str, zone_id: str, i2edge: I2EdgeClient -): - i2edge_app_instance_name = get_app_name_from(namespace, i2edge) - if i2edge_app_instance_name is None: - err_msg = "Couldn't retrieve app instance from I2Edge." - log.error(err_msg) - raise I2EdgeError(err_msg) - i2edge.undeploy_app(i2edge_app_instance_name) - i2edge.delete_flavour(flavour_id=str(flavour_id), zone_id=zone_id) - - -def get_app_name_from(namespace: str, i2edge: I2EdgeClient) -> Union[str, None]: - try: - response = i2edge.get_all_deployed_apps() - for deployment in response: - if deployment.get("bodytosend", {}).get("namespace") == namespace: - return deployment.get("name") - return None - except I2EdgeError as e: - err_msg = "Error getting app name for namespace {}".format(namespace) - log.error("{}. Detailed error: {}".format(err_msg, e)) - raise e - - -def delete_app_by(app_id: UUID, artefact_id: UUID, i2edge: I2EdgeClient): - try: - i2edge.delete_onboarded_app(app_id=str(app_id)) - i2edge.delete_artefact(artefact_id=str(artefact_id)) - except I2EdgeError as e: - err_msg = "Error deleting app {}".format(app_id) - log.error("{}. Detailed error: {}".format(err_msg, e)) - raise e - - -def get_edgecloud_zones(i2edge: I2EdgeClient) -> list[str]: - try: - zone_ids = [] - response = i2edge.get_zones_list() - for zone in response: - zone_id = zone.get("zoneId") - if zone_id is not None: - zone_ids.append(zone_id) - return zone_ids - - except I2EdgeError as e: - err_msg = "Error getting zones from i2edge" - log.error("{}. Detailed error: {}".format(err_msg, e)) - raise e diff --git a/service-resource-manager-implementation/src/clients/edgecloud/clients/piedge/__init__.py b/service-resource-manager-implementation/src/clients/edgecloud/clients/piedge/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/clients/edgecloud/clients/piedge/client.py b/service-resource-manager-implementation/src/clients/edgecloud/clients/piedge/client.py deleted file mode 100644 index b842713..0000000 --- a/service-resource-manager-implementation/src/clients/edgecloud/clients/piedge/client.py +++ /dev/null @@ -1,158 +0,0 @@ -# Mocked API for testing purposes -from typing import Dict, List, Optional -import os -import logging -import requests -from src.clients.edgecloud.core.edgecloud_interface import EdgeCloudManagementInterface -from src.clients.edgecloud.clients.piedge.lib.utils.connector_db import ConnectorDB -from src.clients.edgecloud.clients.piedge.lib.utils.kubernetes_connector import KubernetesConnector -from src.clients.edgecloud.clients.piedge.lib.models.service_function_registration_request import ServiceFunctionRegistrationRequest -from src.clients.edgecloud.clients.piedge.lib.models.deploy_service_function import DeployServiceFunction -from src.clients.edgecloud.clients.piedge.lib.models.app_manifest import AppManifest -from src.clients.edgecloud.clients.piedge.lib.core.piedge_encoder import deploy_service_function - - -class EdgeApplicationManager(EdgeCloudManagementInterface): - - def __init__(self, base_url: str, **kwargs): - self.kubernetes_host = base_url - self.edge_cloud_provider = kwargs.get('PLATFORM_PROVIDER') - kubernetes_token = kwargs.get('KUBERNETES_MASTER_TOKEN') - kubernetes_port = kwargs.get('KUBERNETES_MASTER_PORT') - storage_uri = kwargs.get('EMP_STORAGE_URI') - username = kwargs.get('KUBERNETES_USERNAME') - if self.kubernetes_host is not None: - self.k8s_connector = KubernetesConnector(ip=self.kubernetes_host, port=kubernetes_port, token=kubernetes_token, username=username) - if storage_uri is not None: - self.connector_db = ConnectorDB(storage_uri) - - - def onboard_app(self, app_manifest: AppManifest) -> Dict: - print(f"Submitting application: {app_manifest}") - logging.info('Extracting variables from payload...') - app_name = app_manifest.get('name') - image = app_manifest.get('appRepo').get('imagePath') - package_type = app_manifest.get('packageType') - network_interfaces = app_manifest.get('componentSpec')[0].get('networkInterfaces') - ports = [] - for ni in network_interfaces: - ports.append(ni.get('port')) - insert_doc = ServiceFunctionRegistrationRequest(service_function_image=image, service_function_name=app_name, service_function_type=package_type, application_ports=ports) - result = self.connector_db.insert_document_service_function(insert_doc.to_dict()) - if type(result) is str: - return result - return {'appId': str(result.inserted_id)} - - def get_all_onboarded_apps(self) -> List[Dict]: - logging.info('Retrieving all registered apps from database...') - db_list = self.connector_db.get_documents_from_collection(collection_input="service_functions") - app_list = [] - for sf in db_list: - app_list.append(self.__transform_to_camara(sf)) - return app_list - # return [{"appId": "1234-5678", "name": "TestApp"}] - - def get_onboarded_app(self, app_id: str) -> Dict: - logging.info('Searching for registered app with ID: '+ app_id+' in database...') - app = self.connector_db.get_documents_from_collection("service_functions", input_type="_id", input_value=app_id) - if len(app)>0: - return self.__transform_to_camara(app[0]) - else: - return [] - - def delete_onboarded_app(self, app_id: str) -> None: - logging.info('Deleting registered app with ID: '+ app_id+' from database...') - result, code = self.connector_db.delete_document_service_function(_id=app_id) - print(f"Removing application metadata: {app_id}") - # return result, code - - def deploy_app(self, app_id: str, app_zones: List[Dict]) -> Dict: - logging.info('Searching for registered app with ID: '+ app_id+' in database...') - app = self.connector_db.get_documents_from_collection("service_functions", input_type="_id", input_value=app_id) - # success_response = [] - result = None - if len(app)<1: - return 'Application with ID: '+ app_id+' not found', 404 - if app is not None: - for zone in app_zones: - sf = DeployServiceFunction(service_function_name=app[0].get('name'), - service_function_instance_name=app[0].get('name')+'-'+zone.get('EdgeCloudZone').get('edgeCloudZoneName'), - location=zone.get('edgeCloudZoneName')) - result = deploy_service_function(service_function=sf, connector_db=self.connector_db, kubernetes_connector=self.k8s_connector) - logging.info(result) - # success_response.append(result) - return {"appInstanceId": result} - - - def get_all_deployed_apps(self, app_id: Optional[str] = None, app_instance_id: Optional[str] = None, region: Optional[str] = None) -> List[Dict]: - logging.info('Retreiving all deployed apps in the edge cloud platform') - deployments = self.k8s_connector.get_deployed_service_functions(self.connector_db) - response = [] - for deployment in deployments: - item = {} - item['appInstanceId'] = deployment.get('uid') - item['status'] = deployment.get('status') - item['componentEndpointInfo'] = {} - item['kubernetesClusterRef'] = "" - item['edgeCloudZone'] = {} - response.append(item) - return response - # return [{"appInstanceId": "abcd-efgh", "status": "ready"}] - - def undeploy_app(self, app_instance_id: str) -> None: - logging.info('Searching for deployed app with ID: '+ app_instance_id+' in database...') - print(f"Deleting app instance: {app_instance_id}") - sfs=self.k8s_connector.get_deployed_service_functions(self.connector_db) - response = 'App instance with ID ['+app_instance_id+'] not found' - for service_fun in sfs: - if service_fun["uid"]==app_instance_id: - self.k8s_connector.delete_service_function(self.connector_db, service_fun['service_function_instance_name']) - response = 'App instance with ID ['+app_instance_id+'] successfully removed' - break - return response - - - def get_edge_cloud_zones(self, region: Optional[str] = None, status: Optional[str] = None) -> List[Dict]: - - nodes_response = self.k8s_connector.get_PoPs() - zone_list =[] - - for node in nodes_response: - zone = {} - zone['edgeCloudZoneId'] = node.get('uid') - zone['edgeCloudZoneName'] = node.get('name') - zone['edgeCloudZoneStatus'] = node.get('status') - zone['edgeCloudProvider'] = self.edge_cloud_provider - zone['edgeCloudRegion'] = node.get('location') - zone_list.append(zone) - return zone_list - - def get_edge_cloud_zones_details(self, zone_id: str, flavour_id: Optional[str] = None ) -> Dict: - node_details = self.k8s_connector.get_node_details(zone_id) - node_details = {} - labels = node_details.get('metadata').get('labels') - status = node_details.get('status') - arch_type = labels.get('beta.kubernetes.io/arch') - computeResourceQuotaLimits = [{'cpuArchType': arch_type, 'numCPU': status.get('capacity').get('cpu'), 'memory': int(status.get('capacity').get('memory'))/(1024*1024)}] - reservedComputeResources = [{'cpuArchType': arch_type, 'numCPU': status.get('allocatable').get('cpu'), 'memory': int(status.get('allocatable').get('memory'))/(1024*1024)}] - flavoursSupported = [] - node_details['computeResourceQuotaLimits'] = computeResourceQuotaLimits - node_details['reservedComputeResources'] = reservedComputeResources - node_details['flavoursSupported'] = flavoursSupported - node_details['zoneId'] = zone_id - return node_details - - - def __transform_to_camara(self, app_data): - app = {} - app['appId'] = app_data.get('_id') - app['name'] = app_data.get('name') - app['packageType'] = app_data.get('type') - appRepo = {'imagePath': app_data.get('image')} - app['appRepo'] = appRepo - networkInterfaces = [] - for port in app_data.get('application_ports'): - port_spec = {'protocol': 'TCP', 'port': port} - networkInterfaces.append(port_spec) - app['componentSpec'] = [{'componentName': app_data.get('name'), 'networkInterfaces': networkInterfaces}] - return app diff --git a/service-resource-manager-implementation/src/clients/edgecloud/core/__init__.py b/service-resource-manager-implementation/src/clients/edgecloud/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/clients/edgecloud/core/edgecloud_interface.py b/service-resource-manager-implementation/src/clients/edgecloud/core/edgecloud_interface.py deleted file mode 100644 index 7dd2c6f..0000000 --- a/service-resource-manager-implementation/src/clients/edgecloud/core/edgecloud_interface.py +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -## -# Copyright 2025-present by Software Networks Area, i2CAT. -# All rights reserved. -# -# This file is part of the Open SDK -# -# Contributors: -# - Adrián Pino Martínez (adrian.pino@i2cat.net) -## -from abc import ABC, abstractmethod -from typing import Dict, List, Optional - - -class EdgeCloudManagementInterface(ABC): - """ - Abstract Base Class for Edge Application Management. - """ - - @abstractmethod - def onboard_app(self, app_manifest: Dict) -> Dict: - """ - Onboards an app, submitting application metadata - to the Edge Cloud Provider. - - :param app_manifest: Application metadata in dictionary format. - :return: Dictionary containing created application details. - """ - pass - - @abstractmethod - def get_all_onboarded_apps(self) -> List[Dict]: - """ - Retrieves a list of onboarded applications. - - :return: List of application metadata dictionaries. - """ - pass - - @abstractmethod - def get_onboarded_app(self, app_id: str) -> Dict: - """ - Retrieves information of a specific onboarded application. - - :param app_id: Unique identifier of the application. - :return: Dictionary with application details. - """ - pass - - @abstractmethod - def delete_onboarded_app(self, app_id: str) -> None: - """ - Deletes an application onboarded from the Edge Cloud Provider. - - :param app_id: Unique identifier of the application. - """ - pass - - @abstractmethod - def deploy_app(self, app_id: str, app_zones: List[Dict]) -> Dict: - """ - Requests the instantiation of an application instance. - - :param app_id: Unique identifier of the application. - :param app_zones: List of Edge Cloud Zones where the app should be - instantiated. - :return: Dictionary with instance details. - """ - pass - - @abstractmethod - def get_all_deployed_apps( - self, - app_id: Optional[str] = None, - app_instance_id: Optional[str] = None, - region: Optional[str] = None, - ) -> List[Dict]: - """ - Retrieves information of application instances. - - :param app_id: Filter by application ID. - :param app_instance_id: Filter by instance ID. - :param region: Filter by Edge Cloud region. - :return: List of application instance details. - """ - pass - - @abstractmethod - def undeploy_app(self, app_instance_id: str) -> None: - """ - Terminates a specific application instance. - - :param app_instance_id: Unique identifier of the application instance. - """ - pass - - @abstractmethod - def get_edge_cloud_zones( - self, region: Optional[str] = None, status: Optional[str] = None - ) -> List[Dict]: - """ - Retrieves a list of available Edge Cloud Zones. - - :param region: Filter by geographical region. - :param status: Filter by status (active, inactive, unknown). - :return: List of Edge Cloud Zones. - """ - pass - - @abstractmethod - def get_edge_cloud_zones_details( - self, federation_context_id: str, zone_id: str - ) -> Dict: - """ - Retrieves details of a specific Edge Cloud Zone reserved - for the specified zone by the partner OP. - - :param federation_context_id: Identifier of the federation context. - :param zone_id: Unique identifier of the Edge Cloud Zone. - :return: Dictionary with Edge Cloud Zone details. - """ - pass diff --git a/service-resource-manager-implementation/src/clients/logger.py b/service-resource-manager-implementation/src/clients/logger.py deleted file mode 100644 index 14c3f6b..0000000 --- a/service-resource-manager-implementation/src/clients/logger.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -## -# Copyright 2025-present by Software Networks Area, i2CAT. -# All rights reserved. -# -# This file is part of the Open SDK -# -# Contributors: -# - Sergio Giménez (sergio.gimenez@i2cat.net) -## -import logging -import sys -from pathlib import Path - -from colorlog import ColoredFormatter - -APP_LOGGER_NAME = "edgecloud" -COLORED_FORMATERR = ( - "%(log_color)s%(levelname)s%(reset)s | " - "[%(log_color)s%(name)s%(reset)s:%(log_color)s%(lineno)d%(reset)s] " - "%(log_color)s%(message)s%(reset)s" -) -FILE_FORMATTER = "[%(asctime)s] {%(name)s: %(lineno)d} %(levelname)s - %(message)s" - - -def setup_logger(logger_name=APP_LOGGER_NAME, is_debug=True, file_name=None): - logger = logging.getLogger(logger_name) - logger.setLevel(logging.DEBUG if is_debug else logging.INFO) - - colored_formatter = ColoredFormatter(COLORED_FORMATERR) - sh = logging.StreamHandler(sys.stdout) - sh.setFormatter(colored_formatter) - logger.handlers.clear() - logger.addHandler(sh) - - if file_name: - log_path = Path(file_name) - log_path.parent.mkdir(parents=True, exist_ok=True) - fh = logging.FileHandler(file_name) - fh.setFormatter(logging.Formatter(FILE_FORMATTER)) - logger.addHandler(fh) - - return logger - - -def get_logger(module_name): - return logging.getLogger(APP_LOGGER_NAME).getChild(module_name) diff --git a/service-resource-manager-implementation/src/clients/network/__init__.py b/service-resource-manager-implementation/src/clients/network/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/clients/network/clients/__init__.py b/service-resource-manager-implementation/src/clients/network/clients/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/clients/network/clients/errors.py b/service-resource-manager-implementation/src/clients/network/clients/errors.py deleted file mode 100644 index 6497bd8..0000000 --- a/service-resource-manager-implementation/src/clients/network/clients/errors.py +++ /dev/null @@ -1,3 +0,0 @@ -# -*- coding: utf-8 -*- -class NetworkPlatformError(Exception): - pass diff --git a/service-resource-manager-implementation/src/clients/network/clients/oai/__init__.py b/service-resource-manager-implementation/src/clients/network/clients/oai/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/clients/network/clients/oai/client.py b/service-resource-manager-implementation/src/clients/network/clients/oai/client.py deleted file mode 100644 index a1d4766..0000000 --- a/service-resource-manager-implementation/src/clients/network/clients/oai/client.py +++ /dev/null @@ -1,139 +0,0 @@ -## -# Copyright (c) 2025 Netsoft Group, EURECOM. -# All rights reserved. -# -# This file is part of the Open SDK -# -# Contributors: -# - Giulio Carota (giulio.carota@eurecom.fr) -## - - -from src import logger -from src.network.core.network_interface import NetworkManagementInterface -from src.network.core.schemas import ( - AsSessionWithQoSSubscription, - CreateSession, - CreateTrafficInfluence, - FlowInfo, - Snssai, - TrafficInfluSub, -) - -log = logger.get_logger(__name__) -supportedQos = ["qos-e", "qos-s", "qos-m", "qos-l"] - - -class NetworkManager(NetworkManagementInterface): - def __init__(self, base_url: str, scs_as_id: str = None): - """ - Initialize Network Client for OAI Core Network - The currently supported features are: - - QoD - - Traffic Influence - """ - try: - super().__init__() - self.base_url = base_url - self.scs_as_id = scs_as_id - log.info( - f"Initialized OaiNefClient with base_url: {self.base_url} and scs_as_id: {self.scs_as_id}" - ) - - except Exception as e: - log.error(f"Failed to initialize OaiNefClient: {e}") - raise e - - def core_specific_qod_validation(self, session_info: CreateSession): - """ - Validates core-specific parameters for the session creation. - - args: - session_info: The session information to validate. - - raises: - ValidationError: If the session information does not meet core-specific requirements. - """ - if session_info.qosProfile.root not in supportedQos: - raise OaiValidationError( - f"QoS profile {session_info.qosProfile} not supported by OAI, supported profiles are {supportedQos}" - ) - - if session_info.device is None or session_info.device.ipv4Address is None: - raise OaiValidationError("OAI requires UE IPv4 Address to activate QoS") - - if session_info.applicationServer.ipv4Address is None: - raise OaiValidationError("OAI requires App IPv4 Address to activate QoS") - return - - def add_core_specific_qod_parameters( - self, - session_info: CreateSession, - subscription: AsSessionWithQoSSubscription, - ) -> None: - device_ip = _retrieve_ue_ipv4(session_info) - server_ip = _retrieve_app_ipv4(session_info) - - # build flow descriptor in oai format using device ip and server ip - flow_descriptor = f"permit out ip from {device_ip}/32 to {server_ip}/32" - _add_qod_flow_descriptor(subscription, flow_descriptor) - _add_qod_snssai(subscription, 1, "FFFFFF") - subscription.dnn = "oai" - - def add_core_specific_ti_parameters( - self, - traffic_influence_info: CreateTrafficInfluence, - subscription: TrafficInfluSub, - ): - # todo oai add dnn, ssnai, afServiceId - subscription.dnn = "oai" - subscription.add_snssai(1, "FFFFFF") - subscription.afServiceId = self.scs_as_id - - def core_specific_traffic_influence_validation( - self, traffic_influence_info: CreateTrafficInfluence - ) -> None: - """ - Validates core-specific parameters for the session creation. - - args: - session_info: The session information to validate. - - raises: - ValidationError: If the session information does not meet core-specific requirements. - """ - # Placeholder for core-specific validation logic - # This method should be overridden by subclasses if needed - - if ( - traffic_influence_info.device is None - or traffic_influence_info.device.ipv4Address is None - ): - raise OaiValidationError( - "OAI requires UE IPv4 Address to activate Traffic Influence" - ) - - -def _retrieve_ue_ipv4(session_info: CreateSession): - return session_info.device.ipv4Address.root.privateAddress - - -def _retrieve_app_ipv4(session_info: CreateSession): - return session_info.applicationServer.ipv4Address - - -def _add_qod_flow_descriptor( - qos_sub: AsSessionWithQoSSubscription, flow_desriptor: str -): - qos_sub.flowInfo = list() - qos_sub.flowInfo.append( - FlowInfo(flowId=len(qos_sub.flowInfo) + 1, flowDescriptions=[flow_desriptor]) - ) - - -def _add_qod_snssai(qos_sub: AsSessionWithQoSSubscription, sst: int, sd: str = None): - qos_sub.snssai = Snssai(sst=sst, sd=sd) - - -class OaiValidationError(Exception): - pass diff --git a/service-resource-manager-implementation/src/clients/network/clients/open5gcore/__init__.py b/service-resource-manager-implementation/src/clients/network/clients/open5gcore/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/clients/network/clients/open5gcore/client.py b/service-resource-manager-implementation/src/clients/network/clients/open5gcore/client.py deleted file mode 100644 index 2d9d397..0000000 --- a/service-resource-manager-implementation/src/clients/network/clients/open5gcore/client.py +++ /dev/null @@ -1,14 +0,0 @@ -# -*- coding: utf-8 -*- -from src import logger -from src.network.core.network_interface import NetworkManagementInterface - -log = logger.get_logger(__name__) - - -# TODO: Define any specific parameters or methods needed for Open5GCore -# In case any functionality is not implemented, raise NotImplementedError - - -class NetworkManager(NetworkManagementInterface): - def __init__(self, base_url: str, scs_as_id: str): - pass diff --git a/service-resource-manager-implementation/src/clients/network/clients/open5gs/__init__.py b/service-resource-manager-implementation/src/clients/network/clients/open5gs/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/clients/network/clients/open5gs/client.py b/service-resource-manager-implementation/src/clients/network/clients/open5gs/client.py deleted file mode 100644 index fbc7f6b..0000000 --- a/service-resource-manager-implementation/src/clients/network/clients/open5gs/client.py +++ /dev/null @@ -1,62 +0,0 @@ -# -*- coding: utf-8 -*- -from pydantic import ValidationError - -from src import logger -from src.network.core.network_interface import NetworkManagementInterface, build_flows - -from ...core import schemas - -log = logger.get_logger(__name__) - -flow_id_mapping = {"qos-e": 3, "qos-s": 4, "qos-m": 5, "qos-l": 6} - - -class NetworkManager(NetworkManagementInterface): - """ - This client implements the NetworkManagementInterface and translates the - CAMARA APIs into specific HTTP requests understandable by the Open5GS NEF API. - - Invloved partners and their roles in this implementation: - - I2CAT: Responsible for the CAMARA QoD API and its mapping to the - 3GPP AsSessionWithQoS API exposed by Open5GS NEF. - - NCSRD: Responsible for the CAMARA Location API and its mapping to the - 3GPP Monitoring Event API exposed Open5GS NEF. - """ - - def __init__(self, base_url: str, scs_as_id): - """ - Initializes the Open5GS Client. - """ - try: - self.base_url = base_url - self.scs_as_id = scs_as_id - log.info( - f"Initialized Open5GSClient with base_url: {self.base_url} " - f"and scs_as_id: {self.scs_as_id}" - ) - except Exception as e: - log.error(f"Failed to initialize Open5GSClient: {e}") - raise e - - def core_specific_qod_validation(self, session_info: schemas.CreateSession): - if session_info.qosProfile.root not in flow_id_mapping.keys(): - raise ValidationError( - f"Open5Gs only supports these qos-profiles: {', '.join(flow_id_mapping.keys())}" - ) - - def add_core_specific_qod_parameters( - self, - session_info: schemas.CreateSession, - subscription: schemas.AsSessionWithQoSSubscription, - ) -> None: - subscription.supportedFeatures = schemas.SupportedFeatures("003C") - flow_id = flow_id_mapping[session_info.qosProfile.root] - subscription.flowInfo = build_flows(flow_id, session_info) - - -# Note: -# As this class is inheriting from NetworkManagementInterface, it is -# expected to implement all the abstract methods defined in that interface. -# -# In case this network adapter doesn't support a specific method, it should -# be marked as NotImplementedError. diff --git a/service-resource-manager-implementation/src/clients/network/core/__init__.py b/service-resource-manager-implementation/src/clients/network/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/clients/network/core/common.py b/service-resource-manager-implementation/src/clients/network/core/common.py deleted file mode 100644 index ace91da..0000000 --- a/service-resource-manager-implementation/src/clients/network/core/common.py +++ /dev/null @@ -1,94 +0,0 @@ -# -*- coding: utf-8 -*- -# Common utilities (errors, HTTP helpers) used by the core network interface (network_interface.py). - -import requests -from pydantic import BaseModel - -from src import logger - -log = logger.get_logger(__name__) - - -def _make_request(method: str, url: str, data=None): - try: - headers = None - if method == "POST" or method == "PUT": - headers = { - "Content-Type": "application/json", - "accept": "application/json", - } - elif method == "GET": - headers = { - "accept": "application/json", - } - response = requests.request(method, url, headers=headers, data=data) - response.raise_for_status() - if response.content: - return response.json() - except requests.exceptions.HTTPError as e: - raise CoreHttpError(e) from e - except requests.exceptions.ConnectionError as e: - raise CoreHttpError("connection error") from e - - -# QoD methods -def as_session_with_qos_post( - base_url: str, scs_as_id: str, model_payload: BaseModel -) -> dict: - data = model_payload.model_dump_json(exclude_none=True, by_alias=True) - url = as_session_with_qos_build_url(base_url, scs_as_id) - return _make_request("POST", url, data=data) - - -def as_session_with_qos_get(base_url: str, scs_as_id: str, session_id: str) -> dict: - url = as_session_with_qos_build_url(base_url, scs_as_id, session_id) - return _make_request("GET", url) - - -def as_session_with_qos_delete(base_url: str, scs_as_id: str, session_id: str): - url = as_session_with_qos_build_url(base_url, scs_as_id, session_id) - return _make_request("DELETE", url) - - -def as_session_with_qos_build_url( - base_url: str, scs_as_id: str, session_id: str = None -): - url = f"{base_url}/3gpp-as-session-with-qos/v1/{scs_as_id}/subscriptions" - if session_id is not None and len(session_id) > 0: - return f"{url}/{session_id}" - else: - return url - - -# Traffic Influence Methods -def traffic_influence_post( - base_url: str, scs_as_id: str, model_payload: BaseModel -) -> dict: - data = model_payload.model_dump_json(exclude_none=True) - url = traffic_influence_build_url(base_url, scs_as_id) - return _make_request("POST", url, data=data) - - -def traffic_influence_delete(base_url: str, scs_as_id: str, session_id: str): - url = traffic_influence_build_url(base_url, scs_as_id, session_id) - return _make_request("DELETE", url) - - -def traffic_influence_put( - base_url: str, scs_as_id: str, session_id: str, model_payload: BaseModel -) -> dict: - data = model_payload.model_dump_json(exclude_none=True) - url = traffic_influence_build_url(base_url, scs_as_id, session_id) - return _make_request("PUT", url, data=data) - - -def traffic_influence_build_url(base_url: str, scs_as_id: str, session_id: str = None): - url = f"{base_url}/3gpp-traffic-influence/v1/{scs_as_id}/subscriptions" - if session_id is not None and len(session_id) > 0: - return f"{url}/{session_id}" - else: - return url - - -class CoreHttpError(Exception): - pass diff --git a/service-resource-manager-implementation/src/clients/network/core/network_interface.py b/service-resource-manager-implementation/src/clients/network/core/network_interface.py deleted file mode 100644 index 2fedbcf..0000000 --- a/service-resource-manager-implementation/src/clients/network/core/network_interface.py +++ /dev/null @@ -1,295 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -## -# Copyright 2025-present by Software Networks Area, i2CAT. -# All rights reserved. -# -# This file is part of the Open SDK -# -# Contributors: -# - Reza Mosahebfard (reza.mosahebfard@i2cat.net) -# - Ferran Cañellas (ferran.canellas@i2cat.net) -## -from abc import ABC -from itertools import product -from typing import Dict - -from src import logger -from src.network.core import common, schemas - -log = logger.get_logger(__name__) - - -def flatten_port_spec(ports_spec: schemas.PortsSpec | None) -> list[str]: - has_ports = False - has_ranges = False - flat_ports = [] - if ports_spec and ports_spec.ports: - has_ports = True - flat_ports.extend([str(port) for port in ports_spec.ports]) - if ports_spec and ports_spec.ranges: - has_ranges = True - flat_ports.extend( - [f"{range.from_.root}-{range.to.root}" for range in ports_spec.ranges] - ) - if not has_ports and not has_ranges: - flat_ports.append("0-65535") - return flat_ports - - -def build_flows( - flow_id: int, - session_info: schemas.CreateSession, -) -> list[schemas.FlowInfo]: - device_ports = flatten_port_spec(session_info.devicePorts) - server_ports = flatten_port_spec(session_info.applicationServerPorts) - ports_combis = list(product(device_ports, server_ports)) - - device_ip = session_info.device.ipv4Address or session_info.device.ipv6Address - if isinstance(device_ip, schemas.DeviceIpv6Address): - device_ip = device_ip.root - else: # IPv4 - device_ip = ( - device_ip.root.publicAddress.root or device_ip.root.privateAddress.root - ) - device_ip = str(device_ip) - server_ip = ( - session_info.applicationServer.ipv4Address - or session_info.applicationServer.ipv6Address - ) - server_ip = server_ip.root - flow_descrs = [] - for device_port, server_port in ports_combis: - flow_descrs.append( - f"permit in ip from {device_ip} {device_port} to {server_ip} {server_port}" - ) - flow_descrs.append( - f"permit out ip from {server_ip} {server_port} to {device_ip} {device_port}" - ) - flows = [ - schemas.FlowInfo(flowId=flow_id, flowDescriptions=[", ".join(flow_descrs)]) - ] - return flows - - -class NetworkManagementInterface(ABC): - """ - Abstract Base Class for Network Resource Management. - - This interface defines the standard methods that all - Network Clients (Open5GS, OAI, Open5GCore) must implement. - - Partners implementing a new network client should inherit from this class - and provide concrete implementations for all abstract methods relevant - to their specific NEF capabilities. - """ - - base_url: str - scs_as_id: str - - def add_core_specific_qod_parameters( - self, - session_info: schemas.CreateSession, - subscription: schemas.AsSessionWithQoSSubscription, - ): - """ - Placeholder for adding core-specific parameters to the subscription. - This method should be overridden by subclasses to implement specific logic. - """ - pass - - def add_core_specific_ti_parameters( - self, - traffic_influence_info: schemas.CreateTrafficInfluence, - subscription: schemas.TrafficInfluSub, - ): - """ - Placeholder for adding core-specific parameters to the subscription. - This method should be overridden by subclasses to implement specific logic. - """ - pass - - def core_specific_qod_validation(self, session_info: schemas.CreateSession) -> None: - """ - Validates core-specific parameters for the session creation. - - args: - session_info: The session information to validate. - - raises: - ValidationError: If the session information does not meet core-specific requirements. - """ - # Placeholder for core-specific validation logic - # This method should be overridden by subclasses if needed - pass - - def core_specific_traffic_influence_validation( - self, traffic_influence_info: schemas.CreateTrafficInfluence - ) -> None: - """ - Validates core-specific parameters for the session creation. - - args: - session_info: The session information to validate. - - raises: - ValidationError: If the session information does not meet core-specific requirements. - """ - # Placeholder for core-specific validation logic - # This method should be overridden by subclasses if needed - pass - - def _build_qod_subscription( - self, session_info: Dict - ) -> schemas.AsSessionWithQoSSubscription: - valid_session_info = schemas.CreateSession.model_validate(session_info) - device_ipv4 = None - if valid_session_info.device.ipv4Address: - device_ipv4 = valid_session_info.device.ipv4Address.root.publicAddress.root - - self.core_specific_qod_validation(valid_session_info) - subscription = schemas.AsSessionWithQoSSubscription( - notificationDestination=str(valid_session_info.sink), - qosReference=valid_session_info.qosProfile.root, - ueIpv4Addr=device_ipv4, - ueIpv6Addr=valid_session_info.device.ipv6Address, - usageThreshold=schemas.UsageThreshold(duration=valid_session_info.duration), - ) - self.add_core_specific_qod_parameters(valid_session_info, subscription) - return subscription - - def _build_ti_subscription(self, traffic_influence_info: Dict): - traffic_influence_data = schemas.CreateTrafficInfluence.model_validate( - traffic_influence_info - ) - self.core_specific_traffic_influence_validation(traffic_influence_data) - - device_ip = traffic_influence_data.retrieve_ue_ipv4() - server_ip = ( - traffic_influence_data.appInstanceId - ) # assume that the instance id corresponds to its IPv4 address - sink_url = traffic_influence_data.notificationUri - edge_zone = traffic_influence_data.edgeCloudZoneId - - # build flow descriptor in oai format using device ip and server ip - flow_descriptor = f"permit out ip from {device_ip}/32 to {server_ip}/32" - - subscription = schemas.TrafficInfluSub( - afAppId=traffic_influence_data.appId, - ipv4Addr=str(device_ip), - notificationDestination=sink_url, - ) - subscription.add_flow_descriptor(flow_desriptor=flow_descriptor) - subscription.add_traffic_route(dnai=edge_zone) - - self.add_core_specific_ti_parameters(traffic_influence_data, subscription) - return subscription - - def create_qod_session(self, session_info: Dict) -> Dict: - """ - Creates a QoS session based on CAMARA QoD API input. - - args: - session_info: Dictionary containing session details conforming to - the CAMARA QoD session creation parameters. - - returns: - dictionary containing the created session details, including its ID. - """ - subscription = self._build_qod_subscription(session_info) - return common.as_session_with_qos_post( - self.base_url, self.scs_as_id, subscription - ) - - def get_qod_session(self, session_id: str) -> Dict: - """ - Retrieves details of a specific Quality on Demand (QoS) session. - - args: - session_id: The unique identifier of the QoS session. - - returns: - Dictionary containing the details of the requested QoS session. - """ - session = common.as_session_with_qos_get( - self.base_url, self.scs_as_id, session_id=session_id - ) - log.info(f"QoD session retrived successfully [id={session_id}]") - return session - - def delete_qod_session(self, session_id: str) -> None: - """ - Deletes a specific Quality on Demand (QoS) session. - - args: - session_id: The unique identifier of the QoS session to delete. - - returns: - None - """ - common.as_session_with_qos_delete( - self.base_url, self.scs_as_id, session_id=session_id - ) - log.info(f"QoD session deleted successfully [id={session_id}]") - - def create_traffic_influence_resource(self, traffic_influence_info: Dict) -> Dict: - """ - Creates a Traffic Influence resource based on CAMARA TI API input. - - args: - traffic_influence_info: Dictionary containing traffic influence details conforming to - the CAMARA TI resource creation parameters. - - returns: - dictionary containing the created traffic influence resource details, including its ID. - """ - - subscription = self._build_ti_subscription(traffic_influence_info) - response = common.traffic_influence_post( - self.base_url, self.scs_as_id, subscription - ) - - # retrieve the NEF resource id - if "self" in response.keys(): - subscription_id = response["self"] - else: - subscription_id = None - - traffic_influence_info["trafficInfluenceID"] = subscription_id - return traffic_influence_info - - def put_traffic_influence_resource( - self, resource_id: str, traffic_influence_info: Dict - ) -> Dict: - """ - Retrieves details of a specific Traffic Influence resource. - - args: - resource_id: The unique identifier of the Traffic Influence resource. - - returns: - Dictionary containing the details of the requested Traffic Influence resource. - """ - subscription = self._build_ti_subscription(traffic_influence_info) - common.traffic_influence_put( - self.base_url, self.scs_as_id, resource_id, subscription - ) - - traffic_influence_info.trafficInfluenceID = resource_id - return traffic_influence_info - - def delete_traffic_influence_resource(self, resource_id: str) -> None: - """ - Deletes a specific Traffic Influence resource. - - args: - resource_id: The unique identifier of the Traffic Influence resource to delete. - - returns: - None - """ - common.traffic_influence_delete(self.base_url, self.scs_as_id, resource_id) - return - - # Placeholder for other CAMARA APIs (e.g., Traffic Influence, - # Location-retrieval, etc.) diff --git a/service-resource-manager-implementation/src/clients/network/core/schemas.py b/service-resource-manager-implementation/src/clients/network/core/schemas.py deleted file mode 100644 index ac8dc11..0000000 --- a/service-resource-manager-implementation/src/clients/network/core/schemas.py +++ /dev/null @@ -1,430 +0,0 @@ -# -*- coding: utf-8 -*- -# This file defines the Pydantic models that represent the data structures (schemas) -# for the requests sent to and responses received from the Open5GS NEF API, -# specifically focusing on the APIs needed to support CAMARA QoD. - -import ipaddress -from enum import Enum -from ipaddress import IPv4Address, IPv6Address -from typing import Annotated - -from pydantic import AnyUrl, BaseModel, ConfigDict, Field, NonNegativeInt, RootModel -from pydantic_extra_types.mac_address import MacAddress - - -class FlowDirection(Enum): - """ - DOWNLINK: The corresponding filter applies for traffic to the UE. - UPLINK: The corresponding filter applies for traffic from the UE. - BIDIRECTIONAL: The corresponding filter applies for traffic both to and from the UE. - UNSPECIFIED: The corresponding filter applies for traffic to the UE (downlink), but - has no specific direction declared. The service data flow detection shall apply the - filter for uplink traffic as if the filter was bidirectional. The PCF shall not use - the value UNSPECIFIED in filters created by the network in NW-initiated procedures. - The PCF shall only include the value UNSPECIFIED in filters in UE-initiated - procedures if the same value is received from the SMF. - """ - - DOWNLINK = "DOWNLINK" - UPLINK = "UPLINK" - BIDIRECTIONAL = "BIDIRECTIONAL" - UNSPECIFIED = "UNSPECIFIED" - - -class RequestedQosMonitoringParameter(Enum): - DOWNLINK = "DOWNLINK" - UPLINK = "UPLINK" - ROUND_TRIP = "ROUND_TRIP" - - -class ReportingFrequency(Enum): - EVENT_TRIGGERED = "EVENT_TRIGGERED" - PERIODIC = "PERIODIC" - SESSION_RELEASE = "SESSION_RELEASE" - - -Uinteger = Annotated[int, Field(ge=0)] - - -class DurationSec(RootModel[NonNegativeInt]): - root: NonNegativeInt = Field( - ..., - description="Unsigned integer identifying a period of time in units of \ - seconds.", - ) - - -class Volume(RootModel[NonNegativeInt]): - root: NonNegativeInt = Field( - ..., description="Unsigned integer identifying a volume in units of bytes." - ) - - -class SupportedFeatures(RootModel[str]): - root: str = Field( - ..., - pattern=r"^[A-Fa-f0-9]*$", - description="Hexadecimal string representing supported features.", - ) - - -class Link(RootModel[str]): - root: str = Field( - ..., - description="String formatted according to IETF RFC 3986 identifying a \ - referenced resource.", - ) - - -class FlowDescriptionModel(RootModel[str]): - root: str = Field(..., description="Defines a packet filter of an IP flow.") - - -class EthFlowDescription(BaseModel): - destMacAddr: MacAddress | None = None - ethType: str - fDesc: FlowDescriptionModel | None = None - fDir: FlowDirection | None = None - sourceMacAddr: MacAddress | None = None - vlanTags: list[str] | None = Field(None, max_length=2, min_length=1) - srcMacAddrEnd: MacAddress | None = None - destMacAddrEnd: MacAddress | None = None - - -class UsageThreshold(BaseModel): - duration: DurationSec | None = None - totalVolume: Volume | None = None - downlinkVolume: Volume | None = None - uplinkVolume: Volume | None = None - - -class SponsorInformation(BaseModel): - sponsorId: str = Field(..., description="It indicates Sponsor ID.") - aspId: str = Field(..., description="It indicates Application Service Provider ID.") - - -class QosMonitoringInformationModel(BaseModel): - reqQosMonParams: list[RequestedQosMonitoringParameter] | None = Field( - None, min_length=1 - ) - repFreqs: list[ReportingFrequency] | None = Field(None, min_length=1) - repThreshDl: Uinteger | None = None - repThreshUl: Uinteger | None = None - repThreshRp: Uinteger | None = None - waitTime: int | None = None - repPeriod: int | None = None - - -class FlowInfo(BaseModel): - flowId: int = Field(..., description="Indicates the IP flow.") - flowDescriptions: list[str] | None = Field( - None, - description="Indicates the packet filters of the IP flow. Refer to subclause \ - 5.3.8 of 3GPP TS 29.214 for encoding. It shall contain UL and/or DL IP \ - flow description.", - max_length=2, - min_length=1, - ) - - -class Snssai(BaseModel): - sst: int = Field(default=1) - sd: str = Field(default="FFFFFF") - - -class AsSessionWithQoSSubscription(BaseModel): - model_config = ConfigDict(serialize_by_alias=True) - self_: Link | None = Field(None, alias="self") - supportedFeatures: SupportedFeatures | None = None - notificationDestination: Link - flowInfo: list[FlowInfo] | None = Field( - None, description="Describe the data flow which requires QoS.", min_length=1 - ) - ethFlowInfo: list[EthFlowDescription] | None = Field( - None, description="Identifies Ethernet packet flows.", min_length=1 - ) - qosReference: str | None = Field( - None, description="Identifies a pre-defined QoS information" - ) - altQoSReferences: list[str] | None = Field( - None, - description="Identifies an ordered list of pre-defined QoS information. The \ - lower the index of the array for a given entry, the higher the priority.", - min_length=1, - ) - ueIpv4Addr: ipaddress.IPv4Address | None = None - ueIpv6Addr: ipaddress.IPv6Address | None = None - macAddr: MacAddress | None = None - usageThreshold: UsageThreshold | None = None - sponsorInfo: SponsorInformation | None = None - qosMonInfo: QosMonitoringInformationModel | None = None - - -class SourceTrafficFilters(BaseModel): - sourcePort: int - - -class DestinationTrafficFilters(BaseModel): - destinationPort: int - destinationProtocol: str - - -class TrafficRoute(BaseModel): - dnai: str - - -class TrafficInfluSub(BaseModel): # Replace with a meaningful name - afServiceId: str | None = None - afAppId: str - dnn: str | None = None - snssai: Snssai | None = None - trafficFilters: list[FlowInfo] | None = Field( - None, - description="Describe the data flow which requires Traffic Influence.", - min_length=1, - ) - ipv4Addr: str | None = None - ipv6Addr: str | None = None - - notificationDestination: str - trafficRoutes: list[TrafficRoute] | None = Field( - None, - description="Describe the list of DNAIs to reach the destination", - min_length=1, - ) - suppFeat: str | None = None - - def add_flow_descriptor(self, flow_desriptor: str): - self.trafficFilters = list() - self.trafficFilters.append( - FlowInfo( - flowId=len(self.trafficFilters) + 1, flowDescriptions=[flow_desriptor] - ) - ) - - def add_traffic_route(self, dnai: str): - self.trafficRoutes = list() - self.trafficRoutes.append(TrafficRoute(dnai=dnai)) - - def add_snssai(self, sst: int, sd: str = None): - self.snssai = Snssai(sst=sst, sd=sd) - - -############################################################### -############################################################### -# CAMARA Models - - -class PhoneNumber(RootModel[str]): - root: Annotated[ - str, - Field( - description="A public identifier addressing a telephone subscription. In mobile networks it corresponds to the MSISDN (Mobile Station International Subscriber Directory Number). In order to be globally unique it has to be formatted in international format, according to E.164 standard, prefixed with '+'.", - examples=["+123456789"], - pattern="^\\+[1-9][0-9]{4,14}$", - ), - ] - - -class NetworkAccessIdentifier(RootModel[str]): - root: Annotated[ - str, - Field( - description="A public identifier addressing a subscription in a mobile network. In 3GPP terminology, it corresponds to the GPSI formatted with the External Identifier ({Local Identifier}@{Domain Identifier}). Unlike the telephone number, the network access identifier is not subjected to portability ruling in force, and is individually managed by each operator.", - examples=["123456789@domain.com"], - ), - ] - - -class SingleIpv4Addr(RootModel[IPv4Address]): - root: Annotated[ - IPv4Address, - Field( - description="A single IPv4 address with no subnet mask", - examples=["203.0.113.0"], - ), - ] - - -class Port(RootModel[int]): - root: Annotated[int, Field(description="TCP or UDP port number", ge=0, le=65535)] - - -class DeviceIpv4Addr1(BaseModel): - publicAddress: SingleIpv4Addr - privateAddress: SingleIpv4Addr - publicPort: Port | None = None - - -class DeviceIpv4Addr2(BaseModel): - publicAddress: SingleIpv4Addr - privateAddress: SingleIpv4Addr | None = None - publicPort: Port - - -class DeviceIpv4Addr(RootModel[DeviceIpv4Addr1 | DeviceIpv4Addr2]): - root: Annotated[ - DeviceIpv4Addr1 | DeviceIpv4Addr2, - Field( - description="The device should be identified by either the public (observed) IP address and port as seen by the application server, or the private (local) and any public (observed) IP addresses in use by the device (this information can be obtained by various means, for example from some DNS servers).\n\nIf the allocated and observed IP addresses are the same (i.e. NAT is not in use) then the same address should be specified for both publicAddress and privateAddress.\n\nIf NAT64 is in use, the device should be identified by its publicAddress and publicPort, or separately by its allocated IPv6 address (field ipv6Address of the Device object)\n\nIn all cases, publicAddress must be specified, along with at least one of either privateAddress or publicPort, dependent upon which is known. In general, mobile devices cannot be identified by their public IPv4 address alone.\n", - examples=[{"publicAddress": "203.0.113.0", "publicPort": 59765}], - ), - ] - - -class DeviceIpv6Address(RootModel[IPv6Address]): - root: Annotated[ - IPv6Address, - Field( - description="The device should be identified by the observed IPv6 address, or by any single IPv6 address from within the subnet allocated to the device (e.g. adding ::0 to the /64 prefix).\n\nThe session shall apply to all IP flows between the device subnet and the specified application server, unless further restricted by the optional parameters devicePorts or applicationServerPorts.\n", - examples=["2001:db8:85a3:8d3:1319:8a2e:370:7344"], - ), - ] - - -class Device(BaseModel): - phoneNumber: PhoneNumber | None = None - networkAccessIdentifier: NetworkAccessIdentifier | None = None - ipv4Address: DeviceIpv4Addr | None = None - ipv6Address: DeviceIpv6Address | None = None - - -class ApplicationServerIpv4Address(RootModel[str]): - root: Annotated[ - str, - Field( - description="IPv4 address may be specified in form
as:\n - address - an IPv4 number in dotted-quad form 1.2.3.4. Only this exact IP number will match the flow control rule.\n - address/mask - an IP number as above with a mask width of the form 1.2.3.4/24.\n In this case, all IP numbers from 1.2.3.0 to 1.2.3.255 will match. The bit width MUST be valid for the IP version.\n", - examples=["198.51.100.0/24"], - ), - ] - - -class ApplicationServerIpv6Address(RootModel[str]): - root: Annotated[ - str, - Field( - description="IPv6 address may be specified in form
as:\n - address - The /128 subnet is optional for single addresses:\n - 2001:db8:85a3:8d3:1319:8a2e:370:7344\n - 2001:db8:85a3:8d3:1319:8a2e:370:7344/128\n - address/mask - an IP v6 number with a mask:\n - 2001:db8:85a3:8d3::0/64\n - 2001:db8:85a3:8d3::/64\n", - examples=["2001:db8:85a3:8d3:1319:8a2e:370:7344"], - ), - ] - - -class ApplicationServer(BaseModel): - ipv4Address: ApplicationServerIpv4Address | None = None - ipv6Address: ApplicationServerIpv6Address | None = None - - -class Range(BaseModel): - from_: Annotated[Port, Field(alias="from")] - to: Port - - -class PortsSpec(BaseModel): - ranges: Annotated[ - list[Range] | None, Field(description="Range of TCP or UDP ports", min_length=1) - ] = None - ports: Annotated[ - list[Port] | None, Field(description="Array of TCP or UDP ports", min_length=1) - ] = None - - -class QosProfileName(RootModel[str]): - root: Annotated[ - str, - Field( - description="A unique name for identifying a specific QoS profile.\nThis may follow different formats depending on the API provider implementation.\nSome options addresses:\n - A UUID style string\n - Support for predefined profiles QOS_S, QOS_M, QOS_L, and QOS_E\n - A searchable descriptive name\nThe set of QoS Profiles that an API provider is offering may be retrieved by means of the QoS Profile API (qos-profile) or agreed on onboarding time.\n", - examples=["voice"], - max_length=256, - min_length=3, - pattern="^[a-zA-Z0-9_.-]+$", - ), - ] - - -class CredentialType(Enum): - PLAIN = "PLAIN" - ACCESSTOKEN = "ACCESSTOKEN" - REFRESHTOKEN = "REFRESHTOKEN" - - -class SinkCredential(BaseModel): - credentialType: Annotated[ - CredentialType, - Field( - description="The type of the credential.\nNote: Type of the credential - MUST be set to ACCESSTOKEN for now\n" - ), - ] - - -class NotificationSink(BaseModel): - sink: str | None - sinkCredential: SinkCredential | None - - -class BaseSessionInfo(BaseModel): - device: Device | None = None - applicationServer: ApplicationServer - devicePorts: Annotated[ - PortsSpec | None, - Field( - description="The ports used locally by the device for flows to which the requested QoS profile should apply. If omitted, then the qosProfile will apply to all flows between the device and the specified application server address and ports" - ), - ] = None - applicationServerPorts: Annotated[ - PortsSpec | None, - Field( - description="A list of single ports or port ranges on the application server" - ), - ] = None - qosProfile: QosProfileName - sink: Annotated[ - AnyUrl | None, - Field( - description="The address to which events about all status changes of the session (e.g. session termination) shall be delivered using the selected protocol.", - examples=["https://endpoint.example.com/sink"], - ), - ] = None - sinkCredential: Annotated[ - SinkCredential | None, - Field( - description="A sink credential provides authentication or authorization information necessary to enable delivery of events to a target." - ), - ] = None - - -class CreateSession(BaseSessionInfo): - duration: Annotated[ - int, - Field( - description="Requested session duration in seconds. Value may be explicitly limited for the QoS profile, as specified in the Qos Profile (see qos-profile API). Implementations can grant the requested session duration or set a different duration, based on network policies or conditions.\n", - examples=[3600], - ge=1, - ), - ] - - -class CreateTrafficInfluence(BaseModel): - trafficInfluenceID: str | None = None - apiConsumerId: str | None = None - appId: str - appInstanceId: str - edgeCloudRegion: str | None = None - edgeCloudZoneId: str | None = None - sourceTrafficFilters: SourceTrafficFilters | None = None - destinationTrafficFilters: DestinationTrafficFilters | None = None - notificationUri: str | None = None - notificationAuthToken: str | None = None - device: Device - notificationSink: NotificationSink | None = None - - def retrieve_ue_ipv4(self): - if self.device is not None and self.device.ipv4Address is not None: - return self.device.ipv4Address.root.privateAddress.root - else: - raise KeyError("device.ipv4Address.publicAddress") - - def add_ue_ipv4(self, ipv4: str): - if self.device is None: - self.device = Device() - if self.device.ipv4Address is None: - self.device.ipv4Address = DeviceIpv4Addr(publicAddress=ipv4) diff --git a/service-resource-manager-implementation/src/clients/o-ran/__init__.py b/service-resource-manager-implementation/src/clients/o-ran/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/clients/o-ran/clients/__init__.py b/service-resource-manager-implementation/src/clients/o-ran/clients/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/clients/o-ran/clients/juniper-ric/__init__.py b/service-resource-manager-implementation/src/clients/o-ran/clients/juniper-ric/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/clients/o-ran/clients/juniper-ric/client.py b/service-resource-manager-implementation/src/clients/o-ran/clients/juniper-ric/client.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/clients/o-ran/core/__init__.py b/service-resource-manager-implementation/src/clients/o-ran/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/clients/o-ran/core/o-ran_interface.py b/service-resource-manager-implementation/src/clients/o-ran/core/o-ran_interface.py deleted file mode 100644 index 4640904..0000000 --- a/service-resource-manager-implementation/src/clients/o-ran/core/o-ran_interface.py +++ /dev/null @@ -1 +0,0 @@ -# TODO diff --git a/service-resource-manager-implementation/src/controllers/edge_cloud_management_controller.py b/service-resource-manager-implementation/src/controllers/edge_cloud_management_controller.py index 4873f29..0194906 100644 --- a/service-resource-manager-implementation/src/controllers/edge_cloud_management_controller.py +++ b/service-resource-manager-implementation/src/controllers/edge_cloud_management_controller.py @@ -1,108 +1,77 @@ import connexion import logging -import os -from sunrise6g_opensdk.edgecloud.adapters.aeros.client import EdgeApplicationManager as AerOSClient -from sunrise6g_opensdk.edgecloud.adapters.i2edge.client import EdgeApplicationManager as I2EdgeClient -from sunrise6g_opensdk.edgecloud.adapters.kubernetes.client import EdgeApplicationManager as kubernetesClient +from os import environ +from sunrise6g_opensdk import Sdk as sdkclient +import sys logger=logging.getLogger(__name__) +logging.basicConfig(level=logging.DEBUG) -adapter_name = os.environ['EDGE_CLOUD_ADAPTER_NAME'] -adapter_base_url = os.environ['ADAPTER_BASE_URL'] -adapter = None +if environ['EDGE_CLOUD_ADAPTER_NAME'] is not None: + edgecloud_adapter_name = environ['EDGE_CLOUD_ADAPTER_NAME'] + edgecloud_adapter_base_url = environ['ADAPTER_BASE_URL'] + edgecloud_adapter_specs = {'client_name': edgecloud_adapter_name, 'base_url': edgecloud_adapter_base_url} + edgecloud_adapter_specs.update(environ) + print('Creating edge cloud adapter with env: ', edgecloud_adapter_specs) + adapters = sdkclient.create_adapters_from(adapter_specs={'edgecloud': edgecloud_adapter_specs}) + edgecloud_adapter = adapters.get("edgecloud") +# else: +# logging.error('Edge Cloud adapter has not been specified! Aborting...') +# sys.exit() -if adapter_name=='aeros': - adapter = AerOSClient(base_url=adapter_base_url) -elif adapter_name=='i2edge': - adapter = I2EdgeClient(base_url=adapter_base_url) -elif adapter_name=='kubernetes': - adapter = kubernetesClient(base_url=adapter_base_url, **os.environ) def deregister_service_function(service_function_id: str): # noqa: E501 """Deregister service. - # noqa: E501 - - :param service_function_name: Returns a specific service function from the catalogue. - :type service_function_name: str - - :rtype: None - - + :param service_function_name: Removed app metadata from the catalogue. """ try: - code = adapter.delete_onboarded_app(service_function_id) + code = edgecloud_adapter.delete_onboarded_app(service_function_id) return code except Exception as ce_: raise Exception("An exception occurred :", ce_) def get_service_function(service_function_id: str): # noqa: E501 - """Returns a specific service function from the catalogue. - - # noqa: E501 - - :param service_function_id: Returns a specific service function from the catalogue. - :type service_function_id: str - - :rtype: AppsResponseApps + """Returns a specific app from the catalogue. """ try: - service_function = adapter.get_onboarded_app(service_function_id) + service_function = edgecloud_adapter.get_onboarded_app(service_function_id) return service_function except Exception as ce_: raise Exception("An exception occurred :", ce_) def get_service_functions(): # noqa: E501 - """Returns service functions from the catalogue. - - # noqa: E501 - - - :rtype: AppsResponse + """Returns all apps from the catalogue. """ try: - service_functions = adapter.get_all_onboarded_apps() + service_functions = edgecloud_adapter.get_all_onboarded_apps() return service_functions except Exception as ce_: raise Exception("An exception occurred :", ce_) def register_service_function(body=None): # noqa: E501 - """Register Service. - - # noqa: E501 - - :param body: Registration method to save service function into database - :type body: dict | bytes - - :rtype: None + """Registers app to the database """ if connexion.request.is_json: insert_doc = connexion.request.get_json() try: - return adapter.onboard_app(insert_doc) + return edgecloud_adapter.onboard_app(insert_doc) except Exception as ce_: return ce_ def delete_deployed_service_function(app_id: str): # noqa: E501 - """Deletes a deployed Service function. - - # noqa: E501 - - :param deployed_service_function_name: Represents a service function from the running deployments. - :type deployed_service_function_name: str - - :rtype: None + """Undeployes app """ response = None try: - response = adapter.undeploy_app(app_id) + response = edgecloud_adapter.undeploy_app(app_id) # return response except Exception as ce_: @@ -129,7 +98,7 @@ def deploy_service_function(): # noqa: E501 try: # body = DeployApp.from_dict(connexion.request.get_json()) body = connexion.request.get_json() - response = adapter.deploy_app(body) + response = edgecloud_adapter.deploy_app(body) # body = DeployServiceFunction.from_dict(connexion.request.get_json()) # response = piedge_encoder.deploy_service_function(body) return response @@ -153,7 +122,7 @@ def get_deployed_service_functions(): # noqa: E501 # role = user_authentication.check_role() # if role is not None and role == "admin": try: - response = adapter.get_all_deployed_apps() + response = edgecloud_adapter.get_all_deployed_apps() return response except Exception as ce_: logger.error(ce_) @@ -173,7 +142,7 @@ def get_deployed_service_function(app_id: str): # noqa: E501 # role = user_authentication.check_role() # if role is not None and role == "admin": try: - response = adapter.get_deployed_app(app_id=app_id) + response = edgecloud_adapter.get_deployed_app(app_id=app_id) return response except Exception as ce_: logger.error(ce_) @@ -184,10 +153,23 @@ def get_nodes(): # noqa: E501 # noqa: E501 + :rtype: NodesResponse + """ + # try: + response = edgecloud_adapter.get_edge_cloud_zones() + return response + # except Exception as ce_: + # logger.info(ce_) + +def node_details(node_id: str): # noqa: E501 + """Returns the edge nodes status. + + # noqa: E501 + :rtype: NodesResponse """ try: - response = adapter.get_edge_cloud_zones() + response = edgecloud_adapter.get_edge_cloud_zones_details(zone_id=node_id) return response except Exception as ce_: - logger.info(ce_) \ No newline at end of file + logger.info(ce_) \ No newline at end of file diff --git a/service-resource-manager-implementation/src/controllers/network_functions_controller.py b/service-resource-manager-implementation/src/controllers/network_functions_controller.py index dc332ef..ced3527 100644 --- a/service-resource-manager-implementation/src/controllers/network_functions_controller.py +++ b/service-resource-manager-implementation/src/controllers/network_functions_controller.py @@ -1,33 +1,30 @@ from os import environ import logging import connexion -from sunrise6g_opensdk.network.adapters.oai.client import NetworkManager as OAIClient -from sunrise6g_opensdk.network.adapters.open5gcore.client import NetworkManager as Open5GCoreClient -from sunrise6g_opensdk.network.adapters.open5gs.client import NetworkManager as Open5GSClient +import sys +from sunrise6g_opensdk.common.sdk import Sdk as sdkclient logger = logging.getLogger(__name__) logging.basicConfig(level=logging.DEBUG) -network_client = environ.get('NETWORK_ADAPTER_NAME') -base_url = environ.get('NETWORK_ADAPTER_BASE_URL') -scs_as_id = environ.get('SCS_AS_ID') - -adapter = None - -if network_client is not None: - if network_client=='oai': - adapter = OAIClient(base_url=base_url, scs_as_id=scs_as_id) - elif network_client=='open5gcore': - adapter = Open5GCoreClient(base_url=base_url, scs_as_id=scs_as_id) - else: - adapter = Open5GSClient(base_url=base_url, scs_as_id=scs_as_id) - +if environ.get('NETWORK_ADAPTER_NAME') is not None: + network_adapter_name = environ.get('NETWORK_ADAPTER_NAME') + adapter_base_url = environ.get('NETWORK_ADAPTER_BASE_URL') + scs_as_id = environ.get('SCS_AS_ID') + network_adapter_specs = {'client_name': network_adapter_name, 'base_url': adapter_base_url, 'scs_as_id': scs_as_id} + network_adapter_specs.update(environ) + print('Creating network adapter with env: ', network_adapter_specs) + adapters = sdkclient.create_adapters_from(adapter_specs={'network': network_adapter_specs}) + network_adapter = adapters.get("network") +# else: +# logging.error('Network adapter has not been specified! Aborting...') +# sys.exit() def create_qod_session(): if connexion.request.is_json: try: - response = adapter.create_qod_session(connexion.request.get_json()) + response = network_adapter.create_qod_session(connexion.request.get_json()) return response except Exception as ce_: logger.error(ce_) @@ -38,7 +35,7 @@ def create_qod_session(): def get_qod_session(session_id: str): try: - response = adapter.get_qod_session(id) + response = network_adapter.get_qod_session(id) return {'status': 200, 'session': response.json()} except Exception as ce_: logger.error(ce_) @@ -47,7 +44,7 @@ def get_qod_session(session_id: str): def delete_qod_session(session_id: str): try: - response = adapter.delete_qod_session(id) + response = network_adapter.delete_qod_session(id) return response except Exception as ce_: logger.error(ce_) diff --git a/service-resource-manager-implementation/src/controllers/operations_controller.py b/service-resource-manager-implementation/src/controllers/operations_controller.py index 7073582..528d97b 100644 --- a/service-resource-manager-implementation/src/controllers/operations_controller.py +++ b/service-resource-manager-implementation/src/controllers/operations_controller.py @@ -1,4 +1,4 @@ -import paramiko +# import paramiko import logging from src.models.helm_install_model import HelmInstall from os import environ diff --git a/service-resource-manager-implementation/src/core/__init__.py b/service-resource-manager-implementation/src/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/service-resource-manager-implementation/src/core/piedge_encoder.py b/service-resource-manager-implementation/src/core/piedge_encoder.py deleted file mode 100644 index 9f6f8dd..0000000 --- a/service-resource-manager-implementation/src/core/piedge_encoder.py +++ /dev/null @@ -1,325 +0,0 @@ -import logging -import traceback -import connexion -import six -import json -import sys -import os - -# from src.models.service_function_registration_request import ServiceFunctionRegistrationRequest # noqa: E501 -from src.models.deploy_service_function import DeployServiceFunction # noqa: E501 -from src.core import paas_handler -#from src.utils import connector_db -from src.utils import kubernetes_connector, connector_db, auxiliary_functions, nodes_monitoring - -driver=os.environ['DRIVER'].strip() - - -def deploy_chain(chain_input): - for app_ in chain_input["chain_paas_services_order"]: - for app_details in chain_input["apps"]: - if app_ == app_details["paas_input_name"]: - - app_details["paas_input_name"]= chain_input["chain_service_name"] + "-" + app_ - response = deploy_service_function(app_details) - - break - return "Chain deployed successfully" - - -def deploy_service_function(service_function: DeployServiceFunction, paas_name=None): # noqa: E501 - - # descriptor_paas_input["scaling_type"]="minimize_cost" - # print(descriptor_paas_input) - # we need to create the descriptor_paas_ needed for deployment - # search if app exists in the catalogue - - - - ser_function_ = connector_db.get_documents_from_collection("service_functions", input_type="name", - input_value=service_function.service_function_name) - if not ser_function_: - return "The given service function does not exist in the catalogue" - - - # search if node exists in the node catalogue - # if service_function.location is not None: - # node_ = connector_db.get_documents_from_collection("points_of_presence", input_type="location", - # input_value=service_function.location) - # if not node_: - # return "The given location does not exist in the node catalogue" - - final_deploy_descriptor = {} - # final_deploy_descriptor["name"]=app_[0]["name"] - - # deployed_name= app_[0]["name"] + "emp"+ descriptor_paas_input["paas_input_name"] - if paas_name is not None: - final_deploy_descriptor["paas_name"] = paas_name - - deployed_name = service_function.service_function_instance_name - - - deployed_name= auxiliary_functions.prepare_name(deployed_name, driver) - - final_deploy_descriptor["name"] = deployed_name - - - final_deploy_descriptor["count-min"] = 1 if service_function.count_min is None else service_function.count_min - final_deploy_descriptor["count-max"] = 1 if service_function.count_max is None else service_function.count_max - - if final_deploy_descriptor["count-min"]>final_deploy_descriptor["count-max"]: - final_deploy_descriptor["count-min"]=final_deploy_descriptor["count-max"] - - if service_function.location is not None: - final_deploy_descriptor["location"] = service_function.location - - containers = [] - con_ = {} - con_["image"] = ser_function_[0]["image"] - - if "privileged" in ser_function_[0]: - - con_["privileged"]=ser_function_[0]["privileged"] - - - #con_["imagePullPolicy"] = "Always" - #ports - - application_ports = ser_function_[0].get("application_ports") - con_["application_ports"] = application_ports - - if service_function.all_node_ports is not None: - - if service_function.all_node_ports==False and service_function.node_ports is None: - return "Please provide the application ports in the field exposed_ports or all_node_ports==true" - - if service_function.all_node_ports: - con_["exposed_ports"] = application_ports - else: - - exposed_ports = auxiliary_functions.return_equal_ignore_order(application_ports, - service_function.node_ports) - if exposed_ports: - con_["exposed_ports"] = exposed_ports - # application_ports = ser_function_[0]["application_ports"] - # con_["application_ports"] = application_ports - # containers.append(con_) - else: - if service_function.node_ports is not None: - exposed_ports = auxiliary_functions.return_equal_ignore_order(application_ports, - service_function.node_ports) - if exposed_ports: - - con_["exposed_ports"] = exposed_ports - containers.append(con_) - - - final_deploy_descriptor["containers"] = containers - #final_deploy_descriptor["restartPolicy"] = "Always" - - #check volumes!! - req_volumes = [] - if "required_volumes" in ser_function_[0]: - if ser_function_[0].get("required_volumes") is not None: - for required_volumes in ser_function_[0]["required_volumes"]: - req_volumes.append(required_volumes["name"]) - vol_mount = [] - volume_input = [] - - - if service_function.volume_mounts is not None: - for volume_mounts in service_function.volume_mounts: - - vo_in = {} - - vo_in["name"] = volume_mounts.name - vo_in["storage"] = volume_mounts.storage - volume_input.append(vo_in) - vol_mount.append(volume_mounts.name) - if (len(vol_mount) != len(req_volumes)): - return "The selected service function requires " + str(len(req_volumes)) +" volume/ volumes " - else: - if ser_function_[0].get("required_volumes") is not None: - - result = auxiliary_functions.equal_ignore_order(req_volumes, vol_mount) - - if result is False: - return "The selected service function requires " + str(len(req_volumes)) +" volume/ volumes. Please check volume names" - else: - volumes=[] - for vol in ser_function_[0]["required_volumes"]: - for vol_re in service_function.volume_mounts: - vol_={} - if vol["name"]==vol_re.name: - vol_["name"]=vol_re.name - vol_["storage"]=vol_re.storage - vol_["path"]=vol["path"] - if "hostpath" in vol: - vol_["hostpath"] = vol["hostpath"] - volumes.append(vol_) - final_deploy_descriptor["volumes"] = volumes - - #check env parameters: - req_env_parameters = [] - - - if "required_env_parameters" in ser_function_[0]: - if ser_function_[0].get("required_env_parameters") is not None: - for required_env_parameters in ser_function_[0]["required_env_parameters"]: - req_env_parameters.append(required_env_parameters["name"]) - env_names = [] - env_input = [] - if service_function.env_parameters is not None: - for env_parameters in service_function.env_parameters: - env_in = {} - - env_in["name"] = env_parameters.name - if env_parameters.value is not None: - env_in["value"] = env_parameters.value - elif env_parameters.value_ref is not None: - env_in["value_ref"] = env_parameters.value_ref - env_input.append(env_in) - env_names.append(env_parameters.name) - if (len(env_names) != len(req_env_parameters)): - return "The selected service function requires " + str(len(req_env_parameters)) + " env parameters" - else: - if ser_function_[0].get("required_env_parameters") is not None: - - result = auxiliary_functions.equal_ignore_order(req_env_parameters, env_names) - - if result is False: - return "The selected service function requires " + str( - len(req_env_parameters)) + " env parameters. Please check names of env parameters" - else: - #EnvParameters to dict - paremeters = [] - for reqenv in ser_function_[0]["required_env_parameters"]: - for env_in in service_function.env_parameters: - reqenv_ = {} - if reqenv["name"] == env_in.name: - reqenv_["name"] = env_in.name - if env_in.value is not None: - reqenv_["value"] = env_in.value - elif env_in.value_ref is not None: - reqenv_["value_ref"] = env_in.value_ref - paremeters.append(reqenv_) - final_deploy_descriptor["env_parameters"] = paremeters - - - #check autoscaling policies - if "autoscaling_policies" in ser_function_[0]: - if ser_function_[0].get("autoscaling_policies") is not None: - if service_function.autoscaling_metric is not None: - for scaling_method in ser_function_[0]["autoscaling_policies"]: - if service_function.autoscaling_policy is not None: - if scaling_method["policy"] == service_function.autoscaling_policy: - for metric in scaling_method["monitoring_metrics"]: - - if metric["metric"] == service_function.autoscaling_metric: - scaling_metric_ = [] - scaling_metric_.append(metric) - final_deploy_descriptor["autoscaling_policies"] = scaling_metric_ - break - - ##################START##################### TODO!!!!!!!!!!!!!!!!!!!!1 - - # #Get deployed apps to check if app exist (if yes use patch methods) - # deployed_apps = kubernetes_connector.get_deployed_apps() - # - # - # exists_flag = False - # for deployed_app in deployed_apps: - # if "appname" in deployed_app: - # if final_deploy_descriptor["name"] == deployed_app["appname"]: - # exists_flag = True - # break - - exists_flag=False - ##################END##################### - - - if exists_flag: - response = kubernetes_connector.patch_service_function(final_deploy_descriptor) - else: - - response = kubernetes_connector.deploy_service_function(final_deploy_descriptor) - # insert it to mongo db - deployed_service_function_db = {} - deployed_service_function_db["service_function_name"] = ser_function_[0]["name"] - if service_function.location is not None: - deployed_service_function_db["location"] = service_function.location - deployed_service_function_db["instance_name"] = deployed_name - - if service_function.autoscaling_policy is not None: - deployed_service_function_db["autoscaling_policy"] = service_function.autoscaling_policy - - if "volumes" in final_deploy_descriptor: - deployed_service_function_db["volumes"] = final_deploy_descriptor["volumes"] - if "env_parameters" in final_deploy_descriptor: - deployed_service_function_db["env_parameters"] = final_deploy_descriptor["env_parameters"] - - if service_function.monitoring_services: - monitor_url = nodes_monitoring.create_monitoring_for_service_function(service_function) - deployed_service_function_db["monitoring_service_URL"] = monitor_url - - if "paas_name" in final_deploy_descriptor: - deployed_service_function_db["paas_name"] = final_deploy_descriptor["paas_name"] - - if "Conflict" not in response: - - if "location" not in deployed_service_function_db: - deployed_service_function_db["location"]= "Node is selected by the K8s scheduler" - connector_db.insert_document_deployed_service_function(document=deployed_service_function_db) - - return response - # return "PaaS deployed successfully" - # except Exception as ce_: - - # logging.error(traceback.format_exc()) - # # logging.error("ERROR NAME: ", fname) - # # logging.error("ERROR INFO: ", exc_tb.tb_lineno) - # return ("An exception occurred :", ce_) - - -def initiliaze_edge_nodes(): - try: - - nodes = kubernetes_connector.get_PoPs() - kubernetes_connector.create_immediate_storageclass() - # write it to mongodb - nodes_mon=[] - - nodes_num=0 - for node in nodes: - nodes_num=nodes_num+1 - node_ = {} - node_["name"] = node.name - node_["location"] = node.location - node_["_id"] = node.id - node_["serial"] = node.serial - node_["node_type"] = node.node_type - #create monitoring url - # http://146.124.106.230:3000/d/piedge-k8smaster/k8smaster-node?orgId=1&refresh=1m - #node_["stats_url"]="http://146.124.106.230:3000/d/piedge-k8smaster/k8smaster-node?orgId=1&refresh=1m" - - mon_url=nodes_monitoring.create_monitoring_infra_per_node(node_,nodes_num) - # print('MON_URL: '+mon_url) - node_["nodeUsageMonitoringURL"]=mon_url - # print(node_) - connector_db.insert_document_nodes(node_) - nodes_mon.append(node_) - - # #Creating storageclass for each node - Will be mostly used for migrating stateful applications. - # for node in nodes: - # kubernetes_connector.create_node_storageclass(node) - - # #creates storage class with immediate volume binding mode - will be used for pvc migration - # for node in nodes: - # kubernetes_connector.create_immediate_storageclass(node) - - - nodes_monitoring.create_monitoring_for_all_infra(nodes_mon) - return "Nodes initialized" - except Exception as ce_: - logging.error(traceback.format_exc()) - raise Exception("An exception occurred :", ce_) diff --git a/service-resource-manager-implementation/src/encoder.py b/service-resource-manager-implementation/src/encoder.py index 3451299..1de7e66 100644 --- a/service-resource-manager-implementation/src/encoder.py +++ b/service-resource-manager-implementation/src/encoder.py @@ -1,10 +1,9 @@ -from connexion.apps.flask_app import FlaskJSONEncoder +from connexion.jsonifier import Jsonifier import six from src.models.base_model_ import Model - -class JSONEncoder(FlaskJSONEncoder): +class JSONEncoder(Jsonifier): include_nulls = False def default(self, o): @@ -17,4 +16,4 @@ class JSONEncoder(FlaskJSONEncoder): attr = o.attribute_map[attr] dikt[attr] = value return dikt - return FlaskJSONEncoder.default(self, o) + return Jsonifier.default(self, o) diff --git a/service-resource-manager-implementation/src/swagger/swagger.yaml b/service-resource-manager-implementation/src/swagger/swagger.yaml index f69163b..46550ce 100644 --- a/service-resource-manager-implementation/src/swagger/swagger.yaml +++ b/service-resource-manager-implementation/src/swagger/swagger.yaml @@ -16,14 +16,14 @@ externalDocs: servers: - url: http://vitrualserver:8080/srm/1.0.0 paths: - /zone-details/{zone_id}: + /node/{node_id}: get: tags: - Nodes summary: Get Node details by Node identifier operationId: node_details parameters: - - name: zone_id + - name: node_id in: path description: Gets node details required: true @@ -38,7 +38,7 @@ paths: description: Method not allowed "404": description: Node does not exist - x-openapi-router-controller: src.controllers.nodes_controller + x-openapi-router-controller: src.controllers.edge_cloud_management_controller /helm: post: tags: diff --git a/srm-deployment.yaml b/srm-deployment.yaml index 3424da5..6b94526 100644 --- a/srm-deployment.yaml +++ b/srm-deployment.yaml @@ -33,9 +33,9 @@ spec: containers: - env: - name: KUBERNETES_MASTER_IP - value: k3d-sunriseop-server-0 + value: 146.124.106.200 - name: KUBERNETES_MASTER_PORT - value: "6443" + value: "16443" - name: KUBERNETES_USERNAME value: cluster-admin - name: K8S_NAMESPACE @@ -43,22 +43,16 @@ spec: - name: EMP_STORAGE_URI value: mongodb://mongosrm:27017 - name: KUBERNETES_MASTER_TOKEN - value: eyJhbGciOiJSUzI1NiIsImtpZCI6IkRRS3VMNktkc1BOYk5ZeDhfSnFvVmJQdkJ6em1FODhPeHNIMHFya3JEQzgifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6InNybS1zZWNyZXQiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiY2x1c3Rlci1hZG1pbiIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6ImU1MjUxZjhiLWY2ODItNDU0Ni1hOTgxLWNlNTk0YTg2NmZiNCIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmNsdXN0ZXItYWRtaW4ifQ.rnZyHFEE1ywceWqcio0UKQrp5GdfVGQOCXxx3RJpb_vvDj65GvNwN0VgA_anOlzj8kKJ9JQjWrA7an2k-5w0ycjeu8Ei_5Z0dvgRSpvKc4O5kCHddOB1kJl480hKWtZqgL0Vi6YbOziFGqvPd8hxHSTquxUgXEN2BStqII8MpVEK8z8iU2pJE5CNIaukGBozjlgc1Vb6HiEU4_UhlqG61uO6ReRVrzaYa4T1j4Zvvx1JN8t2HYcuv50QlHPrEAfW2F3ed0SBbb_X8AT0pGJrVas_uqZgMcN1j5BLO51RNmCY27ADHwCbj8HWuiHhyuLKQxYw8yKB-iMNQmq2fk3ezw + value: T3FRNnNVK25FY3I5ZHlNYmxrSEFpd2VPcW5WTlliTnRVNVo3bitNY1B3az0K - name: ARTIFACT_MANAGER_ADDRESS value: http://artefact-manager-service:8000 - name: EDGE_CLOUD_ADAPTER_NAME value: kubernetes - name: ADAPTER_BASE_URL - value: k3d-sunriseop-server-0 - - name: NETWORK_ADAPTER_NAME - value: open5gs - - name: NETWORK_ADAPTER_BASE_URL - value: http://192.168.124.233:8002/ - - name: SCS_AS_ID - value: scs + value: 146.124.106.200 - name: PLATFORM_PROVIDER - value: ISI - image: ghcr.io/sunriseopenoperatorplatform/srm/srm:1.0.0 + value: ICOM + image: ghcr.io/sunriseopenoperatorplatform/srm/srm:1.0.1 name: srmcontroller ports: - containerPort: 8080 @@ -79,12 +73,10 @@ metadata: io.kompose.service: srm name: srm spec: - type: NodePort + type: ClusterIP ports: - name: "8080" - nodePort: 32415 port: 8080 - targetPort: 8080 selector: io.kompose.service: srmcontroller status: @@ -186,3 +178,127 @@ spec: io.kompose.service: mongosrm status: loadBalancer: {} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.cmd: kompose convert + kompose.version: 1.26.0 (40646f47) + creationTimestamp: null + labels: + io.kompose.service: oegmongo + name: oegmongo +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: oegmongo + strategy: + type: Recreate + template: + metadata: + annotations: + kompose.cmd: kompose convert + kompose.version: 1.26.0 (40646f47) + creationTimestamp: null + labels: + #io.kompose.network/netEMPkub: "true" + io.kompose.service: oegmongo + spec: + containers: + - image: mongo + name: oegmongo + ports: + - containerPort: 27017 + resources: {} + restartPolicy: Always +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.cmd: kompose convert + kompose.version: 1.26.0 (40646f47) + creationTimestamp: null + labels: + io.kompose.service: oegmongo + name: oegmongo +spec: + type: ClusterIP + ports: + - name: "27018" + port: 27018 + targetPort: 27017 + selector: + io.kompose.service: oegmongo +status: + loadBalancer: {} + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.cmd: kompose convert + kompose.version: 1.26.0 (40646f47) + creationTimestamp: null + labels: + io.kompose.service: oegcontroller + name: oegcontroller +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: oegcontroller + strategy: {} + template: + metadata: + annotations: + kompose.cmd: kompose convert + kompose.version: 1.26.0 (40646f47) + creationTimestamp: null + labels: + io.kompose.service: oegcontroller + spec: + containers: + - env: + - name: MONGO_URI + value: mongodb://oegmongo/sample_db?authSource=admin + - name: SRM_HOST + value: http://srm:8080/srm/1.0.0 + - name: FEDERATION_MANAGER_HOST + value: http://federation-manager:8989 + image: ghcr.io/sunriseopenoperatorplatform/oeg/oeg + name: oegcontroller + ports: + - containerPort: 8080 + resources: {} + imagePullPolicy: Always + restartPolicy: Always + +status: {} +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.cmd: kompose convert + kompose.version: 1.26.0 (40646f47) + creationTimestamp: null + labels: + io.kompose.service: oeg + name: oeg +spec: + type: NodePort + ports: + - name: "8080" + nodePort: 32414 + port: 8080 + targetPort: 8080 + selector: + io.kompose.service: oegcontroller +status: + loadBalancer: {} + + -- GitLab