Add target node and storage overrides to single and bulk restore flows, and track Proxmox task progress (parse_task_progress in the monitoring worker, new ProxmoxTask progress column + migration, enriched task API responses). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
169 lines
7.1 KiB
Python
169 lines
7.1 KiB
Python
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)
|
||
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}",
|
||
'reinstall': f"Переустановка {self.name}",
|
||
'clone': f"Клонирование {self.name}",
|
||
'change_password': f"Смена пароля: {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,
|
||
'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}')>"
|