hive-c0re: per-agent btrfs disk usage + optional quota (#1793)

Follow-up to the btrfs-subvolume migration. Operator-opt-in disk
accounting + quotas on agent state subvolumes via btrfs qgroups:

- three privileged ops (qgroup ops need root): EnsureBtrfsQuota
  (btrfs quota enable on the agent-state filesystem — statfs-gated,
  idempotent, no-op off btrfs), ReadSubvolumeUsage (btrfs qgroup show
  -f --raw for one agent), SetSubvolumeQuota (btrfs qgroup limit, or
  clear). Reuses the is_on_btrfs helper from the subvolume work.
- priv_client wrappers, incl parse_qgroup_usage -> (referenced,
  exclusive) bytes.
- hivectl 'quota' subcommand: enable / show [agent] / limit <agent>
  <size|none>, with a K/M/G/T size parser + human-readable output.

Quota is deliberately NOT auto-enabled: btrfs quota enable triggers a
full rescan that is I/O-heavy on a large filesystem, and the operator
should choose when to pay that. 'quota show' on a plain-dir agent (no
subvolume) reports no qgroup data rather than erroring.
This commit is contained in:
atlas 2026-06-19 14:10:49 +02:00 committed by mara
commit 9ff55399e5
5 changed files with 373 additions and 0 deletions

View file

@ -326,6 +326,52 @@ pub async fn delete_agent_subvolume(agent_name: &str) -> Result<()> {
.await?)
}
/// Enable btrfs qgroup accounting on the agent-state filesystem (operator
/// opt-in; prerequisite for usage reads + quotas). Idempotent; no-op off
/// btrfs. Via hive-priv (root).
pub async fn ensure_btrfs_quota() -> Result<()> {
ok(call(&PrivRequest::EnsureBtrfsQuota).await?)
}
/// Read an agent state subvolume's btrfs qgroup usage, returning
/// `(referenced_bytes, exclusive_bytes)`. Errors propagate (e.g. quota not
/// enabled) so the caller can surface them. Via hive-priv (root).
pub async fn read_subvolume_usage(agent_name: &str) -> Result<(u64, u64)> {
let (stdout, _) = check(
call(&PrivRequest::ReadSubvolumeUsage {
agent_name: agent_name.to_owned(),
})
.await?,
)?;
parse_qgroup_usage(&stdout).with_context(|| format!("parse qgroup usage for {agent_name}"))
}
/// Set or clear a btrfs qgroup size limit on an agent's state subvolume.
/// `limit_bytes = None` clears it. Requires quota enabled. Via hive-priv.
pub async fn set_subvolume_quota(agent_name: &str, limit_bytes: Option<u64>) -> Result<()> {
ok(call(&PrivRequest::SetSubvolumeQuota {
agent_name: agent_name.to_owned(),
limit_bytes,
})
.await?)
}
/// Parse `(referenced, exclusive)` bytes from `btrfs qgroup show -f --raw`
/// output. Skips the header rows and reads the last data row (a qgroup row
/// is `<id-with-slash> <rfer> <excl> …`).
fn parse_qgroup_usage(out: &str) -> Result<(u64, u64)> {
for line in out.lines().rev() {
let cols: Vec<&str> = line.split_whitespace().collect();
if cols.len() >= 3
&& cols[0].contains('/')
&& let (Ok(rfer), Ok(excl)) = (cols[1].parse::<u64>(), cols[2].parse::<u64>())
{
return Ok((rfer, excl));
}
}
bail!("no qgroup data row in `btrfs qgroup show` output: {out:?}")
}
fn check(resp: PrivResponse) -> Result<(String, String)> {
if resp.ok {
Ok((resp.stdout, resp.stderr))