134 lines
4.6 KiB
Rust
134 lines
4.6 KiB
Rust
//! Fleet/node layout on disk and the service configs the launcher writes.
|
|
//!
|
|
//! The launcher owns per-fleet state under
|
|
//! `$XDG_STATE_HOME|~/.local/state/tines-console/`, and each node's
|
|
//! `config.json` is exactly the service's [`tines::config::Config`] schema —
|
|
//! the service rejects unknown keys, so the generated config must match it
|
|
//! byte-for-byte (pinned by `tests/config.rs`).
|
|
|
|
use std::fs::{self, Permissions};
|
|
use std::io::{self, Read};
|
|
use std::os::unix::fs::PermissionsExt;
|
|
use std::path::PathBuf;
|
|
|
|
/// The per-fleet state dir, created `0700` on first use:
|
|
/// `$XDG_STATE_HOME|~/.local/state` + `tines-console/fleets/<fleet>`.
|
|
pub fn fleet_dir(fleet: &str) -> PathBuf {
|
|
let base = state_home();
|
|
let dir = base.join("tines-console").join("fleets").join(fleet);
|
|
if let Err(e) = fs::create_dir_all(&dir) {
|
|
eprintln!("tines-gui: cannot create fleet dir {}: {e}", dir.display());
|
|
}
|
|
// create_dir_all honors the umask; the fleets carry node sockets and
|
|
// logs, so they are private by design.
|
|
if let Err(e) = fs::set_permissions(&dir, Permissions::from_mode(0o700)) {
|
|
eprintln!("tines-gui: cannot chmod fleet dir {}: {e}", dir.display());
|
|
}
|
|
dir
|
|
}
|
|
|
|
/// A node's dir inside a fleet: `<fleet_dir>/<node>`. Not created here —
|
|
/// [`NodeConfig::write`] creates it when a config lands.
|
|
pub fn node_dir(fleet: &str, node: &str) -> PathBuf {
|
|
fleet_dir(fleet).join(node)
|
|
}
|
|
|
|
/// A fresh random node id: 32 lowercase hex chars from 16 bytes of
|
|
/// `/dev/urandom`.
|
|
pub fn generate_node_id() -> String {
|
|
let mut buf = [0u8; 16];
|
|
let mut f =
|
|
std::fs::File::open("/dev/urandom").expect("/dev/urandom is available on this platform");
|
|
f.read_exact(&mut buf)
|
|
.expect("reading 16 bytes from /dev/urandom");
|
|
buf.iter().map(|b| format!("{b:02x}")).collect()
|
|
}
|
|
|
|
/// A node's service config, serializing to exactly the service schema.
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
pub struct NodeConfig {
|
|
/// This node's id (32 lowercase hex chars).
|
|
pub node_id: String,
|
|
/// bt: the peer; bus: the allowlist of one, `None` for any peer.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub peer: Option<String>,
|
|
/// Absolute path to the LogStore log inside the container.
|
|
pub log_path: String,
|
|
/// Absolute path to the IPC socket inside the container.
|
|
pub socket_path: String,
|
|
/// `"bt"` or `"bus"`.
|
|
pub transport: String,
|
|
/// bus only: the bus daemon's socket inside the container.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub bus_socket: Option<String>,
|
|
/// `"system"` when the host clock is trustworthy (the container's is).
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub trusted_clock: Option<String>,
|
|
}
|
|
|
|
impl NodeConfig {
|
|
/// A bt-mode config: one peer over the BLE transport.
|
|
pub fn bt(
|
|
node_id: String,
|
|
peer: Option<String>,
|
|
log_path: String,
|
|
socket_path: String,
|
|
) -> Self {
|
|
NodeConfig {
|
|
node_id,
|
|
peer,
|
|
log_path,
|
|
socket_path,
|
|
transport: "bt".into(),
|
|
bus_socket: None,
|
|
trusted_clock: Some("system".into()),
|
|
}
|
|
}
|
|
|
|
/// A bus-mode config: peers are every node on the bus daemon (or the
|
|
/// allowlisted one).
|
|
pub fn bus(
|
|
node_id: String,
|
|
peer: Option<String>,
|
|
log_path: String,
|
|
socket_path: String,
|
|
bus_socket: String,
|
|
) -> Self {
|
|
NodeConfig {
|
|
node_id,
|
|
peer,
|
|
log_path,
|
|
socket_path,
|
|
transport: "bus".into(),
|
|
bus_socket: Some(bus_socket),
|
|
trusted_clock: Some("system".into()),
|
|
}
|
|
}
|
|
|
|
/// Write this config to `<node_dir(fleet, node)>/config.json` (creating
|
|
/// the node dir, `0700`) and return the path.
|
|
pub fn write(&self, fleet: &str, node: &str) -> io::Result<PathBuf> {
|
|
let dir = node_dir(fleet, node);
|
|
fs::create_dir_all(&dir)?;
|
|
fs::set_permissions(&dir, Permissions::from_mode(0o700))?;
|
|
let path = dir.join("config.json");
|
|
let json = serde_json::to_string_pretty(self).map_err(io::Error::other)?;
|
|
fs::write(&path, json)?;
|
|
Ok(path)
|
|
}
|
|
}
|
|
|
|
/// `$XDG_STATE_HOME` when set to an absolute path, else `~/.local/state`.
|
|
fn state_home() -> PathBuf {
|
|
if let Some(dir) = std::env::var_os("XDG_STATE_HOME") {
|
|
let p = PathBuf::from(dir);
|
|
if p.is_absolute() {
|
|
return p;
|
|
}
|
|
}
|
|
std::env::var_os("HOME")
|
|
.map(PathBuf::from)
|
|
.unwrap_or_else(std::env::temp_dir)
|
|
.join(".local/state")
|
|
}
|