Align FM EWBI with GSMA and integrate through SRM

Summary

  • Align Open Exposure Gateway behavior with CAMARA Edge Application Management expectations for app lifecycle and response handling.
  • Harden federated cleanup flows so local and remote state stay consistent during delete and undeploy operations.
  • Normalize payload handling used by local kubernetes-backed deployments and federated app workflows.

Included Changes

  • validate app manifests and deployment payloads more strictly against CAMARA expectations
  • return CAMARA-compliant responses for delete operations, including 204 where required
  • preserve and clean up federated artefact metadata correctly
  • avoid leaving stale local federation state when downstream cleanup fails
  • improve app and zone handling in federated deployment and cleanup paths

Why

These changes make OEG easier to integrate with CAMARA EAM clients and reduce cleanup inconsistencies observed in federated dual-OP flows.

Merge request reports

Loading
+2 −2
Changes for edge_cloud_management_api/configs/env_config.py: 2 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -8,8 +8,8 @@ load_dotenv()
class Configuration(BaseSettings):
    MONGO_URI: str = os.getenv("MONGO_URI")
    SRM_HOST: str = os.getenv("SRM_HOST")
    PI_EDGE_USERNAME: str = os.getenv("PI_EDGE_USERNAME")
    PI_EDGE_PASSWORD: str = os.getenv("PI_EDGE_PASSWORD")
    SRM_USERNAME: str = os.getenv("SRM_USERNAME")
    SRM_PASSWORD: str = os.getenv("SRM_PASSWORD")
    HTTP_PROXY: str = os.getenv("HTTP_PROXY")
    FEDERATION_MANAGER_HOST=os.getenv("FEDERATION_MANAGER_HOST")
    TOKEN_ENDPOINT = os.getenv('TOKEN_ENDPOINT')
+225 −542

File changed.

Preview size limit exceeded, changes collapsed.

+165 −0
Changes for edge_cloud_management_api/controllers/app_federation_helpers.py: 165 added lines, 0 removed lines.
Original line number Diff line number Diff line
import re
import uuid


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 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 normalize_federated_app_id(app_id):
    pattern = (
        r"^(?:[A-Za-z][A-Za-z0-9_]{7,63}|"
        r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-"
        r"[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$"
    )
    if app_id and re.match(pattern, str(app_id)):
        return app_id
    return str(uuid.uuid5(uuid.NAMESPACE_URL, f"federated-app:{app_id}"))


def normalize_federated_app_provider_id(app_provider_id, fallback_source):
    return ensure_gsma_id(
        app_provider_id,
        r"^[A-Za-z][A-Za-z0-9_]{7,63}$",
        "provider",
        8,
        64,
        fallback_source,
    )


def resolve_federated_app_provider_id(app_id_value, app_provider_id):
    if not app_provider_id:
        return None
    return normalize_federated_app_provider_id(app_provider_id, app_id_value)


def resolve_federated_app_identity(app_id_value, app_provider_id):
    if not app_id_value or not app_provider_id:
        return None, None
    return (
        normalize_federated_app_id(app_id_value),
        resolve_federated_app_provider_id(app_id_value, app_provider_id),
    )


def normalize_federated_artefact_id(federation_context_id, artefact_id, fallback_source):
    if artefact_id and re.match(
        r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-"
        r"[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$",
        str(artefact_id),
    ):
        return str(uuid.uuid5(uuid.NAMESPACE_URL, f"{federation_context_id}:{artefact_id}"))
    return str(uuid.uuid5(uuid.NAMESPACE_URL, f"{federation_context_id}:{fallback_source}"))


def resolve_app_provider(srm_client, app_id_value, app_payload=None):
    if isinstance(app_payload, dict):
        provider = app_payload.get("appProvider") or app_payload.get("appProviderId")
        if provider:
            return provider
        manifest = app_payload.get("appManifest")
        if isinstance(manifest, dict):
            provider = manifest.get("appProvider") or manifest.get("appProviderId")
            if provider:
                return provider

    app_response = srm_client.get_app(app_id_value)
    if isinstance(app_response, dict):
        manifest = app_response.get("appManifest")
        if isinstance(manifest, dict):
            provider = manifest.get("appProvider") or manifest.get("appProviderId")
            if provider:
                return provider
        provider = app_response.get("appProvider") or app_response.get("appProviderId")
        if provider:
            return provider
    return None


def get_catalog_app_provider_map(srm_client):
    app_provider_map = {}
    apps = srm_client.get_service_functions_catalogue()
    if not isinstance(apps, list):
        return app_provider_map

    for app in apps:
        if not isinstance(app, dict):
            continue
        app_id_value = app.get("appId") or app.get("id")
        if not app_id_value:
            continue
        provider = resolve_app_provider(srm_client, app_id_value, app_payload=app)
        if provider:
            app_provider_map[app_id_value] = provider
    return app_provider_map


def iter_federated_instances(federation_client, feds, app_id_provider_pairs):
    for fed in feds:
        fed_token = fed.get("token")
        federation_context_id = fed.get("_id")
        if not federation_context_id or not fed_token:
            continue

        zone_provider = fed.get("partnerOPFederationId") or "unknown"
        for app_id_value, app_provider_id in app_id_provider_pairs:
            federated_app_id, federated_app_provider_id = resolve_federated_app_identity(
                app_id_value,
                app_provider_id,
            )
            if not federated_app_id or not federated_app_provider_id:
                continue
            fed_instances, fed_code = federation_client.get_all_app_instances(
                federation_context_id=federation_context_id,
                app_id=federated_app_id,
                app_provider_id=federated_app_provider_id,
                token=fed_token,
            )
            if fed_code != 200 or not isinstance(fed_instances, list):
                continue
            yield app_id_value, federated_app_id, federation_context_id, fed_token, zone_provider, fed_instances
+186 −0
Changes for edge_cloud_management_api/controllers/app_instance_helpers.py: 186 added lines, 0 removed lines.
Original line number Diff line number Diff line
from edge_cloud_management_api.services.storage_service import get_zone
from edge_cloud_management_api.controllers.edge_cloud_controller import get_local_zones


def _find_local_zone(zone_id, zone_provider=None):
    for zone in get_local_zones():
        if not isinstance(zone, dict):
            continue
        if zone.get("edgeCloudZoneId") != zone_id:
            continue
        if zone_provider and zone.get("edgeCloudProvider") != zone_provider:
            continue
        return zone
    return None


def normalize_local_app_instance(instance):
    if not isinstance(instance, dict):
        return None

    app_instance_id = instance.get("appInstanceId") or instance.get("appInstIdentifier")
    if not app_instance_id:
        return None

    status = instance.get("status") or instance.get("appInstanceState") or "unknown"
    edge_cloud_zone = instance.get("edgeCloudZone")
    if not isinstance(edge_cloud_zone, dict):
        zone_id = instance.get("edgeCloudZoneId") or instance.get("zoneId")
        zone_name = instance.get("edgeCloudZoneName") or instance.get("zoneName") or "unknown"
        edge_cloud_provider = instance.get("edgeCloudProvider") or "unknown"
        if zone_id:
            edge_cloud_zone = {
                "edgeCloudZoneId": zone_id,
                "edgeCloudZoneName": zone_name,
                "edgeCloudProvider": edge_cloud_provider,
                "edgeCloudZoneStatus": instance.get("edgeCloudZoneStatus") or "unknown",
                "edgeCloudRegion": instance.get("edgeCloudRegion") or "unknown",
            }

    normalized = {
        "appInstanceId": app_instance_id,
        "status": status,
    }
    if instance.get("appId"):
        normalized["appId"] = instance.get("appId")
    if edge_cloud_zone:
        normalized["edgeCloudZone"] = edge_cloud_zone
    if instance.get("componentEndpointInfo"):
        normalized["componentEndpointInfo"] = instance.get("componentEndpointInfo")
    elif instance.get("accesspointInfo"):
        normalized["componentEndpointInfo"] = instance.get("accesspointInfo")
    if instance.get("kubernetesClusterRef"):
        normalized["kubernetesClusterRef"] = instance.get("kubernetesClusterRef")
    return normalized


def enrich_instance_zone_from_catalog(instance):
    if not isinstance(instance, dict):
        return instance

    zone = instance.get("edgeCloudZone")
    if not isinstance(zone, dict):
        return instance

    zone_id = zone.get("edgeCloudZoneId")
    if not zone_id:
        return instance

    zone_provider = zone.get("edgeCloudProvider")
    stored_zone = None
    if zone_provider and zone_provider != "unknown":
        stored_zone = get_zone(zone_id, zone_provider)
    else:
        stored_zone = _find_local_zone(zone_id)
        if not isinstance(stored_zone, dict):
            stored_zone = get_zone(zone_id)
    if not isinstance(stored_zone, dict):
        return instance

    enriched = dict(instance)
    enriched_zone = dict(zone)
    for key in (
        "edgeCloudZoneId",
        "edgeCloudZoneName",
        "edgeCloudProvider",
        "edgeCloudZoneStatus",
        "edgeCloudRegion",
    ):
        if enriched_zone.get(key) in (None, "unknown") and stored_zone.get(key) is not None:
            enriched_zone[key] = stored_zone.get(key)
    enriched["edgeCloudZone"] = enriched_zone
    return enriched


def normalize_federated_app_instances(fed_instances, zone_provider=None, region=None):
    normalized_instances = []
    if not isinstance(fed_instances, list):
        return normalized_instances

    for zone_info in fed_instances:
        if not isinstance(zone_info, dict):
            continue
        zone_id = zone_info.get("zoneId")
        for instance in zone_info.get("appInstanceInfo", []) or []:
            if not isinstance(instance, dict):
                continue
            app_instance_id = instance.get("appInstIdentifier") or instance.get("appInstanceId")
            if not app_instance_id:
                continue
            status = instance.get("appInstanceState") or "unknown"
            if isinstance(status, str) and status.startswith("Error 404"):
                continue
            normalized = {
                "appInstanceId": app_instance_id,
                "status": status,
            }
            if instance.get("appId"):
                normalized["appId"] = instance.get("appId")
            if zone_id:
                normalized["edgeCloudZone"] = {
                    "edgeCloudZoneId": zone_id,
                    "edgeCloudZoneName": "unknown",
                    "edgeCloudProvider": zone_provider or "unknown",
                    "edgeCloudZoneStatus": "unknown",
                    "edgeCloudRegion": region or "unknown",
                }
            normalized_instances.append(normalized)
    return normalized_instances


def dedupe_app_instances(instances):
    deduped = []
    by_instance_id = {}

    def zone_quality(instance):
        zone = instance.get("edgeCloudZone")
        if not isinstance(zone, dict):
            return 0

        score = 0
        if zone.get("edgeCloudProvider") and zone.get("edgeCloudProvider") != "unknown":
            score += 4
        if zone.get("edgeCloudZoneName") and zone.get("edgeCloudZoneName") != "unknown":
            score += 2
        if zone.get("edgeCloudZoneStatus") and zone.get("edgeCloudZoneStatus") != "unknown":
            score += 1
        return score

    def merge_instances(existing, incoming):
        merged = dict(existing)

        for key, value in incoming.items():
            if key == "edgeCloudZone" and isinstance(value, dict):
                existing_zone = merged.get("edgeCloudZone")
                if not isinstance(existing_zone, dict) or zone_quality(incoming) > zone_quality(existing):
                    merged["edgeCloudZone"] = dict(value)
                else:
                    zone = dict(existing_zone)
                    for zone_key, zone_value in value.items():
                        if zone_key not in zone or zone.get(zone_key) in (None, "unknown"):
                            zone[zone_key] = zone_value
                    merged["edgeCloudZone"] = zone
                continue

            if key not in merged or merged.get(key) in (None, "unknown", []):
                merged[key] = value

        return merged

    for instance in instances:
        if not isinstance(instance, dict):
            continue
        app_instance_id = instance.get("appInstanceId")
        if app_instance_id:
            existing = by_instance_id.get(app_instance_id)
            if existing is not None:
                merged = merge_instances(existing, instance)
                by_instance_id[app_instance_id] = merged
                for index, deduped_instance in enumerate(deduped):
                    if deduped_instance.get("appInstanceId") == app_instance_id:
                        deduped[index] = merged
                        break
                continue
            by_instance_id[app_instance_id] = instance
        deduped.append(instance)
    return deduped
+415 −0
Changes for edge_cloud_management_api/controllers/app_partner_orchestration.py: 415 added lines, 0 removed lines.
Original line number Diff line number Diff line
import json

from flask import jsonify

from edge_cloud_management_api.controllers.edge_cloud_controller import get_local_zones
from edge_cloud_management_api.managers.log_manager import logger
from edge_cloud_management_api.services.storage_service import get_fed
from edge_cloud_management_api.services.storage_service import get_zone
from edge_cloud_management_api.services.storage_service import insert_zones


def resolve_target_zone(srm_client, edge_cloud_zone_id, edge_cloud_provider, zone_payload):
    zone = get_zone(edge_cloud_zone_id, edge_cloud_provider) if edge_cloud_zone_id else None
    if not zone and edge_cloud_zone_id:
        try:
            zones = get_local_zones()
            if isinstance(zones, list):
                matching_local_zones = []
                for candidate_zone in zones:
                    if not isinstance(candidate_zone, dict):
                        continue
                    if edge_cloud_provider and candidate_zone.get("edgeCloudProvider") == edge_cloud_provider:
                        matching_local_zones.append(candidate_zone)
                    if candidate_zone.get("edgeCloudZoneId") != edge_cloud_zone_id:
                        continue
                    if edge_cloud_provider and candidate_zone.get("edgeCloudProvider") != edge_cloud_provider:
                        continue
                    if candidate_zone.get("edgeCloudProvider") == edge_cloud_provider or not edge_cloud_provider:
                        zone = candidate_zone
                        break
                if not zone and edge_cloud_zone_id == "default" and len(matching_local_zones) == 1:
                    zone = matching_local_zones[0]
        except Exception as exc:
            logger.info(f"Failed to refresh zones from SRM: {exc}")

    if not zone and edge_cloud_zone_id and isinstance(zone_payload, dict):
        zone = dict(zone_payload)
        zone.setdefault("isLocal", "true")

    return zone


def cleanup_federated_app(
    federation_client,
    feds,
    app_id,
    app_provider_id,
    normalize_federated_app_id,
    artefact_id,
    normalize_federated_artefact_id,
    resolve_federated_app_identity,
):
    if not feds:
        return None

    cleanup_performed = False
    federated_app_id = normalize_federated_app_id(app_id)
    for fed in feds:
        fed_token = fed.get("token")
        federation_context_id = fed.get("_id")
        if not federation_context_id or not fed_token:
            continue
        if app_provider_id:
            federated_app_id, federated_app_provider_id = resolve_federated_app_identity(app_id, app_provider_id)
            fed_instances, fed_code = federation_client.get_all_app_instances(
                federation_context_id=federation_context_id,
                app_id=federated_app_id,
                app_provider_id=federated_app_provider_id,
                token=fed_token,
            )
            if fed_code == 200 and isinstance(fed_instances, list):
                cleanup_performed = True
                for zone_info in fed_instances:
                    if not isinstance(zone_info, dict):
                        continue
                    zone_id = zone_info.get("zoneId")
                    instances_list = zone_info.get("appInstanceInfo", [])
                    if not zone_id or not isinstance(instances_list, list):
                        continue
                    for instance in instances_list:
                        if not isinstance(instance, dict):
                            continue
                        instance_id = instance.get("appInstIdentifier")
                        if not instance_id:
                            continue
                        federation_client.remove_app_instance(
                            federation_context_id=federation_context_id,
                            app_id=federated_app_id,
                            app_instance_id=instance_id,
                            zone_id=zone_id,
                            token=fed_token,
                        )
            elif fed_code not in (404, 422):
                return jsonify(fed_instances), fed_code

        remove_response = federation_client.delete_onboarded_app(
            federation_context_id,
            federated_app_id,
            fed_token,
        )
        remove_status = int(remove_response.get("status_code", 500)) if isinstance(remove_response, dict) else 500
        if remove_status in (200, 202, 204, 404):
            cleanup_performed = True
            if app_provider_id:
                federated_artefact_id = normalize_federated_artefact_id(
                    federation_context_id,
                    artefact_id,
                    app_id,
                )
                artefact_response = federation_client.delete_artefact(
                    federation_context_id,
                    federated_artefact_id,
                    fed_token,
                )
                artefact_status = int(artefact_response.get("status_code", 500)) if isinstance(artefact_response, dict) else 500
                if artefact_status not in (200, 202, 204, 404):
                    return jsonify(artefact_response), artefact_status
            continue
        return jsonify(remove_response), remove_status

    if cleanup_performed:
        return "", 204
    return None


def deploy_to_partner(
    federation_client,
    zone,
    app_id,
    app_data,
    split_image_reference,
    split_image_name_tag,
    ensure_gsma_id,
    ensure_service_name,
    ensure_res_pool,
    normalize_federated_app_id,
    normalize_federated_app_provider_id,
    normalize_federated_artefact_id,
):
    federated_app_id = normalize_federated_app_id(app_id)

    artefact_id = None
    app_provider_id = app_data.get("appProvider") or app_data.get("appProviderId")
    app_name = app_data.get("name") or app_data.get("appName") or app_id
    app_version = app_data.get("version") or "v1"
    app_repo = app_data.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 = app_data.get("componentSpec", [])
    network_interfaces = []
    required_resources = app_data.get("requiredResources") or {}
    if component_specs and isinstance(component_specs, list):
        network_interfaces = component_specs[0].get("networkInterfaces", []) or []
    app_component_specs = app_data.get("appComponentSpecs", [])
    if isinstance(app_component_specs, list) and app_component_specs:
        artefact_id = app_component_specs[0].get("artefactId")
    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
    component_name = ensure_service_name(component_name, "cmp", app_name)
    artefact_id = normalize_federated_artefact_id(zone.get("fedContextId"), artefact_id, app_id)

    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,
    )
    federated_app_provider_id = normalize_federated_app_provider_id(app_provider_id, 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(
        app_data.get("accessToken"),
        r"^[A-Za-z][A-Za-z0-9_]{31,63}$",
        "token",
        32,
        64,
        app_id,
    )
    package_type = (app_data.get("packageType") or "CONTAINER").upper()
    artefact_descriptor_type = "HELM" if package_type == "HELM" else "COMPONENTSPEC"

    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)
    edge_cloud_zone_id = zone.get("edgeCloudZoneId")
    app_deployment_zones = app_data.get("appDeploymentZones")
    if not isinstance(app_deployment_zones, list) or not app_deployment_zones:
        app_deployment_zones = [edge_cloud_zone_id]
    app_qos = app_data.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 = (
        app_data.get("appStatusCallbackLink")
        or app_data.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": artefact_descriptor_type,
        "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")

    artefact_body, artefact_status = federation_client.create_artefact(
        artefact=artefact,
        federation_context_id=zone.get("fedContextId"),
        token=fed_token,
    )
    print("\n========== ARTEFACT CREATION RESPONSE ==========")
    print(f"Status: {artefact_status}")
    print(f"Response: {json.dumps(artefact_body, indent=2)}")
    print("================================================\n")

    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

    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 exc:
        print("\n========== PARTNER CHECK ERROR ==========")
        print(f"Error getting partner status: {str(exc)}")
        print("=========================================\n")

    onboard_app = {
        "appId": federated_app_id,
        "appProviderId": federated_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,
            "exposedInterfaces": [
                {
                    "interfaceId": interface.get("interfaceId") or f"{component_name}_{index}",
                    "commProtocol": interface.get("protocol", "TCP"),
                    "commPort": interface.get("port"),
                    "visibilityType": interface.get("visibilityType", "VISIBILITY_EXTERNAL"),
                }
                for index, interface in enumerate(network_interfaces)
                if interface.get("port") is not None
            ],
            "computeResourceProfile": {
                "cpuArchType": "ISA_X86_64",
                "numCPU": required_resources.get("numCPU") or "100m",
                "memory": required_resources.get("memory") or 128,
                "diskStorage": required_resources.get("storage"),
                "gpu": required_resources.get("gpu") or [],
            },
        }],
        "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("\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

    res_pool_value = ensure_res_pool(zone.get("resPool"), zone.get("edgeCloudZoneId"))
    deploy_app = {
        "appId": federated_app_id,
        "appProviderId": federated_app_provider_id,
        "appVersion": app_version,
        "appInstCallbackLink": app_data.get("appInstCallbackLink", ""),
        "zoneInfo": {
            "zoneId": zone.get("edgeCloudZoneId"),
            "flavourId": zone.get("flavourId") or zone.get("edgeCloudZoneId") or "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

    return jsonify({
        "error": "Application deployment failed",
        "fm_response": deploy_app_body,
    }), deploy_app_status
Loading
Loading