hivectl: rename hivectl agents to hivectl agent <name> <verb>

This commit is contained in:
damocles 2026-07-27 19:00:44 +02:00 committed by mara
commit 03afbd1316
19 changed files with 367 additions and 525 deletions

View file

@ -1,15 +1,18 @@
//! `hivectl agents` — everything scoped to a managed agent: container
//! lifecycle over the host admin socket
//! (list/restart/restart-all/pause/resume/spawn/kill/destroy/rebuild/
//! set-parent/set-limits), plus the `quota` and `subvol` groups, whose
//! handlers live in their own modules.
//! `hivectl agent <name>` — everything scoped to ONE managed agent:
//! container lifecycle over the host admin socket (restart/pause/resume/
//! spawn/kill/destroy/rebuild/set-parent/set-limits/choom), plus the
//! `quota` and `subvol` groups, whose handlers live in their own modules.
//! `agents_list` (`hivectl list-agents`) is the one genuinely hive-wide
//! read that lives in this module too since it shares the same daemon
//! request as everything else here, even though it's dispatched from a
//! top-level `Cmd` variant, not `AgentCmd`.
use std::path::Path;
use anyhow::{Context as _, Result, bail};
use hive_host_sock::HostRequest;
use crate::cli::{AgentsCmd, QuotaCmd};
use crate::cli::{AgentCmd, AgentQuotaCmd};
use crate::dag_progress::wait_for_dags;
use crate::util::render;
@ -33,11 +36,11 @@ async fn agents_restart(socket: &Path, name: &str, no_wait: bool) -> Result<()>
}
}
/// `hivectl agents list` — fetch the per-agent status roster from the
/// `hivectl list-agents` — fetch the per-agent status roster from the
/// daemon (`HostRequest::AgentStatus`) and render it as a padded table,
/// or the raw JSON rows with `--json`. Reuses the dashboard's
/// `ContainerView` aggregation, so the CLI and the web UI never drift.
async fn agents_list(socket: &Path, json: bool) -> Result<()> {
pub(crate) async fn agents_list(socket: &Path, json: bool) -> Result<()> {
let resp = crate::client::request(socket, hive_host_sock::HostRequest::AgentStatus)
.await
.with_context(|| format!("connect to daemon socket {}", socket.display()))?;
@ -112,28 +115,7 @@ async fn agents_list(socket: &Path, json: bool) -> Result<()> {
Ok(())
}
async fn agents_restart_all(socket: &Path, no_wait: bool) -> Result<()> {
let resp = crate::client::request(socket, hive_host_sock::HostRequest::RestartAll)
.await
.with_context(|| format!("connect to daemon socket {}", socket.display()))?;
let agents = resp.agents.as_deref().unwrap_or(&[]);
if agents.is_empty() {
println!("restart-all: no managed containers found");
} else {
for a in agents {
println!("restart queued: {a}");
}
}
if !resp.ok {
bail!(
"restart-all: {}",
resp.error.as_deref().unwrap_or("unknown error")
);
}
wait_for_dags(socket, resp.queued_dags.unwrap_or_default(), no_wait).await
}
/// `hivectl agents pause|resume` — flip the agent's pause marker. Not a
/// `hivectl agent <name> pause|resume` — flip the agent's pause marker. Not a
/// DAG, so there's nothing to wait on: the daemon writes the marker and
/// the harness picks it up on its next poll.
async fn set_paused(socket: &Path, name: &str, paused: bool) -> Result<()> {
@ -159,41 +141,36 @@ async fn set_paused(socket: &Path, name: &str, paused: bool) -> Result<()> {
}
}
/// Dispatch `hivectl agents <verb>` — container lifecycle over the host
/// admin socket.
pub(crate) async fn run_agents(socket: &Path, cmd: AgentsCmd) -> Result<()> {
/// Dispatch `hivectl agent <name> <verb>` — container lifecycle over the
/// host admin socket, all scoped to the single `name` hoisted from the
/// parent command.
pub(crate) async fn run_agent(socket: &Path, name: &str, cmd: AgentCmd) -> Result<()> {
match cmd {
AgentsCmd::List { json } => agents_list(socket, json).await,
AgentsCmd::Restart { name, no_wait } => agents_restart(socket, &name, no_wait).await,
AgentsCmd::RestartAll { no_wait } => agents_restart_all(socket, no_wait).await,
AgentsCmd::Pause { name } => set_paused(socket, &name, true).await,
AgentsCmd::Resume { name } => set_paused(socket, &name, false).await,
AgentsCmd::Spawn { name } => {
let name = crate::util::parse_ident(&name)?;
AgentCmd::Restart { no_wait } => agents_restart(socket, name, no_wait).await,
AgentCmd::Pause => set_paused(socket, name, true).await,
AgentCmd::Resume => set_paused(socket, name, false).await,
AgentCmd::Spawn => {
let name = crate::util::parse_ident(name)?;
render(crate::client::request(socket, HostRequest::Spawn { name }).await?)
}
AgentsCmd::RequestSpawn { name } => {
let name = crate::util::parse_ident(&name)?;
AgentCmd::RequestSpawn => {
let name = crate::util::parse_ident(name)?;
render(crate::client::request(socket, HostRequest::RequestSpawn { name }).await?)
}
AgentsCmd::Kill { name } => {
let name = crate::util::parse_ident(&name)?;
AgentCmd::Kill => {
let name = crate::util::parse_ident(name)?;
render(crate::client::request(socket, HostRequest::Kill { name }).await?)
}
AgentsCmd::Destroy { name, purge } => {
let name = crate::util::parse_ident(&name)?;
AgentCmd::Destroy { purge } => {
let name = crate::util::parse_ident(name)?;
render(crate::client::request(socket, HostRequest::Destroy { name, purge }).await?)
}
AgentsCmd::Rebuild { name } => {
let name = crate::util::parse_ident(&name)?;
AgentCmd::Rebuild => {
let name = crate::util::parse_ident(name)?;
render(crate::client::request(socket, HostRequest::Rebuild { name }).await?)
}
AgentsCmd::SetParent {
child,
parent,
root,
} => {
let child = crate::util::parse_ident(&child)?;
AgentCmd::SetParent { parent, root } => {
let child = crate::util::parse_ident(name)?;
let new_parent = if root {
None
} else {
@ -204,16 +181,15 @@ pub(crate) async fn run_agents(socket: &Path, cmd: AgentsCmd) -> Result<()> {
.await?,
)
}
AgentsCmd::SetLimits {
name,
AgentCmd::SetLimits {
cpu_quota,
memory_max,
reset,
} => {
let name = crate::util::parse_ident(&name)?;
let name = crate::util::parse_ident(name)?;
// `--reset` is the only way to reach an all-`None` request;
// clap rejects a bare `set-limits <name>` with neither flag,
// so a forgotten value can't silently clear the overrides.
// clap rejects a bare `set-limits` with neither flag, so a
// forgotten value can't silently clear the overrides.
let (cpu_quota, memory_max) = if reset {
(None, None)
} else {
@ -231,13 +207,16 @@ pub(crate) async fn run_agents(socket: &Path, cmd: AgentsCmd) -> Result<()> {
.await?,
)
}
// `quota` and `subvol` keep their own modules — this arm is just
// the reparenting glue that moved them under `agents`.
AgentsCmd::Quota { cmd } => match cmd {
QuotaCmd::Enable => crate::quota::quota_enable(socket).await,
QuotaCmd::Show { name } => crate::quota::quota_show(socket, name.as_deref()).await,
QuotaCmd::Set { name, size } => crate::quota::quota_limit(socket, &name, &size).await,
AgentCmd::Choom { resume_session } => {
crate::choom::choom(socket, name, resume_session.as_deref()).await
}
// `quota` and `subvol` keep their own modules — these arms are
// just the reparenting glue that hoists `name` in from the
// parent `agent <name>` command.
AgentCmd::Quota { cmd } => match cmd {
AgentQuotaCmd::Show => crate::quota::quota_show(socket, name).await,
AgentQuotaCmd::Set { size } => crate::quota::quota_limit(socket, name, &size).await,
},
AgentsCmd::Subvol { cmd } => crate::subvol::dispatch_subvol(socket, cmd).await,
AgentCmd::Subvol { cmd } => crate::subvol::dispatch_subvol(socket, name, cmd).await,
}
}

View file

@ -1,4 +1,4 @@
//! `hivectl choom <agent>` — drop into an interactive Claude session inside
//! `hivectl agent <name> choom` — drop into an interactive Claude session inside
//! an agent container by exec-ing `machinectl shell` running claude as the
//! agent user, mirroring the harness's per-turn claude invocation.

View file

@ -17,8 +17,9 @@ recovery / debugging verbs.\
)]
pub struct Cli {
/// Path to the hive-c0re host admin socket, used by the daemon-assisted
/// verbs (`agents`, `stop`, `start`). Global: accepted before or after
/// the subcommand. Verbs that don't talk to the daemon ignore it.
/// verbs (`agent`, `list-agents`, `stop`, `start`). Global: accepted
/// before or after the subcommand. Verbs that don't talk to the daemon
/// ignore it.
#[arg(long, global = true, default_value = DEFAULT_HOST_SOCKET)]
pub(crate) socket: PathBuf,
#[command(subcommand)]
@ -61,14 +62,36 @@ pub enum Cmd {
#[command(subcommand)]
cmd: GatewayCmd,
},
/// Agent container management.
/// Lifecycle actions on ONE managed agent container. Needs the
/// hive-c0re daemon running.
///
/// Lifecycle actions on managed agent containers. Needs the hive-c0re
/// daemon running.
Agents {
/// Everything here targets a single named agent (`hivectl agent foo
/// restart`, `hivectl agent foo choom`, …) — anything that acts
/// hive-wide lives at the top level instead (`list-agents`,
/// `restart`/`stop`/`start` with a scope, `quota-enable`).
Agent {
/// Agent name (e.g. `damocles`, `iris`).
name: String,
#[command(subcommand)]
cmd: AgentsCmd,
cmd: AgentCmd,
},
/// Show all managed agents with their status and technical state.
///
/// Global — not scoped to one agent, so it lives at the top level
/// rather than under `hivectl agent <name>`. Needs the hive-c0re
/// daemon running.
ListAgents {
/// Emit the raw JSON rows instead of the padded table (for
/// scripting). The table is the default human-readable shape.
#[arg(long)]
json: bool,
},
/// Enable btrfs qgroup accounting on the agent-state filesystem.
///
/// Global one-shot toggle (not per-agent), hence top-level rather than
/// under `hivectl agent <name>`. Idempotent — safe to re-run. Once
/// enabled, `hivectl agent <name> quota show`/`quota set` work.
QuotaEnable,
/// Operator approval queue: list, approve, or deny pending requests.
///
/// Needs the hive-c0re daemon running.
@ -98,22 +121,6 @@ pub enum Cmd {
#[arg(long)]
wg_endpoint: Option<String>,
},
/// Open an interactive Claude session inside an agent container.
///
/// A fresh session by default, or resume a prior one. Requires root
/// and a running container.
Choom {
/// Agent name (e.g. `damocles`, `iris`).
name: String,
/// Resume a prior claude session by its session id, passed
/// through as `claude --resume <value>` (claude's `--continue`
/// takes no value — it resumes the cwd's latest session, which
/// is the harness's, so choom never uses it; this flag matches
/// the claude flag it maps to). Omit for a fresh blank session.
/// A value is required when the flag is given.
#[arg(long = "resume", value_name = "SESSION")]
resume_session: Option<String>,
},
/// Stop containers hive-wide in one operator action.
///
/// Bare `hivectl stop` stops everything; scope flags narrow it to
@ -435,128 +442,68 @@ pub enum WgCmd {
Status,
}
#[derive(Subcommand)]
pub enum QuotaCmd {
/// Enable btrfs qgroup accounting on the agent-state filesystem.
///
/// Run once before `show` / `limit`. No-op on non-btrfs hosts.
Enable,
/// Report per-agent disk usage from btrfs qgroups (all agents, or one
/// by name).
Show {
/// Agent to show (omit for all agents with a state subvolume).
name: Option<String>,
},
/// Set or clear an agent's disk-usage quota.
///
/// Named `set` rather than `set-quota` because the enclosing `quota`
/// group already carries the noun — `agents quota set iris 5G`. The
/// `set-<noun>` spelling stays for the flat verbs (`set-parent`,
/// `set-limits`), which have no group to inherit it from.
Set {
/// Agent whose state subvolume to limit.
name: String,
/// Size cap (`5G`, `500M`, `1073741824`) or `none` to clear.
size: String,
},
}
// Default host admin socket path. Shared with `hive-c0re`'s `main.rs`
// default via `hive_host_sock::HOST_SOCKET` — the daemon binds there
// and `hivectl agents` connects to it.
// and `hivectl` connects to it.
pub(crate) use hive_host_sock::HOST_SOCKET as DEFAULT_HOST_SOCKET;
/// Verbs under `hivectl agent <name> <verb>` — every one of these targets
/// the single agent named on the parent command, so none of them carry
/// their own `name` field.
#[derive(Subcommand)]
pub enum AgentsCmd {
/// Show all managed agents with their status and technical state.
List {
/// Emit the raw JSON rows instead of the padded table (for
/// scripting). The table is the default human-readable shape.
#[arg(long)]
json: bool,
},
/// Stop and start a single agent container without rebuilding config.
pub enum AgentCmd {
/// Stop and start this agent container without rebuilding config.
Restart {
/// Agent name (e.g. `damocles`, `ruth`).
name: String,
/// Return immediately after the restart DAG is queued.
#[arg(long)]
no_wait: bool,
},
/// Restart all managed agent containers.
RestartAll {
/// Return immediately after the restart DAGs are queued.
#[arg(long)]
no_wait: bool,
},
/// Park an agent's turn loop, leaving the container running.
/// Park this agent's turn loop, leaving the container running.
///
/// The harness stops driving turns but keeps serving its web UI and
/// MCP daemons, so the container, its mounts and its warm caches stay
/// up while it burns no tokens. Inbox messages queue unacked and the
/// backlog drains on `resume`. Sticky: it survives a restart, and
/// pausing a stopped agent makes it come up paused.
Pause {
/// Agent name.
name: String,
},
/// Resume a paused agent — it drains whatever queued up while parked.
Resume {
/// Agent name.
name: String,
},
/// Spawn a new agent container directly, bypassing the approval queue.
Pause,
/// Resume this paused agent — it drains whatever queued up while parked.
Resume,
/// Spawn this agent container directly, bypassing the approval queue.
///
/// Operator-on-the-host only; use `request-spawn` for an approval-gated
/// spawn.
Spawn {
/// Agent name (e.g. `iris`).
name: String,
},
Spawn,
/// Queue a spawn request for operator approval.
RequestSpawn {
/// Agent name.
name: String,
},
/// Stop a managed container (graceful).
Kill {
/// Agent name.
name: String,
},
/// Tear down a sub-agent container, keeping its state by default. No
/// undo.
RequestSpawn,
/// Stop this managed container (graceful).
Kill,
/// Tear down this sub-agent container, keeping its state by default.
/// No undo.
Destroy {
/// Agent name.
name: String,
/// Also wipe the agent's state dirs (config + creds + notes).
#[arg(long)]
purge: bool,
},
/// Apply pending config to a managed container.
Rebuild {
/// Agent name.
name: String,
},
/// Move an agent in the topology tree — under a new parent, or to root.
/// Apply pending config to this managed container.
Rebuild,
/// Move this agent in the topology tree — under a new parent, or to
/// root.
SetParent {
/// Agent to move.
child: String,
/// New parent agent name. Mutually exclusive with `--root`.
#[arg(long, conflicts_with = "root", required_unless_present = "root")]
parent: Option<String>,
/// Promote `child` to root (no parent).
/// Promote this agent to root (no parent).
#[arg(long)]
root: bool,
},
/// Declare an agent's CPU/memory limits, overriding the hive-wide defaults.
/// Declare this agent's CPU/memory limits, overriding the hive-wide
/// defaults.
///
/// Replaces the agent's whole override entry rather than merging into
/// it: any limit you don't pass returns to the hive-wide default. To
/// change one and keep the other, pass both. Disk is a separate
/// resource with its own group — see `agents quota`.
/// resource with its own group — see `quota`.
SetLimits {
/// Agent name.
name: String,
/// systemd `CPUQuota=` value, e.g. `400%` (100% = one full core).
#[arg(long, conflicts_with = "reset")]
cpu_quota: Option<String>,
@ -573,15 +520,29 @@ pub enum AgentsCmd {
)]
reset: bool,
},
/// Per-agent disk accounting + optional quotas via btrfs qgroups.
/// Open an interactive Claude session inside this agent's container.
///
/// Opt-in: enable qgroup accounting, then report per-agent usage or
/// cap an agent. No-op on non-btrfs hosts.
/// A fresh session by default, or resume a prior one. Requires root
/// and a running container.
Choom {
/// Resume a prior claude session by its session id, passed
/// through as `claude --resume <value>` (claude's `--continue`
/// takes no value — it resumes the cwd's latest session, which
/// is the harness's, so choom never uses it; this flag matches
/// the claude flag it maps to). Omit for a fresh blank session.
/// A value is required when the flag is given.
#[arg(long = "resume", value_name = "SESSION")]
resume_session: Option<String>,
},
/// This agent's disk accounting + optional quota via btrfs qgroups.
///
/// Needs `hivectl quota-enable` run once hive-wide first. No-op on
/// non-btrfs hosts.
Quota {
#[command(subcommand)]
cmd: QuotaCmd,
cmd: AgentQuotaCmd,
},
/// btrfs subvolume management for agent state dirs.
/// btrfs subvolume management for this agent's state dir.
///
/// Upgrade an existing plain-dir agent's state into a btrfs subvolume
/// so it gains snapshots and per-subvol usage/quota.
@ -591,6 +552,17 @@ pub enum AgentsCmd {
},
}
#[derive(Subcommand)]
pub enum AgentQuotaCmd {
/// Report this agent's disk usage from btrfs qgroups.
Show,
/// Set or clear this agent's disk-usage quota.
Set {
/// Size cap (`5G`, `500M`, `1073741824`) or `none` to clear.
size: String,
},
}
/// Operator approval queue: list, approve, or deny pending requests.
#[derive(Subcommand)]
pub enum ApprovalsCmd {
@ -615,14 +587,12 @@ pub enum SubvolCmd {
///
/// Bounces the agent to migrate its state, so it requires `--yes`.
Upgrade {
/// Agent name (e.g. `damocles`, `iris`).
name: String,
/// Confirm: this stops the agent, migrates its state dir, and
/// restarts it. Required — the command refuses without it.
#[arg(long)]
yes: bool,
},
/// Read-only snapshots of an agent's state subvolume.
/// Read-only snapshots of this agent's state subvolume.
Snapshot {
#[command(subcommand)]
cmd: SnapshotCmd,
@ -633,8 +603,6 @@ pub enum SubvolCmd {
pub enum SnapshotCmd {
/// Create a read-only snapshot (agent must already be a subvolume).
Create {
/// Agent name (e.g. `damocles`, `iris`).
name: String,
/// Snapshot label. Mandatory, and must start with `hive-` — the
/// prefix doubles as an allow-list hive-priv checks so only
/// hivectl-issued snapshot names can reach the `btrfs subvolume
@ -644,8 +612,6 @@ pub enum SnapshotCmd {
},
/// Delete a snapshot created by `subvol snapshot create`.
Delete {
/// Agent name the snapshot belongs to.
name: String,
/// Snapshot label passed to `subvol snapshot create --label`.
label: String,
},
@ -655,8 +621,6 @@ pub enum SnapshotCmd {
/// point-in-time backup: a full send with no `--parent` produces a
/// self-contained archive of the snapshot.
Send {
/// Agent name the snapshot belongs to.
name: String,
/// Snapshot label passed to `subvol snapshot create --label`.
label: String,
/// Optional parent snapshot label for an incremental send

View file

@ -2,15 +2,16 @@
//!
//! A thin client for the `hive-c0re` daemon: it speaks the host admin
//! socket protocol (`hive-host-sock`) and does NOT link the daemon crate.
//! Container lifecycle + the approval queue (`agents <spawn|kill|rebuild|
//! restart|…>`, `approvals <pending|approve|deny>`, `stop` / `start`) and
//! provisioning (`forge` / `matrix` / `github` / `gateway`) all forward to
//! the daemon, which owns the broker, the credentials, and the provisioning
//! logic — a running daemon is required for those. A couple of verbs work
//! off local host state directly instead (`wg` / `peer-config` read the mesh
//! key + TLS CA), so they don't need the socket. `choom` execs into a
//! container rather than asking the daemon to do anything, but still uses the
//! socket for its "is this an agent?" pre-flight — that answer lives in a
//! Container lifecycle + the approval queue (`agent <name> <spawn|kill|
//! rebuild|restart|choom|…>`, `list-agents`, `approvals <pending|approve|
//! deny>`, `stop` / `start`) and provisioning (`forge` / `matrix` /
//! `github` / `gateway`) all forward to the daemon, which owns the broker,
//! the credentials, and the provisioning logic — a running daemon is
//! required for those. A couple of verbs work off local host state
//! directly instead (`wg` / `peer-config` read the mesh key + TLS CA), so
//! they don't need the socket. `agent <name> choom` execs into a container
//! rather than asking the daemon to do anything, but still uses the socket
//! for its "is this an agent?" pre-flight — that answer lives in a
//! directory only the daemon's user can read.
//!
//! One module per subcommand family (see the `mod` list below); `main` is
@ -40,13 +41,12 @@ use open::open_url;
mod wg;
use wg::{peer_config, require_hive_domain, wg_init, wg_peer, wg_status};
mod choom;
use choom::choom;
mod github;
use github::github_set_token;
mod forge;
use forge::{forge_create_user, forge_reconcile_config};
mod agents;
use agents::run_agents;
use agents::{agents_list, run_agent};
mod power;
use power::{restart, start, stop};
mod approvals;
@ -93,7 +93,9 @@ async fn main() -> Result<()> {
GatewayCmd::DeleteUser { username } => gateway_delete_user(&socket, &username).await,
GatewayCmd::ListUsers => gateway_list_users(&socket).await,
},
Cmd::Agents { cmd } => run_agents(&socket, cmd).await,
Cmd::Agent { name, cmd } => run_agent(&socket, &name, cmd).await,
Cmd::ListAgents { json } => agents_list(&socket, json).await,
Cmd::QuotaEnable => quota::quota_enable(&socket).await,
Cmd::Approvals { cmd } => run_approvals(&socket, cmd).await,
Cmd::Wg { cmd } => match cmd {
WgCmd::Init { address } => wg_init(&socket, address.as_deref()).await,
@ -123,10 +125,6 @@ async fn main() -> Result<()> {
} => stop(&socket, scope.to_scope(), graceful, no_wait).await,
Cmd::Start { scope, no_wait } => start(&socket, scope.to_scope(), no_wait).await,
Cmd::Restart { scope, graceful } => restart(&socket, scope.to_scope(), graceful).await,
Cmd::Choom {
name,
resume_session,
} => choom(&socket, &name, resume_session.as_deref()).await,
Cmd::MarkdownDocs => {
print!("{}", clap_markdown::help_markdown::<Cli>());
Ok(())

View file

@ -1,6 +1,7 @@
//! `hivectl agents quota` — per-agent disk accounting + optional quotas via
//! btrfs qgroups. The daemon holds the privileged helper that reads /
//! sets qgroups; hivectl relays the request and formats the reply.
//! `hivectl quota-enable` + `hivectl agent <name> quota` — per-agent disk
//! accounting + optional quotas via btrfs qgroups. The daemon holds the
//! privileged helper that reads / sets qgroups; hivectl relays the
//! request and formats the reply.
use std::path::Path;
@ -13,20 +14,20 @@ pub(crate) async fn quota_enable(socket: &Path) -> Result<()> {
daemon_request(socket, hive_host_sock::HostRequest::QuotaEnable, "quota").await
}
/// `quota show [name]` — report per-agent disk usage from btrfs qgroups.
/// The daemon resolves the agent set + reads each subvolume's usage (it
/// holds the privileged helper); the client just formats the returned rows.
pub(crate) async fn quota_show(socket: &Path, name: Option<&str>) -> Result<()> {
/// `agent <name> quota show` — report one agent's disk usage from btrfs
/// qgroups. The daemon reads that subvolume's usage (it holds the
/// privileged helper); the client just formats the returned row(s).
pub(crate) async fn quota_show(socket: &Path, name: &str) -> Result<()> {
let resp = crate::client::request(
socket,
hive_host_sock::HostRequest::QuotaShow {
name: name.map(crate::util::parse_ident).transpose()?,
name: Some(crate::util::parse_ident(name)?),
},
)
.await
.with_context(|| format!("connect to daemon socket {}", socket.display()))?;
if !resp.ok {
// Carries the "btrfs quota not enabled — run `hivectl agents quota enable`
// Carries the "btrfs quota not enabled — run `hivectl quota-enable`
// first" hint when qgroups are off.
bail!("{}", resp.error.as_deref().unwrap_or("quota show failed"));
}

View file

@ -1,5 +1,6 @@
//! `hivectl agents subvol` — btrfs state-subvolume ops: migrate a plain-dir agent
//! state root to a subvolume (`upgrade`), and snapshot create/delete/send.
//! `hivectl agent <name> subvol` — btrfs state-subvolume ops: migrate a
//! plain-dir agent state root to a subvolume (`upgrade`), and snapshot
//! create/delete/send.
use std::path::Path;
@ -28,25 +29,21 @@ fn single_agent_scope(name: &str) -> hive_host_sock::LifecycleScope {
/// 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; the migration error (if any) is surfaced afterwards.
/// Route a `hivectl agents subvol …` subcommand. Split out of `main`'s top-level
/// match so the CLI router stays within the clippy line budget and the
/// subvolume-op subcommands are dispatched in one place.
pub(crate) async fn dispatch_subvol(socket: &Path, cmd: SubvolCmd) -> Result<()> {
/// Route a `hivectl agent <name> subvol …` subcommand. Split out of
/// `main`'s top-level match so the CLI router stays within the clippy
/// line budget and the subvolume-op subcommands are dispatched in one
/// place. `name` is hoisted in from the parent `agent <name>` command.
pub(crate) async fn dispatch_subvol(socket: &Path, name: &str, cmd: SubvolCmd) -> Result<()> {
match cmd {
SubvolCmd::Upgrade { name, yes } => subvol_upgrade(socket, &name, yes).await,
SubvolCmd::Upgrade { yes } => subvol_upgrade(socket, name, yes).await,
SubvolCmd::Snapshot { cmd } => match cmd {
SnapshotCmd::Create { name, label } => {
subvol_snapshot_create(socket, &name, label).await
}
SnapshotCmd::Delete { name, label } => {
subvol_snapshot_delete(socket, &name, &label).await
}
SnapshotCmd::Create { label } => subvol_snapshot_create(socket, name, label).await,
SnapshotCmd::Delete { label } => subvol_snapshot_delete(socket, name, &label).await,
SnapshotCmd::Send {
name,
label,
parent,
dest,
} => subvol_snapshot_send(socket, &name, &label, parent.as_deref(), &dest).await,
} => subvol_snapshot_send(socket, name, &label, parent.as_deref(), &dest).await,
},
}
}