Commit 35bf5beb authored by vpitsilis's avatar vpitsilis
Browse files

Update aerOS adapter framework. Adapt aerOS-CAMARA responses. Implement edge-zones GSMA TFs.

parent 5bfb634c
Loading
Loading
Loading
Loading
+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
+602 −324

File changed.

Preview size limit exceeded, changes collapsed.

+5 −5
Original line number Diff line number Diff line
@@ -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
@@ -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:
@@ -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
@@ -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
+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
    



"""
+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