feat(#2391): btrfs SnapshotAgentSubvolume/DeleteAgentSnapshot priv ops

Adds the first missing piece from #2391's migration-gaps list: a
read-only btrfs snapshot priv op so hivectl migrate can freeze a
consistent point-in-time copy of an agent's state subvolume for
btrfs send, without stopping the live agent.

- PrivRequest::SnapshotAgentSubvolume / DeleteAgentSnapshot (hive-sh4re)
- hive-priv handlers: btrfs subvolume snapshot -r / delete, sibling
  dot-prefixed path (<AGENT_STATE_ROOT>/.<agent>.snapshot.<label>)
- hive-c0re::priv_client wrappers
- hivectl subvol snapshot / delete-snapshot verbs (no agent stop needed
  — btrfs snapshots are atomic against a live subvolume)

Does not yet wire actual btrfs send/receive or the hivectl migrate
verb — those stay tracked on #2391 as separate follow-up pieces.
This commit is contained in:
atlas 2026-07-14 17:52:08 +02:00 committed by mara
commit 7799e0762a
4 changed files with 219 additions and 0 deletions

View file

@ -439,6 +439,41 @@ pub async fn upgrade_agent_subvolume(agent_name: &str) -> Result<()> {
.await?)
}
/// Create a read-only btrfs snapshot of an agent's state subvolume (via
/// hive-priv as root) — the first step of `hivectl migrate`'s send/receive
/// path. Returns the snapshot's absolute host path. Fails if the agent's
/// state root isn't a subvolume yet, or a snapshot with the same
/// `snapshot_name` already exists.
///
/// # Errors
/// Returns an error if the hive-priv call fails, the state dir isn't a
/// btrfs subvolume, or the snapshot already exists.
pub async fn snapshot_agent_subvolume(agent_name: &str, snapshot_name: &str) -> Result<String> {
let (stdout, _) = check(
call(&PrivRequest::SnapshotAgentSubvolume {
agent_name: agent_name.to_owned(),
snapshot_name: snapshot_name.to_owned(),
})
.await?,
)?;
Ok(stdout)
}
/// Delete a previously-created read-only agent-state snapshot (cleanup
/// counterpart to [`snapshot_agent_subvolume`]). No-op if the snapshot
/// doesn't exist. Via hive-priv as root.
///
/// # Errors
/// Returns an error if the hive-priv call fails or the underlying
/// `btrfs subvolume delete` fails.
pub async fn delete_agent_snapshot(agent_name: &str, snapshot_name: &str) -> Result<()> {
ok(call(&PrivRequest::DeleteAgentSnapshot {
agent_name: agent_name.to_owned(),
snapshot_name: snapshot_name.to_owned(),
})
.await?)
}
/// Write `/etc/tmpfiles.d/hyperhive-agents.conf` for `agents` (logical names,
/// e.g. `"atlas"`) and immediately apply it with `systemd-tmpfiles --create`.
/// See [`PrivRequest::SyncAgentTmpfiles`] for the full semantics.