diff --git a/requirements.txt b/requirements.txt index 4339896ba5eeef899c5e8fb39b80bbe600de9eca..3c3913cb8836c5955df44d1e0a7ea2b08dcd64cf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,6 +18,7 @@ pydantic-settings==2.7.1 pydantic_core==2.27.2 python-dotenv==1.0.1 pytz==2025.1 +PyYAML==6.0.2 redis==5.2.1 sniffio==1.3.1 SQLAlchemy==2.0.36 @@ -29,4 +30,3 @@ typing_extensions==4.12.2 uvicorn==0.34.0 watchdog==4.0.2 zipp==3.21.0 -python-dotenv==1.0.1 \ No newline at end of file diff --git a/src/backend/api/api_v1/endpoints/cluster.py b/src/backend/api/api_v1/endpoints/cluster.py index 59fb29e7cedbc8ea3e23e77a09893e66954d7358..694223af4b5c52ccf0dbb54b2aa24803b5364da0 100644 --- a/src/backend/api/api_v1/endpoints/cluster.py +++ b/src/backend/api/api_v1/endpoints/cluster.py @@ -2,22 +2,34 @@ from backend.core.database import async_session from backend.schemas.task import TaskStatus from backend.api.api_v1 import deps import backend.engines.cluster as engine +import backend.exceptions as exceptions import backend.schemas as schemas import backend.models as models import backend.utils as utils import backend.tasks as tasks from fastapi import APIRouter, Depends, Request +from fastapi.responses import Response from starlette import status from typing import Annotated from uuid import UUID +import yaml import logging logger = logging.getLogger(__name__) cluster_router = APIRouter() -# = Endpoints +async def checked_get_cluster(cluster_id: str, cluster: Annotated[models.Cluster, Depends(engine.get_cluster)]) -> models.Cluster: + msg = {"Cluster":cluster.id, "Status": cluster.status.value} + if cluster.status in {models.cluster.ClusterStatus.FINISHED, models.cluster.ClusterStatus.FAILED}: + raise exceptions.BackendGoneException(msg) + elif cluster.status == models.cluster.ClusterStatus.SUBMITTED: + raise exceptions.BackendTooEarlyException(msg) + + return cluster + +# == cluster @cluster_router.post( "/", response_model=TaskStatus, @@ -26,7 +38,7 @@ cluster_router = APIRouter() ) async def _post_cluster_id(cluster_info: schemas.ClusterSchema, cluster_id: Annotated[UUID, Depends(utils.gen_id)],): # Create entry in DB - cluster = models.Cluster(id=str(cluster_id), name=cluster_info.name, status="submitted") + cluster = models.Cluster(id=str(cluster_id), name=cluster_info.name, status=models.cluster.ClusterStatus.SUBMITTED) async with async_session() as db: db.add(cluster) await db.commit() @@ -37,16 +49,56 @@ async def _post_cluster_id(cluster_info: schemas.ClusterSchema, cluster_id: Anno # Return current status return TaskStatus(id=cluster.id, status=cluster.status) + +from sqlalchemy import update + +# == cluster +@cluster_router.delete( + "/{cluster_id}/", + response_model=TaskStatus, + status_code=status.HTTP_200_OK, + operation_id="delete_cluster", +) +async def delete_cluster(cluster_id: str): + print(f"delete {cluster_id}") + status = models.cluster.ClusterStatus.FINISHED + async with async_session() as db: + # db = async_session() + await db.execute( + update(models.Cluster).where(models.Cluster.id == cluster_id).values(status=status) + ) + await db.commit() + + return TaskStatus(id=cluster_id, status=status) + + @cluster_router.get( "/{cluster_id}/", response_model=TaskStatus, status_code=status.HTTP_200_OK, operation_id="get_cluster", ) -async def get_cluster(cluster: Annotated[models.Cluster, Depends(engine.get_cluster)], -): +async def get_cluster(cluster: Annotated[models.Cluster, Depends(engine.get_cluster)]): return TaskStatus(id=cluster.id, status=cluster.status) +@cluster_router.get( + "/{cluster_id}/kubeconfig", + status_code=status.HTTP_200_OK, + operation_id="get_cluster_kubeconfig", +) +async def get_cluster_kubeconfig(cluster_id: str, cluster: Annotated[models.Cluster, Depends(checked_get_cluster)]): + # Retrieve kubeconfig + data = {"test":1} + + # Dump kubeconfig to the client + yaml_data = yaml.dump(data, default_flow_style=False) + return Response( + content=yaml_data, + media_type="application/x-yaml", + headers={"Content-Disposition": f"attachment; filename=kubeconfig-{cluster_id}.yaml"} + ) + +# == flavors @cluster_router.post( "/flavors/", response_model=schemas.FlavorSchema, diff --git a/src/backend/engines/cluster/cluster.py b/src/backend/engines/cluster/cluster.py index 0afa32527a116ae280c0c00ee3e9917f10d5b773..38a2f1cfbc3525d8011fc9e4336f0cdbf33ebdaf 100644 --- a/src/backend/engines/cluster/cluster.py +++ b/src/backend/engines/cluster/cluster.py @@ -29,7 +29,7 @@ async def create_cluster(id: str, cluster_info: schemas.ClusterSchema): flavor = await get_flavor(vm.flavor) logger.info(f"\tShould create VM {node.model_dump_json()} with flavor {flavor.model_dump_json()}") - await asyncio.sleep(15) + await asyncio.sleep(20) logger.info("cluster created") # == flavor diff --git a/src/backend/exception.py b/src/backend/exception.py index c5d4ee5b774b6395f981d1f89fcfcb0c896ddbd4..b3de008d61faa3d47399a23aacdaae73b0e5d0a0 100644 --- a/src/backend/exception.py +++ b/src/backend/exception.py @@ -19,3 +19,11 @@ def register_exception_handlers(app: FastAPI): @app.exception_handler(exceptions.BackendException) def backend_exception_handler(_request: Request, e: exceptions.BackendException): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"{e}") + + @app.exception_handler(exceptions.BackendTooEarlyException) + def backend_not_ready_exception_handler(_request: Request, e: exceptions.BackendTooEarlyException): + raise HTTPException(status_code=status.HTTP_425_TOO_EARLY, detail=f"{e}") + + @app.exception_handler(exceptions.BackendGoneException) + def backend_gone_exception_handler(_request: Request, e: exceptions.BackendGoneException): + raise HTTPException(status_code=status.HTTP_410_GONE, detail=f"{e}") \ No newline at end of file diff --git a/src/backend/exceptions/__init__.py b/src/backend/exceptions/__init__.py index e4ebd880dbbc3755ac816558b7502020824eb880..cba333ff90e7bbc20883b9610020ab643df95fb8 100644 --- a/src/backend/exceptions/__init__.py +++ b/src/backend/exceptions/__init__.py @@ -1,5 +1,5 @@ from .cluster import ClusterException, ClusterNotFoundException from .vm import FlavorNotFoundException -from .deps import BackendException, BackendNotFoundException +from .deps import BackendException, BackendNotFoundException, BackendTooEarlyException, BackendGoneException -__all__ = ['ClusterException', 'ClusterNotFoundException', 'FlavorNotFoundException', 'BackendException', 'BackendNotFoundException'] \ No newline at end of file +__all__ = ['ClusterException', 'ClusterNotFoundException', 'FlavorNotFoundException', 'BackendException', 'BackendNotFoundException', 'BackendTooEarlyException', 'BackendGoneException'] \ No newline at end of file diff --git a/src/backend/exceptions/deps.py b/src/backend/exceptions/deps.py index 635a99aaf05916f82598a19a33739819099ce560..6cc5e39fb53caac3828cd92e19b95a1271d9257a 100644 --- a/src/backend/exceptions/deps.py +++ b/src/backend/exceptions/deps.py @@ -2,4 +2,10 @@ class BackendException(Exception): """Base class for Backend Exceptions.""" class BackendNotFoundException(BackendException): - """Base class for Backend Not Found Exceptions.""" \ No newline at end of file + """Base class for Backend Not Found Exceptions.""" + +class BackendTooEarlyException(BackendException): + """Base class for Backend Too Early Exceptions.""" + +class BackendGoneException(BackendException): + """Base class for Backend Gone Exceptions.""" \ No newline at end of file diff --git a/src/backend/models/cluster.py b/src/backend/models/cluster.py index 9bd580ff89c1a6587213e85b4eee70f977c0f5cb..2e98d1fbc90ee592d0a48fb22ecdb35a9d2dc381 100644 --- a/src/backend/models/cluster.py +++ b/src/backend/models/cluster.py @@ -1,15 +1,24 @@ from .deps import * +from enum import Enum + from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy import Enum as SQLEnum import datetime import uuid +class ClusterStatus(Enum): + SUBMITTED = "submitted" + ACTIVE = "active" + FINISHED = "finished" + FAILED = "failed" + class Cluster(Base): __tablename__ = 'clusters' # This will be the name of the table id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) name: Mapped[str] = mapped_column(String, nullable=False) - status: Mapped[str] = mapped_column(String, default="submitted") + status: Mapped[ClusterStatus] = mapped_column(SQLEnum(ClusterStatus), default=ClusterStatus.SUBMITTED, nullable=False) creation_time = Column(DateTime, nullable=False, default=datetime.datetime.now(datetime.timezone.utc)) # Auto-filled with UTC time def __repr__(self): diff --git a/src/backend/tasks/cluster.py b/src/backend/tasks/cluster.py index 0b515fe87349711587128c06134a33245a6bd434..f0c0d2cf57f02d5cec7bd2b2fa55e6a2f87e453b 100644 --- a/src/backend/tasks/cluster.py +++ b/src/backend/tasks/cluster.py @@ -25,10 +25,10 @@ async def create_cluster( # Create the cluster try: await engine.create_cluster(id=id, cluster_info=cluster_info) - status = "completed" + status = models.cluster.ClusterStatus.ACTIVE # ... failed to create it except exceptions.BackendException: - status = "failed" + status = models.cluster.ClusterStatus.FAILED # ... commmit new state finally: db = async_session()