Files
pvemanager/backend/app/models/tasks.py
markmorado 09186e23b9
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Has been cancelled
Deploy to Production / backend-tests (push) Has been cancelled
Deploy to Production / frontend-build (push) Has been cancelled
Frontend Checks / Build & typecheck (push) Has been cancelled
SonarCloud / SonarCloud Analysis (push) Has been cancelled
Deploy to Production / Trigger production update (push) Has been cancelled
Security audit: tenant isolation, injection fixes, hardening and performance
Security
- Enforce guest ownership and server assignment across Proxmox endpoints
  (VM/LXC actions, firewall, backups, tasks, bulk operations, deploy,
  LXC/ISO creation, golden template) and WebSocket channels
- Keep the panel's PVE root ticket server-side for consoles; single-use
  WebSocket tickets instead of the JWT in URLs; re-check access on open
  WebSocket bridges
- Validate identifiers that reach Proxmox API paths and node shell
  commands; reject backup archives / foreign volumes in create requests
- Close privilege escalation in user management, TOTP replay, reset-link
  poisoning, X-Forwarded-For spoofing, stored XSS (group icons, SVG logos)
- SSH host key pinning (TOFU), encrypted 2FA seeds and notification
  secrets, password changes end other sessions, redact secrets in logs
- Quotas count in-flight creations; 2FA failures count toward lockout

Fixes
- Consoles on non-default API ports, large App Store/script files,
  interrupted tasks after restart, impersonation lost on token refresh,
  HTTPExceptions swallowed into 500s, WebSocket client reconnects

Performance and process
- Fewer DB writes and queries on hot paths, lazy-loaded locales and
  heavy pages, off-loop blocking work, indexes (migrations 44-46)
- Deploy waits for backend tests and frontend build; frontend checks
  workflow; API bound to loopback without the SSL profile
2026-10-23 00:04:16 +05:00

177 lines
7.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from .base import *
class TaskQueue(Base):
"""Background task queue for bulk operations"""
__tablename__ = "task_queue"
id = Column(Integer, primary_key=True, index=True)
task_type = Column(String(50), nullable=False, index=True)
status = Column(String(20), nullable=False, default='pending', index=True)
user_id = Column(Integer, nullable=False, index=True)
total_items = Column(Integer, nullable=False, default=0)
completed_items = Column(Integer, nullable=False, default=0)
failed_items = Column(Integer, nullable=False, default=0)
task_data = Column(JSON, nullable=False)
results = Column(JSON, nullable=True)
error_message = Column(Text, nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
started_at = Column(DateTime(timezone=True), nullable=True)
completed_at = Column(DateTime(timezone=True), nullable=True)
__table_args__ = (
Index('idx_task_status_created', 'status', 'created_at'),
Index('idx_task_user_status', 'user_id', 'status'),
)
def __repr__(self):
return f"<TaskQueue(id={self.id}, type='{self.task_type}', status='{self.status}')>"
@property
def progress_percent(self) -> int:
if self.total_items == 0:
return 0
return int((self.completed_items + self.failed_items) / self.total_items * 100)
def to_dict(self) -> dict:
_desc_map = {
'bulk_start': 'Массовый запуск ВМ/LXC',
'bulk_stop': 'Массовая остановка ВМ/LXC',
'bulk_restart': 'Массовый перезапуск ВМ/LXC',
'bulk_delete': 'Массовое удаление ВМ/LXC',
'bulk_shutdown': 'Массовое выключение ВМ/LXC',
}
return {
'id': self.id,
'kind': 'bulk',
'user_id': self.user_id,
'task_type': self.task_type,
'description': _desc_map.get(self.task_type, self.task_type),
'status': self.status,
'total_items': self.total_items,
'completed_items': self.completed_items,
'failed_items': self.failed_items,
'processed_items': self.completed_items + self.failed_items,
'progress_percent': self.progress_percent,
'results': self.results,
'error_message': self.error_message,
'created_at': self.created_at.isoformat() if self.created_at else None,
'started_at': self.started_at.isoformat() if self.started_at else None,
'completed_at': self.completed_at.isoformat() if self.completed_at else None,
}
class ProxmoxTask(Base):
"""Tracks long-running Proxmox tasks identified by UPID."""
__tablename__ = "proxmox_tasks"
id = Column(Integer, primary_key=True, index=True)
upid = Column(String(200), nullable=False, index=True)
server_id = Column(Integer, ForeignKey("proxmox_servers.id", ondelete="SET NULL"), nullable=True, index=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
node = Column(String(100), nullable=True)
vmid = Column(Integer, nullable=True)
vm_type = Column(String(10), nullable=True)
action = Column(String(100), nullable=False)
description = Column(String(500), nullable=True)
status = Column(String(20), nullable=False, default="running", index=True)
exit_status = Column(String(100), nullable=True)
log_text = Column(Text, nullable=True)
# 0-100 percentage parsed from the task log (e.g. qmrestore "progress N%").
# NULL when the task type does not report progress.
progress = Column(Integer, nullable=True)
created_at = Column(DateTime(timezone=True), default=utcnow, nullable=False, index=True)
completed_at = Column(DateTime(timezone=True), nullable=True)
def to_dict(self) -> dict:
return {
'id': self.id,
'kind': 'proxmox',
'upid': self.upid,
'server_id': self.server_id,
'user_id': self.user_id,
'node': self.node,
'vmid': self.vmid,
'vm_type': self.vm_type,
'action': self.action,
'description': self.description,
'status': self.status,
'exit_status': self.exit_status,
# log_text is deliberately left out: this dict is sent in task lists
# (polled every few seconds) and on every WS task_update tick. The
# log is served on demand by /api/proxmox-tasks/{id}/log.
'progress': self.progress,
'created_at': self.created_at.isoformat() if self.created_at else None,
'completed_at': self.completed_at.isoformat() if self.completed_at else None,
}
def __repr__(self):
return f"<ProxmoxTask(id={self.id}, upid='{self.upid[:30]}...', status='{self.status}')>"
class DeployTask(Base):
"""Tracks async VM deployment tasks from templates"""
__tablename__ = "deploy_tasks"
id = Column(Integer, primary_key=True, index=True)
status = Column(String(20), nullable=False, default='pending', index=True)
kind = Column(String(30), nullable=False, default='deploy', index=True)
step = Column(String(200), nullable=True)
progress = Column(Integer, nullable=False, default=0)
name = Column(String(100), nullable=False)
template_id = Column(Integer, nullable=True)
server_id = Column(Integer, nullable=True)
target_server_id = Column(Integer, nullable=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
vmid = Column(Integer, nullable=True)
node = Column(String(100), nullable=True)
error_message = Column(Text, nullable=True)
created_at = Column(DateTime(timezone=True), default=utcnow, nullable=False, index=True)
started_at = Column(DateTime(timezone=True), nullable=True)
completed_at = Column(DateTime(timezone=True), nullable=True)
__table_args__ = (
Index('idx_deploy_task_user_status', 'user_id', 'status'),
)
def to_dict(self) -> dict:
kind = self.kind or 'deploy'
descriptions = {
'deploy': f"Создание {self.name}",
'vm_iso': f"Создание ВМ под ISO: {self.name}",
'reinstall': f"Переустановка {self.name}",
'clone': f"Клонирование {self.name}",
'change_password': f"Смена пароля: {self.name}",
'image_download': f"Загрузка образа: {self.name}",
'image_template': f"Шаблон из образа: {self.name}",
'remote_migrate': f"Remote-миграция: {self.name}",
}
return {
'id': self.id,
'kind': kind,
'status': self.status,
'step': self.step,
'current_step': self.step,
'progress': self.progress,
'progress_percent': self.progress,
'description': descriptions.get(kind, self.name),
'name': self.name,
'template_id': self.template_id,
'server_id': self.server_id,
'target_server_id': self.target_server_id,
'vmid': self.vmid,
'node': self.node,
'error_message': self.error_message,
'created_at': self.created_at.isoformat() if self.created_at else None,
'started_at': self.started_at.isoformat() if self.started_at else None,
'completed_at': self.completed_at.isoformat() if self.completed_at else None,
}
def __repr__(self):
return f"<DeployTask(id={self.id}, kind='{self.kind}', name='{self.name}', status='{self.status}')>"