diff --git a/service-resource-manager-implementation/requirements.txt b/service-resource-manager-implementation/requirements.txt index bd15bd202eaf655ceea95ad7166329c26ce9e7c1..e2b52f2793ae2389a11566a43a3f5642c0063412 100644 --- a/service-resource-manager-implementation/requirements.txt +++ b/service-resource-manager-implementation/requirements.txt @@ -5,4 +5,4 @@ requests==2.32.4 psycopg2-binary urllib3 pydantic-extra-types==2.10.3 -sunrise6g-opensdk==1.1.1 \ No newline at end of file +sunrise6g-opensdk==2.0.0 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 5b6267e1adaee386c24472ea80d3a085374a4eff..abff91e01ca54550e800e33f7de8717fad1f00a9 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 @@ -48,6 +48,10 @@ def _safe_http_json_response(response): if isinstance(response, (list, dict)): return response, 200 + # Adapter returned normalized tuple + if isinstance(response, tuple) and len(response) == 2: + return response + # Adapter returned nothing if response is None: return {"error": "Adapter returned no response"}, 502 diff --git a/service-resource-manager-implementation/src/controllers/fm_internal_controller.py b/service-resource-manager-implementation/src/controllers/fm_internal_controller.py new file mode 100644 index 0000000000000000000000000000000000000000..4e59fa8cbd632126df7b11b62ff385e23ed595d5 --- /dev/null +++ b/service-resource-manager-implementation/src/controllers/fm_internal_controller.py @@ -0,0 +1,380 @@ +import connexion +import logging + +from requests import Response as RequestsResponse + +from src.controllers.edge_cloud_management_controller import edgecloud_adapter + + +logger = logging.getLogger(__name__) + + +def _normalize_adapter_response(response): + if isinstance(response, RequestsResponse): + try: + data = response.json() + except ValueError: + data = response.text + return data, response.status_code + return response + + +def _call_first_available(target, *method_names): + for method_name in method_names: + method = getattr(target, method_name, None) + if method: + return method + raise AttributeError(f"None of these methods exist on {type(target).__name__}: {method_names}") + + +def _repo_type_to_package_type(repo_data): + descriptor_type = (repo_data.get("artefactDescriptorType") or "").upper() + virt_type = (repo_data.get("artefactVirtType") or "").upper() + if descriptor_type == "HELM": + return "HELM" + if virt_type == "VM_TYPE": + return "VM" + return "CONTAINER" + + +def _repo_credentials(repo_location): + if not isinstance(repo_location, dict): + return {} + return { + "repoURL": repo_location.get("repoURL"), + "userName": repo_location.get("userName") or "", + "password": repo_location.get("password") or "", + "token": repo_location.get("token") or "", + } + + +def _build_app_repo_from_artefact(artefact_data): + repo_location = _repo_credentials(artefact_data.get("artefactRepoLocation")) + repo_url = repo_location.get("repoURL") + image_path = repo_url + artefact_name = artefact_data.get("artefactName") + version = artefact_data.get("artefactVersionInfo") + + if repo_url and version: + if repo_url.endswith(f":{version}"): + image_path = repo_url + elif repo_url.endswith(f"/{artefact_name}"): + image_path = f"{repo_url}:{version}" + elif artefact_name: + image_path = f"{repo_url.rstrip('/')}/{artefact_name}:{version}" + elif artefact_name and version: + image_path = f"{artefact_name}:{version}" + + return { + "imagePath": image_path, + "repoURL": repo_url, + "userName": repo_location.get("userName"), + "credentials": repo_location.get("password") or repo_location.get("token"), + } + + +def _normalize_component_specs(component_specs): + normalized_specs = [] + for component in component_specs or []: + normalized_component = dict(component) + network_interfaces = [] + for interface in component.get("exposedInterfaces") or []: + port = interface.get("commPort") + if port is None: + continue + network_interfaces.append( + { + "port": port, + "protocol": (interface.get("commProtocol") or "TCP").upper(), + "visibilityType": interface.get("visibilityType") or "VISIBILITY_EXTERNAL", + } + ) + if network_interfaces: + normalized_component["networkInterfaces"] = network_interfaces + normalized_specs.append(normalized_component) + return normalized_specs + + +def _build_manifest_from_gsma_onboarding(request_data): + component_specs = _normalize_component_specs(request_data.get("appComponentSpecs") or []) + artefact_id = None + if component_specs: + artefact_id = component_specs[0].get("artefactId") + + artefact_data = None + if artefact_id: + get_artefact_method = _call_first_available( + edgecloud_adapter, + "get_artefact_gsma", + "get_artefact", + ) + artefact_response = _normalize_adapter_response(get_artefact_method(artefact_id)) + if isinstance(artefact_response, tuple): + artefact_data, status = artefact_response + if status != 200: + return artefact_data, status + + app_meta_data = request_data.get("appMetaData") or {} + manifest = { + "appId": request_data.get("appId"), + "name": app_meta_data.get("appName") or request_data.get("appId"), + "appName": app_meta_data.get("appName") or request_data.get("appId"), + "version": app_meta_data.get("version") or "1.0.0", + "appDescription": app_meta_data.get("appDescription") or "", + "appProvider": request_data.get("appProviderId"), + "packageType": _repo_type_to_package_type(artefact_data or {}), + "appComponentSpecs": component_specs, + "appDeploymentZones": [ + {"zoneId": zone_id} for zone_id in request_data.get("appDeploymentZones") or [] + ], + } + + if artefact_data: + manifest["appRepo"] = _build_app_repo_from_artefact(artefact_data) + + return manifest, 200 + + +def _normalize_onboarding_response(response): + normalized = _normalize_adapter_response(response) + if isinstance(normalized, tuple): + data, status = normalized + if status == 201: + return data, 200 + return data, status + return normalized + + +def _normalize_delete_response(response): + normalized = _normalize_adapter_response(response) + if isinstance(normalized, tuple): + data, status = normalized + if status == 204: + return "deleted", 200 + return data, status + return normalized + + +def _build_deploy_request_from_gsma(request_data): + zone_info = request_data.get("zoneInfo") or {} + zone_id = zone_info.get("zoneId") + app_zones = [] + if zone_id: + app_zones.append({"zoneId": zone_id}) + return request_data.get("appId"), app_zones + + +def _build_zone_summary(zone): + zone_id = zone.get("zoneId") or zone.get("edgeCloudZoneId") + return { + "zoneId": zone_id, + "edgeCloudRegion": zone.get("edgeCloudRegion", "unknown"), + } + + +def _build_zone_details(zone_id, zone): + return { + "zoneId": zone_id, + "edgeCloudRegion": zone.get("edgeCloudRegion", "unknown"), + "reservedComputeResources": [], + "computeResourceQuotaLimits": [], + "flavoursSupported": [ + { + "flavourId": zone_id, + "numCPU": 0, + "memorySize": 0, + "storageSize": 0, + "supportedOSTypes": ["OTHER"], + "cpuArchType": "OTHER", + "gpu": [], + "fpga": 0, + "vpu": 0, + "hugepages": [], + "cpuExclusivity": False, + } + ], + "networkResources": {}, + "zoneServiceLevelObjsInfo": {}, + } + + +def _extract_onboarded_app(response): + normalized = _normalize_adapter_response(response) + if isinstance(normalized, tuple): + data, status = normalized + if isinstance(data, dict) and "appManifest" in data: + return data["appManifest"], status + return data, status + return normalized + + +def _extract_deployed_app(response): + normalized = _normalize_adapter_response(response) + if isinstance(normalized, tuple): + data, status = normalized + if isinstance(data, dict) and data.get("edgeCloudZoneId") is None and data.get("zoneId") is not None: + data = dict(data) + data["edgeCloudZoneId"] = data.get("zoneId") + if isinstance(data, dict) and "appInstance" in data: + return data["appInstance"], status + return data, status + return normalized + + +def get_zones_list(): + response = edgecloud_adapter.get_edge_cloud_zones() + normalized = _normalize_adapter_response(response) + if isinstance(normalized, tuple): + data, status = normalized + if isinstance(data, list): + data = [_build_zone_summary(zone) for zone in data] + return data, status + return normalized + + +def get_zones(): + response = edgecloud_adapter.get_edge_cloud_zones() + normalized = _normalize_adapter_response(response) + if isinstance(normalized, tuple): + data, status = normalized + if isinstance(data, list): + data = [ + _build_zone_details(zone.get("zoneId") or zone.get("edgeCloudZoneId"), zone) + for zone in data + ] + return data, status + return normalized + + +def get_zone(zone_id: str): + response = edgecloud_adapter.get_edge_cloud_zones() + normalized = _normalize_adapter_response(response) + if isinstance(normalized, tuple): + data, status = normalized + if status != 200: + return data, status + for zone in data: + current_zone_id = zone.get("zoneId") or zone.get("edgeCloudZoneId") + if current_zone_id == zone_id: + return _build_zone_details(zone_id, zone), 200 + return {"error": "Zone not found"}, 404 + return normalized + + +def create_artefact(): + if not connexion.request.is_json: + return {"error": "Wrong request schema"}, 400 + create_artefact_method = _call_first_available( + edgecloud_adapter, + "create_artefact_gsma", + "create_artefact", + ) + response = create_artefact_method(connexion.request.get_json()) + return _normalize_adapter_response(response) + + +def delete_artefact(artefact_id: str): + delete_artefact_method = _call_first_available( + edgecloud_adapter, + "delete_artefact", + "delete_artefact_gsma", + ) + response = delete_artefact_method(artefact_id) + return _normalize_delete_response(response) + + +def create_onboarding(): + if not connexion.request.is_json: + return {"error": "Wrong request schema"}, 400 + manifest, status = _build_manifest_from_gsma_onboarding(connexion.request.get_json()) + if status != 200: + return manifest, status + response = edgecloud_adapter.onboard_app(manifest) + return _normalize_onboarding_response(response) + + +def get_onboarding(app_id: str): + response = edgecloud_adapter.get_onboarded_app(app_id) + return _extract_onboarded_app(response) + + +def update_onboarding(app_id: str): + if not connexion.request.is_json: + return {"error": "Wrong request schema"}, 400 + request_data = connexion.request.get_json() + response = edgecloud_adapter.get_onboarded_app(app_id) + normalized = _extract_onboarded_app(response) + if isinstance(normalized, tuple): + data, status = normalized + if status != 200: + return data, status + if isinstance(data, dict): + if request_data.get("appUpdQoSProfile"): + data["appUpdQoSProfile"] = request_data.get("appUpdQoSProfile") + if request_data.get("appComponentSpecs"): + data["appComponentSpecs"] = _normalize_component_specs(request_data.get("appComponentSpecs")) + patch_method = _call_first_available( + edgecloud_adapter, + "patch_onboarded_app", + "patch_onboarded_app_gsma", + ) + patch_response = patch_method(app_id, data) + normalized_patch = _normalize_adapter_response(patch_response) + if isinstance(normalized_patch, tuple): + patch_data, patch_status = normalized_patch + return patch_data, patch_status + return normalized_patch + return normalized + + +def delete_onboarding(app_id: str): + response = edgecloud_adapter.delete_onboarded_app(app_id) + return _normalize_delete_response(response) + + +def deploy_app(): + if not connexion.request.is_json: + return {"error": "Wrong request schema"}, 400 + request_data = connexion.request.get_json() + app_id, app_zones = _build_deploy_request_from_gsma(request_data) + response = edgecloud_adapter.deploy_app(app_id, app_zones) + normalized = _extract_deployed_app(response) + if isinstance(normalized, tuple): + data, status = normalized + if status >= 400: + return data, status + if isinstance(data, dict): + return { + "appInstIdentifier": data.get("appInstanceId"), + "zoneID": data.get("edgeCloudZoneId"), + }, status + return normalized + + +def get_deployed_app(app_id: str, app_instance_id: str, zone_id: str): + response = edgecloud_adapter.get_deployed_app(app_instance_id) + normalized = _extract_deployed_app(response) + if isinstance(normalized, tuple): + data, status = normalized + if status != 200: + return data, status + if data.get("appId") != app_id or data.get("edgeCloudZoneId") != zone_id: + return {"error": "App instance not found"}, 404 + return { + "appInstanceState": data.get("appInstanceState") or data.get("status"), + "accesspointInfo": data.get("accesspointInfo", []), + }, status + return normalized + + +def undeploy_app(app_id: str, app_instance_id: str, zone_id: str): + response = edgecloud_adapter.get_deployed_app(app_instance_id) + normalized = _extract_deployed_app(response) + if isinstance(normalized, tuple): + data, status = normalized + if status != 200: + return data, status + if data.get("appId") != app_id or data.get("edgeCloudZoneId") != zone_id: + return {"error": "App instance not found"}, 404 + response = edgecloud_adapter.undeploy_app(app_instance_id) + return _normalize_delete_response(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 cd95b55b7cb3aa0b739a64c8ca4f447f3a324336..5751c5f0ec32a6287824c18459ce19f60b311611 100644 --- a/service-resource-manager-implementation/src/controllers/network_functions_controller.py +++ b/service-resource-manager-implementation/src/controllers/network_functions_controller.py @@ -2,6 +2,7 @@ from os import environ import logging import connexion from sunrise6g_opensdk.common.sdk import Sdk as sdkclient +from sunrise6g_opensdk.network.core.common import CoreHttpError from sunrise6g_opensdk.network.core.schemas import RetrievalLocationRequest logger = logging.getLogger(__name__) @@ -49,6 +50,15 @@ def _safe_http_json_response(response): return {"error": "Invalid response from adapter", "details": str(e)}, 502 +def _map_core_http_error(err: CoreHttpError): + status_code = err.status_code or 502 + if isinstance(err.body, (dict, list)): + return err.body, status_code + if err.body: + return {"error": err.body}, status_code + return {"error": str(err)}, status_code + + def create_qod_session(): if connexion.request.is_json: try: @@ -79,6 +89,9 @@ def get_qod_session(session_id: str): "code": "UNAVAILABLE", "message": "Service unavailable: Network Adapter", }, 503 + except CoreHttpError as ce_: + logger.error(ce_) + return _map_core_http_error(ce_) except Exception as ce_: logger.error(ce_) return {"error": str(ce_)}, 500 @@ -95,6 +108,9 @@ def delete_qod_session(session_id: str): "code": "UNAVAILABLE", "message": "Service unavailable: Network Adapter", }, 503 + except CoreHttpError as ce_: + logger.error(ce_) + return _map_core_http_error(ce_) except Exception as ce_: logger.error(ce_) return {"error": str(ce_)}, 500 diff --git a/service-resource-manager-implementation/src/swagger/swagger.yaml b/service-resource-manager-implementation/src/swagger/swagger.yaml index a5a7645bbd5dbc753c1c5123a09c20633ca4c612..66b1c2a62697025a6470861ad4b436dbf2349ca2 100644 --- a/service-resource-manager-implementation/src/swagger/swagger.yaml +++ b/service-resource-manager-implementation/src/swagger/swagger.yaml @@ -16,6 +16,193 @@ externalDocs: servers: - url: http://vitrualserver:8080/srm/1.0.0 paths: + /internal/fm/zones/list: + get: + tags: + - FM Internal + summary: Returns GSMA zone list for FM + operationId: get_zones_list + responses: + "200": + description: Zone list retrieved + x-openapi-router-controller: src.controllers.fm_internal_controller + /internal/fm/zones: + get: + tags: + - FM Internal + summary: Returns GSMA zone details for FM + operationId: get_zones + responses: + "200": + description: Zone details retrieved + x-openapi-router-controller: src.controllers.fm_internal_controller + /internal/fm/zones/{zone_id}: + get: + tags: + - FM Internal + summary: Returns GSMA zone detail by id for FM + operationId: get_zone + parameters: + - name: zone_id + in: path + required: true + schema: + type: string + responses: + "200": + description: Zone detail retrieved + x-openapi-router-controller: src.controllers.fm_internal_controller + /internal/fm/artefacts: + post: + tags: + - FM Internal + summary: Create artefact through GSMA adapter + operationId: create_artefact + responses: + "200": + description: Artefact created + "201": + description: Artefact created + x-openapi-router-controller: src.controllers.fm_internal_controller + /internal/fm/artefacts/{artefact_id}: + delete: + tags: + - FM Internal + summary: Delete artefact through GSMA adapter + operationId: delete_artefact + parameters: + - name: artefact_id + in: path + required: true + schema: + type: string + responses: + "200": + description: Artefact deleted + "204": + description: Artefact deleted + x-openapi-router-controller: src.controllers.fm_internal_controller + /internal/fm/onboardings: + post: + tags: + - FM Internal + summary: Create onboarding through GSMA adapter + operationId: create_onboarding + responses: + "200": + description: Onboarding created + x-openapi-router-controller: src.controllers.fm_internal_controller + /internal/fm/onboardings/{app_id}: + get: + tags: + - FM Internal + summary: Get onboarding through GSMA adapter + operationId: get_onboarding + parameters: + - name: app_id + in: path + required: true + schema: + type: string + responses: + "200": + description: Onboarding retrieved + x-openapi-router-controller: src.controllers.fm_internal_controller + patch: + tags: + - FM Internal + summary: Update onboarding through GSMA adapter + operationId: update_onboarding + parameters: + - name: app_id + in: path + required: true + schema: + type: string + responses: + "200": + description: Onboarding updated + x-openapi-router-controller: src.controllers.fm_internal_controller + delete: + tags: + - FM Internal + summary: Delete onboarding through GSMA adapter + operationId: delete_onboarding + parameters: + - name: app_id + in: path + required: true + schema: + type: string + responses: + "200": + description: Onboarding deleted + "204": + description: Onboarding deleted + x-openapi-router-controller: src.controllers.fm_internal_controller + /internal/fm/deployments: + post: + tags: + - FM Internal + summary: Deploy app through GSMA adapter + operationId: deploy_app + responses: + "202": + description: Deployment accepted + x-openapi-router-controller: src.controllers.fm_internal_controller + /internal/fm/deployments/{app_id}/instances/{app_instance_id}/zones/{zone_id}: + get: + tags: + - FM Internal + summary: Get deployed app through GSMA adapter + operationId: get_deployed_app + parameters: + - name: app_id + in: path + required: true + schema: + type: string + - name: app_instance_id + in: path + required: true + schema: + type: string + - name: zone_id + in: path + required: true + schema: + type: string + responses: + "200": + description: Deployment retrieved + x-openapi-router-controller: src.controllers.fm_internal_controller + delete: + tags: + - FM Internal + summary: Delete deployed app through GSMA adapter + operationId: undeploy_app + parameters: + - name: app_id + in: path + required: true + schema: + type: string + - name: app_instance_id + in: path + required: true + schema: + type: string + - name: zone_id + in: path + required: true + schema: + type: string + responses: + "200": + description: Deployment deleted + "204": + description: Deployment deleted + x-openapi-router-controller: src.controllers.fm_internal_controller /node/{node_id}: get: tags: