feat(quotas): per-user resource limits for VM/LXC deployment
All checks were successful
Backend Tests / pytest (Python 3.12) (push) Successful in 54s
Deploy to Production / Trigger production update (push) Successful in 8s

Add per-user quotas (max instances, vCPU, RAM, disk) enforced at deploy
time and surfaced to users self-service.

Backend:
- UserQuota model (one-to-one, NULL = unlimited) + migration #28
- quota_service: usage from vm_instances cache, check_quota raises 429
- enforce in VM (templates.py) and LXC (lxc.py) deploy paths
- RBAC quota:view / quota:manage, granted to admin
- admin API GET/PUT /admin/api/users/{id}/quota
- self-service GET /settings/api/quota
- tighten VM list isolation via can_view_all_instances

Frontend:
- admin quota dialog in UsersPage with usage bars
- quota card on Settings page (own limits)
- remaining-quota hint in CreateInstanceWizard config step
- useUserQuota/useSetUserQuota, useMyQuota hooks; RU/EN locales

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 18:15:02 +05:00
parent d59d787bed
commit 35dcbfd18f
21 changed files with 707 additions and 67 deletions

View File

@@ -1,5 +1,5 @@
from .base import *
from .auth import Role, user_servers, User, ActiveSession, LoginAttempt, BlockedIP, UserSSHKey, PasswordResetToken
from .auth import Role, user_servers, User, ActiveSession, LoginAttempt, BlockedIP, UserSSHKey, PasswordResetToken, UserQuota
from .proxmox import OSTemplateGroup, OSTemplate, ProxmoxServer, VMInstance, VMSnapshotArchive
from .ipam import IPAMNetwork, IPAMPool, IPAMAllocation, IPAMHistory
from .tasks import TaskQueue, ProxmoxTask, DeployTask
@@ -10,7 +10,7 @@ from .catalog import ImageMirror
__all__ = [
'Base', 'utcnow',
'Role', 'user_servers', 'User', 'ActiveSession', 'LoginAttempt', 'BlockedIP', 'UserSSHKey', 'PasswordResetToken',
'Role', 'user_servers', 'User', 'ActiveSession', 'LoginAttempt', 'BlockedIP', 'UserSSHKey', 'PasswordResetToken', 'UserQuota',
'OSTemplateGroup', 'OSTemplate', 'ProxmoxServer', 'VMInstance', 'VMSnapshotArchive',
'IPAMNetwork', 'IPAMPool', 'IPAMAllocation', 'IPAMHistory',
'TaskQueue', 'ProxmoxTask', 'DeployTask',

View File

@@ -1,4 +1,5 @@
from .base import *
from sqlalchemy.orm import backref
# Association table: User <-> ProxmoxServer (assigned servers)
user_servers = Table(
@@ -116,6 +117,30 @@ class User(Base):
def __repr__(self):
return f"<User(id={self.id}, username='{self.username}', is_admin={self.is_admin})>"
class UserQuota(Base):
"""Per-user resource quota (limits).
A row is created only when an admin sets at least one limit. Absence of a
row — or a ``NULL`` value in any column — means *unlimited* for that metric.
Memory is stored in MB and disk in GB, matching the units used by the
``vm_instances`` cache (see monitoring worker conversions).
"""
__tablename__ = "user_quotas"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), unique=True, nullable=False, index=True)
max_instances = Column(Integer, nullable=True) # NULL = unlimited
max_cores = Column(Integer, nullable=True) # sum of vCPUs
max_memory_mb = Column(Integer, nullable=True) # sum of RAM in MB
max_disk_gb = Column(Integer, nullable=True) # sum of disk in GB
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)
user = relationship("User", backref=backref("quota", uselist=False, cascade="all, delete-orphan"))
def __repr__(self):
return f"<UserQuota(user_id={self.user_id})>"
class PasswordResetToken(Base):
"""One-time tokens for the self-service 'forgot password' flow.