//! devendra-agent CLI.
//!
//! - `run` — join the controller document and reconcile this host toward its
//! published manifest (the long-running daemon mode).
//! - `adopt` — scan an existing quadlet directory and publish its files as an
//! adoption proposal for the controller to review, then exit.
use std::path::PathBuf;
use std::str::FromStr;
use anyhow::{Context, Result};
use clap::{Args, Parser, Subcommand};
use devendra_agent::adopt::{self, AdoptConfig};
use devendra_agent::reconcile::systemd::Scope;
use devendra_agent::sync::{self, AgentConfig};
use devendra_node::DocTicket;
use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
#[derive(Parser, Debug)]
#[command(name = "devendra-agent", about = "devendra host agent")]
struct Cli {
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand, Debug)]
enum Cmd {
/// Join the controller document and reconcile this host continuously.
Run(RunArgs),
/// Scan a directory and publish its files as an adoption proposal.
Adopt(AdoptArgs),
}
/// Flags shared by every subcommand: which host, which document, where to store.
#[derive(Args, Debug)]
struct CommonArgs {
/// Stable identifier for this host; selects which manifest/proposal to use.
/// Defaults to the system hostname when not given.
#[arg(long, env = "DEVENDRA_HOST_ID")]
host_id: Option<String>,
/// Controller join ticket (from `devendra-controller ticket`).
#[arg(long, env = "DEVENDRA_TICKET")]
ticket: String,
/// Directory for the agent's iroh blob/doc stores.
#[arg(
long,
env = "DEVENDRA_DATA_DIR",
default_value = "/var/lib/devendra/agent"
)]
data_dir: PathBuf,
}
impl CommonArgs {
fn ticket(&self) -> Result<DocTicket> {
DocTicket::from_str(&self.ticket)
.map_err(|e| anyhow::anyhow!("invalid ticket: {e}"))
.context("parsing --ticket")
}
/// Resolve the host id: the `--host-id` flag/env when set, otherwise the
/// system hostname. Logs when the hostname fallback is used so it's clear
/// which manifest the agent selected.
fn host_id(&self) -> Result<String> {
if let Some(id) = &self.host_id {
return Ok(id.clone());
}
let host = nix::unistd::gethostname()
.context("reading system hostname (no --host-id / DEVENDRA_HOST_ID set)")?
.into_string()
.map_err(|raw| anyhow::anyhow!("system hostname is not valid UTF-8: {raw:?}"))?;
if host.is_empty() {
anyhow::bail!("no --host-id given and the system hostname is empty");
}
tracing::info!(host_id = %host, "inferred host id from system hostname");
Ok(host)
}
}
#[derive(Args, Debug)]
struct RunArgs {
#[command(flatten)]
common: CommonArgs,
/// Manage per-user systemd units (`systemctl --user`) instead of system units.
#[arg(long, env = "DEVENDRA_USER_SCOPE")]
user_scope: bool,
/// Don't undo files/services that drop out of the desired state. By default
/// the agent prunes them (e.g. when a fragment is removed from a host).
#[arg(long, env = "DEVENDRA_NO_PRUNE")]
no_prune: bool,
}
#[derive(Args, Debug)]
struct AdoptArgs {
#[command(flatten)]
common: CommonArgs,
/// Directory to scan for existing files to adopt.
#[arg(long, default_value = "/etc/containers/systemd")]
dir: PathBuf,
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::registry()
.with(
EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "devendra_agent=info,devendra_node=info".into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
match Cli::parse().cmd {
Cmd::Run(args) => {
let config = AgentConfig {
host_id: args.common.host_id()?,
ticket: args.common.ticket()?,
data_dir: args.common.data_dir.clone(),
scope: if args.user_scope {
Scope::User
} else {
Scope::System
},
prune: !args.no_prune,
};
// Ctrl-C ends the loop and flushes the node cleanly.
sync::run_until(config, async {
let _ = tokio::signal::ctrl_c().await;
tracing::info!("received Ctrl-C");
})
.await
}
Cmd::Adopt(args) => {
let config = AdoptConfig {
host_id: args.common.host_id()?,
ticket: args.common.ticket()?,
data_dir: args.common.data_dir.clone(),
scan_dir: args.dir,
};
adopt::adopt(config).await
}
}
}
neiam
/devendra
public · Issues · Pulls · Labels · Forks · Compare · Actions failure · Badges · Packages
⭐
Log in to mark this repository.
4.7 KiB · text
History
e832e77