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:
parent
3a2cdaa37b
commit
9ff55399e5
5 changed files with 373 additions and 0 deletions
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Reference in a new issue