Files
pvemanager/backend/app/models/tasks.py
markmorado ebc692a324
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Has been cancelled
Deploy to Production / Trigger production update (push) Has been cancelled
feat(instances): create blank VMs for ISO installs and download ISOs
Add a third instance kind to the create wizard: a blank KVM VM with an
empty disk, the installation ISO mounted on ide2 and put first in the
boot order, so the guest boots straight into the installer over noVNC.
Picking a Windows guest presets OVMF, a SATA disk and an e1000 NIC,
which the installer sees without virtio drivers loaded.

Also teach the image downloader content=iso, so ISOs can be fetched
onto a node from the panel: by URL from the images page or from a saved
mirror (mirrors now accept kind=iso), and straight from the ISO step of
the wizard.

The bundled catalog stays qcow2-only on purpose — install ISOs have no
stable "latest" URLs, so hardcoded links would rot on every point release.
2026-07-24 05:00:51 +05:00

175 lines
7.5 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': self.log_text,
'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}')>"