Refactor backend: Split models and proxmox_client into modules, add alembic
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Failing after 2m27s
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Failing after 2m27s
This commit is contained in:
17
backend/app/models/__init__.py
Normal file
17
backend/app/models/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from .base import *
|
||||
from .auth import Role, user_servers, User, ActiveSession, LoginAttempt, BlockedIP, UserSSHKey
|
||||
from .proxmox import OSTemplateGroup, OSTemplate, ProxmoxServer, VMInstance, VMSnapshotArchive
|
||||
from .ipam import IPAMNetwork, IPAMPool, IPAMAllocation, IPAMHistory
|
||||
from .tasks import TaskQueue, ProxmoxTask, DeployTask
|
||||
from .settings import PanelSettings, SecuritySetting, Workspace, WorkspaceServer, WorkspaceUser
|
||||
from .misc import Notification, NotificationPreference, BackupJob, AuditLog
|
||||
|
||||
__all__ = [
|
||||
'Base', 'utcnow',
|
||||
'Role', 'user_servers', 'User', 'ActiveSession', 'LoginAttempt', 'BlockedIP', 'UserSSHKey',
|
||||
'OSTemplateGroup', 'OSTemplate', 'ProxmoxServer', 'VMInstance', 'VMSnapshotArchive',
|
||||
'IPAMNetwork', 'IPAMPool', 'IPAMAllocation', 'IPAMHistory',
|
||||
'TaskQueue', 'ProxmoxTask', 'DeployTask',
|
||||
'PanelSettings', 'SecuritySetting', 'Workspace', 'WorkspaceServer', 'WorkspaceUser',
|
||||
'Notification', 'NotificationPreference', 'BackupJob', 'AuditLog'
|
||||
]
|
||||
204
backend/app/models/auth.py
Normal file
204
backend/app/models/auth.py
Normal file
@@ -0,0 +1,204 @@
|
||||
from .base import *
|
||||
|
||||
# Association table: User <-> ProxmoxServer (assigned servers)
|
||||
user_servers = Table(
|
||||
'user_servers',
|
||||
Base.metadata,
|
||||
Column('user_id', Integer, ForeignKey('users.id', ondelete='CASCADE'), primary_key=True),
|
||||
Column('server_id', Integer, ForeignKey('proxmox_servers.id', ondelete='CASCADE'), primary_key=True)
|
||||
)
|
||||
|
||||
class Role(Base):
|
||||
"""User roles with permissions"""
|
||||
__tablename__ = "roles"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(50), unique=True, nullable=False, index=True)
|
||||
display_name = Column(String(100), nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
permissions = Column(JSON, nullable=False, default={})
|
||||
is_system = Column(Boolean, default=False, nullable=False) # System roles can't be deleted
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||
|
||||
# Relationships
|
||||
users = relationship("User", back_populates="role")
|
||||
|
||||
def has_permission(self, permission: str) -> bool:
|
||||
"""Check if role has specific permission"""
|
||||
if not self.permissions:
|
||||
return False
|
||||
return self.permissions.get(permission, False)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Role(id={self.id}, name='{self.name}')>"
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
username = Column(String(50), unique=True, nullable=False, index=True)
|
||||
email = Column(String(255), unique=True, nullable=False, index=True)
|
||||
hashed_password = Column(String(255), nullable=False)
|
||||
full_name = Column(String(100), nullable=True)
|
||||
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
is_admin = Column(Boolean, default=False, nullable=False) # Legacy, use role instead
|
||||
|
||||
# Role-based access
|
||||
role_id = Column(Integer, ForeignKey("roles.id"), nullable=True, index=True)
|
||||
role = relationship("Role", back_populates="users", lazy="joined")
|
||||
|
||||
# SSH Public Key for VM/LXC deployment
|
||||
ssh_public_key = Column(Text, nullable=True)
|
||||
|
||||
# Security fields
|
||||
failed_login_attempts = Column(Integer, default=0, nullable=False)
|
||||
locked_until = Column(DateTime(timezone=True), nullable=True)
|
||||
last_password_change = Column(DateTime(timezone=True), nullable=True)
|
||||
require_password_change = Column(Boolean, default=False, nullable=False)
|
||||
two_factor_enabled = Column(Boolean, default=False, nullable=False)
|
||||
two_factor_secret = Column(String(100), nullable=True)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||
last_login = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Relationships
|
||||
sessions = relationship("ActiveSession", back_populates="user", cascade="all, delete-orphan")
|
||||
assigned_servers = relationship("ProxmoxServer", secondary=user_servers, back_populates="assigned_users", lazy="select")
|
||||
|
||||
def has_permission(self, permission: str) -> bool:
|
||||
"""
|
||||
Check if user has specific permission.
|
||||
Uses new RBAC engine with legacy format support.
|
||||
"""
|
||||
try:
|
||||
from ..rbac import PermissionEngine
|
||||
return PermissionEngine.has_permission(self, permission)
|
||||
except ImportError:
|
||||
if self.is_admin:
|
||||
return True
|
||||
if self.role:
|
||||
return self.role.has_permission(permission)
|
||||
return False
|
||||
|
||||
def get_permissions(self) -> set:
|
||||
"""Get all effective permissions for this user"""
|
||||
try:
|
||||
from ..rbac import PermissionEngine
|
||||
return PermissionEngine.get_user_permissions(self)
|
||||
except ImportError:
|
||||
if self.is_admin:
|
||||
return set() # Admin has all
|
||||
if self.role and self.role.permissions:
|
||||
return {k for k, v in self.role.permissions.items() if v}
|
||||
return set()
|
||||
|
||||
def is_locked(self) -> bool:
|
||||
"""Check if account is locked"""
|
||||
if self.locked_until and make_tz_aware(self.locked_until) > utcnow():
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def role_name(self) -> str:
|
||||
"""Get role name"""
|
||||
if self.role:
|
||||
return self.role.name
|
||||
return "admin" if self.is_admin else "user"
|
||||
|
||||
def __repr__(self):
|
||||
return f"<User(id={self.id}, username='{self.username}', is_admin={self.is_admin})>"
|
||||
|
||||
class ActiveSession(Base):
|
||||
"""Active user sessions for single-session enforcement"""
|
||||
__tablename__ = "active_sessions"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
||||
session_token = Column(String(64), unique=True, nullable=False, index=True)
|
||||
ip_address = Column(String(45), nullable=True)
|
||||
user_agent = Column(String(500), nullable=True)
|
||||
device_info = Column(String(200), nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
expires_at = Column(DateTime(timezone=True), nullable=False)
|
||||
last_activity = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", back_populates="sessions")
|
||||
|
||||
def is_expired(self) -> bool:
|
||||
"""Check if session is expired"""
|
||||
from datetime import timezone
|
||||
now = datetime.now(timezone.utc)
|
||||
if self.expires_at is None:
|
||||
return True
|
||||
return now > make_tz_aware(self.expires_at)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ActiveSession(id={self.id}, user_id={self.user_id}, active={self.is_active})>"
|
||||
|
||||
class LoginAttempt(Base):
|
||||
"""Login attempts for brute-force protection"""
|
||||
__tablename__ = "login_attempts"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
username = Column(String(100), nullable=True, index=True)
|
||||
ip_address = Column(String(45), nullable=False, index=True)
|
||||
user_agent = Column(String(500), nullable=True)
|
||||
success = Column(Boolean, default=False, nullable=False)
|
||||
failure_reason = Column(String(200), nullable=True)
|
||||
attempted_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<LoginAttempt(id={self.id}, ip={self.ip_address}, success={self.success})>"
|
||||
|
||||
class BlockedIP(Base):
|
||||
"""Blocked IP addresses"""
|
||||
__tablename__ = "blocked_ips"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
ip_address = Column(String(45), unique=True, nullable=False, index=True)
|
||||
reason = Column(String(500), nullable=True)
|
||||
blocked_by = Column(String(100), nullable=True)
|
||||
blocked_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
expires_at = Column(DateTime(timezone=True), nullable=True)
|
||||
is_permanent = Column(Boolean, default=False, nullable=False)
|
||||
attempts_count = Column(Integer, default=0, nullable=False)
|
||||
|
||||
def is_blocked(self) -> bool:
|
||||
"""Check if IP is currently blocked"""
|
||||
if self.is_permanent:
|
||||
return True
|
||||
if self.expires_at and make_tz_aware(self.expires_at) > utcnow():
|
||||
return True
|
||||
return False
|
||||
|
||||
def __repr__(self):
|
||||
return f"<BlockedIP(id={self.id}, ip={self.ip_address})>"
|
||||
|
||||
class UserSSHKey(Base):
|
||||
"""SSH key attached to a user account."""
|
||||
__tablename__ = "user_ssh_keys"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
name = Column(String(100), nullable=False)
|
||||
public_key = Column(Text, nullable=False)
|
||||
private_key = Column(EncryptedString(8000), nullable=True)
|
||||
fingerprint = Column(String(100), nullable=True, index=True)
|
||||
comment = Column(String(255), nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
|
||||
user = relationship("User", backref="ssh_keys_pool")
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_user_ssh_keys_user', 'user_id'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<UserSSHKey(id={self.id}, user_id={self.user_id}, name='{self.name}')>"
|
||||
16
backend/app/models/base.py
Normal file
16
backend/app/models/base.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from sqlalchemy import Boolean, Column, Integer, BigInteger, String, DateTime, Text, Index, JSON, ForeignKey, Table
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
from ..db import Base
|
||||
from ..crypto import EncryptedString
|
||||
from ..config import utcnow, make_tz_aware
|
||||
|
||||
# Just exposing them for the other models to import easily
|
||||
__all__ = [
|
||||
'Base', 'EncryptedString', 'utcnow', 'make_tz_aware',
|
||||
'Boolean', 'Column', 'Integer', 'BigInteger', 'String', 'DateTime', 'Text', 'Index', 'JSON', 'ForeignKey', 'Table',
|
||||
'func', 'relationship', 'datetime', 'timezone', 'Optional'
|
||||
]
|
||||
139
backend/app/models/ipam.py
Normal file
139
backend/app/models/ipam.py
Normal file
@@ -0,0 +1,139 @@
|
||||
from .base import *
|
||||
|
||||
class IPAMNetwork(Base):
|
||||
"""Networks/subnets for IPAM"""
|
||||
__tablename__ = "ipam_networks"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(100), nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
|
||||
network = Column(String(18), nullable=False, index=True)
|
||||
gateway = Column(String(45), nullable=True)
|
||||
vlan_id = Column(Integer, nullable=True)
|
||||
|
||||
dns_primary = Column(String(45), nullable=True)
|
||||
dns_secondary = Column(String(45), nullable=True)
|
||||
dns_domain = Column(String(255), nullable=True)
|
||||
|
||||
proxmox_server_id = Column(Integer, nullable=True, index=True)
|
||||
proxmox_node = Column(String(100), nullable=True)
|
||||
proxmox_bridge = Column(String(20), nullable=True)
|
||||
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_ipam_network_name', 'name'),
|
||||
Index('idx_ipam_network_active', 'is_active'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<IPAMNetwork(id={self.id}, name='{self.name}', network='{self.network}')>"
|
||||
|
||||
class IPAMPool(Base):
|
||||
"""IP pools inside a network"""
|
||||
__tablename__ = "ipam_pools"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
network_id = Column(Integer, nullable=False, index=True)
|
||||
|
||||
name = Column(String(100), nullable=False)
|
||||
pool_type = Column(String(20), default="static", nullable=False)
|
||||
|
||||
range_start = Column(String(45), nullable=False)
|
||||
range_end = Column(String(45), nullable=False)
|
||||
|
||||
auto_assign = Column(Boolean, default=True, nullable=False)
|
||||
|
||||
description = Column(Text, nullable=True)
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_ipam_pool_network', 'network_id'),
|
||||
Index('idx_ipam_pool_type', 'pool_type'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<IPAMPool(id={self.id}, name='{self.name}', range='{self.range_start}-{self.range_end}')>"
|
||||
|
||||
class IPAMAllocation(Base):
|
||||
"""Allocated IP addresses"""
|
||||
__tablename__ = "ipam_allocations"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
network_id = Column(Integer, nullable=False, index=True)
|
||||
pool_id = Column(Integer, nullable=True, index=True)
|
||||
|
||||
ip_address = Column(String(45), nullable=False, unique=True, index=True)
|
||||
mac_address = Column(String(17), nullable=True)
|
||||
|
||||
resource_type = Column(String(20), nullable=True)
|
||||
resource_id = Column(Integer, nullable=True)
|
||||
resource_name = Column(String(100), nullable=True)
|
||||
|
||||
proxmox_server_id = Column(Integer, nullable=True, index=True)
|
||||
proxmox_vmid = Column(Integer, nullable=True)
|
||||
proxmox_node = Column(String(100), nullable=True)
|
||||
|
||||
status = Column(String(20), default="allocated", nullable=False)
|
||||
allocation_type = Column(String(20), default="static", nullable=False)
|
||||
|
||||
hostname = Column(String(255), nullable=True, index=True)
|
||||
fqdn = Column(String(255), nullable=True)
|
||||
dns_ptr_record = Column(Boolean, default=False, nullable=False)
|
||||
|
||||
allocated_by = Column(String(100), nullable=True)
|
||||
allocated_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
expires_at = Column(DateTime(timezone=True), nullable=True)
|
||||
last_seen = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
notes = Column(Text, nullable=True)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_ipam_alloc_network', 'network_id'),
|
||||
Index('idx_ipam_alloc_resource', 'resource_type', 'resource_id'),
|
||||
Index('idx_ipam_alloc_proxmox', 'proxmox_server_id', 'proxmox_vmid'),
|
||||
Index('idx_ipam_alloc_status', 'status'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<IPAMAllocation(id={self.id}, ip='{self.ip_address}', resource='{self.resource_name}')>"
|
||||
|
||||
class IPAMHistory(Base):
|
||||
"""History of IPAM changes"""
|
||||
__tablename__ = "ipam_history"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
|
||||
ip_address = Column(String(45), nullable=False, index=True)
|
||||
network_id = Column(Integer, nullable=True, index=True)
|
||||
|
||||
action = Column(String(50), nullable=False)
|
||||
|
||||
old_value = Column(JSON, nullable=True)
|
||||
new_value = Column(JSON, nullable=True)
|
||||
|
||||
resource_type = Column(String(20), nullable=True)
|
||||
resource_id = Column(Integer, nullable=True)
|
||||
resource_name = Column(String(100), nullable=True)
|
||||
|
||||
performed_by = Column(String(100), nullable=True)
|
||||
performed_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
|
||||
notes = Column(Text, nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_ipam_history_ip', 'ip_address'),
|
||||
Index('idx_ipam_history_action', 'action'),
|
||||
Index('idx_ipam_history_date', 'performed_at'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<IPAMHistory(id={self.id}, ip='{self.ip_address}', action='{self.action}')>"
|
||||
200
backend/app/models/misc.py
Normal file
200
backend/app/models/misc.py
Normal file
@@ -0,0 +1,200 @@
|
||||
from .base import *
|
||||
|
||||
class Notification(Base):
|
||||
"""User notifications"""
|
||||
__tablename__ = "notifications"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
||||
type = Column(String(50), nullable=False)
|
||||
level = Column(String(20), nullable=False)
|
||||
title = Column(String(255), nullable=False)
|
||||
message = Column(Text)
|
||||
data = Column(JSON)
|
||||
link = Column(String(500))
|
||||
source = Column(String(50))
|
||||
source_id = Column(String(100))
|
||||
read = Column(Boolean, default=False)
|
||||
read_at = Column(DateTime(timezone=True))
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
expires_at = Column(DateTime(timezone=True))
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"user_id": self.user_id,
|
||||
"type": self.type,
|
||||
"level": self.level,
|
||||
"title": self.title,
|
||||
"message": self.message,
|
||||
"data": self.data,
|
||||
"link": self.link,
|
||||
"source": self.source,
|
||||
"source_id": self.source_id,
|
||||
"read": self.read,
|
||||
"read_at": self.read_at.isoformat() if self.read_at else None,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"expires_at": self.expires_at.isoformat() if self.expires_at else None,
|
||||
}
|
||||
|
||||
class NotificationPreference(Base):
|
||||
"""User notification preferences"""
|
||||
__tablename__ = "notification_preferences"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), unique=True, nullable=False)
|
||||
enabled = Column(Boolean, default=True)
|
||||
email_enabled = Column(Boolean, default=False)
|
||||
email_critical_only = Column(Boolean, default=True)
|
||||
telegram_enabled = Column(Boolean, default=False)
|
||||
telegram_chat_id = Column(String(100))
|
||||
webhook_url = Column(String(500))
|
||||
notification_levels = Column(JSON, default=["critical", "warning", "info", "success"])
|
||||
notification_types = Column(JSON, default=["vm_status", "resource_alert", "system"])
|
||||
quiet_hours_start = Column(String(5))
|
||||
quiet_hours_end = Column(String(5))
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"user_id": self.user_id,
|
||||
"enabled": self.enabled,
|
||||
"email_enabled": self.email_enabled,
|
||||
"email_critical_only": self.email_critical_only,
|
||||
"telegram_enabled": self.telegram_enabled,
|
||||
"telegram_chat_id": self.telegram_chat_id,
|
||||
"webhook_url": self.webhook_url,
|
||||
"notification_levels": self.notification_levels,
|
||||
"notification_types": self.notification_types,
|
||||
"quiet_hours_start": self.quiet_hours_start,
|
||||
"quiet_hours_end": self.quiet_hours_end,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
||||
}
|
||||
|
||||
class BackupJob(Base):
|
||||
"""Scheduled backup job"""
|
||||
__tablename__ = "backup_jobs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
server_id = Column(Integer, ForeignKey("proxmox_servers.id", ondelete="CASCADE"), nullable=False)
|
||||
node = Column(String(100), nullable=False)
|
||||
vmids = Column(JSON, nullable=False, default=[])
|
||||
storage = Column(String(100), nullable=False)
|
||||
|
||||
mode = Column(String(20), default="snapshot", nullable=False)
|
||||
compress = Column(String(20), default="zstd", nullable=False)
|
||||
notes = Column(String(500), nullable=True)
|
||||
|
||||
keep_last = Column(Integer, default=3, nullable=False)
|
||||
keep_daily = Column(Integer, default=7, nullable=False)
|
||||
keep_weekly = Column(Integer, default=4, nullable=False)
|
||||
keep_monthly = Column(Integer, default=6, nullable=False)
|
||||
|
||||
cron_expression = Column(String(100), nullable=False)
|
||||
enabled = Column(Boolean, default=True, nullable=False)
|
||||
|
||||
owner_id = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||
last_run_at = Column(DateTime(timezone=True), nullable=True)
|
||||
last_status = Column(String(20), nullable=True)
|
||||
last_error = Column(Text, nullable=True)
|
||||
last_upid = Column(String(200), nullable=True)
|
||||
|
||||
server = relationship("ProxmoxServer", backref="backup_jobs")
|
||||
owner = relationship("User", foreign_keys=[owner_id])
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_backup_jobs_server', 'server_id'),
|
||||
Index('idx_backup_jobs_enabled', 'enabled'),
|
||||
Index('idx_backup_jobs_owner', 'owner_id'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<BackupJob(id={self.id}, server_id={self.server_id}, cron='{self.cron_expression}')>"
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
'id': self.id,
|
||||
'server_id': self.server_id,
|
||||
'node': self.node,
|
||||
'vmids': self.vmids,
|
||||
'storage': self.storage,
|
||||
'mode': self.mode,
|
||||
'compress': self.compress,
|
||||
'notes': self.notes,
|
||||
'keep_last': self.keep_last,
|
||||
'keep_daily': self.keep_daily,
|
||||
'keep_weekly': self.keep_weekly,
|
||||
'keep_monthly': self.keep_monthly,
|
||||
'cron_expression': self.cron_expression,
|
||||
'enabled': self.enabled,
|
||||
'owner_id': self.owner_id,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
||||
'last_run_at': self.last_run_at.isoformat() if self.last_run_at else None,
|
||||
'last_status': self.last_status,
|
||||
'last_error': self.last_error,
|
||||
'last_upid': self.last_upid,
|
||||
}
|
||||
|
||||
class AuditLog(Base):
|
||||
"""Логи аудита системы"""
|
||||
__tablename__ = "audit_logs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
|
||||
level = Column(String(20), nullable=False, index=True)
|
||||
category = Column(String(50), nullable=False, index=True)
|
||||
|
||||
action = Column(String(100), nullable=False, index=True)
|
||||
message = Column(Text, nullable=False)
|
||||
|
||||
request_id = Column(String(36), nullable=True, index=True)
|
||||
session_id = Column(String(64), nullable=True, index=True)
|
||||
|
||||
user_id = Column(Integer, nullable=True, index=True)
|
||||
username = Column(String(100), nullable=True, index=True)
|
||||
ip_address = Column(String(45), nullable=True, index=True)
|
||||
user_agent = Column(String(500), nullable=True)
|
||||
geo_location = Column(String(100), nullable=True)
|
||||
|
||||
resource_type = Column(String(50), nullable=True, index=True)
|
||||
resource_id = Column(String(100), nullable=True)
|
||||
resource_name = Column(String(200), nullable=True)
|
||||
server_id = Column(Integer, nullable=True, index=True)
|
||||
server_name = Column(String(100), nullable=True)
|
||||
node_name = Column(String(100), nullable=True)
|
||||
|
||||
details = Column(JSON, nullable=True)
|
||||
request_body = Column(JSON, nullable=True)
|
||||
response_body = Column(JSON, nullable=True)
|
||||
|
||||
request_method = Column(String(10), nullable=True)
|
||||
request_path = Column(String(500), nullable=True)
|
||||
query_params = Column(String(1000), nullable=True)
|
||||
response_status = Column(Integer, nullable=True)
|
||||
duration_ms = Column(Integer, nullable=True)
|
||||
|
||||
success = Column(Boolean, default=True, nullable=False)
|
||||
error_message = Column(Text, nullable=True)
|
||||
error_traceback = Column(Text, nullable=True)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True)
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_audit_level_category', 'level', 'category'),
|
||||
Index('idx_audit_user_time', 'username', 'created_at'),
|
||||
Index('idx_audit_resource', 'resource_type', 'resource_id'),
|
||||
Index('idx_audit_created', 'created_at'),
|
||||
Index('idx_audit_request_id', 'request_id'),
|
||||
Index('idx_audit_ip', 'ip_address'),
|
||||
Index('idx_audit_server', 'server_id'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<AuditLog(id={self.id}, level='{self.level}', action='{self.action}')>"
|
||||
220
backend/app/models/proxmox.py
Normal file
220
backend/app/models/proxmox.py
Normal file
@@ -0,0 +1,220 @@
|
||||
from .base import *
|
||||
|
||||
class OSTemplateGroup(Base):
|
||||
"""Group of OS templates"""
|
||||
__tablename__ = "os_template_groups"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(100), nullable=False, index=True)
|
||||
icon = Column(String(200), nullable=True)
|
||||
description = Column(Text, nullable=True)
|
||||
sort_order = Column(Integer, nullable=False, default=0)
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<OSTemplateGroup(id={self.id}, name='{self.name}')>"
|
||||
|
||||
class OSTemplate(Base):
|
||||
"""OS Template"""
|
||||
__tablename__ = "os_templates"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
group_id = Column(Integer, nullable=False, index=True)
|
||||
server_id = Column(Integer, nullable=False, index=True)
|
||||
|
||||
name = Column(String(100), nullable=False)
|
||||
vmid = Column(Integer, nullable=True)
|
||||
vm_type = Column(String(10), nullable=False, default="qemu", server_default="qemu")
|
||||
volid = Column(String(500), nullable=True)
|
||||
node = Column(String(100), nullable=True)
|
||||
|
||||
source_node = Column(String(100), nullable=True)
|
||||
replicated_nodes = Column(JSON, nullable=True, default={})
|
||||
|
||||
default_cores = Column(Integer, nullable=False, default=1)
|
||||
default_memory = Column(Integer, nullable=False, default=1024)
|
||||
default_disk = Column(Integer, nullable=False, default=10)
|
||||
min_cores = Column(Integer, nullable=False, default=1)
|
||||
min_memory = Column(Integer, nullable=False, default=512)
|
||||
min_disk = Column(Integer, nullable=False, default=5)
|
||||
|
||||
description = Column(Text, nullable=True)
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
sort_order = Column(Integer, nullable=False, default=0)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_os_template_group', 'group_id'),
|
||||
Index('idx_os_template_server', 'server_id'),
|
||||
Index('idx_os_template_vmid', 'server_id', 'vmid'),
|
||||
)
|
||||
|
||||
def get_source_node(self) -> str:
|
||||
return self.source_node or self.node
|
||||
|
||||
def get_vmid_for_node(self, node_name: str) -> Optional[int]:
|
||||
source = self.get_source_node()
|
||||
if node_name == source:
|
||||
return self.vmid
|
||||
if self.replicated_nodes and node_name in self.replicated_nodes:
|
||||
return self.replicated_nodes[node_name]
|
||||
return None
|
||||
|
||||
def add_replicated_node(self, node_name: str, vmid: int):
|
||||
if not self.replicated_nodes:
|
||||
self.replicated_nodes = {}
|
||||
self.replicated_nodes[node_name] = vmid
|
||||
|
||||
def __repr__(self):
|
||||
return f"<OSTemplate(id={self.id}, name='{self.name}', vmid={self.vmid})>"
|
||||
|
||||
class ProxmoxServer(Base):
|
||||
"""Proxmox VE server"""
|
||||
__tablename__ = "proxmox_servers"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(100), nullable=False, index=True)
|
||||
hostname = Column(String(255), nullable=False, index=True)
|
||||
ip_address = Column(String(50), nullable=False, index=True)
|
||||
port = Column(Integer, nullable=False, default=8006)
|
||||
|
||||
api_user = Column(String(100), nullable=False, default="root@pam")
|
||||
api_token_name = Column(String(100), nullable=True)
|
||||
api_token_value = Column(String(255), nullable=True)
|
||||
|
||||
use_password = Column(Boolean, nullable=False, default=False)
|
||||
password = Column(EncryptedString(255), nullable=True)
|
||||
|
||||
verify_ssl = Column(Boolean, nullable=False, default=True)
|
||||
cluster_name = Column(String(100), nullable=True, index=True)
|
||||
|
||||
description = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||
last_check = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
is_online = Column(Boolean, nullable=True, default=None)
|
||||
last_error = Column(Text, nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_proxmox_status', 'is_online', 'last_check'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ProxmoxServer(id={self.id}, name='{self.name}', ip='{self.ip_address}')>"
|
||||
|
||||
@property
|
||||
def connection_info(self) -> dict:
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'hostname': self.hostname,
|
||||
'ip_address': self.ip_address,
|
||||
'port': self.port,
|
||||
'api_user': self.api_user,
|
||||
'verify_ssl': self.verify_ssl,
|
||||
'is_online': self.is_online,
|
||||
'last_check': self.last_check
|
||||
}
|
||||
|
||||
def update_status(self, is_online: bool, error: str = None):
|
||||
self.is_online = is_online
|
||||
self.last_check = utcnow()
|
||||
if error:
|
||||
self.last_error = error
|
||||
elif is_online:
|
||||
self.last_error = None
|
||||
|
||||
assigned_users = relationship("User", secondary="user_servers", back_populates="assigned_servers", lazy="select")
|
||||
|
||||
class VMInstance(Base):
|
||||
"""VM/Container instance cache"""
|
||||
__tablename__ = "vm_instances"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
server_id = Column(Integer, nullable=False, index=True)
|
||||
vmid = Column(Integer, nullable=False, index=True)
|
||||
node = Column(String(100), nullable=False)
|
||||
vm_type = Column(String(20), nullable=False)
|
||||
name = Column(String(100), nullable=False)
|
||||
|
||||
owner_id = Column(Integer, ForeignKey('users.id'), nullable=True, index=True)
|
||||
owner = relationship("User", backref="instances", foreign_keys=[owner_id])
|
||||
|
||||
status = Column(String(20), default='unknown')
|
||||
is_template = Column(Boolean, default=False)
|
||||
|
||||
cores = Column(Integer, nullable=True)
|
||||
memory = Column(BigInteger, nullable=True)
|
||||
disk_size = Column(BigInteger, nullable=True)
|
||||
|
||||
os_type = Column(String(50), nullable=True)
|
||||
|
||||
ip_address = Column(String(50), nullable=True)
|
||||
ip_prefix = Column(Integer, nullable=True, default=24)
|
||||
gateway = Column(String(50), nullable=True)
|
||||
nameserver = Column(String(50), nullable=True)
|
||||
|
||||
cloud_init_user = Column(String(100), nullable=True)
|
||||
cloud_init_password = Column(EncryptedString(255), nullable=True)
|
||||
ssh_keys = Column(Text, nullable=True)
|
||||
|
||||
template_id = Column(Integer, nullable=True)
|
||||
template_name = Column(String(100), nullable=True)
|
||||
|
||||
description = Column(Text, nullable=True)
|
||||
tags = Column(String(255), nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
||||
last_sync_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
extra_config = Column(JSON, nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_vm_instance_server_vmid', 'server_id', 'vmid', unique=True),
|
||||
Index('idx_vm_instance_active', 'server_id', 'deleted_at'),
|
||||
Index('idx_vm_instance_owner', 'owner_id'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<VMInstance(id={self.id}, server_id={self.server_id}, vmid={self.vmid}, name='{self.name}')>"
|
||||
|
||||
class VMSnapshotArchive(Base):
|
||||
"""Archive of VM/Container snapshots"""
|
||||
__tablename__ = "vm_snapshot_archives"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
|
||||
server_id = Column(Integer, nullable=False, index=True)
|
||||
server_name = Column(String(100), nullable=True)
|
||||
vmid = Column(Integer, nullable=False, index=True)
|
||||
vm_name = Column(String(100), nullable=True)
|
||||
vm_type = Column(String(20), nullable=False)
|
||||
node = Column(String(100), nullable=False)
|
||||
|
||||
snapname = Column(String(100), nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
snaptime = Column(BigInteger, nullable=True)
|
||||
parent = Column(String(100), nullable=True)
|
||||
|
||||
vmstate = Column(Boolean, default=False, nullable=False)
|
||||
|
||||
snapshot_config = Column(JSON, nullable=True)
|
||||
|
||||
deleted_by = Column(String(100), nullable=True)
|
||||
deletion_reason = Column(Text, nullable=True)
|
||||
archived_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_snapshot_archive_server_vmid', 'server_id', 'vmid'),
|
||||
Index('idx_snapshot_archive_archived', 'archived_at'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<VMSnapshotArchive(id={self.id}, vmid={self.vmid}, snapname='{self.snapname}')>"
|
||||
86
backend/app/models/settings.py
Normal file
86
backend/app/models/settings.py
Normal file
@@ -0,0 +1,86 @@
|
||||
from .base import *
|
||||
|
||||
class PanelSettings(Base):
|
||||
"""Panel settings"""
|
||||
__tablename__ = "panel_settings"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
key = Column(String(100), unique=True, nullable=False, index=True)
|
||||
value = Column(String(500), nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<PanelSettings(id={self.id}, key='{self.key}', value='{self.value}')>"
|
||||
|
||||
class SecuritySetting(Base):
|
||||
"""Security settings"""
|
||||
__tablename__ = "security_settings"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
key = Column(String(100), unique=True, nullable=False, index=True)
|
||||
value = Column(String(500), nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<SecuritySetting(key='{self.key}', value='{self.value}')>"
|
||||
|
||||
class Workspace(Base):
|
||||
"""Workspace — named group of Proxmox servers"""
|
||||
__tablename__ = "workspaces"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(100), unique=True, nullable=False, index=True)
|
||||
description = Column(Text, nullable=True)
|
||||
color = Column(String(20), nullable=True, default="#667eea")
|
||||
is_default = Column(Boolean, default=False, nullable=False)
|
||||
created_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||
|
||||
creator = relationship("User", foreign_keys=[created_by])
|
||||
workspace_servers = relationship("WorkspaceServer", back_populates="workspace", cascade="all, delete-orphan")
|
||||
workspace_users = relationship("WorkspaceUser", back_populates="workspace", cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Workspace(id={self.id}, name='{self.name}')>"
|
||||
|
||||
class WorkspaceServer(Base):
|
||||
"""Junction table: servers belonging to a workspace"""
|
||||
__tablename__ = "workspace_servers"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
workspace_id = Column(Integer, ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False)
|
||||
server_id = Column(Integer, ForeignKey("proxmox_servers.id", ondelete="CASCADE"), nullable=False)
|
||||
|
||||
workspace = relationship("Workspace", back_populates="workspace_servers")
|
||||
server = relationship("ProxmoxServer")
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_ws_servers_workspace', 'workspace_id'),
|
||||
Index('idx_ws_servers_server', 'server_id'),
|
||||
Index('idx_ws_servers_unique', 'workspace_id', 'server_id', unique=True),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<WorkspaceServer(workspace_id={self.workspace_id}, server_id={self.server_id})>"
|
||||
|
||||
class WorkspaceUser(Base):
|
||||
"""Junction table: users with access to a workspace"""
|
||||
__tablename__ = "workspace_users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
workspace_id = Column(Integer, ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False)
|
||||
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
||||
|
||||
workspace = relationship("Workspace", back_populates="workspace_users")
|
||||
user = relationship("User")
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_ws_users_workspace', 'workspace_id'),
|
||||
Index('idx_ws_users_user', 'user_id'),
|
||||
Index('idx_ws_users_unique', 'workspace_id', 'user_id', unique=True),
|
||||
)
|
||||
164
backend/app/models/tasks.py
Normal file
164
backend/app/models/tasks.py
Normal file
@@ -0,0 +1,164 @@
|
||||
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)
|
||||
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,
|
||||
'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}')>"
|
||||
Reference in New Issue
Block a user