diff --git a/.gitignore b/.gitignore index 6a6635ff05d73b5c971772eaaf95de1dceb3ebca..cef1e82f2277e914445cc7ab2e0521718102026a 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,4 @@ wheels/ htmlcov/ .mypy_cache/ .pytest_cache/ -.ruff_cache/ \ No newline at end of file +.ruff_cache/.idea/ diff --git a/e -i --root b/e -i --root new file mode 100644 index 0000000000000000000000000000000000000000..456481700d99d379fd9d6088e80d527c99e8143d --- /dev/null +++ b/e -i --root @@ -0,0 +1,133 @@ +f1b2b7b papathanail +5805914 geop4p +ab2153c gpapathan87 +1aa166a George Papathanail +95b4d76 George Papathanail +7e0a1a8 Laskaratos Dimitris +e7c0779 George Papathanail +ba67f75 Laskaratos Dimitris +006a3f4 Laskaratos Dimitris +b5c88bb Dimitrios Laskaratos +f2ede60 Laskaratos Dimitris +62d4b26 George Papathanail +8db04a1 gpapathan87 +0dc3e23 gpapathan87 +668f15a gpapathan87 +9fdc3f8 gpapathan87 +92af725 gpapathan87 +23918d2 gpapathan87 +9f30d28 gpapathan87 +21c04a4 gpapathan87 +310f9d3 gpapathan87 +c7cdf37 gpapathan87 +99e91e4 gpapathan87 +9676d5f gpapathan87 +49a1e8b gpapathan87 +3b64b8a gpapathan87 +33dde08 gpapathan87 +08bf480 gpapathan87 +36d67df Dimitrios Laskaratos +9d28ad8 Laskaratos Dimitris +aa74771 Laskaratos Dimitris +89a9f0b Dimitrios Laskaratos +4c1a5a7 gpapathan87 +e5bbc70 Laskaratos Dimitris +ddc9829 Dimitrios Laskaratos +3ae87ee Laskaratos Dimitris +2c7fadb Laskaratos Dimitris +f18e931 gpapathan87 +f5245d1 gpapathan87 +be4c643 gpapathan87 +9ab4cd1 gpapathan87 +d16b9e2 George Papathanail +e776570 George Papathanail +344b19b gpapathan87 +69359fb Dimitrios Laskaratos +615a75e Laskaratos Dimitris +ca80137 Laskaratos Dimitris +f64494c George Papathanail +1883fe7 George Papathanail +2c710c7 George Papathanail +c2b19a5 George Papathanail +f00cc34 George Papathanail +2ef4e00 George Papathanail +ace7d2f George Papathanail +427da14 George Papathanail +234aa2d George Papathanail +c4fac3f Laskaratos Dimitris +5019220 gpapathan87 +1c306f8 Laskaratos Dimitris +ffbdd36 gpapathan87 +115dc2b gpapathan87 +6853558 George Papathanail +d5b16ec George Papathanail +8fb1a20 George Papathanail +3cd0065 George Papathanail +e3d5a42 George Papathanail +0d19f78 George Papathanail +200f522 gpapathan87 +14a961a gpapathan87 +d1699f7 gpapathan87 +d97ed44 gpapathan87 +d9c0779 gpapathan87 +b9fcf2f gpapathan87 +691c4df gpapathan87 +26f6c9a gpapathan87 +9baba9a gpapathan87 +ed1acfe gpapathan87 +4c6e50b gpapathan87 +26c8c0e gpapathan87 +5c06d78 gpapathan87 +bc1f66d gpapathan87 +fed9024 gpapathan87 +40a326e gpapathan87 +cb477d5 gpapathan87 +91fa911 gpapathan87 +07c60a6 Laskaratos Dimitris +1eee74c Laskaratos Dimitris +081fb06 gpapathan87 +2d9454f Laskaratos Dimitris +4df132c Laskaratos Dimitris +470d794 gpapathan87 +f74b284 gpapathan87 +bf8adc9 Laskaratos Dimitris +27eb9a9 gpapathan87 +5529ce2 gpapathan87 +97e88f3 gpapathan87 +993ace0 Karagkounis Dimitris +c3fd363 Giorgos Papathanail +d2ef265 Giorgos Papathanail +2ea3991 Gr3at <33185243+Gr3at@users.noreply.github.com> +82f3614 Karagkounis Dimitris +6f1df82 Karagkounis Dimitris +9f45c7e Karagkounis Dimitris +1552918 Gr3at <33185243+Gr3at@users.noreply.github.com> +48c19e0 Gr3at <33185243+Gr3at@users.noreply.github.com> +aad20b0 Karagkounis Dimitris <33185243+Gr3at@users.noreply.github.com> +94cbd48 Karagkounis Dimitris <33185243+Gr3at@users.noreply.github.com> +66e9937 Karagkounis Dimitris <33185243+Gr3at@users.noreply.github.com> +3df1e4b Karagkounis Dimitris <33185243+Gr3at@users.noreply.github.com> +51c6362 Karagkounis Dimitris <33185243+Gr3at@users.noreply.github.com> +2773a8d Karagkounis Dimitris <33185243+Gr3at@users.noreply.github.com> +a4bf76c Gr3at <33185243+Gr3at@users.noreply.github.com> +f1ecd0b Karagkounis Dimitris <33185243+Gr3at@users.noreply.github.com> +15d89d8 Karagkounis Dimitris <33185243+Gr3at@users.noreply.github.com> +ba152ed Karagkounis Dimitris <33185243+Gr3at@users.noreply.github.com> +ea53f89 Karagkounis Dimitris <33185243+Gr3at@users.noreply.github.com> +a06eae6 Karagkounis Dimitris <33185243+Gr3at@users.noreply.github.com> +04ca70a Karagkounis Dimitris <33185243+Gr3at@users.noreply.github.com> +85d6d40 Karagkounis Dimitris <33185243+Gr3at@users.noreply.github.com> +098dde3 Karagkounis Dimitris <33185243+Gr3at@users.noreply.github.com> +fb80e5e Karagkounis Dimitris <33185243+Gr3at@users.noreply.github.com> +9964e70 Karagkounis Dimitris <33185243+Gr3at@users.noreply.github.com> +e2d6d85 Karagkounis Dimitris <33185243+Gr3at@users.noreply.github.com> +f7938fb Karagkounis Dimitris <33185243+Gr3at@users.noreply.github.com> +9cee43e Karagkounis Dimitris +37c8856 Karagkounis Dimitris +f28bc07 Karagkounis Dimitris +a5e2f46 Karagkounis Dimitris +4f2ed3a Karagkounis Dimitris +4a231db Karagkounis Dimitris +ed9f3db Karagkounis Dimitris +ab440c5 Karagkounis Dimitris +cd80d90 Gr3at <33185243+Gr3at@users.noreply.github.com> \ No newline at end of file diff --git a/edge_cloud_management_api/controllers/app_controllers.py b/edge_cloud_management_api/controllers/app_controllers.py index 01afae22df6f30a9b7b20519e39dfe168ca76db3..d1a98c06a02295008c5c18ff445278b5167eaa76 100644 --- a/edge_cloud_management_api/controllers/app_controllers.py +++ b/edge_cloud_management_api/controllers/app_controllers.py @@ -2,15 +2,91 @@ from flask import jsonify, request from pydantic import ValidationError from edge_cloud_management_api.managers.log_manager import logger from edge_cloud_management_api.services.edge_cloud_services import PiEdgeAPIClientFactory -from edge_cloud_management_api.services.federation_services import FederationManagerClientFactory -from edge_cloud_management_api.services.storage_service import get_zone -from edge_cloud_management_api.services.storage_service import get_fed +from edge_cloud_management_api.services.federation_services import FederationManagerClientFactory +from edge_cloud_management_api.services.storage_service import get_zone +from edge_cloud_management_api.services.storage_service import get_fed +import json +import re +import uuid factory = FederationManagerClientFactory() federation_client = factory.create_federation_client() -class NotFound404Exception(Exception): - pass +class NotFound404Exception(Exception): + pass + + +def _ensure_gsma_id(value, pattern, prefix, min_len, max_len, fallback_source): + if value and re.match(pattern, value): + return value + base = re.sub(r"[^A-Za-z0-9_]", "", str(fallback_source or "")) + candidate = f"{prefix}{base}" + if not re.match(r"^[A-Za-z]", candidate): + candidate = f"{prefix}{candidate}" + candidate = candidate[:max_len] + if len(candidate) < min_len: + candidate = (candidate + uuid.uuid4().hex)[:max_len] + if not re.match(r"^[A-Za-z]", candidate): + candidate = f"{prefix}{candidate}" + candidate = candidate[:max_len] + return candidate + + +def _ensure_service_name(value, prefix, fallback_source): + pattern = r"^[A-Za-z0-9][A-Za-z0-9_]{6,62}[A-Za-z0-9]$" + if value and re.match(pattern, value): + return value + base = re.sub(r"[^A-Za-z0-9_]", "", str(fallback_source or "")) + candidate = f"{prefix}{base}" + candidate = re.sub(r"^_+", "", candidate) + candidate = candidate[:64] + if len(candidate) < 8: + candidate = (candidate + uuid.uuid4().hex)[:64] + if not re.match(r"^[A-Za-z0-9]", candidate): + candidate = f"s{candidate}" + if not re.match(r"[A-Za-z0-9]$", candidate): + candidate = f"{candidate}0" + return candidate[:64] + + +def _split_image_reference(image_path): + if not image_path: + return None, None + clean_path = str(image_path).lstrip("/") + parts = clean_path.split("/", 1) + if len(parts) == 1: + return "docker.io", clean_path + registry_candidate = parts[0] + if "." in registry_candidate or ":" in registry_candidate: + return registry_candidate, parts[1] + return "docker.io", clean_path + + +def _split_image_name_tag(image_ref): + if not image_ref: + return None, None + if "@" in image_ref: + name, digest = image_ref.split("@", 1) + return name, digest + if ":" in image_ref: + name, tag = image_ref.rsplit(":", 1) + return name, tag + return image_ref, "latest" + + +def _ensure_res_pool(value, fallback_source): + pattern = r"^[A-Za-z0-9][A-Za-z0-9_]{6,30}[A-Za-z0-9]$" + if value and re.match(pattern, value): + return value + base = re.sub(r"[^A-Za-z0-9_]", "", str(fallback_source or "")) + candidate = f"respool{base}"[:32] + if len(candidate) < 8: + candidate = (candidate + uuid.uuid4().hex)[:32] + if not re.match(r"^[A-Za-z0-9]", candidate): + candidate = f"r{candidate}" + if not re.match(r"[A-Za-z0-9]$", candidate): + candidate = f"{candidate}0" + return candidate[:32] def submit_app(body: dict): @@ -88,122 +164,631 @@ def delete_app(appId, x_correlator=None): 500, ) + def create_app_instance(): logger.info("Received request to create app instance") + try: - body = request.get_json() - logger.debug(f"Request body: {body}") - - app_id = body.get("appId") - app_zones = body.get("appZones") - pi_edge_client_factory = PiEdgeAPIClientFactory() - pi_edge_client = pi_edge_client_factory.create_pi_edge_api_client() - - if not app_id or not app_zones : - return jsonify({"error": "Missing required fields: appId, edgeCloudZoneId, or kubernetesCLusterRef"}), 400 - - zone = get_zone(app_zones[0].get('EdgeCloudZone').get('edgeCloudZoneId')) - if zone.get('isLocal')=='false': - # Step 1: retrieve app metadata - appData = pi_edge_client.get_app(appId=app_id).get('appManifest') - #Step 2: compose GSMA artefact payload - artefact = {} - artefact['artefactId'] = app_id - artefact['appProviderId'] = appData.get('appProvider') - artefact['artefactName'] = appData.get('name') - artefact['artefactVersionInfo'] = appData.get('version') - artefact['artefactDescription'] = '' - repoInfo = appData.get('appRepo') - artefact['repoType'] = repoInfo.get('type') - artefact['artefactRepoLocation'] = {'repoURL': repoInfo.get('imagePath'), 'userName': repoInfo.get('userName'), 'password': repoInfo.get('credentials'), 'token': ''} - exposedInterfaces = [] - networkInterfaces = appData.get('componentSpec')[0].get('networkInterfaces') - for ni in networkInterfaces: - interface = {'interfaceId': '', 'commProtocol': ni.get('protocol'), 'commPort': ni.get('port'), 'visibilityType': ni.get('visibilityType'), 'network': '', 'InterfaceName': ''} - exposedInterfaces.append(interface) - artefact['componentSpec'] = [ - { - 'componentName': appData.get('name'), - 'numOfInstances': 0, - 'restartPolicy': 'RESTART_POLICY_ALWAYS', - 'exposedInterfaces': exposedInterfaces, - 'compEnvParams': [], - 'persistentVolumes': [] + body = request.get_json() + logger.debug(f"Request body: {body}") + + app_id = body.get("appId") + app_zones = body.get("appZones") + + if not app_id or not app_zones: + return jsonify({ + "error": "Missing required fields: appId, appZones" + }), 400 + + pi_edge_client_factory = PiEdgeAPIClientFactory() + pi_edge_client = pi_edge_client_factory.create_pi_edge_api_client() + + first_zone = app_zones[0] if isinstance(app_zones, list) and app_zones else {} + if not isinstance(first_zone, dict): + first_zone = {} + edge_cloud_zone_id = ( + first_zone + .get("EdgeCloudZone", {}) + .get("edgeCloudZoneId") + ) + zone = get_zone(edge_cloud_zone_id) + if not zone: + return jsonify({ + "error": "Edge Cloud Zone not found", + "edgeCloudZoneId": edge_cloud_zone_id + }), 404 + + # ============================================================ + # PARTNER DEPLOYMENT (Federation path) + # ============================================================ + if zone.get("isLocal") == "false": + + # ============================================================ + # Step 1: Retrieve application metadata from SRM + # ============================================================ + app_response = pi_edge_client.get_app(appId=app_id) + appData = app_response.get("appManifest") if isinstance(app_response, dict) else None + if not isinstance(appData, dict): + appData = None + + if not appData: + return jsonify({ + "error": "Application manifest not found", + "appId": app_id + }), 404 + + # ============================================================ + # Step 2: Compose GSMA artefact payload + # artefactId == appId (INTENTIONAL) + # ============================================================ + artefact_id = app_id + app_provider_id = appData.get("appProvider") or appData.get("appProviderId") + app_name = appData.get("name") or appData.get("appName") or app_id + app_version = appData.get("version") or "v1" + app_repo = appData.get("appRepo", {}) + if not isinstance(app_repo, dict): + app_repo = {} + image_path = app_repo.get("imagePath") + repo_type = app_repo.get("type", "PUBLICREPO") + component_specs = appData.get("componentSpec", []) + component_name = None + if component_specs and isinstance(component_specs, list): + component_name = component_specs[0].get("componentName") + component_name = component_name or app_name + + service_name_nb = _ensure_service_name( + (component_specs[0].get("serviceNameNB") if component_specs else None), + "nb", + component_name, + ) + service_name_ew = _ensure_service_name( + (component_specs[0].get("serviceNameEW") if component_specs else None), + "ew", + component_name, + ) + + app_provider_id = _ensure_gsma_id( + app_provider_id, + r"^[A-Za-z][A-Za-z0-9_]{7,63}$", + "provider", + 8, + 64, + app_id, + ) + app_name = _ensure_gsma_id( + app_name, + r"^[A-Za-z][A-Za-z0-9_]{7,31}$", + "app", + 8, + 32, + app_id, + ) + access_token = _ensure_gsma_id( + appData.get("accessToken"), + r"^[A-Za-z][A-Za-z0-9_]{31,63}$", + "token", + 32, + 64, + app_id, + ) + repo_url, image_ref = _split_image_reference(image_path) + if not repo_url or not image_ref: + return jsonify({ + "error": "Application manifest missing imagePath", + "appId": app_id + }), 400 + image_name, image_tag = _split_image_name_tag(image_ref) + app_deployment_zones = appData.get("appDeploymentZones") + if not isinstance(app_deployment_zones, list) or not app_deployment_zones: + app_deployment_zones = [edge_cloud_zone_id] + app_qos = appData.get("appQoSProfile", {}) + if not isinstance(app_qos, dict): + app_qos = {} + bandwidth_required = app_qos.get("bandwidthRequired", 1) + multi_user_clients = app_qos.get("multiUserClients", "APP_TYPE_SINGLE_USER") + no_of_users = app_qos.get("noOfUsersPerAppInst", 1) + app_provisioning = app_qos.get("appProvisioning", True) + latency_constraints = app_qos.get("latencyConstraints", "NONE") + app_status_callback_link = ( + appData.get("appStatusCallbackLink") + or appData.get("statusCallbackLink") + or "http://callback.local" + ) + edge_app_fqdn = f"{app_name.lower().replace('_', '-')}.edge.local" + + artefact = { + "artefactId": artefact_id, + "appProviderId": app_provider_id, + "artefactName": image_name, + "artefactVersionInfo": image_tag, + "artefactVirtType": "CONTAINER_TYPE", + "artefactDescriptorType": "COMPONENTSPEC", + "repoType": repo_type, + "artefactRepoLocation": { + "repoURL": repo_url + }, + "componentSpec": [ + { + "componentName": component_name or app_name, + "images": [image_ref], + "numOfInstances": 1, + "restartPolicy": "RESTART_POLICY_ALWAYS", + "computeResourceProfile": { + "cpuArchType": "ISA_X86_64", + "numCPU": "100m", + "memory": 128 + } + } + ] + } + + fed_record = get_fed( + zone.get("fedContextId") + ) + if not fed_record or "token" not in fed_record: + return jsonify({ + "error": "Federation token not found", + "federationContextId": zone.get("fedContextId") + }), 404 + fed_token = fed_record.get("token") + + print("\n========== OEG → FM ARTEFACT PAYLOAD ==========") + print(json.dumps(artefact, indent=2)) + print("================================================\n") + + # ============================================================ + # Step 3: Create artefact at Federation Manager + # ============================================================ + artefact_body, artefact_status = federation_client.create_artefact( + artefact=artefact, + federation_context_id=zone.get("fedContextId"), + token=fed_token + ) + print(f"\n========== ARTEFACT CREATION RESPONSE ==========") + print(f"Status: {artefact_status}") + print(f"Response: {json.dumps(artefact_body, indent=2)}") + print("================================================\n") + + # Idempotency: duplicate artefact = success + if artefact_status == 422 and "duplicate key" in str(artefact_body): + logger.info("Artefact already exists in FM, continuing") + artefact_status = 200 + + if artefact_status not in (200, 409): + return jsonify({ + "error": "Artefact creation failed", + "fm_response": artefact_body + }), artefact_status + + # ============================================================ + # Step 4: Onboard application at partner OP (HARDCODED FOR TESTING) + # ============================================================ + fed_context_id = zone.get("fedContextId") + logger.info(f"Federation Context ID from zone: {fed_context_id}") + logger.info(f"Federation Manager URL: {federation_client.base_url}") + print(f"\n========== FEDERATION CONTEXT DEBUG ==========") + print(f"Using Federation Context ID: {fed_context_id}") + print(f"Federation Manager Base URL: {federation_client.base_url}") + print(f"Full Onboard URL: {federation_client.base_url}/{fed_context_id}/application/onboarding") + print("===============================================\n") + print("\n========== CHECKING FEDERATION IDS ==========") + fed_ids_body, fed_ids_status = federation_client.get_federation_context_ids(token=fed_token) + print(f"Status: {fed_ids_status}") + print(f"Federation IDs: {json.dumps(fed_ids_body, indent=2)}") + print(f"Looking for: {zone.get('fedContextId')}") + print("=============================================\n") + print(f"\n========== HEADERS DEBUG ==========") + print(f"X-Partner-API-Root: {federation_client.partner_root}") + print(f"Authorization: Bearer {fed_token[:20]}...") + print("===================================\n") + print ("DEBUG: About to check partner status...") + try: + print("\n========== CHECKING PARTNER STATUS ==========") + partner_body, partner_status = federation_client.get_partner( + federation_context_id=zone.get("fedContextId"), + token=fed_token + ) + print(f"Partner Status: {partner_status}") + print(f"Partner Info: {json.dumps(partner_body, indent=2)}") + print("=============================================\n") + except Exception as e: + print(f"\n========== PARTNER CHECK ERROR ==========") + print(f"Error getting partner status: {str(e)}") + print("=========================================\n") + + + #print("\n========== VERIFYING FEDERATION ==========") + #fed_verify_body, fed_verify_status = federation_client.get_federation( + # federation_context_id=zone.get("fedContextId"), + # token=fed_token + # ) + # print(f"Federation Verification Status: {fed_verify_status}") + # print(f"Federation Verification Response: {json.dumps(fed_verify_body, indent=2)}") + # print("==========================================\n") + + #if fed_verify_status != 200: + # return jsonify({ + # "error": "Federation context not found or not accessible", + # "federation_context_id": zone.get("fedContextId"), + # "verification_response": fed_verify_body + # }), fed_verify_status + onboard_app = { + "appId": app_id, + "appProviderId": app_provider_id, + "appMetaData": { + "appName": app_name, + "version": app_version, + "accessToken": access_token + }, + "appQoSProfile": { + "latencyConstraints": latency_constraints, + "bandwidthRequired": bandwidth_required, + "multiUserClients": multi_user_clients, + "noOfUsersPerAppInst": no_of_users, + "appProvisioning": app_provisioning + }, + "appComponentSpecs": [ + { + "artefactId": artefact_id, + "componentName": component_name, + "serviceNameNB": service_name_nb, + "serviceNameEW": service_name_ew + } + ], + "appStatusCallbackLink": app_status_callback_link, + "appDeploymentZones": app_deployment_zones, + "edgeAppFQDN": edge_app_fqdn + } + + print("\n========== OEG → FM ONBOARD PAYLOAD ==========") + print(json.dumps(onboard_app, indent=2)) + print("==============================================\n") + + + + onboard_app_body, onboard_app_status = federation_client.onboard_application( + federation_context_id=zone.get("fedContextId"), + body=onboard_app, + token=fed_token + ) + print(f"\n========== ONBOARD RESPONSE DEBUG ==========") + print(f"Status: {onboard_app_status}") + print(f"Response Body: {json.dumps(onboard_app_body, indent=2)}") + print("============================================\n") + + if onboard_app_status not in (200, 202, 409): + return jsonify({ + "error": "Application onboarding failed", + "fm_response": onboard_app_body + }), onboard_app_status + + + + # ============================================================ + # Step 5: Deploy application at partner OP (HARDCODED FOR TESTING) + # ============================================================ + res_pool_value = _ensure_res_pool( + zone.get("resPool"), + zone.get("edgeCloudZoneId") + ) + deploy_app = { + "appId": app_id, + "appProviderId": app_provider_id, + "appVersion": app_version, + "appInstCallbackLink": appData.get("appInstCallbackLink", ""), + "zoneInfo": { + "zoneId": zone.get("edgeCloudZoneId"), + "flavourId": zone.get("flavourId", "default"), + "resPool": res_pool_value, + "resourceConsumption": "RESERVED_RES_AVOID" + } + } + + print("\n========== OEG → FM DEPLOY PAYLOAD ==========") + print(json.dumps(deploy_app, indent=2)) + print("=============================================\n") + + deploy_app_body, deploy_app_status = federation_client.deploy_app_partner( + federation_context_id=zone.get("fedContextId"), + body=deploy_app, + token=fed_token + ) + + if deploy_app_status in (200, 201, 202): + return jsonify({ + "message": "Application deployed successfully at partner OP", + "appId": app_id, + "deployment_response": deploy_app_body + }), deploy_app_status + else: + return jsonify({ + "error": "Application deployment failed", + "fm_response": deploy_app_body + }), deploy_app_status + + # ============================================================ + # LOCAL DEPLOYMENT (SRM path) + # ============================================================ + logger.info(f"Proceeding with LOCAL deployment for appId={app_id}") + + try: + logger.debug("Sending deployment request to SRM") + response = pi_edge_client.deploy_service_function(data=body) + + if isinstance(response, dict) and "error" in response: + logger.warning( + "SRM returned an error, deployment not completed" + ) + return jsonify({ + "warning": "Deployment request accepted but not completed", + "details": response + }), 202 + + logger.info("Local deployment request successfully sent to SRM") + return jsonify({ + "message": "Application deployed locally", + "appId": app_id, + "response": response + }), 202 + + except Exception as e: + logger.error(f"SRM deployment failed: {str(e)}") + return jsonify({ + "warning": "SRM backend unavailable", + "details": str(e) + }), 202 + + except Exception as e: + logger.exception("Unexpected error in create_app_instance") + return jsonify({ + "error": "Unexpected error", + "details": str(e) + }), 500 +''' + def create_app_instance(): + + + logger.info("Received request to create app instance") + + try: + body = request.get_json() + logger.debug(f"Request body: {body}") + + app_id = body.get("appId") + app_zones = body.get("appZones") + + if not app_id or not app_zones: + return jsonify({ + "error": "Missing required fields: appId, appZones" + }), 400 + + pi_edge_client_factory = PiEdgeAPIClientFactory() + pi_edge_client = pi_edge_client_factory.create_pi_edge_api_client() + + zone = get_zone( + app_zones[0] + .get("EdgeCloudZone", {}) + .get("edgeCloudZoneId") + ) + + # ============================================================ + # PARTNER DEPLOYMENT (Federation path) + # ============================================================ + if zone.get("isLocal") == "false": + + # ============================================================ + # Step 1: Retrieve application metadata from SRM + # ============================================================ + app_response = pi_edge_client.get_app(appId=app_id) + appData = app_response.get("appManifest") + + if not appData: + return jsonify({ + "error": "Application manifest not found", + "appId": app_id + }), 404 + + # ============================================================ + # Step 2: Compose GSMA artefact payload + # artefactId == appId (INTENTIONAL) + # ============================================================ + artefact_id = app_id + + artefact = { + "artefactId": artefact_id, + "appProviderId": appData.get("appProvider"), + "artefactName": "library/nginx", + "artefactVersionInfo": "latest", + "artefactVirtType": "CONTAINER_TYPE", + "artefactDescriptorType": "COMPONENTSPEC", + + "repoType": "PUBLICREPO", + "artefactRepoLocation": { + "repoURL": "docker.io" + }, + + "componentSpec": [ + { + "componentName": "nginx", + "images": ["nginx:latest"], + "numOfInstances": 1, + "restartPolicy": "RESTART_POLICY_ALWAYS", + "computeResourceProfile": { + "cpuArchType": "ISA_X86_64", + "numCPU": "100m", + "memory": 128 + } + } + ] + } + + fed_token = get_fed( + zone.get("fedContextId") + ).get("token") + + print("\n========== OEG → FM ARTEFACT PAYLOAD ==========") + print(json.dumps(artefact, indent=2)) + print("================================================\n") + + # ============================================================ + # Step 3: Create artefact at Federation Manager + # ============================================================ + artefact_body, artefact_status = federation_client.create_artefact( + artefact=artefact, + federation_context_id=zone.get("fedContextId"), + token=fed_token + ) + + # Idempotency: duplicate artefact = success + if artefact_status == 422 and "duplicate key" in str(artefact_body): + logger.info("Artefact already exists in FM, continuing") + artefact_status = 200 + + if artefact_status not in (200, 409): + return jsonify({ + "error": "Artefact creation failed", + "fm_response": artefact_body + }), artefact_status + + # ============================================================ + # Step 4: Onboard application at partner OP + # ============================================================ + onboard_app = { + "appId": app_id, + "appProviderId": appData.get("appProvider"), + + "appDeploymentZones": [ + { + "edgeCloudZoneId": zone.get("edgeCloudZoneId"), + "edgeCloudProvider": zone.get("edgeCloudProvider") + } + ], + + "appMetaData": { + "appName": appData.get("name"), + "version": appData.get("version") or "1.0.0", + "appDescription": appData.get( + "description", "Federated application" + ), + "mobilitySupport": False, + "accessToken": "dummy-access-token", + "category": "IOT" + }, + + "appQoSProfile": { + "latencyConstraints": "NONE", + "bandwidthRequired": 1, + "multiUserClients": "APP_TYPE_SINGLE_USER", + "noOfUsersPerAppInst": 1, + "appProvisioning": True + }, + + "appComponentSpecs": [ + { + "serviceNameNB": appData.get("name"), + "serviceNameEW": appData.get("name"), + "componentName": appData.get("name"), + "artefactId": artefact_id + } + ], + + "appStatusCallbackLink": "http://oeg/api/status" + } + + print("\n========== OEG → FM ONBOARD PAYLOAD ==========") + print(json.dumps(onboard_app, indent=2)) + print("==============================================\n") + + onboard_app_body, onboard_app_status = federation_client.onboard_application( + federation_context_id=zone.get("fedContextId"), + body=onboard_app, + token=fed_token + ) + + if onboard_app_status != 200: + return jsonify({ + "error": "Application onboarding failed", + "fm_response": onboard_app_body + }), onboard_app_status + + # ============================================================ + # Step 5: Deploy application at partner OP + # ============================================================ + deploy_app = { + "appId": app_id, + "appVersion": appData.get("version") or "1.0.0", + "appProviderId": appData.get("appProvider"), + "appInstCallbackLink": "http://oeg/api/status", + "zoneInfo": { + "zoneId": zone.get("edgeCloudZoneId"), + "flavourId": "string", + "resPool": zone.get("resPool", "string"), + "resourceConsumption": "RESERVED_RES_AVOID" } - ] - # Step 3: Send artefact to local fed manager - fed_token = get_fed(zone.get('fedContextId')).get('token') - create_artefact_response = federation_client.create_artefact(artefact=artefact, federation_context_id=zone.get('fedContextId'), token=fed_token) - # Step 4: Onboard app - if create_artefact_response.status_code == 200 or create_artefact_response.status_code ==409: - # Step 5: Create GSM onboard app payload - onboard_app = {} - onboard_app['appId'] = app_id - onboard_app['appProviderId'] = appData.get('appProvider') - onboard_app['appDeploymentZones'] = [] - appMetaData = {} - appMetaData['appName'] = appData.get('name') - appMetaData['version'] = appData.get('version') - onboard_app['appMetaData'] = appMetaData - onboard_app['appComponentSpecs'] = [{'serviceNameNB': appData.get('name'), - 'serviceNameEW': appData.get('name'), - 'componentName': appData.get('name'), - 'artefactId': app_id - } - ] - # Step 6: Onboard app at partner - onboard_app_response = federation_client.onboard_application(federation_context_id=zone.get('fedContextId'), body=onboard_app, token=fed_token) - if onboard_app_response.status_code==200: - # Step 7: Construct GSMA deployment payload - deploy_app = {} - deploy_app['appId'] = app_id - deploy_app['appVersion'] = appData.get('version') - deploy_app['appProviderId'] = appData.get('appProvider') - deploy_app['zoneInfo'] = {'zoneId': zone.get('edgeCloudZoneId')} - # Step 8: Deploy app at partner - deploy_app_response = federation_client.deploy_app_partner(federation_context_id=zone.get('fedContextId'), body=deploy_app, token = fed_token) - return deploy_app_response - else: - return onboard_app_response - else: - return create_artefact_response - - - logger.info(f"Preparing to send deployment request to SRM for appId={app_id}") - - print("\n === Preparing Deployment Request ===") - print(f" Endpoint: {pi_edge_client.base_url}/deployedServiceFunction") - print(f" Headers: {pi_edge_client._get_headers()}") - print(f"Payload: {body}") - print("=== End of Deployment Request ===\n") - - try: - response = pi_edge_client.deploy_service_function(data=body) - - if isinstance(response, dict) and "error" in response: - logger.warning(f"Failed to deploy service function: {response}") - return jsonify({ - "warning": "Deployment not completed (SRM service unreachable)", - "details": response - - }), 202 - - logger.info(f"Deployment response from SRM: {response}") - except Exception as inner_error: - logger.error(f"Exception while trying to deploy to SRM: {inner_error}") - return jsonify({ - "warning": "SRM backend unavailable. Deployment request was built correctly.", - "details": str(inner_error) - }),202 - return response - # return jsonify({"message": f"Application {app_id} instantiation accepted"}), 202 - except ValidationError as e: - logger.error(f"Validation error: {str(e)}") - return jsonify({"error": "Validation error", "details": str(e)}), 400 + } + + print("\n========== OEG → FM DEPLOYMENT PAYLOAD ==========") + print(json.dumps(deploy_app, indent=2)) + print("==================================================\n") + + deploy_app_body, deploy_app_status = federation_client.deploy_app_partner( + federation_context_id=zone.get("fedContextId"), + body=deploy_app, + token=fed_token + ) + + if deploy_app_status in (200, 201, 202): + return jsonify({ + "message": "Application deployed successfully at partner OP", + "appId": app_id, + "deployment_response": deploy_app_body + }), deploy_app_status + else: + return jsonify({ + "error": "Application deployment failed", + "fm_response": deploy_app_body + }), deploy_app_status + + # ============================================================ + # LOCAL DEPLOYMENT (SRM path) + # ============================================================ + logger.info(f"Proceeding with LOCAL deployment for appId={app_id}") + + try: + logger.debug("Sending deployment request to SRM") + response = pi_edge_client.deploy_service_function(data=body) + + if isinstance(response, dict) and "error" in response: + logger.warning( + "SRM returned an error, deployment not completed" + ) + return jsonify({ + "warning": "Deployment request accepted but not completed", + "details": response + }), 202 + + logger.info("Local deployment request successfully sent to SRM") + return jsonify({ + "message": "Application deployed locally", + "appId": app_id, + "response": response + }), 202 + + except Exception as e: + logger.error(f"SRM deployment failed: {str(e)}") + return jsonify({ + "warning": "SRM backend unavailable", + "details": str(e) + }), 202 + except Exception as e: - logger.error(f"Unexpected error in create_app_instance:{str(e)}") - return jsonify({"error": "An unexpected error occurred", "details": str(e)}), 500 + logger.exception("Unexpected error in create_app_instance") + return jsonify({ + "error": "Unexpected error", + "details": str(e) + }), 500 +''' def get_app_instance(app_id=None, x_correlator=None, app_instance_id=None, region=None): """ Retrieve application instances from the database. @@ -245,8 +830,14 @@ def delete_app_instance(appInstanceId: str, x_correlator=None): try: pi_edge_client_factory = PiEdgeAPIClientFactory() pi_edge_client = pi_edge_client_factory.create_pi_edge_api_client() - response = pi_edge_client.delete_app_instance(appInstanceId) - return jsonify({'result': response.text, 'status': response.status_code}) + response = pi_edge_client.delete_app_instance(appInstanceId) + if isinstance(response, dict): + status_code = response.get("status_code", 500) + return jsonify(response), status_code + return jsonify({ + "result": response.text, + "status": response.status_code + }), response.status_code except Exception as e: return ( diff --git a/edge_cloud_management_api/services/federation_services.py b/edge_cloud_management_api/services/federation_services.py index 3c136e3a1a9743908cffabb8233584a4497df64e..28ea4cb2fa21e3e936e1bf91981a4b1c95ac8658 100644 --- a/edge_cloud_management_api/services/federation_services.py +++ b/edge_cloud_management_api/services/federation_services.py @@ -87,6 +87,7 @@ class FederationManagerClient: logger.error(f"DELETE /{id}/partner unexpected error: {e}") return {"error": str(e)}, 500 + def get_federation_context_ids(self, token: str): url = f"{self.base_url}/fed-context-id" try: @@ -105,27 +106,63 @@ class FederationManagerClient: except Exception as e: logger.error(f"GET /fed-context-id unexpected error: {e}") return {"error": str(e)}, 500 + + def get_federation(self, federation_context_id: str, token: str): + """Verify that a federation context exists""" + url = f"{self.base_url}/{federation_context_id}" + try: + response = requests.get( + url, + headers=self._get_headers(token), + timeout=10 + ) + try: + response_body = response.json() + except ValueError: + response_body = response.text + return response_body, response.status_code + except Timeout: + logger.error(f"GET /{federation_context_id} timed out") + return {"error": "Request timed out"}, 408 + except ConnectionError: + logger.error(f"GET /{federation_context_id} connection error") + return {"error": "Connection error"}, 503 + except requests.exceptions.HTTPError as http_err: + logger.error(f"GET /{federation_context_id} HTTP error: {http_err}") + return {"error": str(http_err)}, response.status_code + except Exception as e: + logger.error(f"GET /{federation_context_id} unexpected error: {e}") + return {"error": str(e)}, 500 + - '''---PARTNER APP ONBOARDING---''' + '''---PARTNER APP ONBOARDING---''' def onboard_application(self, federation_context_id: str, body: dict, token: str): url = f"{self.base_url}/{federation_context_id}/application/onboarding" try: - response = requests.post(url, headers=self._get_headers(token), json=body, timeout=10) - response.raise_for_status() - return response.json() + response = requests.post( + url, + headers=self._get_headers(token), + json=body, + timeout=10 + ) + try: + response_body = response.json() + except ValueError: + response_body = response.text + return response_body, response.status_code except Timeout: - logger.error("POST /application/onboarding timed out") - return {"error": "Request timed out"} + logger.error("POST /application/onboarding timed out") + return {"error": "Request timed out"}, 408 except ConnectionError: logger.error("POST /application/onboarding connection error") - return {"error": "Connection error"} + return {"error": "Connection error"}, 503 except requests.exceptions.HTTPError as http_err: logger.error(f"POST /application/onboarding HTTP error: {http_err}") - return {"error": str(http_err), "status_code": response.status_code} + return {"error": str(http_err)}, response.status_code except Exception as e: logger.error(f"POST /application/onboarding unexpected error: {e}") - return {"error": str(e)} + return {"error": str(e)}, 500 def get_onboarded_app(self, federation_context_id: str, app_id: str, token: str): @@ -171,11 +208,29 @@ class FederationManagerClient: def deploy_app_partner(self, federation_context_id: str, body: dict, token: str): url = f"{self.base_url}/{federation_context_id}/application/lcm" try: - response = requests.post(url, headers=self._get_headers(token), json=body, timeout=10) - return response + response = requests.post( + url, + headers=self._get_headers(token), + json=body, + timeout=10 + ) + try: + response_body = response.json() + except ValueError: + response_body = response.text + return response_body, response.status_code + except Timeout: + logger.error("POST /application/lcm timed out") + return {"error": "Request timed out"}, 408 + except ConnectionError: + logger.error("POST /application/lcm connection error") + return {"error": "Connection error"}, 503 + except requests.exceptions.HTTPError as http_err: + logger.error(f"POST /application/lcm HTTP error: {http_err}") + return {"error": str(http_err)}, response.status_code except Exception as e: - logger.error(f"DELETE onboarding app unexpected error: {e}") - return {"error": str(e), "status_code": 500} + logger.error(f"POST /application/lcm unexpected error: {e}") + return {"error": str(e)}, 500 '''---AVAILABILITY ZONE INFO SYNCHRONIZATION---''' @@ -238,14 +293,23 @@ class FederationManagerClient: '''---ARTEFACT API---''' - def create_artefact(self, artefact: dict, federation_context_id, token: str): + def create_artefact(self, artefact: dict, federation_context_id: str, token: str): url = f"{self.base_url}/{federation_context_id}/artefact" try: - response = requests.post(url, headers=self._get_headers(token), json=artefact, timeout=10) - return response + response = requests.post( + url, + headers=self._get_headers(token), + json=artefact, + timeout=120 + ) + try: + body = response.json() + except ValueError: + body = response.text + return body, response.status_code except Exception as e: logger.error(f"Create artefact unexpected error: {e}") - return {"error": str(e), "status_code": 500} + return {"error": str(e)}, 500 class FederationManagerClientFactory: def __init__(self): diff --git a/edge_cloud_management_api/specification/openapi.yaml b/edge_cloud_management_api/specification/openapi.yaml index ddcbcacd069c0fe219ad746174cfbd2a7298fa6f..fa05184d38f418b97c1ac69716cb328d661f56c9 100644 --- a/edge_cloud_management_api/specification/openapi.yaml +++ b/edge_cloud_management_api/specification/openapi.yaml @@ -183,6 +183,7 @@ paths: KubernetesExample: summary: Example for Kubernetes app value: + appId: 3fa85f64-5717-4562-b3fc-2c963f66afa6 name: nginx_web_app appProvider: nginx_inc version: 1.0.0