Commit c14054a1 authored by Adriana Fernández-Fernández's avatar Adriana Fernández-Fernández
Browse files

Adapts zone info synchronization API

parent e35a2f10
Loading
Loading
Loading
Loading
+221 −0
Original line number Original line Diff line number Diff line
# -------------------------------------------------------------------------- #
# 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.                                             #
# -------------------------------------------------------------------------- #
import json
from mongoengine.errors import ValidationError

from adapters.error import APIError
from clients import fed_manager as fm_client
from models.zone_registered_data import ZoneRegisteredData  # noqa: E501
from models.zone_registration_response_data import ZoneRegistrationResponseData  # noqa: E501
from models.mongo_document import OriginatingOperatorPlatformOriginatingOP
from models.mongo_document import OriginatingZoneInfoOriginatingOP


def verify_required_header(partner_api_root):
    if not partner_api_root:
        raise APIError(400, "X-Partner-API-Root header is missing")


def get_zone_data(federation_context_id, zone_id, bearer_token, partner_api_root):  # noqa: E501
    """Retrieves details about the computation and network resources that partner OP has reserved for this zone.

     # noqa: E501

    :param federation_context_id:
    :type federation_context_id: dict | bytes
    :param zone_id:
    :type zone_id: dict | bytes

    :rtype: ZoneRegisteredData
    """

    # Verify partner API root is provided
    verify_required_header(partner_api_root)

    try:
        # Find federation at originating OP
        originating_op_objects = OriginatingOperatorPlatformOriginatingOP.objects(id=federation_context_id)
        if not originating_op_objects:
            raise APIError(404, "Federation not found")
        originating_op_instance = originating_op_objects[0]

        # Forward request to partner OP
        zone_response_data = fm_client.get_availability_zones(originating_op_instance.partner_federation_id, zone_id,
                                                              bearer_token, partner_api_root)

        # Check if the response contains an error from the partner API
        if "error" in zone_response_data and "status_code" in zone_response_data:
            status_code = zone_response_data["status_code"]
            error_message = zone_response_data["error"]
            raise APIError(status_code, f"Partner API error: {error_message}")
        elif "zoneId" in zone_response_data:
            # Find zone in originating OP
            if find_zone_at_originating_op(federation_context_id, zone_id):
                zone_response_data = ZoneRegisteredData.from_dict(zone_response_data)
                return zone_response_data, 200
            else:
                raise APIError(409, "Zone exists at partner operator but not in originating operator")
        else:
            if find_zone_at_originating_op(federation_context_id, zone_id):
                raise APIError(409, "Zone exists in originating operator but not at partner operator")
            else:
                raise APIError(404, "Zone not found")
    except ValidationError:
        raise APIError(400, f"Invalid federation context ID or zone ID format: {federation_context_id}, {zone_id}")
    except APIError:
        raise  # Re-raise APIError as-is
    except Exception as error:
        raise APIError(500, f"Error while getting zone data. Reason: {error}")


def zone_subscribe(federation_context_id, body, bearer_token, partner_api_root):  # noqa: E501
    """Originating OP informs partner OP that it is willing to access the specified zones and partner OP shall reserve compute and network resources for these zones.

     # noqa: E501

    :param body:
    :type body: dict | bytes
    :param federation_context_id:
    :type federation_context_id: dict | bytes

    :rtype: ZoneRegistrationResponseData
    """

    # Verify partner API root is provided
    verify_required_header(partner_api_root)

    try:
        # Find federation at originating OP
        originating_op_objects = OriginatingOperatorPlatformOriginatingOP.objects(id=federation_context_id)
        if not originating_op_objects:
            raise APIError(404, "Federation not found")
        originating_op_instance = originating_op_objects[0]

        # Forward request to partner OP
        zone_response_data = fm_client.create_availability_zones(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 zone_response_data and "status_code" in zone_response_data:
            status_code = zone_response_data["status_code"]
            error_message = zone_response_data["error"]
            raise APIError(status_code, f"Partner API error: {error_message}")
        elif "acceptedZoneResourceInfo" in zone_response_data:
            # Create zone subscription in originating OP
            create_zone_subscription_originating_op(federation_context_id,
                                                    originating_op_instance.partner_federation_id, body)
            zone_response_data = ZoneRegistrationResponseData.from_dict(zone_response_data)
            return zone_response_data, 200
        else:
            raise APIError(422, f"Unexpected response from partner API: {zone_response_data}")
    except ValidationError:
        raise APIError(400, f"Invalid federation context ID format: {federation_context_id}")
    except APIError:
        raise  # Re-raise APIError as-is
    except Exception as error:
        raise APIError(500, f"Error while creating zone subscription. Reason: {error}")


def zone_unsubscribe(federation_context_id, zone_id, bearer_token, partner_api_root):  # noqa: E501
    """Assert usage of a partner OP zone. Originating OP informs partner OP that it will no longer access the specified zone.

     # noqa: E501

    :param federation_context_id:
    :type federation_context_id: dict | bytes
    :param zone_id:
    :type zone_id: dict | bytes

    :rtype: None
    """

    # Verify partner API root is provided
    verify_required_header(partner_api_root)

    try:
        # Find federation at originating OP
        originating_op_objects = OriginatingOperatorPlatformOriginatingOP.objects(id=federation_context_id)
        if not originating_op_objects:
            raise APIError(404, "Federation not found")
        originating_op_instance = originating_op_objects[0]

        # Find zone subscription at partner
        zone_response_data = fm_client.get_availability_zones(originating_op_instance.partner_federation_id, zone_id,
                                                              bearer_token, partner_api_root)

        # Check if the response contains an error from the partner API
        if "error" in zone_response_data and "status_code" in zone_response_data:
            status_code = zone_response_data["status_code"]
            error_message = zone_response_data["error"]
            raise APIError(status_code, f"Partner API error: {error_message}")
        elif "zoneId" in zone_response_data:
            originating_zi_objects = find_zone_at_originating_op(federation_context_id, zone_id)
            if originating_zi_objects:
                # Delete zone subscription from partner
                response_data = fm_client.delete_availability_zones(originating_op_instance.partner_federation_id,
                                                                    zone_id, 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 "deregistered" in response_data:
                    # If zone subscription has been removed, remove zone subscription from originating
                    delete_zone_subscription_originating_op(originating_zi_objects)
                    return response_data, 200
                else:
                    raise APIError(422, f"Unexpected response from partner API: {response_data}")
            else:
                raise APIError(409, "Zone subscription exists at partner operator but not in originating operator")
        else:
            if find_zone_at_originating_op(federation_context_id, zone_id):
                raise APIError(409, "Zone subscription exists in originating operator but not at partner operator")
            else:
                raise APIError(404, "Zone subscription not found")
    except ValidationError:
        raise APIError(400, f"Invalid federation context ID or zone ID format: {federation_context_id}, {zone_id}")
    except APIError:
        raise  # Re-raise APIError as-is
    except Exception as error:
        raise APIError(500, f"Error while deleting zone subscription. Reason: {error}")


def create_zone_subscription_originating_op(federation_id, partner_federation_id, body):
    zone_data = {
        "orig_zi_federation_context_id": federation_id,
        "orig_zi_acceptedAvailabilityZones": body.accepted_availability_zones,
        "partner_federation_id": partner_federation_id
    }

    # Create a new MongoEngine document and save it to MongoDB
    new_zone_subscription = OriginatingZoneInfoOriginatingOP(**zone_data)
    new_zone_subscription.save()


def delete_zone_subscription_originating_op(originating_zi_objects):
    # Delete zone subscription
    originating_zi_objects.delete()


def find_zone_at_originating_op(federation_id, zone_id):
    originating_zi_objects = OriginatingZoneInfoOriginatingOP.objects(
        orig_zi_federation_context_id=federation_id)

    for zone_info in originating_zi_objects:
        if zone_id in zone_info.orig_zi_acceptedAvailabilityZones:
            return zone_info
    return None
+277 −0
Original line number Original line Diff line number Diff line
# -------------------------------------------------------------------------- #
# 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.                                             #
# -------------------------------------------------------------------------- #
import json
from mongoengine.errors import ValidationError

from models.zone_registered_data import ZoneRegisteredData  # noqa: E501
from models.zone_registration_response_data import ZoneRegistrationResponseData  # noqa: E501
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 open_sdk


def get_zone_data(federation_context_id, zone_id, bearer_token=None, partner_api_root=None):  # noqa: E501
    """Retrieves details about the computation and network resources that partner OP has reserved for this zone.

     # noqa: E501

    :param federation_context_id:
    :type federation_context_id: dict | bytes
    :param zone_id:
    :type zone_id: dict | bytes

    :rtype: ZoneRegisteredData
    """

    # Check if exist Federation Context Id in Operator Platform
    try:
        originating_op_objects = OriginatingOperatorPlatform.objects(id=federation_context_id)
        if not originating_op_objects:
            raise APIError(404, "Federation not found at Operator Platform")
    except ValidationError:
        raise APIError(400, f"Invalid federation context ID format: {federation_context_id}")
    except APIError:
        raise  # Re-raise APIError as-is
    except Exception as error:
        raise APIError(500, f"Error while retrieving federation. Reason: {error}")

    # Check if Federation Context Id exists in Availability Zones
    originating_zi_objects = OriginatingZoneInfo.objects(orig_zi_federation_context_id=federation_context_id)
    if not originating_zi_objects:
        raise APIError(404, "Availability Zones not found for Federation Context")

    # Check if exist Zone at Edge Cloud Platform
    try:
        response_data = open_sdk.get_zone_by_zone_id(zone_id)
        resource = response_data.get("computeResourceQuotaLimits")
        for d in resource:
            huge = d.get("hugepages")
            for h in huge:
                page = h.get("pageSize")
                page = page.replace("Gi", "GB")
                page = page.replace("Mi", "MB")
                h["pageSize"] = page
            d["hugepages"] = huge
        response_data["computeResourceQuotaLimits"] = resource
        pass
    except Exception as error:
        raise APIError(422, f"Unable to retrieve zone from Edge Cloud Platform. Error: {error}")
    if not response_data:
        raise APIError(404, "Zone id do not exist at Edge Cloud Platform")

    # check if Zone id is in the list of availability zones of the document
    zones = originating_zi_objects.get()
    if zone_id not in zones.orig_zi_acceptedAvailabilityZones:
        raise APIError(404, "Zone id not found for this Federation Context")

    try:
        zone_response_data = ZoneRegisteredData.from_dict(response_data)
    except Exception as error:
        raise APIError(422, f"Retrieving zone information with issues from Edge Cloud Platform. Error: {error}")

    return zone_response_data


def zone_subscribe(federation_context_id, body, bearer_token=None, partner_api_root=None):  # noqa: E501
    """Originating OP informs partner OP that it is willing to access the specified zones and partner OP shall reserve compute and network resources for these zones.

     # noqa: E501

    :param body:
    :type body: dict | bytes
    :param federation_context_id:
    :type federation_context_id: dict | bytes

    :rtype: ZoneRegistrationResponseData
    """
    # Check if exist Federation Context Id in Operator Platform
    try:
        originating_op_objects = OriginatingOperatorPlatform.objects(id=federation_context_id)
        if not originating_op_objects:
            raise APIError(404, "Federation not found at Operator Platform")
    except ValidationError:
        raise APIError(400, f"Invalid federation context ID format: {federation_context_id}")
    except APIError:
        raise  # Re-raise APIError as-is
    except Exception as error:
        raise APIError(500, f"Error while retrieving federation. Reason: {error}")

    # Check if exist availability zones in Edge Cloud Platform
    try:
        if not check_availability_zones(body.accepted_availability_zones):
            raise APIError(404, "Availability Zones do not exist at Edge Cloud Platform")
    except Exception as error:
        raise APIError(422, f"Unable to get zones list from Edge Cloud Platform. Error: {error}")

    # Verifies this is a new zone request
    all_originating_zones = OriginatingZoneInfo.objects()
    for originating_zi in all_originating_zones:
        if originating_zi.orig_zi_federation_context_id == federation_context_id:
            raise APIError(409, "Availability Zone already exists for this Federation Context")

    try:
        availability_zones = get_info_availability_zones_from_zones_edge_cloud_platform(body.accepted_availability_zones)
    except Exception as error:
        raise APIError(422, f"Unable to get zones from Edge Cloud Platform. Error: {error}")
    response_data = {
        "acceptedZoneResourceInfo": availability_zones
    }

    try:
        zone_response_data = ZoneRegistrationResponseData.from_dict(response_data)
    except Exception as error:
        raise APIError(422, f"Retrieving zone information with issues from Edge Cloud Platform. Error: {error}")

    # Convert the original model instance to the MongoEngine document
    zone_data = {
        "orig_zi_federation_context_id": federation_context_id,
        "orig_zi_acceptedAvailabilityZones": body.accepted_availability_zones,
        "orig_zi_availZoneNotifLink": body.avail_zone_notif_link
    }

    # Create a new MongoEngine document and save it to MongoDB
    new_zone = OriginatingZoneInfo(**zone_data)
    new_zone.save()

    return zone_response_data


def zone_unsubscribe(federation_context_id, zone_id, bearer_token=None, partner_api_root=None):  # noqa: E501
    """Assert usage of a partner OP zone. Originating OP informs partner OP that it will no longer access the specified zone.

     # noqa: E501

    :param federation_context_id:
    :type federation_context_id: dict | bytes
    :param zone_id:
    :type zone_id: dict | bytes

    :rtype: None
    """

    # Check if exist Federation Context Id in Operator Platform
    try:
        originating_op_objects = OriginatingOperatorPlatform.objects(id=federation_context_id)
        if not originating_op_objects:
            raise APIError(404, "Federation not found at Operator Platform")
    except ValidationError:
        raise APIError(400, f"Invalid federation context ID format: {federation_context_id}")
    except APIError:
        raise  # Re-raise APIError as-is
    except Exception as error:
        raise APIError(500, f"Error while retrieving federation. Reason: {error}")

    # Check if exist Federation Context Id in Availability Zones
    originating_zi_objects = OriginatingZoneInfo.objects(orig_zi_federation_context_id=federation_context_id)
    if not originating_zi_objects:
        raise APIError(404, "Federation Context not found at Availability Zones")

    # check if Zone id is in the list of availability zones of the document
    zones = originating_zi_objects.get()
    if zone_id not in zones.orig_zi_acceptedAvailabilityZones:
        raise APIError(404, "Zone id not found for this Federation Context")

    # Check if there are onboardings dependents of the zone
    if check_child_availability_zones(federation_context_id, zone_id):
        raise APIError(409, "Unable to remove Zone. There are onboardings dependent. Remove it and try again ")

    # Get the list of the zones assigned to the federation context
    availability_zones = zones.orig_zi_acceptedAvailabilityZones
    try:
        # To prevent if returns list as a Str
        availability_zones = json.loads(availability_zones)
    except Exception as error:
        print(f"Unable to parse zone list JSON. Error: {error}")

    # Remove from the list the zone id passed as a parameter
    availability_zones.remove(zone_id)

    # If the list of zones becomes empty, we delete the document from the collection
    if len(availability_zones) == 0:
        originating_zi_objects.delete()
    else:
        # The list is not empty and we update the document with the new list
        zones.orig_zi_acceptedAvailabilityZones = availability_zones
        zones.save()

    return 'Zone deregistered successfully', 200


def check_availability_zones(accepted_availability_zones):

    # Get the zones list from Edge Cloud Platform
    zones = open_sdk.get_list_zones()
    # Creates an array only with zone id from zones list
    zone_id_array = []
    # zones = zones.json()
    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):
            zone_id_array.append(zone.get("zoneId"))
    # remove duplicates
    set_zones = set(zone_id_array)
    zone_id_array = list(set_zones)

    # Compare matches between zones from Edge Cloud Platform and zones declared in the body
    common = set(accepted_availability_zones) & set(zone_id_array)

    # if the matches are equal to the number of zones declared in the body returns True
    if len(common) == len(accepted_availability_zones):
        return True
    else:
        return False


def get_info_availability_zones_from_zones_edge_cloud_platform(availability_zones):

    # Retrieve zones from Edge Cloud Platform
    info_zones_list = open_sdk.get_zones()
    zones_for_federation = []

    # Loop zones assigned to our federation
    for availability_zone in availability_zones:

        # If the availability zone matches with one of the zones of Edge Cloud Platform
        # includes this zone in a table to mount response data
        exit = False
        for zone in info_zones_list:
            # If the zone value is a dict, is a correct zone, else there is an issue and returns a str
            if isinstance(zone, dict):
                if availability_zone == zone.get("zoneId"):
                    zones_for_federation.append(zone)
                    break

    return zones_for_federation


def check_child_availability_zones(federation_context_id, zone_id):
    found = False

    originating_ao_objects = OriginatingApplicationOnboardingManagement.objects(
        orig_ao_federation_context_id=federation_context_id
    )
    if not originating_ao_objects:
        return False

    instances_onboarding = originating_ao_objects.filter()

    for elem in instances_onboarding:
        list_zones = elem.orig_ao_app_deployment_zones
        for l in list_zones:
            if l == zone_id:
                return True
+105 −0

File added.

Preview size limit exceeded, changes collapsed.

+0 −483

File deleted.

Preview size limit exceeded, changes collapsed.