From 3797177e7f174e50058e5796bca30547d144fcb1 Mon Sep 17 00:00:00 2001 From: damocles Date: Tue, 14 Jul 2026 23:58:55 +0200 Subject: [PATCH] refactor(#2352): quota show via daemon wire command --- hive-c0re/src/bin/hivectl.rs | 63 +++++++++++++++++++----------------- hive-c0re/src/server.rs | 40 +++++++++++++++++++++++ hive-host-sock/src/lib.rs | 43 ++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 30 deletions(-) diff --git a/hive-c0re/src/bin/hivectl.rs b/hive-c0re/src/bin/hivectl.rs index f692cbc0..4ba1a7f0 100644 --- a/hive-c0re/src/bin/hivectl.rs +++ b/hive-c0re/src/bin/hivectl.rs @@ -20,7 +20,6 @@ use std::path::{Path, PathBuf}; use anyhow::{Context as _, Result, bail}; use clap::{Args, Parser, Subcommand}; -use hive_c0re::coordinator::Coordinator; /// Rebuild-queue DAG progress rendering (`wait_for_dags` + the spinner / /// plain renderers), split out to keep this file manageable. `#[path]` keeps @@ -760,7 +759,7 @@ async fn main() -> Result<()> { } => choom(&name, resume_session.as_deref()), Cmd::Quota { cmd } => match cmd { QuotaCmd::Enable => quota_enable(&socket).await, - QuotaCmd::Show { name } => quota_show(name.as_deref()).await, + QuotaCmd::Show { name } => quota_show(&socket, name.as_deref()).await, QuotaCmd::Limit { name, size } => quota_limit(&socket, &name, &size).await, }, Cmd::MarkdownDocs => { @@ -1046,37 +1045,41 @@ async fn quota_enable(socket: &Path) -> Result<()> { } /// `quota show [name]` — report per-agent disk usage from btrfs qgroups. -async fn quota_show(name: Option<&str>) -> Result<()> { - let agents: Vec = match name { - Some(n) => vec![n.to_owned()], - None => Coordinator::kept_state_names(), - }; - if agents.is_empty() { +/// The daemon resolves the agent set + reads each subvolume's usage (it +/// holds the privileged helper); the client just formats the returned rows. +async fn quota_show(socket: &Path, name: Option<&str>) -> Result<()> { + let resp = hive_c0re::client::request( + socket, + hive_host_sock::HostRequest::QuotaShow { + name: name.map(str::to_owned), + }, + ) + .await + .with_context(|| format!("connect to daemon socket {}", socket.display()))?; + if !resp.ok { + // 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")); + } + let rows = resp.quota.unwrap_or_default(); + if rows.is_empty() { println!("no agents with a state dir found"); return Ok(()); } - for agent in &agents { - match hive_c0re::priv_client::read_subvolume_usage(agent).await { - Ok((rfer, excl)) => { - println!( - "{agent:<12} referenced {:>10} exclusive {:>10}", - human_bytes(rfer), - human_bytes(excl) - ); - } - Err(e) => { - let msg = format!("{e:#}"); - // btrfs-progs prints "ERROR: ... quota not enabled" to stderr - // when qgroups are off; match the stable fragment case- - // insensitively rather than an exact line (the surrounding - // wording varies across btrfs-progs versions). - if msg.to_ascii_lowercase().contains("quota not enabled") { - bail!("btrfs quota not enabled — run `hivectl quota enable` first"); - } - // A plain-dir agent (no subvolume) has no qgroup; report it - // inline and keep going rather than aborting the whole sweep. - println!("{agent:<12} (no qgroup data — plain dir or: {msg})"); - } + for row in &rows { + match (row.referenced, row.exclusive) { + (Some(rfer), Some(excl)) => println!( + "{:<12} referenced {:>10} exclusive {:>10}", + row.agent, + human_bytes(rfer), + human_bytes(excl) + ), + // Plain-dir agent (no qgroup) — the daemon set an explanatory note. + _ => println!( + "{:<12} ({})", + row.agent, + row.note.as_deref().unwrap_or("no qgroup data") + ), } } Ok(()) diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs index a50f9481..d2d4d27c 100644 --- a/hive-c0re/src/server.rs +++ b/hive-c0re/src/server.rs @@ -201,6 +201,7 @@ async fn dispatch(req: &HostRequest, coord: Arc) -> HostResponse { } HostRequest::QuotaEnable => handle_quota_enable().await?, HostRequest::QuotaLimit { name, limit } => handle_quota_limit(name, *limit).await?, + HostRequest::QuotaShow { name } => handle_quota_show(name.as_deref()).await?, }) } .await; @@ -429,6 +430,45 @@ async fn handle_quota_limit(name: &str, limit: Option) -> Result) -> Result { + let agents: Vec = match name { + Some(n) => vec![n.to_owned()], + None => Coordinator::kept_state_names(), + }; + let mut rows = Vec::with_capacity(agents.len()); + for agent in &agents { + match crate::priv_client::read_subvolume_usage(agent).await { + Ok((referenced, exclusive)) => rows.push(hive_host_sock::QuotaRow { + agent: agent.clone(), + referenced: Some(referenced), + exclusive: Some(exclusive), + note: None, + }), + Err(e) => { + let msg = format!("{e:#}"); + // btrfs-progs prints "ERROR: ... quota not enabled" to stderr + // when qgroups are off; short-circuit the whole sweep with the + // enable hint (case-insensitive fragment match — the wording + // varies across btrfs-progs versions). + if msg.to_ascii_lowercase().contains("quota not enabled") { + return Ok(HostResponse::error( + "btrfs quota not enabled — run `hivectl quota enable` first", + )); + } + // A plain-dir agent (no subvolume) has no qgroup; note it + // inline and keep going rather than aborting the whole sweep. + rows.push(hive_host_sock::QuotaRow { + agent: agent.clone(), + referenced: None, + exclusive: None, + note: Some(format!("no qgroup data — plain dir or: {msg}")), + }); + } + } + } + Ok(HostResponse::quota(rows)) +} + async fn handle_matrix_sync_admin() -> Result { require_matrix_present().await?; let register_token = diff --git a/hive-host-sock/src/lib.rs b/hive-host-sock/src/lib.rs index d84d4752..0b08de25 100644 --- a/hive-host-sock/src/lib.rs +++ b/hive-host-sock/src/lib.rs @@ -178,6 +178,35 @@ pub enum HostRequest { #[serde(default)] limit: Option, }, + /// Report per-agent btrfs qgroup usage (`hivectl quota show [name]`). + /// The daemon resolves the agent set (all kept state dirs when `name` + /// is absent) and reads each subvolume's referenced/exclusive usage via + /// the privileged helper. Result rows land in [`HostResponse::quota`]; + /// a "btrfs quota not enabled" error short-circuits the whole sweep as a + /// plain [`HostResponse::error`] so the client can print the enable hint. + QuotaShow { + #[serde(default)] + name: Option, + }, +} + +/// One agent's btrfs qgroup usage row — the [`HostRequest::QuotaShow`] +/// result unit. `referenced` / `exclusive` are byte counts when the agent +/// has a live qgroup; both are `None` (with an explanatory `note`) for a +/// plain-dir agent that has no subvolume to account. The client formats the +/// byte counts into human-readable columns. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct QuotaRow { + pub agent: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub referenced: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub exclusive: Option, + /// Set instead of the byte counts when the agent has no qgroup data + /// (plain dir, or a non-"quota not enabled" read error), so the client + /// can print an inline explanation and keep going. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub note: Option, } /// Selects which container classes a hive-wide [`HostRequest::Stop`] / @@ -280,6 +309,10 @@ pub struct HostResponse { /// Empty for requests that produce no such output. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub messages: Vec, + /// `QuotaShow` result — one row per agent with its btrfs qgroup usage. + /// `None` for every other request kind. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub quota: Option>, } impl HostResponse { @@ -369,4 +402,14 @@ impl HostResponse { ..Self::default() } } + + /// `QuotaShow` result — one usage row per agent. + #[must_use] + pub fn quota(rows: Vec) -> Self { + Self { + ok: true, + quota: Some(rows), + ..Self::default() + } + } }