feat: real-time server CRUD sync and pve CLI auto-install
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Has been cancelled

Backend:
- servers.py: instant connectivity check + VM cache sync after create/update;
  broadcast server_added/updated/deleted events via WebSocket
- websocket_manager: add broadcast_event() helper for global fire-and-forget events

Frontend:
- use-nodes: invalidate topology, VM list and resources after server create/update/delete
- use-realtime-sync: new hook with useGlobalRealtimeSync subscribing to server_*
  WS events and invalidating relevant React Query caches
- AppLayout: mount useGlobalRealtimeSync globally

Deploy:
- deploy.sh: auto-install pve CLI during main/quick deploy (idempotent, --auto mode)
This commit is contained in:
markmorado
2026-05-11 05:31:52 +02:00
parent 742ff27fb4
commit 453c18f192
6 changed files with 284 additions and 11 deletions

View File

@@ -99,7 +99,49 @@ def create_proxmox_server(
db.add(server)
db.commit()
db.refresh(server)
# Сразу проверяем подключение, чтобы статус is_online отобразился без задержки
# в 30 секунд (период monitoring_worker.run_server_availability_check).
try:
client = _get_proxmox_client(server)
if client.is_connected():
server.update_status(True)
else:
server.update_status(False, "Failed to connect")
db.commit()
db.refresh(server)
except Exception as e:
logger.warning(f"Initial connectivity check failed for {server.name}: {e}")
try:
server.update_status(False, str(e))
db.commit()
db.refresh(server)
except Exception:
db.rollback()
# Если сервер онлайн — мгновенно подтягиваем VM/LXC в локальный кэш,
# чтобы NodeDetailPage и InstancesPage сразу увидели инстансы (без ожидания
# 10-секундного цикла vm_cache_sync).
if server.is_online:
try:
from ...workers.monitoring_worker import monitoring_worker
monitoring_worker.sync_vm_cache()
except Exception as e:
logger.warning(f"Initial VM cache sync failed for server {server.name}: {e}")
# Уведомляем все подключённые клиенты, что сервер добавлен — фронтенд
# сразу инвалидирует список серверов и список VM.
try:
from ...websocket_manager import broadcast_event
broadcast_event(
"server_added",
server_id=server.id,
name=server.name,
is_online=bool(server.is_online),
)
except Exception as e:
logger.debug(f"Failed to broadcast server_added: {e}")
logger.info(f"User {current_user.username} added Proxmox server: {server.name}")
return server
@@ -305,7 +347,19 @@ async def auto_setup_proxmox_server(
db.commit()
logger.info(f"User {current_user.username} auto-setup Proxmox cluster with {len(created_servers)} nodes")
# Мгновенный sync VM-кэша + WS-broadcast для real-time обновления UI
try:
from ...workers.monitoring_worker import monitoring_worker
monitoring_worker.sync_vm_cache()
except Exception as _e:
logger.debug(f"VM sync after auto-setup failed: {_e}")
try:
from ...websocket_manager import broadcast_event
broadcast_event("server_added", cluster=True, count=len(created_servers))
except Exception as _e:
logger.debug(f"Failed to broadcast server_added (cluster): {_e}")
return {
"cluster": True,
"nodes_count": len(created_servers),
@@ -334,7 +388,24 @@ async def auto_setup_proxmox_server(
db.refresh(server)
logger.info(f"User {current_user.username} auto-setup Proxmox server: {server.name}")
# Мгновенный sync VM-кэша + WS-broadcast для real-time обновления UI
try:
from ...workers.monitoring_worker import monitoring_worker
monitoring_worker.sync_vm_cache()
except Exception as _e:
logger.debug(f"VM sync after auto-setup failed: {_e}")
try:
from ...websocket_manager import broadcast_event
broadcast_event(
"server_added",
server_id=server.id,
name=server.name,
is_online=bool(server.is_online),
)
except Exception as _e:
logger.debug(f"Failed to broadcast server_added: {_e}")
return {
"cluster": False,
"id": server.id,
@@ -442,6 +513,50 @@ def update_proxmox_server(
db.refresh(server)
logger.info(f"User {current_user.username} updated Proxmox server: {server.name}")
# Если изменились параметры подключения — сразу проверяем доступность,
# чтобы статус обновился без 30-секундной задержки monitoring_worker.
creds_changed = any(
key in update_data
for key in ('password', 'api_token_name', 'api_token_value', 'api_user',
'ip_address', 'hostname', 'port', 'verify_ssl', 'use_password')
)
if creds_changed:
try:
client = _get_proxmox_client(server)
if client.is_connected():
server.update_status(True)
else:
server.update_status(False, "Failed to connect")
db.commit()
db.refresh(server)
if server.is_online:
try:
from ...workers.monitoring_worker import monitoring_worker
monitoring_worker.sync_vm_cache()
except Exception as _e:
logger.debug(f"VM sync after update failed: {_e}")
except Exception as e:
logger.warning(f"Connectivity check after update failed for {server.name}: {e}")
try:
server.update_status(False, str(e))
db.commit()
db.refresh(server)
except Exception:
db.rollback()
# Broadcast — фронтенд инвалидирует кэши списков серверов и инстансов
try:
from ...websocket_manager import broadcast_event
broadcast_event(
"server_updated",
server_id=server.id,
name=server.name,
is_online=bool(server.is_online),
)
except Exception as e:
logger.debug(f"Failed to broadcast server_updated: {e}")
return server
@@ -519,7 +634,18 @@ def delete_proxmox_server(
monitoring_worker.cleanup_server_state(server_id)
except Exception as e:
logger.warning(f"Failed to cleanup monitoring state for server {server_id}: {e}")
# Broadcast — фронтенд мгновенно убирает удалённый сервер и его инстансы
try:
from ...websocket_manager import broadcast_event
broadcast_event(
"server_deleted",
server_id=server_id,
name=server_name,
)
except Exception as e:
logger.debug(f"Failed to broadcast server_deleted: {e}")
return None

View File

@@ -167,3 +167,13 @@ def broadcast_task_update(user_id: int, event_type: str, task_data: dict) -> Non
"""
payload = {"type": event_type, "task": task_data}
run_async_safe(ws_manager.send_to_user(user_id, payload))
def broadcast_event(event_type: str, **fields) -> None:
"""
Fire-and-forget global broadcast of an event to all connected WebSocket
clients from a sync context. Used to push real-time invalidations such as
`server_added`, `server_updated`, `server_deleted`.
"""
payload = {"type": event_type, **fields}
run_async_safe(ws_manager.broadcast(payload))