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,
})
}