feat: SSH keys management, backups fixes, instances/nodes/templates improvements, dev mode support
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Has been cancelled
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Has been cancelled
This commit is contained in:
@@ -137,20 +137,59 @@ def delete_storage(
|
||||
@router.get("/api/backups/list/{server_id}")
|
||||
def list_backups(
|
||||
server_id: int,
|
||||
node: str,
|
||||
storage: str,
|
||||
node: Optional[str] = None,
|
||||
vmid: Optional[int] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("backup:view")),
|
||||
):
|
||||
"""List backup files stored in a given storage on a node"""
|
||||
"""List backup files stored in a given storage.
|
||||
|
||||
If ``node`` is omitted (or empty), backup files are aggregated from all
|
||||
cluster nodes that have the storage available. Duplicate ``volid`` entries
|
||||
(which appear on shared storages) are de-duplicated.
|
||||
"""
|
||||
server = db.query(ProxmoxServer).filter(ProxmoxServer.id == server_id).first()
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Server not found")
|
||||
try:
|
||||
client = _get_proxmox_client(server)
|
||||
backups = client.list_backups(node, storage, vmid)
|
||||
|
||||
nodes_to_query: list[str] = []
|
||||
if node:
|
||||
nodes_to_query = [node]
|
||||
else:
|
||||
try:
|
||||
cluster_nodes = client.get_nodes() or []
|
||||
nodes_to_query = [
|
||||
n.get("node") for n in cluster_nodes
|
||||
if n.get("node") and (n.get("status") in (None, "online"))
|
||||
]
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to enumerate cluster nodes for backup listing: {e}")
|
||||
|
||||
seen: set[str] = set()
|
||||
backups: list[dict] = []
|
||||
for n in nodes_to_query:
|
||||
try:
|
||||
items = client.list_backups(n, storage, vmid) or []
|
||||
except Exception as e:
|
||||
logger.warning(f"Skipping node {n} when listing backups: {e}")
|
||||
continue
|
||||
for item in items:
|
||||
volid = item.get("volid")
|
||||
if volid and volid in seen:
|
||||
continue
|
||||
if volid:
|
||||
seen.add(volid)
|
||||
# annotate with the node where it was discovered (helpful for
|
||||
# restore / delete actions on non-shared storages)
|
||||
item.setdefault("node", n)
|
||||
backups.append(item)
|
||||
|
||||
return JSONResponse(content={"backups": backups})
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing backups: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@@ -53,6 +53,8 @@ class LXCDeployRequest(BaseModel):
|
||||
ipam_pool_id: Optional[int] = None
|
||||
password: Optional[str] = None
|
||||
ssh_keys: Optional[str] = None
|
||||
ssh_key_ids: Optional[List[int]] = None
|
||||
owner_id: Optional[int] = None
|
||||
nameserver: Optional[str] = None
|
||||
searchdomain: Optional[str] = None
|
||||
unprivileged: bool = True
|
||||
@@ -105,7 +107,8 @@ def _update_task(task_id: int, status: str, step: str, progress: int,
|
||||
db.close()
|
||||
|
||||
|
||||
def _do_lxc_deploy(task_id: int, req: LXCDeployRequest, user_id: int, username: str):
|
||||
def _do_lxc_deploy(task_id: int, req: LXCDeployRequest, user_id: int, username: str,
|
||||
owner_id: Optional[int] = None):
|
||||
"""Blocking LXC deploy — runs in thread pool."""
|
||||
db = SessionLocal()
|
||||
server = None
|
||||
@@ -286,7 +289,7 @@ def _do_lxc_deploy(task_id: int, req: LXCDeployRequest, user_id: int, username:
|
||||
template_id=None,
|
||||
template_name=req.ostemplate,
|
||||
description=f"Created from CT template {req.ostemplate}",
|
||||
owner_id=user_id,
|
||||
owner_id=owner_id or user_id,
|
||||
)
|
||||
|
||||
LoggingService.log_proxmox_action(
|
||||
@@ -434,6 +437,23 @@ async def deploy_lxc_from_template(
|
||||
if not req.name or len(req.name) < 1:
|
||||
raise HTTPException(status_code=400, detail="Укажите имя контейнера")
|
||||
|
||||
# Resolve owner / SSH keys
|
||||
is_admin = current_user.has_permission("users.manage") or current_user.is_admin
|
||||
instance_owner_id = current_user.id
|
||||
if req.owner_id and req.owner_id != current_user.id:
|
||||
if not is_admin:
|
||||
raise HTTPException(status_code=403, detail="Only admins can assign instance to another user")
|
||||
owner = db.query(User).filter(User.id == req.owner_id).first()
|
||||
if not owner:
|
||||
raise HTTPException(status_code=404, detail="Owner user not found")
|
||||
instance_owner_id = owner.id
|
||||
|
||||
if req.ssh_key_ids:
|
||||
from ..ssh_keys import resolve_ssh_keys_for_deploy
|
||||
resolved = resolve_ssh_keys_for_deploy(db, current_user, req.ssh_key_ids, instance_owner_id)
|
||||
existing = (req.ssh_keys or "").strip()
|
||||
req.ssh_keys = (existing + "\n" + resolved).strip() if existing else resolved
|
||||
|
||||
# Create task record
|
||||
task = DeployTask(
|
||||
status="pending",
|
||||
@@ -456,6 +476,7 @@ async def deploy_lxc_from_template(
|
||||
req,
|
||||
current_user.id,
|
||||
current_user.username,
|
||||
instance_owner_id,
|
||||
)
|
||||
|
||||
return LXCDeployResponse(task_id=task_id, status="pending", name=req.name)
|
||||
|
||||
@@ -523,6 +523,37 @@ def delete_proxmox_server(
|
||||
return None
|
||||
|
||||
|
||||
@router.post("/api/servers/test", status_code=status.HTTP_200_OK)
|
||||
def test_proxmox_credentials(
|
||||
payload: ProxmoxServerCreate,
|
||||
current_user: User = Depends(PermissionChecker("proxmox.view"))
|
||||
):
|
||||
"""Проверить произвольные учётные данные Proxmox без сохранения в БД."""
|
||||
try:
|
||||
if payload.use_password:
|
||||
client = ProxmoxClient(
|
||||
host=payload.ip_address,
|
||||
user=payload.api_user,
|
||||
password=payload.password,
|
||||
verify_ssl=payload.verify_ssl,
|
||||
)
|
||||
else:
|
||||
client = ProxmoxClient(
|
||||
host=payload.ip_address,
|
||||
user=payload.api_user,
|
||||
token_name=payload.api_token_name,
|
||||
token_value=payload.api_token_value,
|
||||
verify_ssl=payload.verify_ssl,
|
||||
)
|
||||
|
||||
if client.is_connected():
|
||||
return {"success": True, "status": "success", "message": "Connection successful"}
|
||||
return {"success": False, "status": "error", "message": "Failed to connect to Proxmox server"}
|
||||
except Exception as e:
|
||||
logger.error(f"Error testing Proxmox credentials for {payload.ip_address}: {e}")
|
||||
return {"success": False, "status": "error", "message": str(e)}
|
||||
|
||||
|
||||
@router.post("/api/servers/{server_id}/test", status_code=status.HTTP_200_OK)
|
||||
def test_proxmox_connection(
|
||||
server_id: int,
|
||||
@@ -533,25 +564,25 @@ def test_proxmox_connection(
|
||||
server = db.query(ProxmoxServer).filter(ProxmoxServer.id == server_id).first()
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Proxmox server not found")
|
||||
|
||||
|
||||
try:
|
||||
# Determine auth method
|
||||
client = _get_proxmox_client(server)
|
||||
|
||||
|
||||
if client.is_connected():
|
||||
server.update_status(True)
|
||||
db.commit()
|
||||
return {"status": "success", "message": "Connection successful"}
|
||||
return {"success": True, "status": "success", "message": "Connection successful"}
|
||||
else:
|
||||
server.update_status(False, "Failed to connect")
|
||||
db.commit()
|
||||
raise HTTPException(status_code=503, detail="Failed to connect to Proxmox server")
|
||||
|
||||
return {"success": False, "status": "error", "message": "Failed to connect to Proxmox server"}
|
||||
|
||||
except Exception as e:
|
||||
server.update_status(False, str(e))
|
||||
db.commit()
|
||||
logger.error(f"Error testing Proxmox connection to {server.name}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
return {"success": False, "status": "error", "message": str(e)}
|
||||
|
||||
|
||||
# ==================== Proxmox Resources ====================
|
||||
|
||||
@@ -152,7 +152,24 @@ def get_all_virtual_machines(
|
||||
query = query.filter(VMInstance.owner_id == current_user.id)
|
||||
|
||||
cached_vms = query.order_by(VMInstance.name).all()
|
||||
|
||||
|
||||
# ── Live metrics from Proxmox cluster/resources (CPU, RAM, disk, uptime) ──
|
||||
# One HTTP call per server returns metrics for all VMs/LXC in that cluster.
|
||||
# Map: (server_id_in_db, vmid) -> live data dict
|
||||
live_metrics: dict[tuple[int, int], dict] = {}
|
||||
for server in servers:
|
||||
try:
|
||||
client = _get_proxmox_client(server)
|
||||
if not client.is_connected():
|
||||
continue
|
||||
for res in client.get_cluster_resources(type_='vm'):
|
||||
vmid = res.get('vmid')
|
||||
if vmid is None:
|
||||
continue
|
||||
live_metrics[(server.id, vmid)] = res
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not fetch live metrics from {server.name}: {e}")
|
||||
|
||||
result = []
|
||||
|
||||
# Detect cluster servers (servers with hostnames pve1, pve2, pve3 pattern)
|
||||
@@ -195,7 +212,10 @@ def get_all_virtual_machines(
|
||||
os_template = vm.template_name or vm.os_type or ("QEMU/KVM" if vm.vm_type == "qemu" else "Linux")
|
||||
if not vm.template_name and vm.vm_type == "lxc" and os_template:
|
||||
os_template = os_template.capitalize()
|
||||
|
||||
|
||||
# Live metrics (cpu/mem/disk/uptime/net) from cluster/resources
|
||||
live = live_metrics.get((vm.server_id, vm.vmid)) or {}
|
||||
|
||||
result.append({
|
||||
"server_id": vm.server_id,
|
||||
"server_name": server_name,
|
||||
@@ -204,18 +224,28 @@ def get_all_virtual_machines(
|
||||
"name": vm.name,
|
||||
"hostname": ip_hostname or f"{'vps' if vm.vm_type == 'qemu' else 'lxc'}{vm.vmid}.{server.hostname if server else 'local'}",
|
||||
"type": vm.vm_type,
|
||||
"status": vm.status or "unknown",
|
||||
"status": live.get('status') or vm.status or "unknown",
|
||||
"node": vm.node,
|
||||
"cores": vm.cores or 0,
|
||||
"memory": vm.memory or 0,
|
||||
"disk": vm.disk_size or 0,
|
||||
"disk": live.get('disk') or 0,
|
||||
"ip": ip_address,
|
||||
"ip_address": ip_address,
|
||||
"ip_hostname": ip_network_name,
|
||||
"os": os_template,
|
||||
"os_template": os_template,
|
||||
"owner": owner,
|
||||
"owner_hostname": "",
|
||||
"storage": "Storage1 (DIR)"
|
||||
"storage": "Storage1 (DIR)",
|
||||
"tags": vm.tags or "",
|
||||
# Live runtime metrics
|
||||
"cpu": live.get('cpu'),
|
||||
"mem": live.get('mem'),
|
||||
"maxmem": live.get('maxmem') or vm.memory or 0,
|
||||
"maxdisk": live.get('maxdisk') or vm.disk_size or 0,
|
||||
"uptime": live.get('uptime'),
|
||||
"netin": live.get('netin'),
|
||||
"netout": live.get('netout'),
|
||||
})
|
||||
|
||||
return JSONResponse(content=result)
|
||||
|
||||
Reference in New Issue
Block a user