remove the hivectl-only quota verbs and the trollshell status feed
This commit is contained in:
parent
70a8e54038
commit
cf360b74b9
5 changed files with 51 additions and 521 deletions
|
|
@ -1,7 +1,7 @@
|
|||
//! `hivectl agent <name>` — everything scoped to ONE managed agent:
|
||||
//! container lifecycle over the host admin socket (restart/pause/resume/
|
||||
//! spawn/kill/destroy/rebuild/choom/watch), plus the
|
||||
//! `quota` and `subvol` groups, whose handlers live in their own modules.
|
||||
//! `subvol` group, whose handlers live in their own module.
|
||||
//! `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
|
||||
|
|
@ -12,7 +12,7 @@ use std::path::Path;
|
|||
use anyhow::{Context as _, Result, bail};
|
||||
use hive_host_sock::HostRequest;
|
||||
|
||||
use crate::cli::{AgentCmd, AgentQuotaCmd};
|
||||
use crate::cli::AgentCmd;
|
||||
use crate::dag_progress::wait_for_nodes;
|
||||
use crate::util::render;
|
||||
|
||||
|
|
@ -282,13 +282,8 @@ pub(crate) async fn run_agent(socket: &Path, name: &str, cmd: AgentCmd) -> Resul
|
|||
crate::choom::choom(socket, name, resume_session.as_deref()).await
|
||||
}
|
||||
AgentCmd::Watch => crate::watch::watch(socket, name).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,
|
||||
},
|
||||
// `subvol` keeps its own module — this arm is just the reparenting
|
||||
// glue that hoists `name` in from the parent `agent <name>` command.
|
||||
AgentCmd::Subvol { cmd } => crate::subvol::dispatch_subvol(socket, name, cmd).await,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ pub enum Cmd {
|
|||
/// 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`).
|
||||
/// `restart`/`stop`/`start` with a scope).
|
||||
Agent {
|
||||
/// Agent name (for example `agent1`).
|
||||
name: String,
|
||||
|
|
@ -51,12 +51,6 @@ pub enum Cmd {
|
|||
#[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,
|
||||
/// WireGuard inter-hive mesh setup helpers.
|
||||
///
|
||||
/// Generate this hive's mesh key and print the nix to enable the mesh,
|
||||
|
|
@ -277,39 +271,20 @@ pub enum AgentCmd {
|
|||
/// ends the watch with exit 0. `Ctrl-C` to stop.
|
||||
/// Requires the agent to be running.
|
||||
Watch,
|
||||
/// 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: AgentQuotaCmd,
|
||||
},
|
||||
/// 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.
|
||||
/// so it gains snapshots.
|
||||
Subvol {
|
||||
#[command(subcommand)]
|
||||
cmd: SubvolCmd,
|
||||
},
|
||||
}
|
||||
|
||||
#[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,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum SubvolCmd {
|
||||
/// Convert a plain-dir agent state root into a btrfs subvolume in
|
||||
/// place, so it gains snapshots and per-subvol usage/quota.
|
||||
/// place, so it gains snapshots.
|
||||
///
|
||||
/// Bounces the agent to migrate its state, so it requires `--yes`.
|
||||
Upgrade {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ mod client;
|
|||
mod dag_progress;
|
||||
use cli::{Cli, Cmd, WgCmd};
|
||||
mod completions;
|
||||
mod quota;
|
||||
mod util;
|
||||
use completions::generate_completions;
|
||||
mod wg;
|
||||
|
|
@ -54,7 +53,6 @@ async fn main() -> Result<()> {
|
|||
match cli.cmd {
|
||||
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::Wg { cmd } => match cmd {
|
||||
WgCmd::Init { address } => wg_init(&socket, address.as_deref()).await,
|
||||
WgCmd::Peer {
|
||||
|
|
|
|||
|
|
@ -1,113 +0,0 @@
|
|||
//! `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;
|
||||
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
|
||||
use crate::util::daemon_request;
|
||||
|
||||
/// `quota enable` — turn on btrfs qgroup accounting (operator opt-in).
|
||||
pub(crate) async fn quota_enable(socket: &Path) -> Result<()> {
|
||||
daemon_request(socket, hive_host_sock::HostRequest::QuotaEnable, "quota").await
|
||||
}
|
||||
|
||||
/// `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: 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 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 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(())
|
||||
}
|
||||
|
||||
/// `quota limit <name> <size>` — set or clear an agent's disk quota.
|
||||
pub(crate) async fn quota_limit(socket: &Path, name: &str, size: &str) -> Result<()> {
|
||||
let limit = parse_quota_size(size)?;
|
||||
daemon_request(
|
||||
socket,
|
||||
hive_host_sock::HostRequest::QuotaLimit {
|
||||
name: crate::util::parse_ident(name)?,
|
||||
limit,
|
||||
},
|
||||
"quota",
|
||||
)
|
||||
.await?;
|
||||
match limit {
|
||||
Some(n) => println!("set {name} quota to {} ({n} bytes)", human_bytes(n)),
|
||||
None => println!("cleared {name} quota"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse a quota size: a byte count, a `K`/`M`/`G`/`T`-suffixed value
|
||||
/// (powers of 1024), or `none` to clear. Rejects overflow + bad input.
|
||||
fn parse_quota_size(s: &str) -> Result<Option<u64>> {
|
||||
let t = s.trim();
|
||||
if t.eq_ignore_ascii_case("none") {
|
||||
return Ok(None);
|
||||
}
|
||||
let (num, mult) = match t.chars().last() {
|
||||
Some('K' | 'k') => (&t[..t.len() - 1], 1024u64),
|
||||
Some('M' | 'm') => (&t[..t.len() - 1], 1024 * 1024),
|
||||
Some('G' | 'g') => (&t[..t.len() - 1], 1024 * 1024 * 1024),
|
||||
Some('T' | 't') => (&t[..t.len() - 1], 1024u64 * 1024 * 1024 * 1024),
|
||||
_ => (t, 1),
|
||||
};
|
||||
let n: u64 = num.trim().parse().with_context(|| {
|
||||
format!("invalid size {s:?} — use a byte count, a K/M/G/T suffix (e.g. 5G), or `none`")
|
||||
})?;
|
||||
n.checked_mul(mult)
|
||||
.map(Some)
|
||||
.with_context(|| format!("size {s:?} overflows u64"))
|
||||
}
|
||||
|
||||
/// Format a byte count as a human-friendly KiB/MiB/GiB/TiB string.
|
||||
#[allow(clippy::cast_precision_loss)] // display-only; precision loss is cosmetic
|
||||
fn human_bytes(n: u64) -> String {
|
||||
const UNITS: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"];
|
||||
if n < 1024 {
|
||||
return format!("{n} B");
|
||||
}
|
||||
let mut v = n as f64;
|
||||
let mut i = 0;
|
||||
while v >= 1024.0 && i < UNITS.len() - 1 {
|
||||
v /= 1024.0;
|
||||
i += 1;
|
||||
}
|
||||
format!("{v:.1} {}", UNITS[i])
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ pub(crate) fn parse_ident(name: &str) -> Result<hive_types::Ident> {
|
|||
|
||||
/// Send a provisioning request to the daemon and print its result lines.
|
||||
/// The daemon owns the provisioning logic; hivectl just relays the outcome,
|
||||
/// prefixing any error with `label` (e.g. `forge` / `quota`).
|
||||
/// prefixing any error with `label` (e.g. `forge`).
|
||||
pub(crate) async fn daemon_request(
|
||||
socket: &Path,
|
||||
req: hive_host_sock::HostRequest,
|
||||
|
|
|
|||
Loading…
Reference in a new issue