Распаковал архив Proxmox-VPS-Panel.rar и добавил содержимое в репозиторий

This commit is contained in:
root
2026-07-24 15:10:47 +00:00
parent 0372e2c5f1
commit ac74a700ce
37 changed files with 2353 additions and 0 deletions
View File
+25
View File
@@ -0,0 +1,25 @@
from typing import List
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from .. import models, schemas
from ..database import get_db
from ..deps import require_admin
router = APIRouter(prefix="/admin", tags=["admin"])
@router.get("/users", response_model=List[schemas.UserOut])
def list_users(db: Session = Depends(get_db), _=Depends(require_admin)):
return db.query(models.User).order_by(models.User.created_at.desc()).all()
@router.post("/users/{user_id}/toggle-active")
def toggle_active(user_id: int, db: Session = Depends(get_db), _=Depends(require_admin)):
user = db.query(models.User).filter(models.User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="Пользователь не найден")
user.is_active = not user.is_active
db.commit()
return {"ok": True, "is_active": user.is_active}
+43
View File
@@ -0,0 +1,43 @@
from fastapi import APIRouter, Depends, HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from .. import models, schemas
from ..database import get_db
from ..security import hash_password, verify_password, create_access_token
from ..deps import get_current_user
router = APIRouter(prefix="/auth", tags=["auth"])
@router.post("/register", response_model=schemas.UserOut)
def register(payload: schemas.UserCreate, db: Session = Depends(get_db)):
existing = db.query(models.User).filter(models.User.email == payload.email).first()
if existing:
raise HTTPException(status_code=400, detail="Пользователь с таким email уже существует")
# первый зарегистрированный пользователь становится администратором
is_first_user = db.query(models.User).count() == 0
user = models.User(
email=payload.email,
password_hash=hash_password(payload.password),
role=models.Role.admin if is_first_user else models.Role.client,
)
db.add(user)
db.commit()
db.refresh(user)
return user
@router.post("/login", response_model=schemas.Token)
def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
user = db.query(models.User).filter(models.User.email == form_data.username).first()
if not user or not verify_password(form_data.password, user.password_hash):
raise HTTPException(status_code=401, detail="Неверный email или пароль")
token = create_access_token({"sub": str(user.id)})
return {"access_token": token, "token_type": "bearer"}
@router.get("/me", response_model=schemas.UserOut)
def me(current_user: models.User = Depends(get_current_user)):
return current_user
+69
View File
@@ -0,0 +1,69 @@
import asyncio
import websockets
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Query
from ..config import settings
router = APIRouter(prefix="/console", tags=["console"])
@router.websocket("/ws")
async def console_ws(
websocket: WebSocket,
node: str = Query(...),
vmid: int = Query(...),
guest_type: str = Query(...),
port: int = Query(...),
ticket: str = Query(...),
):
"""
Проксирует бинарный VNC-поток между браузером клиента и websocket-эндпоинтом
Proxmox (nodes/{node}/(qemu|lxc)/{vmid}/vncwebsocket). Тикет и порт берутся
из ответа /instances/{id}/console (см. instances.py -> pve.get_vnc_ticket).
Примечание: в зависимости от версии Proxmox и способа аутентификации
(API-токен vs cookie-тикет) может понадобиться донастройка заголовков —
см. README, раздел "Консоль VNC".
"""
await websocket.accept()
guest_path = "qemu" if guest_type == "vm" else "lxc"
pve_host = settings.pve_host.replace("http://", "").replace("https://", "")
upstream_url = (
f"wss://{pve_host}/api2/json/nodes/{node}/{guest_path}/{vmid}/vncwebsocket"
f"?port={port}&vncticket={ticket}"
)
headers = {
"Authorization": f"PVEAPIToken={settings.pve_token_name}={settings.pve_token_value}"
}
try:
async with websockets.connect(
upstream_url,
extra_headers=headers,
subprotocols=["binary"],
ssl=None if settings.pve_verify_ssl else False,
) as upstream:
async def client_to_upstream():
try:
while True:
data = await websocket.receive_bytes()
await upstream.send(data)
except WebSocketDisconnect:
pass
async def upstream_to_client():
try:
async for message in upstream:
if isinstance(message, str):
message = message.encode()
await websocket.send_bytes(message)
except websockets.ConnectionClosed:
pass
await asyncio.gather(client_to_upstream(), upstream_to_client())
except Exception as exc: # noqa: BLE001
await websocket.close(code=1011, reason=str(exc)[:120])
+231
View File
@@ -0,0 +1,231 @@
from typing import List
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
from sqlalchemy.orm import Session, joinedload
from .. import models, schemas, proxmox_client as pve
from ..config import settings
from ..database import get_db
from ..deps import get_current_user
router = APIRouter(prefix="/instances", tags=["instances"])
@router.get("", response_model=List[schemas.InstanceOut])
def list_my_instances(db: Session = Depends(get_db), user: models.User = Depends(get_current_user)):
query = db.query(models.Instance).options(joinedload(models.Instance.template))
query = query.filter(models.Instance.status != models.InstanceStatus.deleted)
if user.role != models.Role.admin:
query = query.filter(models.Instance.owner_id == user.id)
return query.order_by(models.Instance.created_at.desc()).all()
def _provision(instance_id: int, template_id: int, node: str):
"""Фоновое создание VPS через Proxmox API."""
from ..database import SessionLocal
db = SessionLocal()
try:
instance = db.query(models.Instance).filter(models.Instance.id == instance_id).first()
if not instance:
return
template = db.query(models.Template).filter(models.Template.id == template_id).first()
if not template:
instance.status = models.InstanceStatus.error
instance.root_password = "ERROR: шаблон не найден"
db.commit()
return
try:
if template.guest_type == models.GuestType.vm:
clean_name = ''.join(c for c in instance.name.strip() if c.isalnum() or c == '-').lower() or 'vm'
pve.clone_vm(template.source_vmid, instance.vmid, clean_name, node)
pve.resize_vm(instance.vmid, template.cores, template.memory_mb, node)
pve.resize_disk(instance.vmid, template.disk_gb, node)
if instance.ciuser and instance.root_password:
pve.configure_cloud_init(instance.vmid, instance.ciuser, instance.root_password, node)
pve.guest_action("vm", instance.vmid, "start", node)
else:
clean_name = ''.join(c for c in instance.name.strip() if c.isalnum() or c == '-').lower() or 'lxc'
password = pve.create_lxc(
new_vmid=instance.vmid, name=clean_name,
template_volid=template.source_template,
cores=template.cores, memory_mb=template.memory_mb,
disk_gb=template.disk_gb, node=node,
)
instance.root_password = password
pve.guest_action("lxc", instance.vmid, "start", node)
instance.status = models.InstanceStatus.running
except Exception as exc:
instance.status = models.InstanceStatus.error
instance.root_password = f"ERROR: {exc}"
db.commit()
finally:
db.close()
@router.post("", response_model=schemas.InstanceOut)
def create_instance(
payload: schemas.InstanceCreate,
background_tasks: BackgroundTasks,
db: Session = Depends(get_db),
user: models.User = Depends(get_current_user),
):
template = db.query(models.Template).filter(
models.Template.id == payload.template_id, models.Template.is_active == True
).first()
if not template:
raise HTTPException(status_code=404, detail="Шаблон не найден")
for _ in range(100):
try:
vmid = pve.get_next_vmid()
except Exception as exc:
raise HTTPException(status_code=502, detail=f"Proxmox недоступен: {exc}")
exists = db.query(models.Instance).filter(
models.Instance.vmid == vmid,
models.Instance.status != models.InstanceStatus.deleted,
).first()
if not exists:
break
else:
raise HTTPException(status_code=409, detail="Не удалось найти свободный VMID")
clean_name = ''.join(c for c in payload.name.strip() if c.isalnum() or c == '-').lower() or 'vps'
# Proxmox переиспользует освободившиеся vmid — вычищаем мёртвые записи,
# иначе INSERT упадёт с duplicate key по ix_instances_vmid
stale = db.query(models.Instance).filter(
models.Instance.vmid == vmid,
models.Instance.status.in_([
models.InstanceStatus.error,
models.InstanceStatus.deleted,
models.InstanceStatus.deleting,
]),
).all()
for s in stale:
db.delete(s)
if stale:
db.commit()
instance = models.Instance(
name=clean_name, vmid=vmid, node=settings.pve_node,
guest_type=template.guest_type, status=models.InstanceStatus.creating,
owner_id=user.id, template_id=template.id,
ciuser=payload.ciuser or None,
root_password=payload.cipassword or None,
)
db.add(instance)
db.commit()
db.refresh(instance)
background_tasks.add_task(_provision, instance.id, template.id, settings.pve_node)
return instance
@router.get("/{instance_id}", response_model=schemas.InstanceOut)
def get_instance(instance_id: int, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)):
instance = db.query(models.Instance).filter(models.Instance.id == instance_id).first()
if not instance:
raise HTTPException(status_code=404, detail="VPS не найден")
return instance
@router.get("/{instance_id}/status")
def get_instance_status(instance_id: int, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)):
instance = db.query(models.Instance).filter(models.Instance.id == instance_id).first()
if not instance:
raise HTTPException(status_code=404, detail="VPS не найден")
try:
return pve.get_status(instance.guest_type.value, instance.vmid, instance.node)
except Exception as exc:
raise HTTPException(status_code=502, detail=f"Proxmox недоступен: {exc}")
@router.get("/{instance_id}/ip")
def get_instance_ip(instance_id: int, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)):
instance = db.query(models.Instance).filter(models.Instance.id == instance_id).first()
if not instance:
raise HTTPException(status_code=404, detail="VPS не найден")
try:
ip = pve.get_instance_ip(instance.vmid, instance.node)
return {"ip": ip}
except Exception as exc:
return {"ip": f"ошибка: {exc}"}
@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)):
instance = db.query(models.Instance).filter(models.Instance.id == instance_id).first()
if not instance:
raise HTTPException(status_code=404, detail="VPS не найден")
try:
pve.guest_action(instance.guest_type.value, instance.vmid, payload.action, instance.node)
except Exception as exc:
raise HTTPException(status_code=502, detail=f"Ошибка: {exc}")
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}
@router.get("/{instance_id}/ip")
def get_instance_ip(instance_id: int, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)):
"""Возвращает IP-адрес инстанса."""
instance = db.query(models.Instance).filter(models.Instance.id == instance_id).first()
if not instance:
raise HTTPException(status_code=404, detail="VPS не найден")
if user.role != models.Role.admin and instance.owner_id != user.id:
raise HTTPException(status_code=403, detail="Нет доступа")
try:
ip = pve.get_instance_ip(instance.vmid, instance.node)
return {"ip": ip}
except Exception as exc:
return {"ip": f"ошибка: {exc}"}
@router.get("/{instance_id}/ip")
def get_instance_ip(instance_id: int, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)):
"""Возвращает IP-адрес инстанса."""
instance = db.query(models.Instance).filter(models.Instance.id == instance_id).first()
if not instance:
raise HTTPException(status_code=404, detail="VPS не найден")
if user.role != models.Role.admin and instance.owner_id != user.id:
raise HTTPException(status_code=403, detail="Нет доступа")
try:
ip = pve.get_instance_ip(instance.vmid, instance.node)
return {"ip": ip}
except Exception as exc:
return {"ip": f"ошибка: {exc}"}
@router.get("/{instance_id}/live")
def get_instance_live(instance_id: int, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)):
"""Live CPU/RAM/Memory показатели."""
instance = db.query(models.Instance).filter(models.Instance.id == instance_id).first()
if not instance:
raise HTTPException(status_code=404, detail="VPS не найден")
try:
return pve.get_live_stats(instance.vmid, instance.node)
except Exception as exc:
return {"cpu": 0, "mem_used": 0, "mem_total": 0, "uptime": 0, "status": "error", "error": str(exc)}
@router.delete("/{instance_id}")
def delete_instance(instance_id: int, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)):
instance = db.query(models.Instance).filter(models.Instance.id == instance_id).first()
if not instance:
raise HTTPException(status_code=404, detail="VPS не найден")
try:
pve.delete_guest(instance.guest_type.value, instance.vmid, instance.node)
except Exception as exc:
raise HTTPException(status_code=502, detail=f"Ошибка удаления: {exc}")
instance.status = models.InstanceStatus.deleted
db.commit()
return {"ok": True}
@router.get("/{instance_id}/console")
def instance_console(instance_id: int, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)):
instance = db.query(models.Instance).filter(models.Instance.id == instance_id).first()
if not instance:
raise HTTPException(status_code=404, detail="VPS не найден")
try:
ticket = pve.get_vnc_ticket(instance.guest_type.value, instance.vmid, instance.node)
except Exception as exc:
raise HTTPException(status_code=502, detail=f"Консоль недоступна: {exc}")
ticket["instance_id"] = instance.id
return ticket
+47
View File
@@ -0,0 +1,47 @@
from typing import List
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from .. import models, schemas, proxmox_client as pve
from ..database import get_db
from ..deps import get_current_user, require_admin
router = APIRouter(prefix="/templates", tags=["templates"])
@router.get("", response_model=List[schemas.TemplateOut])
def list_templates(db: Session = Depends(get_db), _=Depends(get_current_user)):
return db.query(models.Template).filter(models.Template.is_active == True).all() # noqa: E712
@router.post("", response_model=schemas.TemplateOut)
def create_template(
payload: schemas.TemplateCreate,
db: Session = Depends(get_db),
_=Depends(require_admin),
):
tpl = models.Template(**payload.model_dump())
db.add(tpl)
db.commit()
db.refresh(tpl)
return tpl
@router.get("/from-proxmox")
def list_proxmox_templates(_=Depends(require_admin)):
try: vm_tpls = pve.list_vm_templates()
except: vm_tpls = []
try: lxc_tpls = pve.list_lxc_templates()
except: lxc_tpls = []
return {"vm_templates": vm_tpls, "lxc_templates": lxc_tpls}
@router.delete("/{template_id}")
def deactivate_template(
template_id: int, db: Session = Depends(get_db), _=Depends(require_admin)
):
tpl = db.query(models.Template).filter(models.Template.id == template_id).first()
if tpl:
tpl.is_active = False
db.commit()
return {"ok": True}