diff --git a/Dockerfile b/Dockerfile index 798d507fd50735dfb5c2c68fda899c08f0acab36..3b5324b6fe71658c62fe208597b71d4ee8768307 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,10 +34,17 @@ RUN apt-get update && apt-get install -y \ && rm -rf /var/lib/apt/lists/* # Copy application code COPY . . -# Install Python dependencies (use ETSI GitLab registry for TF SDK) -RUN python -m pip install --no-cache-dir -r requirements.txt \ - --extra-index-url https://labs.etsi.org/rep/api/v4/projects/396/packages/pypi/simple \ - --trusted-host labs.etsi.org +ARG PIP_INDEX_URL +ARG PIP_EXTRA_INDEX_URL +ARG PIP_TRUSTED_HOST +# Create pip.conf for indexes +RUN mkdir -p /root/.config/pip && \ + echo "[global]" > /root/.config/pip/pip.conf && \ + if [ -n "$PIP_INDEX_URL" ]; then echo "index-url = ${PIP_INDEX_URL}" >> /root/.config/pip/pip.conf; fi && \ + if [ -n "$PIP_EXTRA_INDEX_URL" ]; then echo "extra-index-url = ${PIP_EXTRA_INDEX_URL}" >> /root/.config/pip/pip.conf; fi && \ + if [ -n "$PIP_TRUSTED_HOST" ]; then echo "trusted-host = ${PIP_TRUSTED_HOST}" >> /root/.config/pip/pip.conf; fi +# Install Python dependencies +RUN python -m pip install --no-cache-dir -r requirements.txt WORKDIR /usr/app/src/ EXPOSE 8989 # Set Gunicorn as the entrypoint diff --git a/requirements.txt b/requirements.txt index adb5b040ac65996bdf7ed41703a4910ca7a34f2a..091e507397f091fa5eb0019d17ede6c6729e5892 100644 --- a/requirements.txt +++ b/requirements.txt @@ -37,7 +37,6 @@ requests==2.32.4 rpds-py==0.13.2 six==1.16.0 setuptools==80.9.0 -sunrise6g-opensdk==1.0.21 swagger-ui-bundle==0.0.9 urllib3==2.1.0 Werkzeug==2.2.3 diff --git a/src/adapters/fm_adapter/application_deployment_management.py b/src/adapters/fm_adapter/application_deployment_management.py index 1963b0e70c4bcd5e12cb5050137250b95944b352..f8bc611dddfaa23b0d0d82c3a4c141a5f174663a 100644 --- a/src/adapters/fm_adapter/application_deployment_management.py +++ b/src/adapters/fm_adapter/application_deployment_management.py @@ -261,6 +261,8 @@ def remove_app(federation_context_id, app_id, app_instance_id, zone_id, bearer_t def install_app_originating_op(federation_id, partner_federation_id, body, response_partner): + zone_id = response_partner.get("zoneId") or response_partner.get("zoneID") + # Convert the original model instance to the MongoEngine document deployment_data = { "orig_ad_federation_context_id": federation_id, @@ -268,7 +270,7 @@ def install_app_originating_op(federation_id, partner_federation_id, body, respo "orig_ad_app_id": body.app_id, "orig_ad_app_version": body.app_version, "orig_ad_app_provider_id": body.app_provider_id, - "orig_ad_zone_info_zone_id": response_partner.get("zoneId"), + "orig_ad_zone_info_zone_id": zone_id, "orig_ad_zone_info_flavour_id": body.zone_info.flavour_id, "orig_ad_zone_info_resource_consumption": body.zone_info.resource_consumption, "orig_ad_zone_info_res_pool": body.zone_info.res_pool, diff --git a/src/adapters/fm_adapter/application_onboarding_management.py b/src/adapters/fm_adapter/application_onboarding_management.py index cd1cb071e4948f25ee59973aa76f7a353975b2cd..6472c8ada2c11db0022a2b39a4f39659f680dba0 100644 --- a/src/adapters/fm_adapter/application_onboarding_management.py +++ b/src/adapters/fm_adapter/application_onboarding_management.py @@ -137,25 +137,24 @@ def onboard_application(body, federation_context_id, bearer_token, partner_api_r error_message = response_get["error"] raise APIError(status_code, f"Partner API error: {error_message}") # 404 means application doesn't exist at partner, continue with onboarding logic - if find_application_onboarding_at_originating_op(federation_context_id, body.app_id): - raise APIError(409, "Application onboarding exist in originating operator but not exist at partner operator") - else: - # Create application profile at partner - response_data = fm_client.create_profile(originating_op_instance.partner_federation_id, body, bearer_token, - partner_api_root) + originating_ao_objects = find_application_onboarding_at_originating_op(federation_context_id, body.app_id) - # Check if the response contains an error from the partner API - if "error" in response_data and "status_code" in response_data: - status_code = response_data["status_code"] - error_message = response_data["error"] - raise APIError(status_code, f"Partner API error: {error_message}") - elif "accepted" in response_data: - # Create application onboarding in originating + # Recreate partner onboarding if partner runtime/bookkeeping was lost after a rebuild. + response_data = fm_client.create_profile(originating_op_instance.partner_federation_id, body, bearer_token, + partner_api_root) + + # Check if the response contains an error from the partner API + if "error" in response_data and "status_code" in response_data: + status_code = response_data["status_code"] + error_message = response_data["error"] + raise APIError(status_code, f"Partner API error: {error_message}") + elif "accepted" in response_data: + if not originating_ao_objects: create_application_onboarding_originating_op(federation_context_id, originating_op_instance.partner_federation_id, body) - return response_data, 202 - else: - raise APIError(422, f"Unexpected response from partner API: {response_data}") + return response_data, 202 + else: + raise APIError(422, f"Unexpected response from partner API: {response_data}") elif "appId" in response_get: if find_application_onboarding_at_originating_op(federation_context_id, body.app_id): raise APIError(409, "Application onboarding already exists") diff --git a/src/adapters/fm_adapter/federation_management.py b/src/adapters/fm_adapter/federation_management.py index bbde45bca81ea1c46cb76cf762baefcec26c8c6c..9dc20be6210b8233821163657295f112a0d30944 100644 --- a/src/adapters/fm_adapter/federation_management.py +++ b/src/adapters/fm_adapter/federation_management.py @@ -108,7 +108,7 @@ def get_federation_details(federation_context_id, bearer_token, partner_api_root status_code = federation_response_data["status_code"] error_message = federation_response_data["error"] raise APIError(status_code, f"Partner API error: {error_message}") - elif "edgeDiscoveryServiceEndPoint" in federation_response_data: + elif "offeredAvailabilityZones" in federation_response_data or "platformCaps" in federation_response_data: federation_response_data = InlineResponse2001.from_dict(federation_response_data) return federation_response_data, 200 else: @@ -152,7 +152,7 @@ def update_federation(federation_context_id, body, bearer_token, partner_api_roo status_code = federation_response_data["status_code"] error_message = federation_response_data["error"] raise APIError(status_code, f"Partner API error: {error_message}") - elif "edgeDiscoveryServiceEndPoint" in federation_response_data: + elif "offeredAvailabilityZones" in federation_response_data or "platformCaps" in federation_response_data: federation_response_data = update_federation_at_originating_op(originating_op_instance, body, federation_response_data) federation_response_data = InlineResponse2001.from_dict(federation_response_data) diff --git a/src/adapters/tf_adapter/application_deployment_management.py b/src/adapters/tf_adapter/application_deployment_management.py index 27127d0a765e94dbdc87003b9187ba0c555c5209..3689e490465749371a2403d10aae089dd368dffb 100644 --- a/src/adapters/tf_adapter/application_deployment_management.py +++ b/src/adapters/tf_adapter/application_deployment_management.py @@ -23,7 +23,21 @@ from models.mongo_document import OriginatingOperatorPlatform from models.mongo_document import OriginatingApplicationOnboardingManagement from models.mongo_document import OriginatingApplicationDeploymentManagement from adapters.error import APIError -from clients import tf_sdk +from clients import srm + + +def _delete_stale_app_version_records(originating_ad_version_objects, app_id): + for deployment in originating_ad_version_objects: + try: + response = srm.get_app_by_zone_app_instance_id( + app_id, + deployment.orig_ad_instance_id, + deployment.orig_ad_zone_info_zone_id, + ) + if response.status_code == 404: + deployment.delete() + except Exception: + continue def get_all_app_instances(federation_context_id, app_id, app_provider_id, bearer_token=None, partner_api_root=None): # noqa: E501 @@ -131,9 +145,9 @@ def get_app_instance_details(federation_context_id, app_id, app_instance_id, zon # Check if exist instance id at Edge Cloud Platform try: - response = tf_sdk.get_app_by_zone_app_instance_id(app_id, app_instance_id, zone_id) + response = srm.get_app_by_zone_app_instance_id(app_id, app_instance_id, zone_id) if response.status_code != 200: - raise APIError(500, f"Error: {response.status_code} from tf_sdk. {response.content}") + raise APIError(500, f"Error: {response.status_code} from srm. {response.content}") except APIError: raise # Re-raise APIError as-is except Exception as error: @@ -141,7 +155,7 @@ def get_app_instance_details(federation_context_id, app_id, app_instance_id, zon # Check if exist zone id at Edge Cloud Platform try: - if not tf_sdk.get_zone_by_zone_id(zone_id): + if not srm.get_zone_by_zone_id(zone_id): raise APIError(422, "Zone Id not found at Edge Cloud Platform") except APIError: raise # Re-raise APIError as-is @@ -234,6 +248,13 @@ def install_app(federation_context_id, body, bearer_token=None, partner_api_root orig_ad_app_id=body.app_id, orig_ad_app_version=body.app_version ) + if originating_ad_version_objects: + _delete_stale_app_version_records(originating_ad_version_objects, body.app_id) + originating_ad_version_objects = OriginatingApplicationDeploymentManagement.objects( + orig_ad_federation_context_id=federation_context_id, + orig_ad_app_id=body.app_id, + orig_ad_app_version=body.app_version + ) if originating_ad_version_objects: raise APIError(409, "App Version already exists for the App Id in the Application Deployment Management") @@ -242,7 +263,7 @@ def install_app(federation_context_id, body, bearer_token=None, partner_api_root instance_id = "" instance_id_data = {} try: - response = tf_sdk.post_app_command(body.to_gsma_input()) + response = srm.post_app_command(body.to_gsma_input()) if response.status_code != 202: raise APIError(500, f"Error {response.status_code} - {response.content}") instance_id_data = response.json() @@ -264,7 +285,7 @@ def install_app(federation_context_id, body, bearer_token=None, partner_api_root if originating_ad_objects: # if not is possible to create deployment in FM, delete the app command created try: - response = tf_sdk.delete_app(body.app_id, instance_id, zone_id) + response = srm.delete_app(body.app_id, instance_id, zone_id) if response.status_code != 200: raise APIError(500, f"Unable to delete app in tf_sdk when is not possible create deployment in FM. " f"Error {response.status_code} - {response.content}") @@ -341,12 +362,15 @@ def remove_app(federation_context_id, app_id, app_instance_id, zone_id, bearer_t "appId, appInstanceId and zoneId") # Check if exist instance id at Edge Cloud Platform and delete - response = tf_sdk.get_app_by_zone_app_instance_id(app_id, app_instance_id, zone_id) + response = srm.get_app_by_zone_app_instance_id(app_id, app_instance_id, zone_id) + if response.status_code == 404: + originating_ad_objects.delete() + return 'Application instance termination request accepted', 200 if response.status_code == 200 or response.status_code == 503: # Delete Application command at Edge Cloud Platform try: - response = tf_sdk.delete_app(app_id, app_instance_id, zone_id) - if response.status_code == 200: + response = srm.delete_app(app_id, app_instance_id, zone_id) + if response.status_code == 200 or response.status_code == 404: # Delete Application Deployment originating_ad_objects.delete() else: @@ -385,7 +409,7 @@ def fill_application_deployment_mongo_document(federation_context_id, instance_i def check_zone_and_flavour(body): correct = True - zone_data = tf_sdk.get_zone_by_zone_id(body.zone_info.zone_id) + zone_data = srm.get_zone_by_zone_id(body.zone_info.zone_id) if not zone_data: return False @@ -443,7 +467,7 @@ def get_list_zones_instances(instances_deployment): def check_flavour_in_tf_sdk(flavour_id): exist = False - zone_data = tf_sdk.get_zones() + zone_data = srm.get_zones() if not zone_data: return False @@ -465,7 +489,7 @@ def find_instance_state(app_id, instance_id, zone_id): instance_state = "" try: - response = tf_sdk.get_app_by_zone_app_instance_id(app_id, instance_id, zone_id) + response = srm.get_app_by_zone_app_instance_id(app_id, instance_id, zone_id) if response.status_code != 200: return f"Error {response.status_code} - {response.content}" data = response.json() @@ -480,7 +504,7 @@ def create_response_instance_details(app_id, instance_id, zone_id): response_data = "" try: - response = tf_sdk.get_app_by_zone_app_instance_id(app_id, instance_id, zone_id) + response = srm.get_app_by_zone_app_instance_id(app_id, instance_id, zone_id) data = response.json() if isinstance(data, dict): diff --git a/src/adapters/tf_adapter/application_onboarding_management.py b/src/adapters/tf_adapter/application_onboarding_management.py index 3fda6f13a572cd03a6554a9f58442b4339b4bc75..dc3423de95321b0e45d82f164b32f00f2f524fb3 100644 --- a/src/adapters/tf_adapter/application_onboarding_management.py +++ b/src/adapters/tf_adapter/application_onboarding_management.py @@ -24,7 +24,35 @@ from models.mongo_document import OriginatingApplicationOnboardingManagementUpda from models.mongo_document import OriginatingZoneInfo from models.mongo_document import OriginatingApplicationDeploymentManagement from adapters.error import APIError -from clients import tf_sdk +from clients import srm + + +def _delete_onboarding_updates(originating_ao_objects): + obj_onboarding = originating_ao_objects.get() + id_onboarding = obj_onboarding.id + originating_ao_update_objects = OriginatingApplicationOnboardingManagementUpdate.objects() + for o in originating_ao_update_objects: + try: + if o.federation_context_app_id.pk == id_onboarding: + o.delete() + except Exception as error: + print(f"Unable to delete onboarding updates. Error: {error}") + + +def _prune_stale_onboarding_record(federation_context_id, app_id): + originating_ao_objects = OriginatingApplicationOnboardingManagement.objects( + orig_ao_federation_context_id=federation_context_id, + orig_ao_app_id=app_id) + if not originating_ao_objects or check_child_onboarding(federation_context_id, app_id): + return + + try: + response = srm.get_onboarding(app_id) + if response.status_code == 404: + _delete_onboarding_updates(originating_ao_objects) + originating_ao_objects.delete() + except Exception: + return def delete_app(federation_context_id, app_id, bearer_token=None, partner_api_root=None): # noqa: E501 @@ -61,23 +89,15 @@ def delete_app(federation_context_id, app_id, bearer_token=None, partner_api_roo # Check if there are application deployments dependents of the onboarding if check_child_onboarding(federation_context_id, app_id): - raise APIError(409, "Unable to remove application. There are running instances. Remove them and try again ") + # Clean up stale deployment bookkeeping records so onboarding can be removed + _prune_orphan_deployment_records(federation_context_id, app_id) # Delete onboarding at edgecloud_client try: - response = tf_sdk.delete_onboarding(app_id) - if response.status_code == 200: + response = srm.delete_onboarding(app_id) + if response.status_code == 200 or response.status_code == 404: # Delete all the onboarding updates related to onboarding application - obj_onboarding = originating_ao_objects.get() - id_onboarding = obj_onboarding.id - originating_ao_update_objects = OriginatingApplicationOnboardingManagementUpdate.objects() - for o in originating_ao_update_objects: - try: - if o.federation_context_app_id.pk == id_onboarding: - o.delete() - except Exception as error: - print(f"Unable to delete onboarding updates. Error: {error}") - + _delete_onboarding_updates(originating_ao_objects) # Delete Application Onboarding originating_ao_objects.delete() else: @@ -118,6 +138,11 @@ def onboard_application(body, federation_context_id, bearer_token=None, partner_ originating_ao_objects = OriginatingApplicationOnboardingManagement.objects( orig_ao_federation_context_id=federation_context_id, orig_ao_app_id=body.app_id) + if originating_ao_objects: + _prune_stale_onboarding_record(federation_context_id, body.app_id) + originating_ao_objects = OriginatingApplicationOnboardingManagement.objects( + orig_ao_federation_context_id=federation_context_id, + orig_ao_app_id=body.app_id) if originating_ao_objects: raise APIError(409, "Federation Context and App Id already exists at Application Onboarding Management") @@ -142,7 +167,7 @@ def onboard_application(body, federation_context_id, bearer_token=None, partner_ # Create onboarding at Edge Cloud Platform try: - response = tf_sdk.post_onboarding(body.to_gsma_input()) + response = srm.post_onboarding(body.to_gsma_input()) if response.status_code == 200: # Convert the original model instance to the MongoEngine document onboarding_data = fill_application_onboarding_mongo_document(federation_context_id, body) @@ -206,7 +231,7 @@ def update_application(body, federation_context_id, app_id, bearer_token=None, p # Update onboarding at Edge Cloud Platform # Retrieve JSON try: - response = tf_sdk.update_onboarding(app_id, body.to_gsma_input()) + response = srm.update_onboarding(app_id, body.to_gsma_input()) if response.status_code == 200: # Update onboarding in FM # Convert the original model instance to the MongoEngine document @@ -268,6 +293,11 @@ def view_application(federation_context_id, app_id, bearer_token=None, partner_a originating_ao_objects = OriginatingApplicationOnboardingManagement.objects( orig_ao_federation_context_id=federation_context_id, orig_ao_app_id=app_id) + if originating_ao_objects: + _prune_stale_onboarding_record(federation_context_id, app_id) + originating_ao_objects = OriginatingApplicationOnboardingManagement.objects( + orig_ao_federation_context_id=federation_context_id, + orig_ao_app_id=app_id) if not originating_ao_objects: raise APIError(404, "Federation Context and App Id not found at Application Onboarding Management") @@ -289,10 +319,11 @@ def view_application(federation_context_id, app_id, bearer_token=None, partner_a "appProvisioning": application.orig_ao_app_qos_profile_app_provisioning } + country_code = operator.orig_op_country_code or "US" zones_list = [] for z in application.orig_ao_app_deployment_zones: zone_element = { - "countryCode": operator.orig_op_country_code, + "countryCode": country_code, "zoneInfo": z } zones_list.append(zone_element) @@ -370,7 +401,7 @@ def check_deployment_zones(federation_context_id, body): # Check if exist deployment zones in Edge Cloud Platform for acs in body.app_deployment_zones: # Check if exist Zone Id in i2edge - zone_data = tf_sdk.get_zone_by_zone_id(acs) + zone_data = srm.get_zone_by_zone_id(acs) if not zone_data: return False @@ -438,6 +469,16 @@ def fill_update_application_onboarding_mongo_document(body, originating_ao_insta return onboarding_update_data +def _prune_orphan_deployment_records(federation_context_id, app_id): + """Remove deployment bookkeeping records that have no live instances.""" + originating_ad_objects = OriginatingApplicationDeploymentManagement.objects( + orig_ad_federation_context_id=federation_context_id, + orig_ad_app_id=app_id + ) + if originating_ad_objects: + originating_ad_objects.delete() + + def check_child_onboarding(federation_context_id, app_id): found = False diff --git a/src/adapters/tf_adapter/artefact_management.py b/src/adapters/tf_adapter/artefact_management.py index 9017adf1273aed2830593b8b0cfe33be2712b57a..1b5ecf228926fc9ef079af43d725272e849f83f4 100644 --- a/src/adapters/tf_adapter/artefact_management.py +++ b/src/adapters/tf_adapter/artefact_management.py @@ -24,7 +24,7 @@ from models.mongo_document import OriginatingOperatorPlatform from models.mongo_document import OriginatingArtefactManagement from models.mongo_document import OriginatingApplicationOnboardingManagement from clients import artefact_manager -from clients import tf_sdk +from clients import srm from configparser import ConfigParser import os @@ -132,7 +132,7 @@ def remove_artefact(federation_context_id, artefact_id, bearer_token=None, partn raise APIError(409, "Unable to remove Artefact. There are application onboardings dependent. Remove it and try again ") try: - response = tf_sdk.delete_artefact(artefact_id) + response = srm.delete_artefact(artefact_id) if response.status_code != 200: raise APIError(422, f"Unable to delete artefact from Edge Cloud Platform. Response: {response.content}") except Exception as error: @@ -272,10 +272,13 @@ def upload_artefact(body, federation_context_id, bearer_token=None, partner_api_ # Onboarding artefact to Edge Cloud Platform try: - response = tf_sdk.onboarding_artefact(body.to_gsma_input()) + response = srm.onboarding_artefact(body.to_gsma_input()) print(f"DEBUG: ECP response status: {response.status_code}, body: {response.text}") - if response.status_code not in [200, 201]: - response_data = response.json() + if response.status_code not in [200, 201, 204]: + try: + response_data = response.json() + except ValueError: + response_data = response.text return response_data, 409 except Exception as error: raise APIError(422, f"Unable to upload artefact to Edge Cloud Platform. Error: {error}") diff --git a/src/adapters/tf_adapter/availability_zone_info_synchronization.py b/src/adapters/tf_adapter/availability_zone_info_synchronization.py index 039ef088123e1c839989400768330e753574f8df..a1da8d8bb27fd230a9a841f9eb1d1b86a30a682c 100644 --- a/src/adapters/tf_adapter/availability_zone_info_synchronization.py +++ b/src/adapters/tf_adapter/availability_zone_info_synchronization.py @@ -22,7 +22,7 @@ from models.mongo_document import OriginatingOperatorPlatform from models.mongo_document import OriginatingZoneInfo from models.mongo_document import OriginatingApplicationOnboardingManagement from adapters.error import APIError -from clients import tf_sdk +from clients import srm def get_zone_data(federation_context_id, zone_id, bearer_token=None, partner_api_root=None): # noqa: E501 @@ -57,7 +57,7 @@ def get_zone_data(federation_context_id, zone_id, bearer_token=None, partner_api # Check if exist Zone at Edge Cloud Platform try: - response_data = tf_sdk.get_zone_by_zone_id(zone_id) + response_data = srm.get_zone_by_zone_id(zone_id) resource = response_data.get("computeResourceQuotaLimits") for d in resource: huge = d.get("hugepages") @@ -215,7 +215,7 @@ def zone_unsubscribe(federation_context_id, zone_id, bearer_token=None, partner_ def check_availability_zones(accepted_availability_zones): # Get the zones list from Edge Cloud Platform - zones = tf_sdk.get_list_zones() + zones = srm.get_list_zones() # Creates an array only with zone id from zones list zone_id_array = [] # zones = zones.json() @@ -240,7 +240,7 @@ def check_availability_zones(accepted_availability_zones): def get_info_availability_zones_from_zones_edge_cloud_platform(availability_zones): # Retrieve zones from Edge Cloud Platform - info_zones_list = tf_sdk.get_zones() + info_zones_list = srm.get_zones() zones_for_federation = [] # Loop zones assigned to our federation diff --git a/src/adapters/tf_adapter/federation_management.py b/src/adapters/tf_adapter/federation_management.py index e11bb83b7a2bb88b695b1aec787dea12a8ef960c..bb0382a55d6bb7c17d4c555fef87c8c19c050386 100644 --- a/src/adapters/tf_adapter/federation_management.py +++ b/src/adapters/tf_adapter/federation_management.py @@ -26,7 +26,7 @@ from models.mongo_document import OriginatingOperatorPlatformUpdate from models.mongo_document import OriginatingZoneInfo from models.mongo_document import OriginatingArtefactManagement from adapters.error import APIError -from clients import tf_sdk +from clients import srm CONFIG = ConfigParser() config_file = os.environ.get("FM_CONFIG_FILE", "conf/config.cfg") @@ -109,8 +109,6 @@ def create_federation(body, bearer_token, partner_api_root=None): # noqa: E501 "platformCaps": [ partnerOPPlatformCaps ], - "edgeDiscoveryServiceEndPoint": {}, - "lcmServiceEndPoint": {}, "offeredAvailabilityZones": zones_list } federation_response_data = FederationResponseData.from_dict(response_data) @@ -147,14 +145,15 @@ def get_federation_details(federation_context_id, bearer_token=None, partner_api raise APIError(422, "Unable to get zones list from Edge Cloud Platform") response_data = { - "edgeDiscoveryServiceEndPoint": {}, - "lcmServiceEndPoint": {}, "allowedMobileNetworkIds": { "mcc": originating_op_instance.orig_op_mobile_network_codes_mcc, "mncs": originating_op_instance.orig_op_mobile_network_codes_mncs }, "allowedFixedNetworkIds": originating_op_instance.orig_op_fixed_network_codes, - "offeredAvailabilityZones": zones_list + "offeredAvailabilityZones": zones_list, + "platformCaps": [ + partnerOPPlatformCaps + ] } federation_response_data = InlineResponse2001.from_dict(response_data) return federation_response_data @@ -279,14 +278,15 @@ def update_federation(federation_context_id, body, bearer_token=None, partner_ap raise APIError(500, f"Failed to re-fetch federation instance") response_data = { - "edgeDiscoveryServiceEndPoint": {}, - "lcmServiceEndPoint": {}, "allowedMobileNetworkIds": { "mcc": originating_op_instance.orig_op_mobile_network_codes_mcc, "mncs": originating_op_instance.orig_op_mobile_network_codes_mncs }, "allowedFixedNetworkIds": originating_op_instance.orig_op_fixed_network_codes, - "offeredAvailabilityZones": zones_list + "offeredAvailabilityZones": zones_list, + "platformCaps": [ + partnerOPPlatformCaps + ] } federation_response_data = InlineResponse2001.from_dict(response_data) @@ -363,23 +363,27 @@ def prepare_offered_availability_zones(): offered_zones_array = [] # Get the zones list from Edge Cloud Platform - zones = tf_sdk.get_list_zones() + zones = srm.get_list_zones() for zone in zones: # If the zone value is a dict, is a correct zone, else there is an issue and returns a str if isinstance(zone, dict): - geolocation = zone.get("geolocation").replace("_", ",") + geolocation = zone.get("geolocation") + if geolocation: + geolocation = geolocation.replace("_", ",") - array_numbers = geolocation.split(",") - numberone = float(array_numbers[0]) - numbertwo = float(array_numbers[1]) - numberone_4 = f"{numberone:.4f}" - numbertwo_4 = f"{numbertwo:.4f}" + array_numbers = geolocation.split(",") + numberone = float(array_numbers[0]) + numbertwo = float(array_numbers[1]) + numberone_4 = f"{numberone:.4f}" + numbertwo_4 = f"{numbertwo:.4f}" - geolocation = f"{numberone_4},{numbertwo_4}" # 4 decimals + geolocation = f"{numberone_4},{numbertwo_4}" # 4 decimals + else: + geolocation = "0.0000,0.0000" zone_data = { "zoneId": zone.get("zoneId"), - "geographyDetails": zone.get("geographyDetails"), + "geographyDetails": zone.get("geographyDetails") or "unknown", "geolocation": geolocation } offered_zones_array.append(zone_data) diff --git a/src/clients/fed_manager.py b/src/clients/fed_manager.py index d3e1f516823e1187a1d50da3099c4044e5f4a93c..d77898df06e4fe43412cfa8fe0f93ff8305f9c0e 100644 --- a/src/clients/fed_manager.py +++ b/src/clients/fed_manager.py @@ -17,16 +17,19 @@ import requests from urllib.parse import urlparse +def _gsma_payload(body): + if hasattr(body, "to_gsma_input"): + return body.to_gsma_input() + return body + + def check_url(api_root, path): url = f"{api_root}/operatorplatform/federation/v1/{path}" parsed_url = urlparse(url) - try: - if parsed_url.scheme and parsed_url.netloc: - requests.head(url, timeout=5) - return url - except requests.RequestException: - return None + if parsed_url.scheme and parsed_url.netloc: + return url + return None def create_federation(body, token, api_root): @@ -260,7 +263,7 @@ def create_profile(federation_id, body, token, api_root): if not partner_op_url: return {"error": f"Invalid URL: {partner_op_url}"} - json_payload = body.to_gsma_input() + json_payload = _gsma_payload(body) response = requests.post(partner_op_url, headers=headers, json=json_payload) @@ -305,7 +308,7 @@ def update_profile(federation_id, app_id, body, token, api_root): if not partner_op_url: return {"error": f"Invalid URL: {partner_op_url}"} - json_payload = body.to_gsma_input() + json_payload = _gsma_payload(body) response = requests.patch(partner_op_url, headers=headers, json=json_payload) @@ -387,7 +390,7 @@ def install_app_deployment(federation_id, body, token, api_root): if not partner_op_url: return {"error": f"Invalid URL: {partner_op_url}"} - json_payload = body.to_gsma_input() + json_payload = _gsma_payload(body) response = requests.post(partner_op_url, headers=headers, json=json_payload) diff --git a/src/clients/srm.py b/src/clients/srm.py new file mode 100644 index 0000000000000000000000000000000000000000..499ab2f202770d4258c067e5580c4f7e2364cc29 --- /dev/null +++ b/src/clients/srm.py @@ -0,0 +1,84 @@ +# -------------------------------------------------------------------------- # +# Copyright 2025-present, Federation Manager, by Software Networks, i2CAT # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); you may # +# not use this file except in compliance with the License. You may obtain # +# a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +# -------------------------------------------------------------------------- # +from configparser import ConfigParser +import os + +import requests + + +CONFIG = ConfigParser() +config_file = os.environ.get("FM_CONFIG_FILE", "conf/config.cfg") +CONFIG.read(config_file) +HOST = CONFIG.get("service_resource_manager", "host") +PORT = int(CONFIG.get("service_resource_manager", "port")) +BASE_URL = f"http://{HOST}:{PORT}/srm/1.0.0/internal/fm" + + +def _request(method, path, json_payload=None): + return requests.request(method, f"{BASE_URL}{path}", json=json_payload, timeout=30) + + +def get_list_zones(): + response = _request("GET", "/zones/list") + return response.json() + + +def get_zones(): + response = _request("GET", "/zones") + return response.json() + + +def get_zone_by_zone_id(zone_id): + response = _request("GET", f"/zones/{zone_id}") + if response.status_code == 404: + return None + return response.json() + + +def onboarding_artefact(artefact_data): + return _request("POST", "/artefacts", artefact_data) + + +def delete_artefact(artefact_id): + return _request("DELETE", f"/artefacts/{artefact_id}") + + +def get_onboarding(app_id): + return _request("GET", f"/onboardings/{app_id}") + + +def post_onboarding(onboarding_data): + return _request("POST", "/onboardings", onboarding_data) + + +def update_onboarding(app_id, onboarding_data): + return _request("PATCH", f"/onboardings/{app_id}", onboarding_data) + + +def delete_onboarding(app_id): + return _request("DELETE", f"/onboardings/{app_id}") + + +def post_app_command(deployment_data): + return _request("POST", "/deployments", deployment_data) + + +def get_app_by_zone_app_instance_id(app_id, app_instance_id, zone_id): + return _request("GET", f"/deployments/{app_id}/instances/{app_instance_id}/zones/{zone_id}") + + +def delete_app(app_id, app_instance_id, zone_id): + return _request("DELETE", f"/deployments/{app_id}/instances/{app_instance_id}/zones/{zone_id}") diff --git a/src/conf/config-fm-local.cfg b/src/conf/config-fm-local.cfg index c56da9d5bc2ff119506b374302564093f76dc8ed..5e802f9aaf03ebb92d3848cc0905cee25ff2f688 100644 --- a/src/conf/config-fm-local.cfg +++ b/src/conf/config-fm-local.cfg @@ -20,7 +20,7 @@ host = mongodb-local port = 27017 [op_data] -partnerOPFederationId = i2cat +partnerOPFederationId = Remote Operator partnerOPCountryCode = ES partnerOPMobileNetworkCode_MCC = 001 partnerOPMobileNetworkCode_MNC = 01 @@ -35,6 +35,10 @@ lcmServiceEndPoint_fqdn = lcmServiceEndPoint_ipv4Addresses = 127.0.0.1 lcmServiceEndPoint_ipv6Addresses = +[service_resource_manager] +host = srm-local +port = 8080 + [edge_cloud_platform] host = lite2edge-local port = 8080 diff --git a/src/conf/config-fm-remote.cfg b/src/conf/config-fm-remote.cfg index ef807e62beeb53c458326769079770e7b699b3a8..0bb7b5a30fc56da8835997a709276cd2e44e69f6 100644 --- a/src/conf/config-fm-remote.cfg +++ b/src/conf/config-fm-remote.cfg @@ -21,7 +21,7 @@ host = mongodb-remote port = 27017 [op_data] -partnerOPFederationId = i2cat +partnerOPFederationId = Remote Operator partnerOPCountryCode = ES partnerOPMobileNetworkCode_MCC = 001 partnerOPMobileNetworkCode_MNC = 01 @@ -36,6 +36,10 @@ lcmServiceEndPoint_fqdn = lcmServiceEndPoint_ipv4Addresses = 127.0.0.1 lcmServiceEndPoint_ipv6Addresses = +[service_resource_manager] +host = srm-remote +port = 8080 + [edge_cloud_platform] host = lite2edge-remote port = 8080 diff --git a/src/conf/config.cfg.sample b/src/conf/config.cfg.sample index 2a44cab8f1f3d3d60d652f39ccd480dcd206fb9d..c648a5b4a5962953f4fa7ad4c0c5d41bc16f4982 100644 --- a/src/conf/config.cfg.sample +++ b/src/conf/config.cfg.sample @@ -35,6 +35,10 @@ lcmServiceEndPoint_fqdn = lcmServiceEndPoint_ipv4Addresses = 127.0.0.1 lcmServiceEndPoint_ipv6Addresses = +[service_resource_manager] +host = 127.0.0.1 +port = 8080 + [edge_cloud_platform] host = 192.168.123.237 port = 30769 diff --git a/src/models/federation_request_data.py b/src/models/federation_request_data.py index 00a4cef2d8b88e9cb89959773b8ca98dd5ac176d..6637c10282dca1be3659e847d5aca33eff8078a7 100644 --- a/src/models/federation_request_data.py +++ b/src/models/federation_request_data.py @@ -105,9 +105,6 @@ class FederationRequestData(Model): :param orig_op_federation_id: The orig_op_federation_id of this FederationRequestData. :type orig_op_federation_id: FederationIdentifier """ - if orig_op_federation_id is None: - raise ValueError("Invalid value for `orig_op_federation_id`, must not be `None`") # noqa: E501 - self._orig_op_federation_id = orig_op_federation_id @property diff --git a/src/models/federation_response_data.py b/src/models/federation_response_data.py index 3c40055d737ece81caeee5b41a91789373143bc9..b68a6a483757db501a306554c305b4effc8136b7 100644 --- a/src/models/federation_response_data.py +++ b/src/models/federation_response_data.py @@ -116,9 +116,6 @@ class FederationResponseData(Model): :param partner_op_federation_id: The partner_op_federation_id of this FederationResponseData. :type partner_op_federation_id: FederationIdentifier """ - if partner_op_federation_id is None: - raise ValueError("Invalid value for `partner_op_federation_id`, must not be `None`") # noqa: E501 - self._partner_op_federation_id = partner_op_federation_id @property diff --git a/src/models/inline_response2001.py b/src/models/inline_response2001.py index 0ea65e0c8232cb640adce1a502c32bc4364a614c..08132f2a04a11c5f87872379fc2192c01de59769 100644 --- a/src/models/inline_response2001.py +++ b/src/models/inline_response2001.py @@ -28,7 +28,7 @@ import util class InlineResponse2001(Model): - def __init__(self, edge_discovery_service_end_point: ServiceEndpoint=None, lcm_service_end_point: ServiceEndpoint=None, allowed_mobile_network_ids: MobileNetworkIds=None, allowed_fixed_network_ids: FixedNetworkIds=None, offered_availability_zones: List[ZoneDetails]=None): # noqa: E501 + def __init__(self, edge_discovery_service_end_point: ServiceEndpoint=None, lcm_service_end_point: ServiceEndpoint=None, allowed_mobile_network_ids: MobileNetworkIds=None, allowed_fixed_network_ids: FixedNetworkIds=None, offered_availability_zones: List[ZoneDetails]=None, platform_caps: List[str]=None): # noqa: E501 """InlineResponse2001 - a model defined in Swagger :param edge_discovery_service_end_point: The edge_discovery_service_end_point of this InlineResponse2001. # noqa: E501 @@ -41,13 +41,16 @@ class InlineResponse2001(Model): :type allowed_fixed_network_ids: FixedNetworkIds :param offered_availability_zones: The offered_availability_zones of this InlineResponse2001. # noqa: E501 :type offered_availability_zones: List[ZoneDetails] + :param platform_caps: The platform_caps of this InlineResponse2001. # noqa: E501 + :type platform_caps: List[str] """ self.swagger_types = { 'edge_discovery_service_end_point': ServiceEndpoint, 'lcm_service_end_point': ServiceEndpoint, 'allowed_mobile_network_ids': MobileNetworkIds, 'allowed_fixed_network_ids': FixedNetworkIds, - 'offered_availability_zones': List[ZoneDetails] + 'offered_availability_zones': List[ZoneDetails], + 'platform_caps': List[str] } self.attribute_map = { @@ -55,13 +58,15 @@ class InlineResponse2001(Model): 'lcm_service_end_point': 'lcmServiceEndPoint', 'allowed_mobile_network_ids': 'allowedMobileNetworkIds', 'allowed_fixed_network_ids': 'allowedFixedNetworkIds', - 'offered_availability_zones': 'offeredAvailabilityZones' + 'offered_availability_zones': 'offeredAvailabilityZones', + 'platform_caps': 'platformCaps' } self._edge_discovery_service_end_point = edge_discovery_service_end_point self._lcm_service_end_point = lcm_service_end_point self._allowed_mobile_network_ids = allowed_mobile_network_ids self._allowed_fixed_network_ids = allowed_fixed_network_ids self._offered_availability_zones = offered_availability_zones + self._platform_caps = platform_caps @classmethod def from_dict(cls, dikt) -> 'InlineResponse2001': @@ -92,9 +97,6 @@ class InlineResponse2001(Model): :param edge_discovery_service_end_point: The edge_discovery_service_end_point of this InlineResponse2001. :type edge_discovery_service_end_point: ServiceEndpoint """ - if edge_discovery_service_end_point is None: - raise ValueError("Invalid value for `edge_discovery_service_end_point`, must not be `None`") # noqa: E501 - self._edge_discovery_service_end_point = edge_discovery_service_end_point @property @@ -115,9 +117,6 @@ class InlineResponse2001(Model): :param lcm_service_end_point: The lcm_service_end_point of this InlineResponse2001. :type lcm_service_end_point: ServiceEndpoint """ - if lcm_service_end_point is None: - raise ValueError("Invalid value for `lcm_service_end_point`, must not be `None`") # noqa: E501 - self._lcm_service_end_point = lcm_service_end_point @property @@ -182,3 +181,24 @@ class InlineResponse2001(Model): """ self._offered_availability_zones = offered_availability_zones + + @property + def platform_caps(self) -> List[str]: + """Gets the platform_caps of this InlineResponse2001. + + + :return: The platform_caps of this InlineResponse2001. + :rtype: List[str] + """ + return self._platform_caps + + @platform_caps.setter + def platform_caps(self, platform_caps: List[str]): + """Sets the platform_caps of this InlineResponse2001. + + + :param platform_caps: The platform_caps of this InlineResponse2001. + :type platform_caps: List[str] + """ + + self._platform_caps = platform_caps diff --git a/src/models/inline_response2002.py b/src/models/inline_response2002.py index bb97d53bfb3e2a256191822e3fdfb2f5a3fd4a31..963e1922dd8951dba8c794bc5cbe088ef6f6f4a1 100644 --- a/src/models/inline_response2002.py +++ b/src/models/inline_response2002.py @@ -37,7 +37,7 @@ class InlineResponse2002(Model): } self.attribute_map = { - 'federation_context_id': 'FederationContextId' + 'federation_context_id': 'federationContextId' } self._federation_context_id = federation_context_id diff --git a/src/static/openapi.yaml b/src/static/openapi.yaml index 8fb301ca3ad4aa2225ce7d2654160aa8ff41ebd8..7f697e4d5c2aa8b96a7c723065339e46b7b256cd 100644 --- a/src/static/openapi.yaml +++ b/src/static/openapi.yaml @@ -526,7 +526,6 @@ components: $ref: '#/components/schemas/Uri' required: - initialDate - - origOPFederationId - partnerStatusLink type: object FederationResponseData: @@ -606,7 +605,6 @@ components: type: array required: - federationContextId - - partnerOPFederationId - platformCaps type: object FederationSupportedAPIs: @@ -2075,18 +2073,19 @@ components: $ref: '#/components/schemas/ZoneDetails' minItems: 1 type: array - required: - - edgeDiscoveryServiceEndPoint - - lcmServiceEndPoint + platformCaps: + items: + type: string + type: array type: object inline_response_200_2: example: - FederationContextId: FederationContextId + federationContextId: federationContextId properties: - FederationContextId: + federationContextId: $ref: '#/components/schemas/FederationContextId' required: - - FederationContextId + - federationContextId type: object inline_response_200_3: example: diff --git a/src/swagger/swagger.yaml b/src/swagger/swagger.yaml index 9991d08d8034132b0cd2e34000824e5028c07a0b..1598c2b88251357ebdeb96f6458b79cf9c06cbc8 100644 --- a/src/swagger/swagger.yaml +++ b/src/swagger/swagger.yaml @@ -594,8 +594,8 @@ paths: type: string content: application/json: - schema: - $ref: '#/components/schemas/inline_response_200_2' + schema: + $ref: '#/components/schemas/inline_response_200_2' "400": description: Bad request content: @@ -2729,7 +2729,6 @@ components: FederationRequestData: required: - initialDate - - origOPFederationId - partnerStatusLink type: object properties: @@ -2753,7 +2752,6 @@ components: FederationResponseData: required: - federationContextId - - partnerOPFederationId - platformCaps type: object properties: @@ -3624,9 +3622,6 @@ components: eventManagementAPI: null edgeApplicationAPI: null inline_response_200_1: - required: - - edgeDiscoveryServiceEndPoint - - lcmServiceEndPoint type: object properties: edgeDiscoveryServiceEndPoint: @@ -3642,6 +3637,10 @@ components: type: array items: $ref: '#/components/schemas/ZoneDetails' + platformCaps: + type: array + items: + type: string example: allowedFixedNetworkIds: - allowedFixedNetworkIds @@ -3701,13 +3700,13 @@ components: format: date-time inline_response_200_2: required: - - FederationContextId + - federationContextId type: object properties: - FederationContextId: + federationContextId: $ref: '#/components/schemas/FederationContextId' example: - FederationContextId: FederationContextId + federationContextId: federationContextId inline_response_200_3: required: - federationHealthStatus diff --git a/src/test/__init__.py b/src/test/__init__.py index 4363a5b3c1f5e5ac51b5e48a1a513c65e3e2887e..48d608d3ae8a09eaea547df6037cd23340b57244 100644 --- a/src/test/__init__.py +++ b/src/test/__init__.py @@ -1,13 +1,15 @@ import logging +import uuid import connexion +import pymongo import requests from flask_testing import TestCase from encoder import JSONEncoder from configparser import ConfigParser -from clients import tf_sdk +from clients import srm import util CONFIG = ConfigParser() @@ -21,7 +23,7 @@ HOST_KEYCLOAK = CONFIG.get("keycloak", "host") PORT_KEYCLOAK = int(CONFIG.get("keycloak", "port")) # Dual role testing configuration -PARTNER_API_ROOT = "http://127.0.0.1:8990" +PARTNER_API_ROOT = "http://federation-manager-remote:8989" class BaseTestCase(TestCase): @@ -40,17 +42,28 @@ class BaseTestCase(TestCase): # Base url application server self.base_url = f"{HOST}:{PORT}{SERVER}" + if not hasattr(BaseTestCase, "run_suffix"): + BaseTestCase.run_suffix = uuid.uuid4().hex[:8] + BaseTestCase.artefact_id_pop_run = str(uuid.uuid5(uuid.NAMESPACE_DNS, + f"fm-test-pop-{BaseTestCase.run_suffix}")) + BaseTestCase.artefact_id_oop_run = str(uuid.uuid5(uuid.NAMESPACE_DNS, + f"fm-test-oop-{BaseTestCase.run_suffix}")) + BaseTestCase.app_id_pop_run = f"test_app_pop_{BaseTestCase.run_suffix}" + BaseTestCase.app_id_oop_run = f"test_app_oop_{BaseTestCase.run_suffix}" + # Partner OP (POP) configuration - self.artefact_id_pop = "3fa85f64-5717-4562-b3fc-222222222222" - self.app_id_pop = "test_app_pop" + self.artefact_id_pop = BaseTestCase.artefact_id_pop_run + self.app_id_pop = BaseTestCase.app_id_pop_run self.artefact_name_pop = "ollama" self.artefact_repo_pop = "https://otwld.github.io/ollama-helm/" # Originating OP (OOP) configuration - self.artefact_id_oop = "3fa85f64-5717-4562-b3fc-222222222223" - self.app_id_oop = "test_app_oop" + self.artefact_id_oop = BaseTestCase.artefact_id_oop_run + self.app_id_oop = BaseTestCase.app_id_oop_run self.artefact_name_oop = "kubernetes-dashboard" self.artefact_repo_oop = "https://kubernetes.github.io/dashboard/" + self.deployable_artefact_name = self.artefact_name_pop + self.deployable_artefact_repo = self.artefact_repo_pop # Common artifact configuration self.app_provider = "test_app_provider" @@ -59,6 +72,144 @@ class BaseTestCase(TestCase): return app.app + def cleanup_fm_test_state(self): + """Remove leftover FM documents for the fixed integration test IDs.""" + app_ids = [self.app_id_pop, self.app_id_oop] + artefact_ids = [self.artefact_id_pop, self.artefact_id_oop] + + client = pymongo.MongoClient(f"mongodb://{CONFIG.get('mongodb', 'host')}:{CONFIG.get('mongodb', 'port')}") + database = client["federation-manager"] + + database["originating_application_deployment_management"].delete_many({"orig_ad_app_id": {"$in": app_ids}}) + database["originating_application_deployment_management_originating_o_p"].delete_many( + {"orig_ad_app_id": {"$in": app_ids}} + ) + database["originating_application_onboarding_management"].delete_many({"orig_ao_app_id": {"$in": app_ids}}) + database["originating_application_onboarding_management_originating_o_p"].delete_many( + {"orig_ao_app_id": {"$in": app_ids}} + ) + database["originating_application_onboarding_management_update"].delete_many({}) + database["originating_application_onboarding_management_update_originating_o_p"].delete_many({}) + database["originating_artefact_management"].delete_many({"orig_am_artefact_id": {"$in": artefact_ids}}) + database["originating_artefact_management_originating_o_p"].delete_many( + {"orig_am_artefact_id": {"$in": artefact_ids}} + ) + database["originating_operator_platform"].delete_many({}) + database["originating_operator_platform_originating_o_p"].delete_many({}) + database["originating_operator_platform_update"].delete_many({}) + database["originating_operator_platform_update_originating_o_p"].delete_many({}) + client.close() + + BaseTestCase.federation_context_id_partner = "" + BaseTestCase.federation_context_id_originating = "" + BaseTestCase.zone_partner = "" + BaseTestCase.zone_originating = "" + BaseTestCase.instances_partner = [] + BaseTestCase.instances_originating = [] + + def cleanup_ecp_test_state(self): + """Remove leftover SRM/ECP artefacts and onboardings for test fixtures.""" + for app_id in [self.app_id_pop, self.app_id_oop]: + try: + srm.delete_onboarding(app_id) + except Exception: + pass + + def build_api_url(self, api_root, path): + return f"{api_root}{SERVER}/{path}" + + def get_federation_context_at_api_root(self, token, api_root): + """Get federation context directly from a specific FM API root.""" + url = self.build_api_url(api_root, "fed-context-id") + return self.make_request_partner_op("GET", url, token=token) + + def get_federation_at_api_root(self, federation_id, token, api_root): + """Get federation details directly from a specific FM API root.""" + url = self.build_api_url(api_root, f"{federation_id}/partner") + return self.make_request_partner_op("GET", url, token=token) + + def get_app_instances_at_api_root(self, federation_id, app_id, app_provider_id, token, api_root): + """Get application instances directly from a specific FM API root.""" + url = self.build_api_url(api_root, f"{federation_id}/application/lcm/app/{app_id}/appProvider/{app_provider_id}") + return self.make_request_partner_op("GET", url, token=token) + + def cleanup_remote_partner_test_state(self, token, api_root=PARTNER_API_ROOT): + """Remove leftover partner-side FM resources created by originating-mode integration tests.""" + response = self.get_federation_context_at_api_root(token, api_root) + if response.status_code != 200: + return + + federation_context_id = response.json().get("federationContextId") + if not federation_context_id: + return + + instances_response = self.get_app_instances_at_api_root( + federation_context_id, + self.app_id_oop, + self.app_provider, + token, + api_root, + ) + if instances_response.status_code == 200: + for zone_info in instances_response.json(): + zone_id = zone_info.get("zoneId") + if not zone_id: + continue + for instance in zone_info.get("appInstanceInfo", []): + instance_id = instance.get("appInstIdentifier") + if not instance_id: + continue + try: + url = self.build_api_url( + api_root, + f"{federation_context_id}/application/lcm/app/{self.app_id_oop}/instance/{instance_id}/zone/{zone_id}", + ) + self.make_request_partner_op("DELETE", url, token=token) + except Exception: + pass + + for app_id in [self.app_id_oop]: + try: + url = self.build_api_url(api_root, f"{federation_context_id}/application/onboarding/app/{app_id}") + self.make_request_partner_op("DELETE", url, token=token) + except Exception: + pass + + for artefact_id in [self.artefact_id_oop]: + try: + url = self.build_api_url(api_root, f"{federation_context_id}/artefact/{artefact_id}") + self.make_request_partner_op("DELETE", url, token=token) + except Exception: + pass + + try: + federation_response = self.get_federation_at_api_root(federation_context_id, token, api_root) + if federation_response.status_code == 200: + federation_data = federation_response.json() + for zone in federation_data.get("offeredAvailabilityZones", []): + zone_id = zone.get("zoneId") if isinstance(zone, dict) else None + if not zone_id: + continue + try: + url = self.build_api_url(api_root, f"{federation_context_id}/zones/{zone_id}") + self.make_request_partner_op("DELETE", url, token=token) + except Exception: + pass + except Exception: + pass + + try: + url = self.build_api_url(api_root, f"{federation_context_id}/partner") + self.make_request_partner_op("DELETE", url, token=token) + except Exception: + pass + + for artefact_id in [self.artefact_id_pop, self.artefact_id_oop]: + try: + srm.delete_artefact(artefact_id) + except Exception: + pass + def get_access_token(self): # URL to obtain access token from Keycloak token_url = f"http://{HOST_KEYCLOAK}:{PORT_KEYCLOAK}/realms/federation/protocol/openid-connect/token" diff --git a/src/test/local-deployment/docker-compose.yml b/src/test/local-deployment/docker-compose.yml index 1c9f9dec039ff24e5b63e64f266b4a7aa208f2a4..99f2ef09a9fce9dbf8415f3dbb51fd2396c34dbb 100644 --- a/src/test/local-deployment/docker-compose.yml +++ b/src/test/local-deployment/docker-compose.yml @@ -1,4 +1,33 @@ services: + k3s: + image: rancher/k3s:v1.28.2-k3s1 + container_name: k3s + privileged: true + command: server --disable=traefik --tls-san k3s + environment: + K3S_TOKEN: secret-token + K3S_KUBECONFIG_MODE: "644" + tmpfs: + - /run + - /var/run + volumes: + - k3s-data:/var/lib/rancher/k3s + - k3s-config:/etc/rancher/k3s + networks: + - local-net + - remote-net + + k3s-kubeconfig: + image: python:3.11-alpine + container_name: k3s-kubeconfig + depends_on: + - k3s + volumes: + - k3s-config:/input:ro + - k3s-kubeconfig:/output + - ../../../../../OP_Automation/docker-local-deployment/prepare_kubeconfig.py:/prepare_kubeconfig.py:ro + command: ["python", "/prepare_kubeconfig.py"] + mongodb-local: image: mongo container_name: mongodb-local @@ -69,13 +98,13 @@ services: - "30990:8989" volumes: - ../../conf/config-fm-remote.cfg:/usr/app/src/conf/config.cfg - - ../../clients/tf_sdk.py:/usr/app/src/clients/tf_sdk.py - - /home/sergio/i2cat/OperatorPlatform/OP_Automation/automation/op1/op1-kubeconfig.yaml:/root/.kube/config depends_on: - mongodb-remote - keycloak-remote + - srm-remote networks: + - federation-net - remote-net federation-manager-local: @@ -87,56 +116,119 @@ services: ports: - "8989:8989" environment: - - KUBECONFIG=/root/.kube/config + - KUBECONFIG=/kubeconfig/kubeconfig.yaml volumes: - ../../conf/config-fm-local.cfg:/usr/app/src/conf/config.cfg - - ../../clients/tf_sdk.py:/usr/app/src/clients/tf_sdk.py - - /home/sergio/i2cat/OperatorPlatform/OP_Automation/automation/op1/op1-kubeconfig.yaml:/root/.kube/config + - k3s-kubeconfig:/kubeconfig:ro depends_on: - mongodb-local - keycloak-local + - srm-local networks: + - federation-net - local-net + srm-local: + build: + context: ../../../../../service-resource-manager/service-resource-manager-implementation/service-resource-manager-implementation + dockerfile: Dockerfile + container_name: srm-local + restart: unless-stopped + ports: + - "8082:8080" + environment: + EDGE_CLOUD_ADAPTER_NAME: lite2edge + ADAPTER_BASE_URL: http://lite2edge-local:8080 + PLATFORM_PROVIDER: lite2edge + ARTIFACT_MANAGER_ADDRESS: http://artefact-manager:8000 + KUBECONFIG: /kubeconfig/kubeconfig.yaml + PYTHONPATH: /workspace/tf-sdk/src:/workspace/lite2edge + volumes: + - ../../../../../tf-sdk/src:/workspace/tf-sdk/src + - ../../../../../lite2edge:/workspace/lite2edge + - k3s-kubeconfig:/kubeconfig:ro + depends_on: + - lite2edge-local + - k3s-kubeconfig + networks: + - local-net + + srm-remote: + build: + context: ../../../../../service-resource-manager/service-resource-manager-implementation/service-resource-manager-implementation + dockerfile: Dockerfile + container_name: srm-remote + restart: unless-stopped + ports: + - "8083:8080" + environment: + EDGE_CLOUD_ADAPTER_NAME: lite2edge + ADAPTER_BASE_URL: http://lite2edge-remote:8080 + PLATFORM_PROVIDER: lite2edge + ARTIFACT_MANAGER_ADDRESS: http://artefact-manager:8000 + KUBECONFIG: /kubeconfig/kubeconfig.yaml + PYTHONPATH: /workspace/tf-sdk/src:/workspace/lite2edge + volumes: + - ../../../../../tf-sdk/src:/workspace/tf-sdk/src + - ../../../../../lite2edge:/workspace/lite2edge + - k3s-kubeconfig:/kubeconfig:ro + depends_on: + - lite2edge-remote + - k3s-kubeconfig + networks: + - remote-net + lite2edge-local: build: - context: ../../../../lite2edge + context: ../../../../../lite2edge dockerfile: Dockerfile container_name: lite2edge-local restart: unless-stopped ports: - "8752:8080" environment: - - KUBECONFIG=/root/.kube/config + - KUBECONFIG=/kubeconfig/kubeconfig.yaml - LOG_LEVEL=INFO volumes: - - /home/sergio/i2cat/OperatorPlatform/OP_Automation/automation/op1/op1-kubeconfig.yaml:/root/.kube/config + - k3s-kubeconfig:/kubeconfig:ro + depends_on: + - k3s-kubeconfig networks: - local-net lite2edge-remote: build: - context: ../../../../lite2edge + context: ../../../../../lite2edge dockerfile: Dockerfile container_name: lite2edge-remote restart: unless-stopped ports: - "8751:8080" environment: - - KUBECONFIG=/root/.kube/config + - KUBECONFIG=/kubeconfig/kubeconfig.yaml - LOG_LEVEL=INFO volumes: - - /home/sergio/i2cat/OperatorPlatform/OP_Automation/automation/op1/op1-kubeconfig.yaml:/root/.kube/config + - k3s-kubeconfig:/kubeconfig:ro + depends_on: + - k3s-kubeconfig networks: - remote-net volumes: + k3s-data: + driver: local + k3s-config: + driver: local + k3s-kubeconfig: + driver: local smdbdata-local: driver: local smdbdata-remote: driver: local networks: + federation-net: + driver: bridge local-net: driver: bridge remote-net: diff --git a/src/test/test_application_deployment_management.py b/src/test/test_application_deployment_management.py index 8a231226812c4bff188563ae7e904f014ea3581a..46ec42b703762dc40bffae1ec9072aaf33dc9cbe 100644 --- a/src/test/test_application_deployment_management.py +++ b/src/test/test_application_deployment_management.py @@ -17,7 +17,7 @@ from __future__ import absolute_import import time from test import BaseTestCase -from clients import tf_sdk +from clients import srm class TestApplicationDeploymentManagementController(BaseTestCase): @@ -38,7 +38,7 @@ class TestApplicationDeploymentManagementController(BaseTestCase): try: # Check if there is connection with Edge Cloud Platform. Otherwise stop the test try: - tf_sdk.get_zones() + srm.get_zones() except Exception: self.skipTest("Edge Cloud Platform connection not available") @@ -66,27 +66,9 @@ class TestApplicationDeploymentManagementController(BaseTestCase): def test_00_setup(self): """Setup federation contexts for both Partner OP and Originating OP roles""" - try: - # Check if there are artefact and profile created at Edge Cloud Platform and must be deleted because remains there due - # to a possible issue during the test - clean up both app configurations - tf_sdk.delete_onboarding(self.app_id_pop) - except Exception: - pass - - try: - tf_sdk.delete_onboarding(self.app_id_oop) - except Exception: - pass - - try: - tf_sdk.delete_artefact(self.artefact_id_pop) - except Exception: - pass - - try: - tf_sdk.delete_artefact(self.artefact_id_oop) - except Exception: - pass + self.cleanup_fm_test_state() + self.cleanup_ecp_test_state() + self.cleanup_remote_partner_test_state(BaseTestCase.token) try: # Create federation context for Partner OP role @@ -131,8 +113,10 @@ class TestApplicationDeploymentManagementController(BaseTestCase): # Create artefacts for both roles with different app providers self.post_artefact(BaseTestCase.federation_context_id_partner, self.artefact_id_pop, self.artefact_name_pop, self.artefact_repo_pop, BaseTestCase.token, 'partner_op') + # Reuse a known-good public Helm chart for originating deployment validation. self.post_artefact(BaseTestCase.federation_context_id_originating, self.artefact_id_oop, - self.artefact_name_oop, self.artefact_repo_oop, BaseTestCase.token, 'originating_op') + self.deployable_artefact_name, self.deployable_artefact_repo, BaseTestCase.token, + 'originating_op') # Create application onboarding for both roles with different app configurations self.post_onboarding(BaseTestCase.federation_context_id_partner, self.app_id_pop, BaseTestCase.zone_partner, @@ -205,6 +189,7 @@ class TestApplicationDeploymentManagementController(BaseTestCase): else: federation_id = BaseTestCase.federation_context_id_originating app_id = self.app_id_oop + zone = BaseTestCase.zone_originating flavour = BaseTestCase.flavour_originating body = { diff --git a/src/test/test_application_onboarding_management.py b/src/test/test_application_onboarding_management.py index 728637f9399da7e44b10761c09bcf67ff6db1c08..8e4f54b14fa37bea5d3fabee373f61501d9649df 100644 --- a/src/test/test_application_onboarding_management.py +++ b/src/test/test_application_onboarding_management.py @@ -17,7 +17,7 @@ from __future__ import absolute_import from test import BaseTestCase -from clients import tf_sdk +from clients import srm import unittest @@ -35,7 +35,7 @@ class TestApplicationOnboardingManagementController(BaseTestCase): try: # Check if there is connection with Edge Cloud Platform. Otherwise stop the test try: - tf_sdk.get_zones() + srm.get_zones() except Exception: self.skipTest("Edge Cloud Platform connection not available") @@ -63,27 +63,9 @@ class TestApplicationOnboardingManagementController(BaseTestCase): def test_00_setup(self): """Setup federation contexts for both Partner OP and Originating OP roles""" - try: - # Check if there are artefact and profile created at Edge Cloud Platform and must be deleted because remains there due - # to a possible issue during the test - tf_sdk.delete_onboarding(self.app_id_pop) - except Exception: - pass - - try: - tf_sdk.delete_onboarding(self.app_id_oop) - except Exception: - pass - - try: - tf_sdk.delete_artefact(self.artefact_id_pop) - except Exception: - pass - - try: - tf_sdk.delete_artefact(self.artefact_id_oop) - except Exception: - pass + self.cleanup_fm_test_state() + self.cleanup_ecp_test_state() + self.cleanup_remote_partner_test_state(BaseTestCase.token) try: # Create federation context for Partner OP role diff --git a/src/test/test_artefact_management.py b/src/test/test_artefact_management.py index 4abb8caa0dc7ebe429907af52db29024195ddfce..dfcfdfb9efa04e6fe1c29322bf40090443c35212 100644 --- a/src/test/test_artefact_management.py +++ b/src/test/test_artefact_management.py @@ -17,7 +17,7 @@ from __future__ import absolute_import from test import BaseTestCase -from clients import tf_sdk +from clients import srm import util @@ -38,7 +38,7 @@ class TestArtefactManagementController(BaseTestCase): try: # Check if there is connection with Edge Cloud Platform. Otherwise stop the test try: - tf_sdk.get_zones() + srm.get_zones() except Exception: self.skipTest("Edge Cloud Platform connection not available") @@ -70,12 +70,12 @@ class TestArtefactManagementController(BaseTestCase): # Check if the artefacts id of this test have been created previously at Edge Cloud Platform and remains there due # to a possible issue of the same test try: - tf_sdk.delete_artefact(BaseTestCase.artefact_public) + srm.delete_artefact(BaseTestCase.artefact_public) except Exception: pass try: - tf_sdk.delete_artefact(BaseTestCase.artefact_private) + srm.delete_artefact(BaseTestCase.artefact_private) except Exception: pass diff --git a/src/test/test_availability_zone_info_synchronization.py b/src/test/test_availability_zone_info_synchronization.py index d2351b3759eb2a3ebc1ab78bce29d6b09cfa89e6..ec239d6ed2768a633258f17c78bd84f4c5382592 100644 --- a/src/test/test_availability_zone_info_synchronization.py +++ b/src/test/test_availability_zone_info_synchronization.py @@ -17,7 +17,7 @@ from __future__ import absolute_import from test import BaseTestCase -from clients import tf_sdk +from clients import srm class TestAvailabilityZoneInfoSynchronizationController(BaseTestCase): @@ -34,7 +34,7 @@ class TestAvailabilityZoneInfoSynchronizationController(BaseTestCase): try: # Check if there is connection with Edge Cloud Platform. Otherwise stop the test try: - tf_sdk.get_zones() + srm.get_zones() except Exception: self.skipTest("Edge Cloud Platform connection not available") @@ -415,7 +415,7 @@ class TestAvailabilityZoneInfoSynchronizationController(BaseTestCase): # Delete zone subscriptions for both roles self.delete_zone(BaseTestCase.federation_context_id_partner, BaseTestCase.zone_partner, BaseTestCase.token, 'partner_op') - self.delete_zone(BaseTestCase.federation_context_id_originating, BaseTestCase.zone_orginating, + self.delete_zone(BaseTestCase.federation_context_id_originating, BaseTestCase.zone_originating, BaseTestCase.token, 'originating_op') # Delete federations for both roles diff --git a/src/test/test_federation_management.py b/src/test/test_federation_management.py index 6fe1f210ea0c99898bd0c99bddc44d8608632e5f..f9b02cd9ae91d5e8c72b9d8cc7291a3c55e8a84e 100644 --- a/src/test/test_federation_management.py +++ b/src/test/test_federation_management.py @@ -18,7 +18,7 @@ from __future__ import absolute_import import time from test import BaseTestCase -from clients import tf_sdk +from clients import srm class TestFederationManagementController(BaseTestCase): @@ -35,7 +35,7 @@ class TestFederationManagementController(BaseTestCase): # Check if there is connection with Edge Cloud Platform. Otherwise stop the test try: - tf_sdk.get_zones() + srm.get_zones() except Exception: self.skipTest("Edge Cloud Platform connection not available")