Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

crates/monetize-admin/src/main.rs

//! `monetize-admin` — **the solo monetize box** (tunnr kernel, no shell), one
//! binary in three roles ([`monetize_admin::role`]):
//!
//! * `monetize-admin server …` — monetize-server (`monetize_server::cli`);
//! * `monetize-admin ui …` — the console ([`monetize_admin::console`]);
//! * `monetize-admin offsite open …` — the operator opens an off-box copy
//!   ([`monetize_admin::offsite`]) on their own machine;
//! * no role word — PID 1. The medium's cmdline says everything
//!   (`korp.env.MONETIZE_ADMIN_DATA=/data`), the seed is on the data disk, the
//!   installer feeds the rest.
//!
//! PID 1, in order (the fronts' order, on the shared crates): the boot log kept
//! for the installer from the first line; as PID 1 the reaper and the NICs
//! (DHCP, renewed); the medium's seed; a fresh installation clears the previous
//! pairing; monetize's token and signing key (minted here, only the public key
//! offered); the installer's listener on its WireGuard link (for life: after
//! the close it is the log door); the operator's door (the feed after the
//! install); the feed taken every two seconds and the two children kept
//! running; the edge on :80/:443. As PID 1 it never returns: a refusal is said
//! on every console and the machine stays up, so the installer can still
//! collect the boot log that says why.

use std::path::Path;
use std::sync::Arc;
use std::time::Duration;

use monetize_admin as admin;
use nordisk_pairing::bootlog::BootLog;
use nordisk_pairing::seed::{Kind, Seed};

/// The installer listener's voice, each line at its tone.
type Say = Arc<dyn Fn(nordisk_pairing::bootstrap::Tone, &str) + Send + Sync>;

static BOOT_LOG: BootLog = BootLog::new(nordisk_pairing::bootlog::DEFAULT_CAP);

fn shout(msg: &str) {
    use std::io::Write;
    eprint!("{msg}");
    if let Ok(mut tty0) = std::fs::OpenOptions::new().write(true).open("/dev/tty0") {
        let _ = tty0.write_all(msg.replace('\n', "\r\n").as_bytes());
    }
}

/// Stay up, for ever: PID 1 must not exit.
fn idle(why: &str) -> ! {
    shout(&format!("monetize-admin: {why}\n"));
    loop {
        std::thread::sleep(Duration::from_secs(3600));
    }
}

fn main() {
    match admin::role(std::env::args()) {
        admin::Role::Pid1 => pid1(),
        admin::Role::Server(args) => {
            if let Err(e) = monetize_server::cli::run_args(admin::linked(), args) {
                eprintln!("monetize-admin server: {e:#}");
                std::process::exit(1);
            }
        }
        admin::Role::Offsite(args) => match admin::offsite::cli(&args) {
            Ok(said) => println!("{said}"),
            Err(e) => {
                eprintln!("monetize-admin offsite: {e}");
                std::process::exit(1);
            }
        },
        admin::Role::Ui(args) => {
            tracing_subscriber::fmt().with_env_filter(tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into())).with_ansi(false).with_writer(std::io::stderr).init();
            let served = admin::console::parse_args(&args).and_then(|a| {
                let rt = tokio::runtime::Builder::new_multi_thread().worker_threads(2).enable_all().build().map_err(|e| e.to_string())?;
                rt.block_on(admin::console::serve(a))
            });
            if let Err(e) = served {
                eprintln!("monetize-admin ui: {e}");
                std::process::exit(1);
            }
        }
    }
}

fn pid1() {
    BOOT_LOG.capture_esp(Path::new(nordisk_pairing::bootlog::ESP_BOOTLOG), "monetize-admin");
    std::panic::set_hook(Box::new(|info| shout(&format!("\nmonetize-admin PANIC: {info}\n"))));
    // ALERT-1: every line PID 1 writes (its own and its children's) is heard: what the box
    // marks for its operator is mailed to the operator (`admin::alerts`). ALICA-3: its money
    // lines are also kept in their own home on the data disk (`admin::money_log`), which no
    // log noise pushes out.
    let tap = std::sync::Mutex::new(admin::alerts::Tee::new(admin::money_log::Keep::new(nordisk_pairing::bootlog::Tap::new(&BOOT_LOG, std::io::stderr()), admin::data_root())));
    tracing_subscriber::fmt().with_env_filter(tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into())).with_ansi(false).with_writer(tap).init();
    tracing::info!(build = admin::GIT_HASH, "monetize-admin: the solo monetize box starts");
    let runtime = match tokio::runtime::Builder::new_multi_thread().worker_threads(2).enable_all().build() {
        Ok(r) => r,
        Err(e) => idle(&format!("no async runtime: {e}")),
    };
    runtime.block_on(run())
}

async fn run() -> ! {
    let root = admin::data_root();
    // ── PID 1's duties: the reaper, the network (nothing else brings one up) ──
    if appliance_pid1::net::is_init() {
        // A32-14, stays a task: PID 1's zombie reaper, for the box's life.
        tokio::spawn(appliance_pid1::runtime::reaper());
        let spec = std::env::var("MONETIZE_ADMIN_NET").ok().filter(|s| !s.trim().is_empty()).unwrap_or_else(|| admin::DEFAULT_NET.to_string());
        match appliance_pid1::net::NetConfig::parse_list(&spec) {
            Ok(plans) => {
                let narrate = |note: &str| appliance_pid1::narration::ship_boot_narration(note, &|l| BOOT_LOG.push(l));
                let how = appliance_pid1::net::Bringup { hostname_prefix: "monetize-admin", resolver_probe: None, narrate: &narrate };
                match appliance_pid1::net::bring_up_network(plans, &how) {
                    Ok(sessions) => {
                        tracing::info!(leases = sessions.len(), "admin: the declared interfaces are up");
                        for s in sessions {
                            // A32-14, stays a task: PID 1's lease renewal, one per lease, for the box's life.
                            tokio::spawn(appliance_pid1::runtime::dhcp_renewal(s, None));
                        }
                    }
                    Err(e) => tracing::error!(error = %format!("{e:#}"), "admin: the network could not be brought up — the installer cannot reach this VM"),
                }
            }
            Err(e) => tracing::error!(error = %format!("{e:#}"), "REFUSED [monetize-admin-net] MONETIZE_ADMIN_NET = {spec:?}"),
        }
    }

    // ── the medium's seed: without it there is no installer to answer ────────
    let seed = match std::fs::read_to_string(root.join(admin::SEED_REL)) {
        Err(_) => idle(&format!("no pairing seed at {} — this medium was composed without `--param pairing_seed=@<seed.toml>`, so no installer can reach it", root.join(admin::SEED_REL).display())),
        Ok(text) => match Seed::parse(&text) {
            Ok(s) if s.kind == Kind::Front => s,
            Ok(s) => idle(&format!("REFUSED [pairing-seed-kind] the staged seed is for a `{:?}` box; the admin box is the front of an estate of one", s.kind)),
            Err(e) => idle(&format!("REFUSED [pairing-seed] {}: {e}", root.join(admin::SEED_REL).display())),
        },
    };
    match admin::first_boot(Path::new("/boot"), &root, &seed) {
        Some(cleared) if cleared.is_empty() => tracing::info!("admin: a keep boot of the same estate — the installer's feed, this box's key and certificate are KEPT"),
        Some(cleared) => tracing::warn!(cleared = ?cleared, "admin: the first boot of this installation — it starts UNPAIRED"),
        None => {}
    }

    // ── the box's clock: an estate of one has no front to follow ─────────────
    // It is its own root of time, by the fronts' rule (estate-clock), from the
    // sources its medium names (`korp.env.ESTATE_CLOCK`). Every enrolment and
    // every Stripe signature is judged by it, and the cloud's CMOS runs two
    // hours ahead (i54a run9: `[attach-clock]` 7 199 015 ms). Only PID 1 owns the
    // machine's clock; the installer CA vouches for the bench's local clock.
    if appliance_pid1::net::is_init() {
        let said: Arc<dyn Fn(&str) + Send + Sync> = Arc::new(|l: &str| tracing::info!("clock: {l}"));
        if let Err(e) = estate_clock::offer::start(&root, None, &seed.installer_ca_pem, said) {
            tracing::error!(error = %e, "admin: the box's clock cannot be kept — it runs on whatever it booted with");
        }
    }

    // ── a staged restore (ST-12): the off-box copy written back BEFORE monetize's key is
    // prepared and before any child runs — the copy's signing root is the one it then offers ─
    match admin::restore::apply_staged(&root) {
        Ok(Some(said)) => tracing::warn!("admin: {said}"),
        Ok(None) => {}
        Err(e) => tracing::error!(error = %e, "admin: the staged restore was NOT applied; it stays staged"),
    }

    // ── monetize's token and signing key: minted here, never leave ──────────
    if let Err(e) = monetize_front::intake::prepare(&root) {
        tracing::error!(error = %format!("{e:#}"), "admin: monetize's token and signing key cannot be prepared");
    }

    let product = admin::product::AdminFront::new(&root, seed.tunnel);
    let vm: Arc<nordisk_pairing::frontside::Front<admin::product::AdminFront>> = match nordisk_pairing::frontside::Front::open(&root, seed.clone(), product) {
        Ok(v) => Arc::new(v),
        Err(e) => idle(&format!("this box cannot prepare its own key: {e}")),
    };
    // An estate of one: nothing to relay, so the "relay" is up from the start
    // and the box reads PAIRED as soon as it is fed.
    vm.relay_started();

    // ── the installer's listener (for life) ──────────────────────────────────
    // Each line at its tone (madmin-pairing-warn-level): the ordinary steps INFO.
    let say: Say = Arc::new(admin::say_installer);
    let serve = if vm.closed() { nordisk_pairing::boxside::serve_installer_after_close_toned } else { nordisk_pairing::boxside::serve_installer_toned };
    if let Err(e) = serve(Arc::clone(&vm) as Arc<dyn nordisk_pairing::bootstrap::Vm>, seed.clone(), &BOOT_LOG, say) {
        tracing::error!(error = %e, "installer: the listener's thread could not start");
    }
    // Walk the machine on every feed (the relay flag is already up).
    {
        let v = Arc::clone(&vm);
        // A32-14, stays a task: PID 1's walk of the machine on every feed (async, 2 s), for the box's life.
        tokio::spawn(async move {
            loop {
                v.advance();
                tokio::time::sleep(Duration::from_secs(2)).await;
            }
        });
    }

    // ── the operator's door: the feed after the install ──────────────────────
    {
        let v = Arc::clone(&vm);
        let identity: Arc<dyn Fn() -> nordisk_pairing::mtls::Issued + Send + Sync> = Arc::new(move || nordisk_pairing::bootstrap::Vm::identity(&*v));
        admin::door::start(seed.installer_ca_pem.clone(), identity, root.clone()).await;
    }

    // ── the feed in force (the door's over the installer's), and the children ─
    let fed = Arc::clone(&vm);
    let feed_root = root.clone();
    let feed: Arc<dyn Fn() -> Option<std::collections::BTreeMap<String, String>> + Send + Sync> = Arc::new(move || fed.latest().map(|r| admin::solo::feed_now(&feed_root, &r.feed)));
    // A32-14, stays a task: PID 1's supervision of its children, for the box's life (PID 1 never returns).
    tokio::spawn(admin::solo::keep(root.clone(), seed.estate.clone(), Arc::clone(&feed)));
    // ── the operator's alerts, by mail through the box's own relay (ALERT-1) ─
    // A32-14, stays a task: PID 1's alert mailer, async, for the box's life.
    tokio::spawn(admin::alerts::keep(root.clone(), seed.estate.clone()));
    // ── "Update this box": the console's word, taken by PID 1 (owner 2026-10-18) ──
    // A32-14, stays a task: PID 1 takes the console's word (async, every 2 s; the update itself on the blocking pool, its refusal said).
    tokio::spawn(admin::self_update::keep_console_asks(root.clone()));
    // ── a self update the box rolled back: said once (ALERT + CODE RED), never offered again ──
    // A32-14, stays a task: PID 1 says a rolled-back update once, async, for the box's life.
    tokio::spawn(admin::self_update::keep_rolled_back(root.clone()));

    // ── the off-box copy (BK-0): sealed to the off-site key, shipped off UpCloud ─
    {
        let token_root = root.clone();
        let export: admin::offsite::Export = Box::new(move |dir: &Path| {
            let token = monetize_front::intake::console_token(&token_root).ok_or_else(|| "monetize's bearer is not minted yet".to_string())?;
            monetize_front::ledger_backup::loopback_export(admin::solo::SERVER.into(), token)(dir)
        });
        // PID 1's jera ledger (RA3-5): the off-box copy's looks are rows there. The trial watch
        // (CONFIRM_JOB) looks every 5 s, under jobs::RECORD_MIN, so it writes none (RA4-18).
        if let Err(e) = monetize_server::jobs::set_ledger(&root.join("jobs")) {
            tracing::error!("ALERT jobs.unrecorded PID 1's jobs' ledger: {e}; its jobs run, unrecorded");
        }
        admin::offsite::start(root.clone(), Arc::clone(&feed), export);
    }

    // ── the edge on :80/:443: the box holds its certificate ──────────────────
    let web_feed: estate_front::web::Feed = Arc::new(move || feed().unwrap_or_default());
    let any = std::net::Ipv4Addr::UNSPECIFIED;
    // Held for as long as PID 1 runs.
    let _web = match estate_front::web::start(&root, seed.estate.clone(), web_feed, (any, estate_front::web::HTTP_PORT).into(), (any, estate_front::web::HTTPS_PORT).into()).await {
        Ok(web) => {
            web.app.set(admin::edge::router(admin::solo::SERVER, admin::solo::UI, root.clone()));
            tracing::info!("admin: the edge is served on :80 and :443 (webhooks and estate gRPC to monetize-server, the rest to the console)");
            Some(web)
        }
        Err(e) => {
            tracing::error!(error = %e, "admin: the web face cannot be served");
            None
        }
    };
    // ── a start on TRIAL (the box updated itself): confirmed once monetize serves, else
    // korp-start rolls it back (owner 2026-10-14, point 5) ─────────────────────────────
    if appliance_pid1::net::is_init() {
        // ★ A32-4: confirmed only when this start's monetize says it serves every product.
        let (root, since) = (admin::data_root(), std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis() as u64).unwrap_or(0));
        admin::self_update::spawn_confirm(move || admin::self_update::serving_whole(&root, since));
    }
    // PID 1's resident set, and the peak, said every minute — read back with
    // `cargo xtask logs`.
    // A32-14, stays a task: PID 1's resident-set line every minute, for the box's life.
    tokio::spawn(async {
        loop {
            tokio::time::sleep(Duration::from_secs(60)).await;
            if let Some((rss, hwm)) = std::fs::read_to_string("/proc/self/status").ok().as_deref().and_then(admin::rss_of) {
                tracing::info!(rss_kb = rss, peak_kb = hwm, "admin: resident set");
            }
        }
    });
    if appliance_pid1::net::is_init() {
        let sig = appliance_pid1::runtime::shutdown_signal().await;
        tracing::warn!(signal = ?sig, "admin: shutting down");
        std::process::exit(0);
    }
    loop {
        tokio::time::sleep(Duration::from_secs(3600)).await;
    }
}