[WIP] Daemon & CLI

This commit is contained in:
Alex-Emmet 2026-07-23 23:13:02 +02:00
commit 8b158108bb
100 changed files with 6519 additions and 1596 deletions

11
iota-installer/Cargo.toml Normal file
View file

@ -0,0 +1,11 @@
[package]
name = "iota-installer"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1"
tempfile = "3"
zip = "6"
serde_json = "1"
iota-paths = { path = "../iota-paths" }

187
iota-installer/src/lib.rs Normal file
View file

@ -0,0 +1,187 @@
use anyhow::{Context, Result, bail};
use std::{fs, io, path::Path, process::Command};
use tempfile::tempdir;
use zip::ZipArchive;
const REQUIRED: &[&str] = &[
"bin/iota",
"bin/iota-daemon",
"bin/iota-updater",
"systemd/iota-daemon.service",
"systemd/iota-daemon.socket",
"systemd/sysusers.d/iota.conf",
"systemd/iota-update.service",
"systemd/iota-update.timer",
"manifest.json",
];
pub fn install_linux_bundle(bundle: &Path) -> Result<()> {
install_linux_bundle_with_operator(bundle, None)
}
pub fn install_linux_bundle_with_operator(bundle: &Path, operator: Option<&str>) -> Result<()> {
if std::env::consts::OS != "linux" {
bail!("Linux systemd bundles are not supported on this platform");
}
let staging = tempdir().context("create installer staging directory")?;
let file = fs::File::open(bundle).context("open release bundle")?;
let mut archive = ZipArchive::new(file).context("read release bundle")?;
for name in REQUIRED {
let mut entry = archive
.by_name(name)
.with_context(|| format!("bundle is missing {name}"))?;
let output = staging.path().join(name);
if let Some(parent) = output.parent() {
fs::create_dir_all(parent)?;
}
let mut out = fs::File::create(&output)?;
io::copy(&mut entry, &mut out)?;
}
install(
&staging.path().join("bin/iota"),
&format!(
"{}/versions/{}/bin/iota",
iota_paths::install_root().display(),
product_version(staging.path())
),
"0755",
)?;
install(
&staging.path().join("bin/iota-daemon"),
&format!(
"{}/versions/{}/bin/iota-daemon",
iota_paths::install_root().display(),
product_version(staging.path())
),
"0755",
)?;
let version_dir = format!(
"{}/versions/{}",
iota_paths::install_root().display(),
product_version(staging.path())
);
if !Path::new(&format!("{version_dir}/bin/iota-daemon")).is_file() {
bail!("installed daemon executable is missing: {version_dir}/bin/iota-daemon");
}
install(
&staging.path().join("bin/iota-updater"),
&format!(
"{}/versions/{}/bin/iota-updater",
iota_paths::install_root().display(),
product_version(staging.path())
),
"0755",
)?;
for unit in [
"iota-daemon.service",
"iota-daemon.socket",
"iota-update.service",
"iota-update.timer",
] {
install(
&staging.path().join("systemd").join(unit),
&format!("/etc/systemd/system/{unit}"),
"0644",
)?;
}
install(
&staging.path().join("systemd/sysusers.d/iota.conf"),
"/etc/sysusers.d/iota.conf",
"0644",
)?;
run(
"ln",
&[
"-sfn",
&version_dir,
&iota_paths::current_version_link().to_string_lossy(),
],
)?;
run(
"ln",
&[
"-sfn",
&format!("{}/current/bin/iota", iota_paths::install_root().display()),
"/usr/local/bin/iota",
],
)?;
run(
"ln",
&[
"-sfn",
&format!(
"{}/current/bin/iota-daemon",
iota_paths::install_root().display()
),
"/usr/local/lib/iota/iota-daemon",
],
)?;
run("systemd-sysusers", &[])?;
if let Some(operator) = operator {
run("usermod", &["-aG", "iota-operators", operator])?;
} else {
eprintln!("To grant socket access, run: usermod -aG iota-operators USER");
eprintln!(
"A new login session is required before supplementary group membership is visible."
);
}
run("systemctl", &["daemon-reload"])?;
run("systemctl", &["enable", "--now", "iota-daemon.socket"])?;
run("systemctl", &["is-active", "iota-daemon.socket"])?;
run("systemctl", &["is-enabled", "iota-daemon.socket"])?;
if !Path::new("/run/iota/iota.sock").exists() {
bail!("systemd socket is active but /run/iota/iota.sock was not created");
}
Ok(())
}
fn product_version(staging: &Path) -> String {
fs::read_to_string(staging.join("manifest.json"))
.ok()
.and_then(|value| serde_json::from_str::<serde_json::Value>(&value).ok())
.and_then(|value| {
value
.get("product_version")
.and_then(|v| v.as_str())
.map(str::to_owned)
})
.unwrap_or_else(|| "unversioned".into())
}
fn install(source: &Path, destination: &str, mode: &str) -> Result<()> {
run(
"install",
&["-D", "-m", mode, &source.to_string_lossy(), destination],
)
}
fn run(program: &str, args: &[&str]) -> Result<()> {
let status = Command::new(program)
.args(args)
.status()
.with_context(|| format!("run {program}"))?;
if status.success() {
Ok(())
} else {
bail!("{program} failed; run the installer as root")
}
}
#[cfg(test)]
mod tests {
#[test]
fn service_uses_installed_daemon_and_declared_identities() {
let service = include_str!("../../systemd/iota-daemon.service");
let socket = include_str!("../../systemd/iota-daemon.socket");
let sysusers = include_str!("../../systemd/sysusers.d/iota.conf");
assert!(service.contains("ExecStart=/usr/local/lib/iota/iota-daemon"));
assert!(service.contains("User=iota"));
assert!(service.contains("Group=iota"));
assert!(socket.contains("SocketUser=iota"));
assert!(socket.contains("SocketGroup=iota-operators"));
assert!(socket.contains("NonBlocking=true"));
assert!(sysusers.contains("u iota "));
assert!(sysusers.contains("g iota-operators"));
}
}