Archived
111 lines
6.2 KiB
Python
111 lines
6.2 KiB
Python
import logging
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import Session
|
|
|
|
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, 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)):
|
|
"""Клиент видит только свои инстансы, администратор — все."""
|
|
query = select(models.Instance).where(models.Instance.status != models.InstanceStatus.deleted)
|
|
if user.role != models.UserRole.admin:
|
|
query = query.where(models.Instance.owner_id == user.id)
|
|
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."""
|
|
instance.status = models.InstanceStatus.error
|
|
db.commit()
|
|
logger.exception("Ошибка провижининга instance_id=%s: %s", instance.id, exc)
|
|
|
|
|
|
@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)))
|
|
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)
|
|
try:
|
|
if template.guest_type == models.GuestType.vm:
|
|
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(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)
|
|
|
|
|
|
@router.delete("/{instance_id}")
|
|
def delete_instance(instance_id: int, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)):
|
|
"""Удаляет принадлежащую пользователю 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()
|
|
try:
|
|
delete_guest(instance.guest_type.value, instance.vmid)
|
|
except Exception as exc:
|
|
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()
|
|
return {"ok": True}
|