diff --git a/.env.sample b/.env.sample deleted file mode 100644 index 77929b02131a0ce0bae73774eb47b29bb666e6e5..0000000000000000000000000000000000000000 --- a/.env.sample +++ /dev/null @@ -1,5 +0,0 @@ -MONGO_URI=mongodb://username:password@localhost:27017/sample_db -PI_EDGE_BASE_URL=http://example.com/api -PI_EDGE_USERNAME=username -PI_EDGE_PASSWORD=password -HTTP_PROXY=https://company.proxy:3128 diff --git a/Dockerfile b/Dockerfile index 6c452ebe7d232df804f73b3eb59ce62819d3ea93..1744f1b96c648dc29f2b08c468064710337581ce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ WORKDIR /usr/src/app COPY requirements.txt /usr/src/app/ -RUN pip install --no-cache-dir -r requirements.txt +RUN pip install --no-cache-dir --trusted-host pypi.org --trusted-host files.pythonhosted.org -r requirements.txt COPY . /usr/src/app @@ -13,4 +13,4 @@ EXPOSE 8080 ENTRYPOINT ["python"] -CMD ["-m", "edge_cloud_management_api"] \ No newline at end of file +CMD ["-m", "edge_cloud_management_api"] diff --git a/edge_cloud_management_api/__main__.py b/edge_cloud_management_api/__main__.py index 7f20b6f13b8be279747e68fe304a6c62c5e4d7b6..41bd405efecd8664a1afc1eb87301d22ae46304b 100644 --- a/edge_cloud_management_api/__main__.py +++ b/edge_cloud_management_api/__main__.py @@ -2,4 +2,4 @@ from edge_cloud_management_api.app import get_app_instance if __name__ == "__main__": app = get_app_instance() - app.run(host="127.0.0.1", port=8080) + app.run(host="0.0.0.0", port=8080) diff --git a/edge_cloud_management_api/app.py b/edge_cloud_management_api/app.py index a97d1dff6847629f582acb987c9b6103f0957483..7c0fa0813433ce7e334445abbd18e794711e0479 100644 --- a/edge_cloud_management_api/app.py +++ b/edge_cloud_management_api/app.py @@ -17,4 +17,4 @@ def get_app_instance() -> FlaskApp: if __name__ == "__main__": app = get_app_instance() - app.run(host="127.0.0.1", port=8080) + app.run(host="0.0.0.0", port=8080) diff --git a/edge_cloud_management_api/configs/env_config.py b/edge_cloud_management_api/configs/env_config.py index e8abd42edf872f88784e10bd3cbb2d4e3e87a0ce..1be9acadb197eab96e9f28088eef04700e9e4210 100644 --- a/edge_cloud_management_api/configs/env_config.py +++ b/edge_cloud_management_api/configs/env_config.py @@ -7,7 +7,7 @@ load_dotenv() class Configuration(BaseSettings): MONGO_URI: str = os.getenv("MONGO_URI") - PI_EDGE_BASE_URL: str = os.getenv("PI_EDGE_BASE_URL") + 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") HTTP_PROXY: str = os.getenv("HTTP_PROXY") diff --git a/edge_cloud_management_api/controllers/app_controllers.py b/edge_cloud_management_api/controllers/app_controllers.py index d8ba5dce73945a5d545c0f2a6a862f7271543df5..fa7a818d1087633f1855923a2da2f09474af8d12 100644 --- a/edge_cloud_management_api/controllers/app_controllers.py +++ b/edge_cloud_management_api/controllers/app_controllers.py @@ -1,10 +1,10 @@ import uuid -from flask import jsonify - +from flask import jsonify, request from pydantic import ValidationError from edge_cloud_management_api.managers.db_manager import MongoManager from edge_cloud_management_api.managers.log_manager import logger -from edge_cloud_management_api.models.application_models import AppManifest +from edge_cloud_management_api.models.application_models import AppManifest, AppZones, AppInstance +from edge_cloud_management_api.services.pi_edge_services import PiEdgeAPIClientFactory class NotFound404Exception(Exception): @@ -108,49 +108,111 @@ def delete_app(appId, x_correlator=None): # noqa: E501 ) -def create_app_instance(body, app_id, x_correlator=None): # noqa: E501 - """Instantiation of an Application +def create_app_instance(): + logger.info("Received request to create app instance") - Ask the Edge Cloud Platform to instantiate an application to one or several Edge Cloud Zones with an Application as an input and an Application Instance as the output. # noqa: E501 + try: + # Step 1: Get request body + body = request.get_json() + logger.debug(f"Request body: {body}") - :param body: Array of Edge Cloud Zone - :type body: list | bytes - :param app_id: A globally unique identifier associated with the application. Edge Cloud Provider generates this identifier when the application is submitted. - :type app_id: dict | bytes - :param x_correlator: Correlation id for the different services - :type x_correlator: str + # Step 2: Validate body format + app_id = body.get('appId') + app_zones = body.get('appZones') - :rtype: InlineResponse202 - """ - try: - return {}, 202 # application instantiation accepted - except Exception: - logger.exception("Error while creating app instance") - error = { - "status": 500, - "code": "INTERNAL", - "message": "Internal server error.", - } - return error, 500 + if not app_id or not app_zones: + return jsonify({"error": "Missing required fields: appId or appZones"}), 400 + + # Step 3: Connect to Mongo and check if app exists + with MongoManager() as mongo_manager: + app_data = mongo_manager.find_document("apps", {"_id": app_id}) + + if not app_data: + logger.warning(f"No application found with ID {app_id}") + return jsonify({"error": "App not found", "details": f"No application found with ID {app_id}"}), 404 + + logger.info(f"Application {app_id} found in database") + + # Step 4: Deploy app instance using Pi-Edge client + pi_edge_client_factory = PiEdgeAPIClientFactory() + pi_edge_client = pi_edge_client_factory.create_pi_edge_api_client() + + logger.info(f"Preparing to send deployment request to Pi-Edge for appId={app_id}") + + deployment_payload = [{ + "appId": app_id, + "appZones": app_zones + }] + + # 🖨️ Print everything before sending + print("\n=== Preparing Deployment Request ===") + print(f"Endpoint: {pi_edge_client.base_url}/deployedServiceFunction") + print(f"Headers: {pi_edge_client._get_headers()}") + print(f"Payload: {deployment_payload}") + print("=== End of Deployment Request ===\n") + + # 🛡️ Try sending to Pi-Edge, catch connection errors separately + try: + response = pi_edge_client.deploy_service_function(data=deployment_payload) + if isinstance(response, dict) and "error" in response: + logger.warning(f"Failed to deploy service function: {response}") + return jsonify({ + "warning": "Deployment not completed (SRM service unreachable)", + "details": response + }), 202 # Still accept the request but warn -def get_app_instance(app_id, x_correlator=None, app_instance_id=None, region=None): # noqa: E501 - """Retrieve the information of Application Instances for a given App + logger.info(f"Deployment response from Pi-Edge: {response}") - Ask the Edge Cloud Provider the information of the instances for a given application # noqa: E501 + except Exception as inner_error: + logger.error(f"Exception while trying to deploy to SRM: {inner_error}") + return jsonify({ + "warning": "SRM backend unavailable. Deployment request was built correctly.", + "details": str(inner_error) + }), 202 # Still accept it (because your backend worked) - :param app_id: A globally unique identifier associated with the application. Edge Cloud Provider generates this identifier when the application is submitted. - :type app_id: dict | bytes - :param x_correlator: Correlation id for the different services - :type x_correlator: str - :param app_instance_id: A globally unique identifier associated with a running instance of an application within an specific Edge Cloud Zone. Edge Cloud Provider generates this identifier. - :type app_instance_id: dict | bytes - :param region: Human readable name of the geographical Edge Cloud Region of the Edge Cloud. Defined by the Edge Cloud Provider. - :type region: dict | bytes + return jsonify({"message": f"Application {app_id} instantiation accepted"}), 202 - :rtype: InlineResponse2001 + except ValidationError as e: + logger.error(f"Validation error: {str(e)}") + return jsonify({"error": "Validation error", "details": str(e)}), 400 + except Exception as e: + logger.error(f"Unexpected error in create_app_instance: {str(e)}") + return jsonify({"error": "An unexpected error occurred", "details": str(e)}), 500 + +def get_app_instance(app_id=None, x_correlator=None, app_instance_id=None, region=None): + """ + Retrieve application instances from the database. + Supports filtering by app_id, app_instance_id, and region. """ - return "do some magic!" + try: + query = {} + if app_id: + query["appId"] = app_id + if app_instance_id: + query["appInstanceId"] = app_instance_id + if region: + query["edgeCloudZone.edgeCloudRegion"] = region + + with MongoManager() as db: + instances = list(db.find_documents("appinstances", query)) + + if not instances: + return jsonify({ + "status": 404, + "code": "NOT_FOUND", + "message": "No application instances found for the given parameters." + }), 404 + + return jsonify({"appInstanceInfo": instances}), 200 + + except Exception as e: + logger.exception("Failed to retrieve app instances") + return jsonify({ + "status": 500, + "code": "INTERNAL", + "message": f"Internal server error: {str(e)}" + }), 500 def delete_app_instance(app_id, app_instance_id, x_correlator=None): diff --git a/edge_cloud_management_api/controllers/edge_cloud_controller.py b/edge_cloud_management_api/controllers/edge_cloud_controller.py index 65173b42a642cd98a637bdb44610681138532f91..57292b8929cf1012628b70edb54840d46f910caa 100644 --- a/edge_cloud_management_api/controllers/edge_cloud_controller.py +++ b/edge_cloud_management_api/controllers/edge_cloud_controller.py @@ -1,6 +1,9 @@ from flask import jsonify from pydantic import BaseModel, Field, ValidationError from typing import List +from edge_cloud_management_api.managers.log_manager import logger +from edge_cloud_management_api.services.pi_edge_services import PiEdgeAPIClientFactory + class EdgeCloudZone(BaseModel): @@ -25,9 +28,38 @@ class EdgeCloudQueryParams(BaseModel): ) -def get_local_zones() -> List[EdgeCloudZone]: - """get local Operator Platform available zones from Service Resource Manager""" - return [] +def get_local_zones() -> list[dict]: + """ + Get local Operator Platform available zones from PiEdge Service Resource Manager. + """ + try: + pi_edge_factory = PiEdgeAPIClientFactory() + api_client = pi_edge_factory.create_pi_edge_api_client() + result = api_client.edge_cloud_zones() + + if isinstance(result, dict) and "error" in result: + logger.error(f"PiEdge error: {result['error']}") + return [] + + zones = [] + for node in result: + try: + zone = EdgeCloudZone( + edgeCloudZoneId=node["id"], + edgeCloudZoneName=node.get("name", "unknown"), + edgeCloudZoneStatus=node.get("status", "unknown"), + edgeCloudProvider=node.get("provider", "local-provider"), + edgeCloudRegion=node.get("region", "default-region") + ) + zones.append(zone.model_dump()) + except Exception as e: + logger.warning(f"Failed to parse node into EdgeCloudZone: {e}") + + return zones + + except Exception as e: + logger.exception("Unexpected error while retrieving local zones from PiEdge") + return [] def get_federated_zones() -> List[EdgeCloudZone]: diff --git a/edge_cloud_management_api/models/application_models.py b/edge_cloud_management_api/models/application_models.py index 723e34a0c4d63523fc430faa1428db18bf9e6ce0..617e7de79000626cd3e7c00e26601e701d52b4e6 100644 --- a/edge_cloud_management_api/models/application_models.py +++ b/edge_cloud_management_api/models/application_models.py @@ -1,75 +1,47 @@ -from pydantic import BaseModel, HttpUrl, Field # , UUID4 -from typing import Any, List, Optional -from enum import Enum +#from pydantic import BaseModel, HttpUrl, Field , UUID4 +#from typing import Any, List, Optional +#from enum import Enum # from ipaddress import IPv4Address, IPv6Address +#from edge_cloud_management_api.models.edge_cloud_models import EdgeCloudZone -# from edge_cloud_management_api.models.edge_cloud_models import EdgeCloudZone - - -# Enum definitions - - -# class VisibilityType(str, Enum): -# VISIBILITY_EXTERNAL = "VISIBILITY_EXTERNAL" -# VISIBILITY_INTERNAL = "VISIBILITY_INTERNAL" - - -# class AppInstanceStatus(str, Enum): -# ready = "ready" -# instantiating = "instantiating" -# failed = "failed" -# terminating = "terminating" -# unknown = "unknown" +from pydantic import BaseModel, HttpUrl, Field, UUID4 +from typing import Any, List, Optional +from enum import Enum +from edge_cloud_management_api.models.edge_cloud_models import EdgeCloudZone # <-- you should IMPORT this properly -# class Protocol(str, Enum): -# TCP = "TCP" -# UDP = "UDP" -# ANY = "ANY" - - -# Model definitions - -# class AccessEndpoint(BaseModel): -# port: int # min 0 -# fqdn: Optional[str] -# ipv4Addresses: Optional[List[IPv4Address]] # minItems: 1 -# ipv6Addresses: Optional[List[IPv6Address]] # minItems: 1 +# --- Enums --- -# class Config: -# schema_extra = { -# "example": { -# "port": 8080, -# "fqdn": "example.com", -# "ipv4Addresses": ["192.168.0.1"], -# "ipv6Addresses": ["2001:db8::1"], -# } -# } +class VisibilityType(str, Enum): + VISIBILITY_EXTERNAL = "VISIBILITY_EXTERNAL" + VISIBILITY_INTERNAL = "VISIBILITY_INTERNAL" -# class ComponentEndpointInfo(BaseModel): -# interfaceId: UUID4 # string pattern: ^[A-Za-z0-9][A-Za-z0-9_]{6,30}[A-Za-z0-9]$ -# accessPoints: AccessEndpoint +class AppInstanceStatus(str, Enum): + ready = "ready" + instantiating = "instantiating" + failed = "failed" + terminating = "terminating" + unknown = "unknown" -# class AppInstanceInfo(BaseModel): -# appInstanceId: UUID4 # str = Field(..., regex=r"^[0-9a-fA-F-]{36}$") -# status: AppInstanceStatus = AppInstanceStatus.unknown # [ ready, instantiating, failed, terminating, unknown ] -# componentEndpointInfo: List[ComponentEndpointInfo] -# kubernetesClusterRef: Optional[UUID4] -# edgeCloudZone: EdgeCloudZone +class Protocol(str, Enum): + TCP = "TCP" + UDP = "UDP" + ANY = "ANY" +# --- Model Definitions --- class NetworkInterface(BaseModel): interfaceId: str = Field(..., pattern="^[A-Za-z][A-Za-z0-9_]{3,31}$") - protocol: str # [ TCP, UDP, ANY ] - port: int # minimum: 1, maximum: 65535 - visibilityType: str # [ VISIBILITY_EXTERNAL, VISIBILITY_INTERNAL ] + protocol: Protocol + port: int # 1-65535 + visibilityType: VisibilityType class ComponentSpec(BaseModel): componentName: str - networkInterfaces: List[NetworkInterface] # min one occurrence + networkInterfaces: List[NetworkInterface] class AppRepo(BaseModel): @@ -79,12 +51,12 @@ class AppRepo(BaseModel): HTTP_BEARER = "HTTP_BEARER" NONE = "NONE" - type: str # [ PRIVATEREPO, PUBLICREPO ] + type: str # PRIVATEREPO or PUBLICREPO imagePath: HttpUrl userName: Optional[str] - credentials: Optional[str] # maxLength: 128 + credentials: Optional[str] # max 128 characters authType: Optional[AppRepoAuthType] - checksum: Optional[str] # MD5 checksum for VM and file-based images, sha256 digest for containers + checksum: Optional[str] class AppManifest(BaseModel): @@ -95,32 +67,27 @@ class AppManifest(BaseModel): HELM = "HELM" class OperatingSystem(BaseModel): - architecture: str # [ x86_64, x86 ] - family: str # [ RHEL, UBUNTU, COREOS, WINDOWS, OTHER ] - version: str # Version of the OS # [ OS_VERSION_UBUNTU_2204_LTS, OS_VERSION_RHEL_8, OS_MS_WINDOWS_2022, OTHER ] - license: str # License needed to activate the OS # [ OS_LICENSE_TYPE_FREE, OS_LICENSE_TYPE_ON_DEMAND, OTHER ] - - class RequiredResources(BaseModel): - numCPU: int - memory: int - storage: int + architecture: str # x86_64, x86 + family: str # UBUNTU, RHEL, COREOS, etc + version: str + license: str - # appId: Optional[UUID4] name: str = Field(..., pattern="^[A-Za-z][A-Za-z0-9_]{1,63}$") appProvider: str = Field(..., pattern="^[A-Za-z][A-Za-z0-9_]{7,63}$") - version: str # application version + version: str packageType: PackageType operatingSystem: Optional[OperatingSystem] appRepo: AppRepo - requiredResources: Any # Optional[RequiredResources] + requiredResources: Any # Could be KubernetesResources, ContainerResources, etc. componentSpec: List[ComponentSpec] -# class AppZones(BaseModel): -# kubernetesClusterRef: Optional[UUID4] -# EdgeCloudZone: EdgeCloudZone +class AppZones(BaseModel): + kubernetesClusterRef: Optional[UUID4] + EdgeCloudZone: EdgeCloudZone + +class AppInstance(BaseModel): + appId: UUID4 + appZones: List[AppZones] -# class AppInstance(BaseModel): -# appId: UUID4 -# appZones: List[AppZones] diff --git a/edge_cloud_management_api/models/edge_cloud_models.py b/edge_cloud_management_api/models/edge_cloud_models.py index 23c7e25dec9c466462e84226abbfeca758960a9d..61b419fed7c989b8bcf9580088930e02df729c01 100644 --- a/edge_cloud_management_api/models/edge_cloud_models.py +++ b/edge_cloud_management_api/models/edge_cloud_models.py @@ -10,8 +10,9 @@ class EdgeCloudZoneStatus(str, Enum): class EdgeCloudZone(BaseModel): - edgeCloudZoneId: UUID4 # Field(..., regex=r"^[0-9a-fA-F-]{36}$") + edgeCloudZoneId: UUID4 edgeCloudZoneName: str edgeCloudZoneStatus: Optional[EdgeCloudZoneStatus] edgeCloudProvider: str edgeCloudRegion: Optional[str] + diff --git a/edge_cloud_management_api/services/pi_edge_services.py b/edge_cloud_management_api/services/pi_edge_services.py index f8776dc4a65e5eb7f432e3b3712c6d462cac4f54..93ad16f5fa2d030c241898f6a202b27ec90ff20c 100644 --- a/edge_cloud_management_api/services/pi_edge_services.py +++ b/edge_cloud_management_api/services/pi_edge_services.py @@ -155,16 +155,16 @@ class PiEdgeAPIClientFactory: """ def __init__(self): - self.default_base_url = config.PI_EDGE_BASE_URL + self.default_base_url = config.SRM_HOST self.default_username = config.PI_EDGE_USERNAME self.default_password = config.PI_EDGE_PASSWORD def create_pi_edge_api_client(self, base_url=None, username=None, password=None): """ - Factory method to create a new PiEdgeAPIClient instance. + Factory method to create a new SRMAPIClient instance. Args: - base_url (str): The base URL for the PiEdge API. If None, the default is used. + base_url (str): The base URL for the SRM API. If None, the default is used. username (str): The username for authentication. If None, the default is used. password (str): The password for authentication. If None, the default is used. diff --git a/requirements.txt b/requirements.txt index 35aeb8223e82b66b6cf27d4a712ef22d4c1a7d60..447a804e834b6d4fc9cb8bb78961d327e75f18ff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # This file was autogenerated by uv via the following command: # uv export --e . +#-e . a2wsgi==1.10.7 \ --hash=sha256:6d7c602fb1f9cc6afc6c6d0558d3354f3c7aa281e73e6dc9e001dbfc1d9e80cf \ --hash=sha256:ce462ff7e1daac0bc57183c6f800f09a71c2a7a98ddd5cdeca149e3eabf3338e