The quota subject is a PVE user id (user@realm).
- Single-realm policy: every human maps to exactly one realm on cloud. Otherwise
alice@ldapandalice@oidcwould be two unrelated quota subjects. - For OIDC realms,
username-claimshould be a stable, human-readable claim (preferred_username), notsubjectoremail. It cannot be changed after users have been created. - Group/tenant-level aggregation is out of scope for the MVP (the model leaves room: a quota record could later attach to a group instead of a user).
Allocation-based, not usage-based: a user's consumption is the sum of configured values over all guests in their pool — a stopped VM with 16 GiB configured consumes 16 GiB of quota. (Same choice as the upstream RFC's "config" limits and every surveyed external project; runtime-usage limiting is a non-goal.)
Usage is computed live by summing the pool members' configs (the ProxmoxAAS approach) — no separate accounting database that can drift. The pool is the accounting boundary; RBAC guarantees everything a user creates lands in their pool (see pool-rbac.md).
| Dimension | Unit | Counted as |
|---|---|---|
cores |
vCPU | per guest: sockets × cores (QEMU), cores (LXC); summed |
memory |
MiB | per guest: configured memory (ballooning counts the max); summed |
disk.<storage> |
GiB | per named storage: sum of all volume sizes owned by the user's guests, including unused[n] volumes |
instances |
count | number of guests in the pool |
| Dimension | Notes |
|---|---|
net-rate |
aggregate of per-NIC rate values; requires every NIC to carry a rate (enforced at admission) |
cpu-types |
whitelist of allowed CPU models (+ per-model instance caps) |
pci |
whitelist of passthrough devices, usually per-node |
backups |
max backup count per guest or per user |
| Question | Upstream RFC's position | Our default |
|---|---|---|
Does CT swap count as memory? |
disputed in review ("too cgroupv1") | count memory only; add a separate swap dimension if ever needed |
| EFI disk / TPM state volumes? | unaddressed | count them (they occupy storage) |
| Snapshot space? | excluded (the RFC skipped disk entirely) | not counted in MVP; storage-layer hard quotas backstop it (P6) |
| Linked clones? | unaddressed | count full virtual size (conservative) |
- Global limit per dimension, with optional per-node overrides (the ProxmoxAAS model).
- Default deny: a user with no quota record has zero quota. Provisioning a user means writing their quota record.
- A quota record may also pin: allowed nodes, allowed storages, allowed bridges/VNets, and an optional VMID range (readable ownership + a hard instance cap).
The proxy reads a declarative config (a file in the MVP; pluggable backend later):
# quotas.yaml — draft v0
version: 0
defaults: {} # intentionally empty: default deny
users:
alice@ldap:
pool: uq-alice # must match the provisioned pool
cores: 16
memory-mib: 32768
instances: 8
disk-gib:
tank: 200 # per-storage caps; storages not listed = not allowed
nodes: # optional per-node overrides
node1:
cores: 8
bob@ldap:
pool: uq-bob
cores: 4
memory-mib: 8192
instances: 2
disk-gib:
tank: 50Admission rule (P4): for each dimension touched by a request, used + delta ≤ limit, evaluated under the user's serialization lock; a dimension absent from the user's record counts as 0 (deny).
The proxy's disk-gib cap is the soft limit (it produces a clean GUI reason). The hard backstop lives in the storage layer, so an over-allocation is refused even if the proxy is bypassed entirely (a direct API call from a node, a future unclassified endpoint, a race).
On ZFS the mapping is one dataset + one PVE storage per user:
<zpool>/uq-<user>withzfs set quota=<N>G—quota(notrefquota) bounds the dataset's children and snapshots; a thick zvol's refreservation also counts, so an over-size disk create fails at the ZFS layer.- a PVE
zfspoolstorageuq-<user>pointing at that dataset (content rootdir,images, pinned to the node). UQ-Storagegranted only on/storage/uq-<user>— never on a shared pool storage, or the hard cap is bypassable.- the quota record then uses the per-user storage id:
disk-gib: { uq-<user>: N }(soft cap == hard cap).
Provisioned by Cluster/scripts/40-provision-storage.sh. Ceph (rbd) would use a per-user pool with target_size/quota; not deployed here (the live host is ZFS).
This also backstops what config-based accounting cannot see: raw content allocations and snapshot space (the open questions above) are bounded by the dataset quota regardless of how they were created.