Loading src/sunrise6g_opensdk/edgecloud/adapters/aeros/camara2aeros_converter.py 0 → 100644 +124 −0 Original line number Diff line number Diff line ''' Module: converter.py This module provides functions to convert application manifests into TOSCA models. It includes the `generate_tosca` function that constructs a TOSCA model based on the application manifest and associated app zones. ''' from typing import List, Dict, Any import yaml from sunrise6g_opensdk.edgecloud.adapters.aeros import config from sunrise6g_opensdk.logger import setup_logger from sunrise6g_opensdk.edgecloud.core.camara_schemas import AppManifest, VisibilityType from sunrise6g_opensdk.edgecloud.adapters.aeros.continuum_models import ( TOSCA, NodeTemplate, CustomRequirement, HostRequirement, HostCapability, Property as HostProperty, DomainIdOperator, NodeFilter, NetworkRequirement, NetworkProperties, ExposedPort, PortProperties, ArtifactModel) logger = setup_logger(__name__, is_debug=True, file_name=config.LOG_FILE) def generate_tosca(app_manifest: AppManifest, app_zones: List[Dict[str, Any]]) -> str: ''' Generate a TOSCA model from the application manifest and app zones. Args: app_manifest (AppManifest): The application manifest containing details about the app. app_zones (List[Dict[str, Any]]): List of app zones where the app will be deployed. Returns: TOSCA yaml as string which can be used in a POST request with applcation type yaml ''' component = app_manifest.componentSpec[0] image_path = app_manifest.appRepo.imagePath.root image_file = image_path.split("/")[-1] repository_url = "/".join( image_path.split("/")[:-1]) if "/" in image_path else "docker_hub" zone_id = app_zones[0].get("EdgeCloudZone", {}).get("edgeCloudZoneId", "default-zone") logger.info("DEBUG : %s", app_manifest.requiredResources.root) # Extract minNodeMemory (fallback = 1024 MB) res = app_manifest.requiredResources.root if hasattr(res, "applicationResources") and hasattr( res.applicationResources.cpuPool.topology, "minNodeMemory"): min_node_memory = res.applicationResources.cpuPool.topology.minNodeMemory else: min_node_memory = 1024 # Build exposed network ports ports = { iface.interfaceId: ExposedPort(properties=PortProperties( protocol=[iface.protocol.value.lower()], source=iface.port)) for iface in component.networkInterfaces } expose_ports = any( iface.visibilityType == VisibilityType.VISIBILITY_EXTERNAL for iface in component.networkInterfaces) # Define host property constraints host_props = HostProperty( cpu_arch={"equal": "x64"}, realtime={"equal": False}, cpu_usage={"less_or_equal": "0.4"}, mem_size={"greater_or_equal": str(min_node_memory)}, energy_efficiency={"greater_or_equal": "0"}, green={"greater_or_equal": "0"}, domain_id=DomainIdOperator(equal=zone_id), ) # Create Node compute and network requirements requirements = [ CustomRequirement(network=NetworkRequirement( properties=NetworkProperties(ports=ports, exposePorts=expose_ports))), CustomRequirement(host=HostRequirement(node_filter=NodeFilter( capabilities=[{ "host": HostCapability(properties=host_props) }], properties=None))) ] # Define the NodeTemplate node_template = NodeTemplate( type="tosca.nodes.Container.Application", isJob=False, requirements=requirements, artifacts={ "application_image": ArtifactModel( file=image_file, type="tosca.artifacts.Deployment.Image.Container.Docker", repository=repository_url, is_private=app_manifest.appRepo.type == "PRIVATEREPO", username=app_manifest.appRepo.userName, password=app_manifest.appRepo.credentials) }, interfaces={ "Standard": { "create": { "implementation": "application_image", "inputs": { "cliArgs": [], "envVars": [] } } } }) # Assemble full TOSCA object tosca = TOSCA(tosca_definitions_version="tosca_simple_yaml_1_3", description=f"TOSCA for {app_manifest.name}", serviceOverlay=False, node_templates={component.componentName: node_template}) tosca_dict = tosca.model_dump(by_alias=True, exclude_none=True) for template in tosca_dict.get("node_templates", {}).values(): template["requirements"] = [{ k: v for k, v in req.items() if v is not None } for req in template.get("requirements", [])] yaml_str = yaml.dump(tosca_dict, sort_keys=False) return yaml_str src/sunrise6g_opensdk/edgecloud/adapters/aeros/client.py +602 −324 File changed.Preview size limit exceeded, changes collapsed. Show changes src/sunrise6g_opensdk/edgecloud/adapters/aeros/continuum_client.py +5 −5 Original line number Diff line number Diff line Loading @@ -73,7 +73,7 @@ class ContinuumClient: return response.json() @catch_requests_exceptions def query_entities(self, ngsild_params): def query_entities(self, ngsild_params) -> requests.Response: """ Query entities with ngsi-ld params :input Loading @@ -90,7 +90,7 @@ class ContinuumClient: # self.logger.debug("Query entities URL: %s", entities_url) # self.logger.debug("Query entities response: %s %s", # response.status_code, response.text) return response.json() return response @catch_requests_exceptions def deploy_service(self, service_id: str) -> dict: Loading @@ -116,7 +116,7 @@ class ContinuumClient: return response.json() @catch_requests_exceptions def undeploy_service(self, service_id: str) -> dict: def undeploy_service(self, service_id: str) -> requests.Response: """ Undeploy service :input Loading @@ -136,10 +136,10 @@ class ContinuumClient: response.status_code, response.text, ) return response.json() return response @catch_requests_exceptions def onboard_and_deploy_service(self, service_id: str, tosca_str: str) -> dict: def onboard_and_deploy_service(self, service_id: str, tosca_str: str) -> requests.Response: """ Onboard (& deploy) service on aerOS continuum :input Loading src/sunrise6g_opensdk/edgecloud/adapters/aeros/continuum_models.py 0 → 100644 +269 −0 Original line number Diff line number Diff line ''' aerOS continuum models ''' from enum import Enum from typing import List, Dict, Any, Union, Optional from pydantic import BaseModel, Field class ServiceNotFound(BaseModel): ''' Docstring ''' detail: str = "Service not found" class CPUComparisonOperator(BaseModel): """ CPU requirment for now is that usage should be less than """ less_or_equal: Union[float, None] = None class CPUArchComparisonOperator(BaseModel): """ CPU arch requirment, equal to str """ equal: Union[str, None] = None class MEMComparisonOperator(BaseModel): """ RAM requirment for now is that available RAM should be more than """ greater_or_equal: Union[str, None] = None class EnergyEfficienyComparisonOperator(BaseModel): """ Energy Efficiency requirment for now is that IE should have energy efficiency more than a % """ greater_or_equal: Union[str, None] = None class GreenComparisonOperator(BaseModel): """ IE Green requirment for now is that IE should have green energy mix which us more than a % """ greater_or_equal: Union[str, None] = None class RTComparisonOperator(BaseModel): """ Real Time requirment T/F """ equal: Union[bool, None] = None class CpuArch(str, Enum): ''' Enumeration with possible cpu types ''' x86_64 = "x86_64" arm64 = "arm64" arm32 = "arm32" class Coordinates(BaseModel): ''' IE coordinate requirements ''' coordinates: List[List[float]] class DomainIdOperator(BaseModel): """ CPU arch requirment, equal to str """ equal: Union[str, None] = None class Property(BaseModel): ''' IE capabilities ''' cpu_usage: CPUComparisonOperator = Field( default_factory=CPUComparisonOperator) cpu_arch: CPUArchComparisonOperator = Field( default_factory=CPUArchComparisonOperator) mem_size: MEMComparisonOperator = Field( default_factory=MEMComparisonOperator) realtime: RTComparisonOperator = Field( default_factory=RTComparisonOperator) area: Coordinates = None energy_efficiency: EnergyEfficienyComparisonOperator = Field( default_factory=EnergyEfficienyComparisonOperator) green: GreenComparisonOperator = Field( default_factory=GreenComparisonOperator) domain_id: DomainIdOperator = Field(default_factory=DomainIdOperator) # @field_validator('mem_size') # def validate_mem_size(cls, v): # if not v or "MB" not in v: # raise ValueError("mem_size must be in MB and specified") # mem_size_value = int(v.split(" ")[0]) # if mem_size_value < 2000: # raise ValueError("mem_size must be greater or equal to 2000 MB") # return v class HostCapability(BaseModel): ''' Host properties ''' properties: Property class NodeFilter(BaseModel): ''' Node filter, How to filter continuum IE and select canditate list ''' properties: Optional[Dict[str, List[str]]] = None capabilities: Optional[List[Dict[str, HostCapability]]] = None class HostRequirement(BaseModel): ''' capabilities of node ''' # node_filter: Dict[str, List[Dict[str, HostCapability]]] node_filter: NodeFilter class PortProperties(BaseModel): ''' Workload port description ''' protocol: List[str] = Field(...) source: int = Field(...) class ExposedPort(BaseModel): ''' Workload exposed network ports ''' properties: PortProperties = Field(...) class NetworkProperties(BaseModel): ''' Dict of network requirments, name of port and protperty = [protocol, port] mapping ''' ports: Dict[str, ExposedPort] = Field(...) exposePorts: Optional[bool] class NetworkRequirement(BaseModel): ''' Top level key of network requirments ''' properties: NetworkProperties class CustomRequirement(BaseModel): ''' Define a custom requirement type that can be either a host or a network requirement ''' host: HostRequirement = None network: NetworkRequirement = None class ArtifactModel(BaseModel): ''' Artifact has a useer defined id and then a dict with the following keys: ''' file: str type: str repository: str is_private: Optional[bool] = False username: Optional[str] = None password: Optional[str] = None class NodeTemplate(BaseModel): ''' Node template "tosca.nodes.Container.Application" ''' type: str requirements: List[CustomRequirement] artifacts: Dict[str, ArtifactModel] interfaces: Dict[str, Any] isJob: Optional[bool] = False class TOSCA(BaseModel): ''' The TOSCA structure ''' tosca_definitions_version: str description: str serviceOverlay: Optional[bool] = False node_templates: Dict[str, NodeTemplate] TOSCA_YAML_EXAMPLE = """ tosca_definitions_version: tosca_simple_yaml_1_3 description: A test service for testing TOSCA generation serviceOverlay: false node_templates: auto-component: type: tosca.nodes.Container.Application isJob: False artifacts: application_image: file: aeros-public/common-deployments/nginx:latest repository: registry.gitlab.aeros-project.eu type: tosca.artifacts.Deployment.Image.Container.Docker interfaces: Standard: create: implementation: application_image inputs: cliArgs: - -a: aa envVars: - URL: bb requirements: - network: properties: ports: port1: properties: protocol: - tcp source: 80 port2: properties: protocol: - tcp source: 443 exposePorts: True - host: node_filter: capabilities: - host: properties: cpu_arch: equal: x64 realtime: equal: false cpu_usage: less_or_equal: '0.4' mem_size: greater_or_equal: '1' domain_id: equal: urn:ngsi-ld:Domain:NCSRD energy_efficiency: greater_or_equal: '0.5' green: greater_or_equal: '0.5' domain_id: equal: urn:ngsi-ld:Domain:ncsrd01 properties: null """ src/sunrise6g_opensdk/edgecloud/adapters/aeros/errors.py 0 → 100644 +30 −0 Original line number Diff line number Diff line ''' Custom aerOS adapter exceptions on top of EdgeCloudPlatformError ''' from sunrise6g_opensdk.edgecloud.adapters.errors import EdgeCloudPlatformError class InvalidArgumentError(EdgeCloudPlatformError): """400 Bad Request""" pass class UnauthenticatedError(EdgeCloudPlatformError): """401 Unauthorized""" pass class PermissionDeniedError(EdgeCloudPlatformError): """403 Forbidden""" pass class ResourceNotFoundError(EdgeCloudPlatformError): """404 Not Found""" pass class ServiceUnavailableError(EdgeCloudPlatformError): """503 Service Unavailable""" pass Loading
src/sunrise6g_opensdk/edgecloud/adapters/aeros/camara2aeros_converter.py 0 → 100644 +124 −0 Original line number Diff line number Diff line ''' Module: converter.py This module provides functions to convert application manifests into TOSCA models. It includes the `generate_tosca` function that constructs a TOSCA model based on the application manifest and associated app zones. ''' from typing import List, Dict, Any import yaml from sunrise6g_opensdk.edgecloud.adapters.aeros import config from sunrise6g_opensdk.logger import setup_logger from sunrise6g_opensdk.edgecloud.core.camara_schemas import AppManifest, VisibilityType from sunrise6g_opensdk.edgecloud.adapters.aeros.continuum_models import ( TOSCA, NodeTemplate, CustomRequirement, HostRequirement, HostCapability, Property as HostProperty, DomainIdOperator, NodeFilter, NetworkRequirement, NetworkProperties, ExposedPort, PortProperties, ArtifactModel) logger = setup_logger(__name__, is_debug=True, file_name=config.LOG_FILE) def generate_tosca(app_manifest: AppManifest, app_zones: List[Dict[str, Any]]) -> str: ''' Generate a TOSCA model from the application manifest and app zones. Args: app_manifest (AppManifest): The application manifest containing details about the app. app_zones (List[Dict[str, Any]]): List of app zones where the app will be deployed. Returns: TOSCA yaml as string which can be used in a POST request with applcation type yaml ''' component = app_manifest.componentSpec[0] image_path = app_manifest.appRepo.imagePath.root image_file = image_path.split("/")[-1] repository_url = "/".join( image_path.split("/")[:-1]) if "/" in image_path else "docker_hub" zone_id = app_zones[0].get("EdgeCloudZone", {}).get("edgeCloudZoneId", "default-zone") logger.info("DEBUG : %s", app_manifest.requiredResources.root) # Extract minNodeMemory (fallback = 1024 MB) res = app_manifest.requiredResources.root if hasattr(res, "applicationResources") and hasattr( res.applicationResources.cpuPool.topology, "minNodeMemory"): min_node_memory = res.applicationResources.cpuPool.topology.minNodeMemory else: min_node_memory = 1024 # Build exposed network ports ports = { iface.interfaceId: ExposedPort(properties=PortProperties( protocol=[iface.protocol.value.lower()], source=iface.port)) for iface in component.networkInterfaces } expose_ports = any( iface.visibilityType == VisibilityType.VISIBILITY_EXTERNAL for iface in component.networkInterfaces) # Define host property constraints host_props = HostProperty( cpu_arch={"equal": "x64"}, realtime={"equal": False}, cpu_usage={"less_or_equal": "0.4"}, mem_size={"greater_or_equal": str(min_node_memory)}, energy_efficiency={"greater_or_equal": "0"}, green={"greater_or_equal": "0"}, domain_id=DomainIdOperator(equal=zone_id), ) # Create Node compute and network requirements requirements = [ CustomRequirement(network=NetworkRequirement( properties=NetworkProperties(ports=ports, exposePorts=expose_ports))), CustomRequirement(host=HostRequirement(node_filter=NodeFilter( capabilities=[{ "host": HostCapability(properties=host_props) }], properties=None))) ] # Define the NodeTemplate node_template = NodeTemplate( type="tosca.nodes.Container.Application", isJob=False, requirements=requirements, artifacts={ "application_image": ArtifactModel( file=image_file, type="tosca.artifacts.Deployment.Image.Container.Docker", repository=repository_url, is_private=app_manifest.appRepo.type == "PRIVATEREPO", username=app_manifest.appRepo.userName, password=app_manifest.appRepo.credentials) }, interfaces={ "Standard": { "create": { "implementation": "application_image", "inputs": { "cliArgs": [], "envVars": [] } } } }) # Assemble full TOSCA object tosca = TOSCA(tosca_definitions_version="tosca_simple_yaml_1_3", description=f"TOSCA for {app_manifest.name}", serviceOverlay=False, node_templates={component.componentName: node_template}) tosca_dict = tosca.model_dump(by_alias=True, exclude_none=True) for template in tosca_dict.get("node_templates", {}).values(): template["requirements"] = [{ k: v for k, v in req.items() if v is not None } for req in template.get("requirements", [])] yaml_str = yaml.dump(tosca_dict, sort_keys=False) return yaml_str
src/sunrise6g_opensdk/edgecloud/adapters/aeros/client.py +602 −324 File changed.Preview size limit exceeded, changes collapsed. Show changes
src/sunrise6g_opensdk/edgecloud/adapters/aeros/continuum_client.py +5 −5 Original line number Diff line number Diff line Loading @@ -73,7 +73,7 @@ class ContinuumClient: return response.json() @catch_requests_exceptions def query_entities(self, ngsild_params): def query_entities(self, ngsild_params) -> requests.Response: """ Query entities with ngsi-ld params :input Loading @@ -90,7 +90,7 @@ class ContinuumClient: # self.logger.debug("Query entities URL: %s", entities_url) # self.logger.debug("Query entities response: %s %s", # response.status_code, response.text) return response.json() return response @catch_requests_exceptions def deploy_service(self, service_id: str) -> dict: Loading @@ -116,7 +116,7 @@ class ContinuumClient: return response.json() @catch_requests_exceptions def undeploy_service(self, service_id: str) -> dict: def undeploy_service(self, service_id: str) -> requests.Response: """ Undeploy service :input Loading @@ -136,10 +136,10 @@ class ContinuumClient: response.status_code, response.text, ) return response.json() return response @catch_requests_exceptions def onboard_and_deploy_service(self, service_id: str, tosca_str: str) -> dict: def onboard_and_deploy_service(self, service_id: str, tosca_str: str) -> requests.Response: """ Onboard (& deploy) service on aerOS continuum :input Loading
src/sunrise6g_opensdk/edgecloud/adapters/aeros/continuum_models.py 0 → 100644 +269 −0 Original line number Diff line number Diff line ''' aerOS continuum models ''' from enum import Enum from typing import List, Dict, Any, Union, Optional from pydantic import BaseModel, Field class ServiceNotFound(BaseModel): ''' Docstring ''' detail: str = "Service not found" class CPUComparisonOperator(BaseModel): """ CPU requirment for now is that usage should be less than """ less_or_equal: Union[float, None] = None class CPUArchComparisonOperator(BaseModel): """ CPU arch requirment, equal to str """ equal: Union[str, None] = None class MEMComparisonOperator(BaseModel): """ RAM requirment for now is that available RAM should be more than """ greater_or_equal: Union[str, None] = None class EnergyEfficienyComparisonOperator(BaseModel): """ Energy Efficiency requirment for now is that IE should have energy efficiency more than a % """ greater_or_equal: Union[str, None] = None class GreenComparisonOperator(BaseModel): """ IE Green requirment for now is that IE should have green energy mix which us more than a % """ greater_or_equal: Union[str, None] = None class RTComparisonOperator(BaseModel): """ Real Time requirment T/F """ equal: Union[bool, None] = None class CpuArch(str, Enum): ''' Enumeration with possible cpu types ''' x86_64 = "x86_64" arm64 = "arm64" arm32 = "arm32" class Coordinates(BaseModel): ''' IE coordinate requirements ''' coordinates: List[List[float]] class DomainIdOperator(BaseModel): """ CPU arch requirment, equal to str """ equal: Union[str, None] = None class Property(BaseModel): ''' IE capabilities ''' cpu_usage: CPUComparisonOperator = Field( default_factory=CPUComparisonOperator) cpu_arch: CPUArchComparisonOperator = Field( default_factory=CPUArchComparisonOperator) mem_size: MEMComparisonOperator = Field( default_factory=MEMComparisonOperator) realtime: RTComparisonOperator = Field( default_factory=RTComparisonOperator) area: Coordinates = None energy_efficiency: EnergyEfficienyComparisonOperator = Field( default_factory=EnergyEfficienyComparisonOperator) green: GreenComparisonOperator = Field( default_factory=GreenComparisonOperator) domain_id: DomainIdOperator = Field(default_factory=DomainIdOperator) # @field_validator('mem_size') # def validate_mem_size(cls, v): # if not v or "MB" not in v: # raise ValueError("mem_size must be in MB and specified") # mem_size_value = int(v.split(" ")[0]) # if mem_size_value < 2000: # raise ValueError("mem_size must be greater or equal to 2000 MB") # return v class HostCapability(BaseModel): ''' Host properties ''' properties: Property class NodeFilter(BaseModel): ''' Node filter, How to filter continuum IE and select canditate list ''' properties: Optional[Dict[str, List[str]]] = None capabilities: Optional[List[Dict[str, HostCapability]]] = None class HostRequirement(BaseModel): ''' capabilities of node ''' # node_filter: Dict[str, List[Dict[str, HostCapability]]] node_filter: NodeFilter class PortProperties(BaseModel): ''' Workload port description ''' protocol: List[str] = Field(...) source: int = Field(...) class ExposedPort(BaseModel): ''' Workload exposed network ports ''' properties: PortProperties = Field(...) class NetworkProperties(BaseModel): ''' Dict of network requirments, name of port and protperty = [protocol, port] mapping ''' ports: Dict[str, ExposedPort] = Field(...) exposePorts: Optional[bool] class NetworkRequirement(BaseModel): ''' Top level key of network requirments ''' properties: NetworkProperties class CustomRequirement(BaseModel): ''' Define a custom requirement type that can be either a host or a network requirement ''' host: HostRequirement = None network: NetworkRequirement = None class ArtifactModel(BaseModel): ''' Artifact has a useer defined id and then a dict with the following keys: ''' file: str type: str repository: str is_private: Optional[bool] = False username: Optional[str] = None password: Optional[str] = None class NodeTemplate(BaseModel): ''' Node template "tosca.nodes.Container.Application" ''' type: str requirements: List[CustomRequirement] artifacts: Dict[str, ArtifactModel] interfaces: Dict[str, Any] isJob: Optional[bool] = False class TOSCA(BaseModel): ''' The TOSCA structure ''' tosca_definitions_version: str description: str serviceOverlay: Optional[bool] = False node_templates: Dict[str, NodeTemplate] TOSCA_YAML_EXAMPLE = """ tosca_definitions_version: tosca_simple_yaml_1_3 description: A test service for testing TOSCA generation serviceOverlay: false node_templates: auto-component: type: tosca.nodes.Container.Application isJob: False artifacts: application_image: file: aeros-public/common-deployments/nginx:latest repository: registry.gitlab.aeros-project.eu type: tosca.artifacts.Deployment.Image.Container.Docker interfaces: Standard: create: implementation: application_image inputs: cliArgs: - -a: aa envVars: - URL: bb requirements: - network: properties: ports: port1: properties: protocol: - tcp source: 80 port2: properties: protocol: - tcp source: 443 exposePorts: True - host: node_filter: capabilities: - host: properties: cpu_arch: equal: x64 realtime: equal: false cpu_usage: less_or_equal: '0.4' mem_size: greater_or_equal: '1' domain_id: equal: urn:ngsi-ld:Domain:NCSRD energy_efficiency: greater_or_equal: '0.5' green: greater_or_equal: '0.5' domain_id: equal: urn:ngsi-ld:Domain:ncsrd01 properties: null """
src/sunrise6g_opensdk/edgecloud/adapters/aeros/errors.py 0 → 100644 +30 −0 Original line number Diff line number Diff line ''' Custom aerOS adapter exceptions on top of EdgeCloudPlatformError ''' from sunrise6g_opensdk.edgecloud.adapters.errors import EdgeCloudPlatformError class InvalidArgumentError(EdgeCloudPlatformError): """400 Bad Request""" pass class UnauthenticatedError(EdgeCloudPlatformError): """401 Unauthorized""" pass class PermissionDeniedError(EdgeCloudPlatformError): """403 Forbidden""" pass class ResourceNotFoundError(EdgeCloudPlatformError): """404 Not Found""" pass class ServiceUnavailableError(EdgeCloudPlatformError): """503 Service Unavailable""" pass