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

@@ -61,6 +61,8 @@ DEFAULT_ROLES = [
"users.create": True,
"users.edit": True,
"users.delete": True,
"quota:view": True,
"quota:manage": True,
"roles.view": True,
"roles.manage": True,
"notifications.manage": True,
@@ -1401,6 +1403,33 @@ def migrate_password_reset_tokens(conn):
logger.info("✓ password_reset_tokens table created")
def migrate_user_quotas(conn):
"""Migration 28: Create user_quotas table for per-user resource limits.
No default rows are inserted — absence of a row (or a NULL column) means
unlimited for that metric.
"""
if table_exists(conn, 'user_quotas'):
logger.info("✓ user_quotas table already exists")
return
logger.info("Creating user_quotas table...")
conn.execute(text("""
CREATE TABLE user_quotas (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE,
max_instances INTEGER,
max_cores INTEGER,
max_memory_mb INTEGER,
max_disk_gb INTEGER,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
"""))
conn.execute(text("CREATE INDEX idx_user_quotas_user ON user_quotas(user_id)"))
logger.info("✓ user_quotas table created")
def run_all_migrations(engine, db_session=None):
"""
Run all migrations in order.
@@ -1631,6 +1660,14 @@ def run_all_migrations(engine, db_session=None):
logger.warning(f"Password reset tokens migration: {e}")
conn.rollback()
# Migration 28: user_quotas table
try:
migrate_user_quotas(conn)
conn.commit()
except Exception as e:
logger.warning(f"User quotas migration: {e}")
conn.rollback()
logger.info("=" * 50)
logger.info("✓ All migrations completed successfully")
logger.info("=" * 50)