hive-c0re: hivectl subvol upgrade — migrate an agent state dir to a btrfs subvolume

New agents get a btrfs subvolume state root automatically when the host
FS is btrfs, but agents that predate that migration are left on plain
dirs and miss the subvolume feature set (snapshots, per-subvol
usage/quota, send/receive migration). Add an opt-in operator verb to
convert an existing plain-dir agent in place.

btrfs cannot promote a directory to a subvolume in place, so the new
privileged op stages a sibling subvolume mirroring the dir (create +
`cp -a --reflink=auto` preserving ownership/permissions/xattrs + match
the root's owner and mode), then atomically renames the original aside
and the subvolume into place, then removes the original. Any failure
before the swap leaves the original untouched; idempotent (no-op if
already a subvolume) and btrfs-gated.

The `hivectl subvol upgrade <agent> --yes` verb composes it client-side
like `restart`: stop the agent so its state bind-mount is released, run
the migration via hive-priv, then restart it — the restart is attempted
regardless of the migration outcome so a failed migration never leaves
the agent down.

- hive-sh4re: UpgradeAgentSubvolume priv request variant.
- hive-priv: the migration handler plus stage/cleanup helpers.
- hive-c0re: priv_client wrapper and the hivectl verb; regen CLI docs.
This commit is contained in:
atlas 2026-06-21 14:12:43 +02:00 committed by mara
commit 6b1dbebe5a
5 changed files with 351 additions and 0 deletions

View file

@ -368,6 +368,22 @@ pub async fn set_subvolume_quota(agent_name: &str, limit_bytes: Option<u64>) ->
.await?)
}
/// Convert an existing plain-dir agent state root into a btrfs subvolume in
/// place (operator opt-in; via hive-priv as root). The caller must stop the
/// agent first (so its state bind-mount is gone) and restart it after.
/// Idempotent: a no-op when the root is already a subvolume.
///
/// # Errors
/// Returns an error if the hive-priv call fails, the state dir is missing,
/// the FS isn't btrfs, or the migration (subvolume create / copy / swap)
/// fails — in which case the original dir is left untouched.
pub async fn upgrade_agent_subvolume(agent_name: &str) -> Result<()> {
ok(call(&PrivRequest::UpgradeAgentSubvolume {
agent_name: agent_name.to_owned(),
})
.await?)
}
/// Parse `(referenced, exclusive)` bytes from `btrfs qgroup show -f --raw`
/// output (a qgroup row is `<id-with-slash> <rfer> <excl> …`).
///