2026-08-07 10:53:08 +02:00
|
|
|
# retoor <retoor@molodetz.nl>
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
|
|
|
|
|
from devplacepy.database import get_int_setting, get_table
|
|
|
|
|
|
|
|
|
|
RULES_TABLE = "workspace_quota_rules"
|
|
|
|
|
|
|
|
|
|
DEFAULTS: dict[str, int] = {
|
|
|
|
|
"max_workspaces": 2,
|
|
|
|
|
"max_tunnels": 5,
|
|
|
|
|
"disk_quota_mb": 2048,
|
|
|
|
|
"egress_quota_mb": 10240,
|
|
|
|
|
"idle_warn_minutes": 45,
|
|
|
|
|
"idle_stop_minutes": 60,
|
|
|
|
|
"retention_days": 14,
|
|
|
|
|
"purge_after_days": 7,
|
|
|
|
|
"disk_warn_percent": 80,
|
2026-08-10 00:23:20 +02:00
|
|
|
"cpu_millicores": 2000,
|
|
|
|
|
"memory_mb": 2048,
|
2026-08-07 10:53:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SETTING_KEYS: dict[str, str] = {
|
|
|
|
|
"max_workspaces": "workspace_max_per_user",
|
|
|
|
|
"max_tunnels": "workspace_max_tunnels",
|
|
|
|
|
"disk_quota_mb": "workspace_disk_quota_mb",
|
|
|
|
|
"egress_quota_mb": "workspace_egress_quota_mb",
|
|
|
|
|
"idle_warn_minutes": "workspace_idle_warn_minutes",
|
|
|
|
|
"idle_stop_minutes": "workspace_idle_stop_minutes",
|
|
|
|
|
"retention_days": "workspace_retention_days",
|
|
|
|
|
"purge_after_days": "workspace_purge_after_days",
|
|
|
|
|
"disk_warn_percent": "workspace_disk_warn_percent",
|
2026-08-10 00:23:20 +02:00
|
|
|
"cpu_millicores": "workspace_cpu_millicores",
|
|
|
|
|
"memory_mb": "workspace_memory_mb",
|
2026-08-07 10:53:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RULE_COLUMNS = (
|
|
|
|
|
"max_workspaces",
|
|
|
|
|
"max_tunnels",
|
|
|
|
|
"disk_quota_mb",
|
|
|
|
|
"egress_quota_mb",
|
|
|
|
|
"idle_stop_minutes",
|
|
|
|
|
"retention_days",
|
2026-08-10 00:23:20 +02:00
|
|
|
"cpu_millicores",
|
|
|
|
|
"memory_mb",
|
2026-08-07 10:53:08 +02:00
|
|
|
)
|
|
|
|
|
|
2026-08-10 00:23:20 +02:00
|
|
|
INSTANCE_OVERRIDE_COLUMNS = (
|
|
|
|
|
"cpu_millicores",
|
|
|
|
|
"memory_mb",
|
|
|
|
|
"disk_quota_mb",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_cpu(millicores: int) -> str:
|
|
|
|
|
if millicores <= 0:
|
|
|
|
|
return ""
|
|
|
|
|
return f"{millicores / 1000:.3f}".rstrip("0").rstrip(".")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_memory(megabytes: int) -> str:
|
|
|
|
|
if megabytes <= 0:
|
|
|
|
|
return ""
|
|
|
|
|
return f"{megabytes}m"
|
|
|
|
|
|
2026-08-07 10:53:08 +02:00
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class Limits:
|
|
|
|
|
max_workspaces: int
|
|
|
|
|
max_tunnels: int
|
|
|
|
|
disk_quota_mb: int
|
|
|
|
|
egress_quota_mb: int
|
|
|
|
|
idle_warn_minutes: int
|
|
|
|
|
idle_stop_minutes: int
|
|
|
|
|
retention_days: int
|
|
|
|
|
purge_after_days: int
|
|
|
|
|
disk_warn_percent: int
|
2026-08-10 00:23:20 +02:00
|
|
|
cpu_millicores: int
|
|
|
|
|
memory_mb: int
|
2026-08-07 10:53:08 +02:00
|
|
|
|
|
|
|
|
def disk_quota_bytes(self) -> int:
|
|
|
|
|
return self.disk_quota_mb * 1024 * 1024
|
|
|
|
|
|
|
|
|
|
def egress_quota_bytes(self) -> int:
|
|
|
|
|
return self.egress_quota_mb * 1024 * 1024
|
|
|
|
|
|
2026-08-10 00:23:20 +02:00
|
|
|
def cpu_limit(self) -> str:
|
|
|
|
|
return format_cpu(self.cpu_millicores)
|
|
|
|
|
|
|
|
|
|
def mem_limit(self) -> str:
|
|
|
|
|
return format_memory(self.memory_mb)
|
|
|
|
|
|
2026-08-07 10:53:08 +02:00
|
|
|
|
|
|
|
|
def _global_value(key: str) -> int:
|
|
|
|
|
return get_int_setting(SETTING_KEYS[key], DEFAULTS[key])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _rule_for(owner_kind: str, owner_id: str) -> dict | None:
|
|
|
|
|
if not owner_id:
|
|
|
|
|
return None
|
|
|
|
|
table = get_table(RULES_TABLE)
|
|
|
|
|
return table.find_one(owner_kind=owner_kind, owner_id=owner_id, deleted_at=None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def resolve(user_uid: str = "", instance: dict | None = None) -> Limits:
|
|
|
|
|
rule = _rule_for("user", user_uid) if user_uid else None
|
|
|
|
|
values: dict[str, int] = {}
|
|
|
|
|
for key in DEFAULTS:
|
|
|
|
|
value = _global_value(key)
|
|
|
|
|
if rule and key in RULE_COLUMNS:
|
|
|
|
|
override = rule.get(key)
|
|
|
|
|
if override:
|
|
|
|
|
value = int(override)
|
2026-08-10 00:23:20 +02:00
|
|
|
if instance and key in INSTANCE_OVERRIDE_COLUMNS:
|
2026-08-07 10:53:08 +02:00
|
|
|
instance_override = instance.get(f"workspace_{key}")
|
|
|
|
|
if instance_override:
|
|
|
|
|
value = int(instance_override)
|
|
|
|
|
values[key] = max(0, value)
|
|
|
|
|
if values["idle_warn_minutes"] >= values["idle_stop_minutes"]:
|
|
|
|
|
values["idle_warn_minutes"] = max(1, values["idle_stop_minutes"] - 1)
|
|
|
|
|
return Limits(**values)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def percent_used(used: int, quota: int) -> int:
|
|
|
|
|
if quota <= 0:
|
|
|
|
|
return 0
|
|
|
|
|
return min(100, int(used * 100 / quota))
|