From 380a83d19db896818f4cebc61ea3e669b7855fe9 Mon Sep 17 00:00:00 2001 From: host Date: Sun, 9 Aug 2026 16:27:44 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D1=83=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BF=D0=B8=D1=82=D0=B0=D0=BD=D0=B8=D0=B5=D0=BC?= =?UTF-8?q?=20=D0=B8=20=D0=BC=D0=BE=D0=BD=D0=B8=D1=82=D0=BE=D1=80=D0=B8?= =?UTF-8?q?=D0=BD=D0=B3=20=D0=B8=D0=BD=D1=81=D1=82=D0=B0=D0=BD=D1=81=D0=BE?= =?UTF-8?q?=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/routers/instances.py | 101 ++++++++++++++++--------------- 1 file changed, 52 insertions(+), 49 deletions(-) diff --git a/backend/app/routers/instances.py b/backend/app/routers/instances.py index 562ef73..01c2e52 100644 --- a/backend/app/routers/instances.py +++ b/backend/app/routers/instances.py @@ -8,12 +8,22 @@ from .. import models, proxmox_client as pve, schemas from ..config import settings from ..database import get_db from ..deps import get_current_user -from ..proxmox_actions import delete_guest +from ..proxmox_actions import delete_guest, get_live_stats, guest_action logger = logging.getLogger(__name__) router = APIRouter(prefix="/instances", tags=["instances"]) +def _get_owned_instance(db: Session, user: models.User, instance_id: int) -> models.Instance: + """Возвращает инстанс с проверкой владельца.""" + instance = db.get(models.Instance, instance_id) + if instance is None or instance.status == models.InstanceStatus.deleted: + raise HTTPException(status_code=404, detail="Инстанс не найден") + if user.role != models.UserRole.admin and instance.owner_id != user.id: + raise HTTPException(status_code=404, detail="Инстанс не найден") + return instance + + @router.get("", response_model=list[schemas.InstanceOut]) def list_instances(db: Session = Depends(get_db), user: models.User = Depends(get_current_user)): """Клиент видит только свои инстансы, администратор — все.""" @@ -23,8 +33,36 @@ def list_instances(db: Session = Depends(get_db), user: models.User = Depends(ge return list(db.scalars(query.order_by(models.Instance.created_at.desc()))) +@router.get("/{instance_id}/stats") +def instance_stats(instance_id: int, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)): + """Возвращает актуальные CPU/RAM/диск/uptime из Proxmox.""" + instance = _get_owned_instance(db, user, instance_id) + try: + return get_live_stats(instance.guest_type.value, instance.vmid) + except Exception as exc: + logger.warning("Ошибка статистики instance_id=%s: %s", instance_id, exc) + raise HTTPException(status_code=502, detail="Не удалось получить статистику Proxmox") + + +@router.post("/{instance_id}/action") +def instance_action(instance_id: int, payload: schemas.InstanceAction, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)): + """Запускает, останавливает или перезапускает VM/LXC.""" + instance = _get_owned_instance(db, user, instance_id) + try: + guest_action(instance.guest_type.value, instance.vmid, payload.action) + except Exception as exc: + logger.warning("Ошибка action=%s instance_id=%s: %s", payload.action, instance_id, exc) + raise HTTPException(status_code=502, detail="Не удалось выполнить действие в Proxmox") + if payload.action == "start": + instance.status = models.InstanceStatus.running + elif payload.action in {"stop", "shutdown"}: + instance.status = models.InstanceStatus.stopped + db.commit() + return {"ok": True, "action": payload.action} + + def _save_error(db: Session, instance: models.Instance, exc: Exception) -> None: - """Помечает инстанс ошибочным без сохранения деталей Proxmox пользователю.""" + """Помечает инстанс ошибочным без выдачи деталей Proxmox.""" instance.status = models.InstanceStatus.error db.commit() logger.exception("Ошибка провижининга instance_id=%s: %s", instance.id, exc) @@ -33,75 +71,40 @@ def _save_error(db: Session, instance: models.Instance, exc: Exception) -> None: @router.post("", response_model=schemas.InstanceCreateOut, status_code=status.HTTP_201_CREATED) def create_instance(payload: schemas.InstanceCreate, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)): """Создаёт и запускает VM/LXC из активного шаблона.""" - template = db.scalar(select(models.Template).where( - models.Template.id == payload.template_id, - models.Template.is_active.is_(True), - )) + template = db.scalar(select(models.Template).where(models.Template.id == payload.template_id, models.Template.is_active.is_(True))) if template is None: raise HTTPException(status_code=404, detail="Активный шаблон не найден") - try: vmid = pve.next_vmid() except Exception as exc: logger.exception("Не удалось получить VMID из Proxmox: %s", exc) raise HTTPException(status_code=502, detail="Proxmox недоступен") - - instance = models.Instance( - name=payload.name, vmid=vmid, node=settings.pve_node, - guest_type=template.guest_type, status=models.InstanceStatus.creating, - owner_id=user.id, template_id=template.id, - ) - db.add(instance) - db.commit() - db.refresh(instance) - + instance = models.Instance(name=payload.name, vmid=vmid, node=settings.pve_node, guest_type=template.guest_type, status=models.InstanceStatus.creating, owner_id=user.id, template_id=template.id) + db.add(instance); db.commit(); db.refresh(instance) try: if template.guest_type == models.GuestType.vm: - pve.provision_vm( - source_vmid=template.source_vmid, vmid=vmid, name=payload.name, - cores=template.cores, memory_mb=template.memory_mb, disk_gb=template.disk_gb, - username=payload.username, password=payload.password, - ) + pve.provision_vm(template.source_vmid, vmid, payload.name, template.cores, template.memory_mb, template.disk_gb, payload.username, payload.password) else: - pve.provision_lxc( - source_template=template.source_template, vmid=vmid, name=payload.name, - cores=template.cores, memory_mb=template.memory_mb, disk_gb=template.disk_gb, - password=payload.password, - ) + pve.provision_lxc(template.source_template, vmid, payload.name, template.cores, template.memory_mb, template.disk_gb, payload.password) except Exception as exc: _save_error(db, instance, exc) raise HTTPException(status_code=502, detail="Не удалось создать инстанс в Proxmox") - - instance.status = models.InstanceStatus.running - db.commit() - db.refresh(instance) - return schemas.InstanceCreateOut( - **schemas.InstanceOut.model_validate(instance).model_dump(), - initial_password=payload.password, - ) + instance.status = models.InstanceStatus.running; db.commit(); db.refresh(instance) + return schemas.InstanceCreateOut(**schemas.InstanceOut.model_validate(instance).model_dump(), initial_password=payload.password) @router.delete("/{instance_id}") def delete_instance(instance_id: int, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)): - """Удаляет принадлежащую пользователю VM/LXC и отмечает запись deleted.""" - instance = db.get(models.Instance, instance_id) - if instance is None or instance.status == models.InstanceStatus.deleted: - raise HTTPException(status_code=404, detail="Инстанс не найден") - if user.role != models.UserRole.admin and instance.owner_id != user.id: - raise HTTPException(status_code=404, detail="Инстанс не найден") + """Удаляет принадлежащую пользователю VM/LXC.""" + instance = _get_owned_instance(db, user, instance_id) if instance.vmid is None: raise HTTPException(status_code=409, detail="У инстанса отсутствует VMID") - - instance.status = models.InstanceStatus.deleting - db.commit() + instance.status = models.InstanceStatus.deleting; db.commit() try: delete_guest(instance.guest_type.value, instance.vmid) except Exception as exc: - instance.status = models.InstanceStatus.error - db.commit() + instance.status = models.InstanceStatus.error; db.commit() logger.exception("Ошибка удаления instance_id=%s: %s", instance.id, exc) raise HTTPException(status_code=502, detail="Не удалось удалить инстанс в Proxmox") - - instance.status = models.InstanceStatus.deleted - db.commit() + instance.status = models.InstanceStatus.deleted; db.commit() return {"ok": True}