refactor(#2352): quota show via daemon wire command
This commit is contained in:
parent
e7f8254788
commit
3797177e7f
3 changed files with 116 additions and 30 deletions
|
|
@ -20,7 +20,6 @@ use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use anyhow::{Context as _, Result, bail};
|
use anyhow::{Context as _, Result, bail};
|
||||||
use clap::{Args, Parser, Subcommand};
|
use clap::{Args, Parser, Subcommand};
|
||||||
use hive_c0re::coordinator::Coordinator;
|
|
||||||
|
|
||||||
/// Rebuild-queue DAG progress rendering (`wait_for_dags` + the spinner /
|
/// Rebuild-queue DAG progress rendering (`wait_for_dags` + the spinner /
|
||||||
/// plain renderers), split out to keep this file manageable. `#[path]` keeps
|
/// 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()),
|
} => choom(&name, resume_session.as_deref()),
|
||||||
Cmd::Quota { cmd } => match cmd {
|
Cmd::Quota { cmd } => match cmd {
|
||||||
QuotaCmd::Enable => quota_enable(&socket).await,
|
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,
|
QuotaCmd::Limit { name, size } => quota_limit(&socket, &name, &size).await,
|
||||||
},
|
},
|
||||||
Cmd::MarkdownDocs => {
|
Cmd::MarkdownDocs => {
|
||||||
|
|
@ -1046,37 +1045,41 @@ async fn quota_enable(socket: &Path) -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `quota show [name]` — report per-agent disk usage from btrfs qgroups.
|
/// `quota show [name]` — report per-agent disk usage from btrfs qgroups.
|
||||||
async fn quota_show(name: Option<&str>) -> Result<()> {
|
/// The daemon resolves the agent set + reads each subvolume's usage (it
|
||||||
let agents: Vec<String> = match name {
|
/// holds the privileged helper); the client just formats the returned rows.
|
||||||
Some(n) => vec![n.to_owned()],
|
async fn quota_show(socket: &Path, name: Option<&str>) -> Result<()> {
|
||||||
None => Coordinator::kept_state_names(),
|
let resp = hive_c0re::client::request(
|
||||||
};
|
socket,
|
||||||
if agents.is_empty() {
|
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");
|
println!("no agents with a state dir found");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
for agent in &agents {
|
for row in &rows {
|
||||||
match hive_c0re::priv_client::read_subvolume_usage(agent).await {
|
match (row.referenced, row.exclusive) {
|
||||||
Ok((rfer, excl)) => {
|
(Some(rfer), Some(excl)) => println!(
|
||||||
println!(
|
"{:<12} referenced {:>10} exclusive {:>10}",
|
||||||
"{agent:<12} referenced {:>10} exclusive {:>10}",
|
row.agent,
|
||||||
human_bytes(rfer),
|
human_bytes(rfer),
|
||||||
human_bytes(excl)
|
human_bytes(excl)
|
||||||
);
|
),
|
||||||
}
|
// Plain-dir agent (no qgroup) — the daemon set an explanatory note.
|
||||||
Err(e) => {
|
_ => println!(
|
||||||
let msg = format!("{e:#}");
|
"{:<12} ({})",
|
||||||
// btrfs-progs prints "ERROR: ... quota not enabled" to stderr
|
row.agent,
|
||||||
// when qgroups are off; match the stable fragment case-
|
row.note.as_deref().unwrap_or("no qgroup data")
|
||||||
// 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})");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -201,6 +201,7 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
|
||||||
}
|
}
|
||||||
HostRequest::QuotaEnable => handle_quota_enable().await?,
|
HostRequest::QuotaEnable => handle_quota_enable().await?,
|
||||||
HostRequest::QuotaLimit { name, limit } => handle_quota_limit(name, *limit).await?,
|
HostRequest::QuotaLimit { name, limit } => handle_quota_limit(name, *limit).await?,
|
||||||
|
HostRequest::QuotaShow { name } => handle_quota_show(name.as_deref()).await?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
.await;
|
.await;
|
||||||
|
|
@ -429,6 +430,45 @@ async fn handle_quota_limit(name: &str, limit: Option<u64>) -> Result<HostRespon
|
||||||
Ok(HostResponse::success())
|
Ok(HostResponse::success())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handle_quota_show(name: Option<&str>) -> Result<HostResponse> {
|
||||||
|
let agents: Vec<String> = 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<HostResponse> {
|
async fn handle_matrix_sync_admin() -> Result<HostResponse> {
|
||||||
require_matrix_present().await?;
|
require_matrix_present().await?;
|
||||||
let register_token =
|
let register_token =
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,35 @@ pub enum HostRequest {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
limit: Option<u64>,
|
limit: Option<u64>,
|
||||||
},
|
},
|
||||||
|
/// 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<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<u64>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub exclusive: Option<u64>,
|
||||||
|
/// 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<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Selects which container classes a hive-wide [`HostRequest::Stop`] /
|
/// Selects which container classes a hive-wide [`HostRequest::Stop`] /
|
||||||
|
|
@ -280,6 +309,10 @@ pub struct HostResponse {
|
||||||
/// Empty for requests that produce no such output.
|
/// Empty for requests that produce no such output.
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
pub messages: Vec<String>,
|
pub messages: Vec<String>,
|
||||||
|
/// `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<Vec<QuotaRow>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HostResponse {
|
impl HostResponse {
|
||||||
|
|
@ -369,4 +402,14 @@ impl HostResponse {
|
||||||
..Self::default()
|
..Self::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `QuotaShow` result — one usage row per agent.
|
||||||
|
#[must_use]
|
||||||
|
pub fn quota(rows: Vec<QuotaRow>) -> Self {
|
||||||
|
Self {
|
||||||
|
ok: true,
|
||||||
|
quota: Some(rows),
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue