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

nordisk/monetize

crates/monetize-server/src/estate_order/relay.rs

//! **Lay ONE box of a sold estate again** (5.4, the guardian's `Lay` step): the same path a
//! sold estate is laid with — `plan_for` kept to the one role, the release medium, the seeds,
//! `Cloud::lay`, then the installer (`InstallRequest.role`) — never a second
//! installer path. Called only once the role's old box is fenced (gone at the provider).
//!
//! The new box calls home on the pull channel; the estate's own repair verb (`adopt_twin`,
//! `recharge_from_twin`) is an order it pulls, not something this module does.

use monetize_guardian::LaidBox;

use super::*;

/// The roles a relay may lay: a sold estate's gunnar boxes. The front holds the estate's names
/// and TLS; it is not laid again by the guardian.
pub const RELAY_ROLES: [&str; 2] = monetize_cloud::lay::RELAY_ROLES;

/// **Lay `role` of `company` again and have the installer lay it.** `Ok` is the new box (its
/// server, utility IPv4, and the fleet key the installer's verdict named, if any).
pub fn relay_one(state: &AppState, company: &str, role: &str) -> Result<LaidBox, String> {
    if !RELAY_ROLES.contains(&role) {
        return Err(format!(
            "the guardian lays only {RELAY_ROLES:?} again, not {role:?}"
        ));
    }
    let cfg = state
        .estates
        .cfg
        .clone()
        .ok_or("this shop sells no estates ([estate_orders] is absent)")?;
    let cloud = state
        .registry
        .find_cloud(&cfg.cloud)
        .ok_or_else(|| format!("the cloud {:?} is not linked", cfg.cloud))?;
    let _pen = state.estates.pen(company);
    let mut run = state
        .estates
        .get(company)
        .ok_or_else(|| format!("no estate {company:?} at this shop"))?;
    // An estate on its way out is never laid again (F4): its give-back would race the new box.
    if run.phase == phase::TERMINATING || phase::is_gone(&run.phase) {
        return Err(format!(
            "{company} is {}: no box of it is laid again",
            run.phase
        ));
    }
    // ★ ST-5: laid again under the name of the site it was bought on.
    let cfg = super::terms_of(state, &cfg, &run).unwrap_or(cfg);
    if run.laid.is_none() {
        return Err(format!("{company} has nothing laid"));
    }
    let dir = state
        .estates
        .run_dir(company)
        .ok_or("no estate-orders directory")?;
    let now = state.now();
    let relay_dir = dir.join(format!("relay-{role}-{now}"));
    std::fs::create_dir_all(&relay_dir).map_err(|e| format!("{}: {e}", relay_dir.display()))?;
    // Its own journal: the estate's first lay journaled the old box, which is gone.
    let journal = monetize_cloud::grow::Journal::open(relay_dir.join("journal"))
        .map_err(|e| e.to_string())?;

    let life = super::lifecycle::of(&cfg)?;
    let mut plan = super::lifecycle::plan_of(
        life.as_ref(),
        &cfg,
        company,
        &run.quote.sizes,
        &run.quote.twin_plan,
    )?;
    plan.machines.retain(|m| m.role == role);
    if plan.machines.is_empty() {
        return Err(format!("the estate's plan has no {role}"));
    }
    // Marked a relay (the spend cap commits it under `<reference>#<role>`), and titled
    // afresh: a lay adopts by title, and the fence gave the old box's disks back — a title
    // they held must never be adopted from a half-done retire.
    let fresh = format!(" relay-{}", now / 1_000);
    for m in &mut plan.machines {
        m.labels
            .insert(monetize_cloud::lay::RELAY_LABEL.into(), role.into());
        m.title.push_str(&fresh);
        for d in &mut m.disks {
            d.title.push_str(&fresh);
        }
    }
    let (roles, during, narrate_as) = (
        [role.to_string()],
        format!("the relay of a {role} laid again"),
        format!("relay {role}"),
    );
    let laid = lay_members(
        state,
        &cfg,
        cloud,
        &run,
        life.as_ref(),
        plan,
        &Members {
            roles: &roles,
            dir: &relay_dir,
            journal: &journal,
            during: &during,
            narrate_as: &narrate_as,
        },
    )?;
    let machine = laid
        .machines
        .into_iter()
        .next()
        .ok_or_else(|| format!("the cloud laid no {role}"))?;
    let v = laid
        .verdicts
        .into_iter()
        .next()
        .ok_or("the installer gave no verdict")?;
    let clones = laid.clones;
    let estate = laid.estate;
    for p in &v.peers {
        run.peers.retain(|q| q.name != p.name);
        run.peers.push(p.clone());
    }
    run.laid = Some(estate);
    let words = format!("a new {role} laid (server {}, {}) and installed: the installer's verdict is PASS; {clones} release clone(s) given back", machine.server, machine.utility_ipv4);
    run.history.push(Transition {
        phase: String::new(),
        at_ms: state.now(),
        note: format!("guardian: {words}"),
    });
    super::save(state, &run)?;
    Ok(LaidBox {
        server: machine.server,
        ipv4: machine.utility_ipv4,
        appliance_key: v.appliance_keys.get(role).cloned().unwrap_or_default(),
        words,
    })
}

/// **Which machines [`lay_members`] lays, and where it writes**: the roles (`be-2`), the run
/// directory of this lay (its `laid.json`, hand-off and verdicts), its journal, and the words its
/// custody and its setup log name it by.
#[derive(Clone, Copy)]
pub struct Members<'a> {
    pub roles: &'a [String],
    pub dir: &'a std::path::Path,
    pub journal: &'a monetize_cloud::grow::Journal,
    /// What the custody's seal-back names (`the relay of a main laid again`).
    pub during: &'a str,
    /// What the setup log names the lay (`relay main`, `change change-1`).
    pub narrate_as: &'a str,
}

/// What [`lay_members`] laid and installed.
pub struct LaidMembers {
    /// The new machines, as the cloud laid them, in the plan's order.
    pub machines: Vec<monetize_cloud::lay::LaidMachine>,
    /// The whole estate as it stands now: the machines laid before (any of the same role
    /// replaced) and the new ones.
    pub estate: monetize_cloud::lay::LaidEstate,
    /// The installer's verdict on each new machine, in the plan's order (each a PASS).
    pub verdicts: Vec<InstallVerdict>,
    /// The release clones given back after the install.
    pub clones: usize,
}

/// **Lay `roles` of `run`'s standing estate and have the installer lay each** — the path a
/// sold estate is laid by (the release media, the templates, the seeds, `lay_estate`, the
/// installer with `InstallRequest.role`), for the machines of `plan` (already kept to `roles`,
/// marked and titled as the caller lays them) beside the machines that stand. Shared by the
/// guardian's relay ([`relay_one`]: one box laid again) and a plan change that adds machines
/// ([`super::change`]: `be-2` laid for a Lake whose backends went 1 → 2). The run is not
/// written here: the caller takes [`LaidMembers::estate`] into it.
pub fn lay_members(
    state: &AppState,
    cfg: &EstateOrderConfig,
    cloud: &dyn monetize_cloud::Cloud,
    run: &EstateRun,
    life: &lifecycle::Lifecycle,
    mut plan: monetize_cloud::lay::EstatePlan,
    how: &Members<'_>,
) -> Result<LaidMembers, String> {
    let Members {
        roles,
        dir,
        journal,
        during,
        narrate_as,
    } = *how;
    let company = run.company.as_str();
    let laid = run
        .laid
        .clone()
        .ok_or_else(|| format!("{company} has nothing laid"))?;
    let now = state.now();
    for line in super::dress_plan(state, cfg, life, &mut plan, now)? {
        eprintln!("monetize-server: estate {company} ({during}): {line}");
    }
    // The seed: the custody's while it still holds the estate (before its hand-off); after the
    // hand-off the shop holds nothing of it (2026-10-08), so the pairing mints the new box a
    // fresh one and the estate adopts it by its own verb.
    let custody = state.estates.custody.as_ref().filter(|c| c.holds(company));
    let seeds = match custody {
        Some(c) => c
            .pairing_files(company)
            .map_err(|e| format!("the custody could not hand the seeds: {e}"))?,
        None => state.estates.installer.mint_for(
            &super::pairing_facts(cfg),
            company,
            &cfg.host_of(company),
        )?,
    };
    super::give_seeds_as(&mut plan, &seeds, life);
    let one = monetize_cloud::Cloud::lay(&cloud, &plan, journal).map_err(|e| {
        format!(
            "the lay of a new {} was refused: {e}",
            lifecycle::listed(roles, "and")
        )
    })?;
    let mut machines = Vec::new();
    for role in roles {
        machines.push(
            one.machine(role)
                .cloned()
                .ok_or_else(|| format!("the cloud laid no {role}"))?,
        );
    }

    // The installer lays each: `laid.json` is the whole estate as it stands now (the new boxes
    // in their roles), and each request names the one role.
    let mut estate = laid.clone();
    estate.machines.retain(|m| !roles.contains(&m.role));
    estate.machines.extend(machines.iter().cloned());
    let laid_path = dir.join("laid.json");
    let shop_door = cfg.shop_door()?;
    let mut names = cfg.names_of(company);
    if !run.domain.is_empty() {
        names.push(run.domain.clone());
    }
    let order = LaidOrder {
        company: company.to_string(),
        apex: cfg.apex.clone(),
        domain: run.domain.clone(),
        names,
        estate: estate.clone(),
        spend_cap_monthly_minor: run.spend_cap.minor,
        shop_door,
        monetize_pubkey: state
            .signer_for(cfg.sold_product()?)
            .public_key()
            .iter()
            .map(|b| format!("{b:02x}"))
            .collect(),
        first_login_ttl_secs: cfg.first_login_ttl(),
        pay_page: cfg.term.url_for(company),
    };
    serde_json::to_vec_pretty(&order)
        .map_err(|e| e.to_string())
        .and_then(|b| write_atomic(&laid_path, &b))
        .map_err(|e| format!("laid.json: {e}"))?;
    let handoff_dir = dir.join("handoff");
    let handoff = match custody {
        Some(c) => Some(
            c.hand_off(company, &handoff_dir)
                .map_err(|e| format!("the custody would not hand the secrets over: {e}"))?,
        ),
        None => {
            std::fs::create_dir_all(handoff_dir.join(crate::custody::handoff::RETURN_DIR))
                .map_err(|e| format!("{}: {e}", handoff_dir.display()))?;
            None
        }
    };
    // ★ Lane O1: a box laid again ships the estate's off-site copy to the same recipient.
    let facts = super::pairing_facts(cfg);
    let offsite = super::site_offsite(state, run, &facts.kind);
    // The relay of the estate's OWN product (OX-RELAY2: one per product).
    let relay = super::sold_relay(
        state,
        &crate::fleet::run_product(state, run).unwrap_or_else(|_| cfg.product.clone()),
    );
    // RA5-17: a site's box laid again is fed as the site's boxes were at its lay (its public
    // repositories, its variant, its seed's principal), with NO bearer: only the site's own lay
    // mints one, and its standing front keeps it.
    let site = run.site_order.then(|| super::site::box_feed(state, run));
    let mut verdicts = Vec::new();
    let mut failed = None;
    for role in roles {
        let narrate = super::setup_log::Narrate::for_order(
            state.bus.clone(),
            dir.to_path_buf(),
            &super::instance_product(state),
            &run.account,
            company,
            &if roles.len() == 1 {
                narrate_as.to_string()
            } else {
                format!("{narrate_as} {role}")
            },
        );
        let req = InstallRequest {
            facts: facts.clone(),
            company: company.to_string(),
            apex: cfg.apex.clone(),
            account: run.account.clone(),
            laid: laid_path.clone(),
            handoff: handoff_dir.clone(),
            run: dir.to_path_buf(),
            role: role.clone(),
            narrate,
            relay: relay.clone(),
            site: site.clone(),
            offsite: offsite.clone(),
        };
        match state.estates.installer.install(&req) {
            Ok(v) if v.verdict == "PASS" => verdicts.push(v),
            Ok(v) => {
                failed = Some(format!(
                    "the installer's verdict on the new {role} is {}: {}",
                    v.verdict, v.detail
                ));
                break;
            }
            Err(e) => {
                failed = Some(format!("the installer: {e}"));
                break;
            }
        }
    }
    if let (Some(c), Some(h)) = (custody, handoff.as_ref()) {
        let _sealed = super::seal_back(state, c, company, h, during);
    }
    drop(handoff);
    let _ = std::fs::remove_dir_all(&handoff_dir);
    if let Some(why) = failed {
        return Err(why);
    }
    let clones = monetize_cloud::ask::forget_media(&cloud, &one, journal)
        .map(|g| g.len())
        .unwrap_or(0);
    Ok(LaidMembers {
        machines,
        estate,
        verdicts,
        clones,
    })
}