crates/monetize-cloud/src/lay.rs
//! **Laying a whole estate: the servers and disks of a private gunnar.**
//!
//! `private-gunnar-ops/PRIVATE-GUNNAR.md`: a company buys `<company>.gunnar.rs`
//! on gunnar.rs's front page, and the order is one more job in gunnar.rs's own
//! monetize queue. Its `laying` step is this call: the provider creates every
//! disk and every server of the estate (front, main, twin), stamps each with
//! [`crate::LABEL_REF`] = the order's reference, and answers what it laid.
//!
//! # The contract every implementation keeps
//!
//! * **Idempotent on the reference.** A resumed job calls `lay_estate` again
//! with the same plan; a disk or server that already carries the reference
//! and the same title is ADOPTED, never bought twice.
//! * **Journaled.** Each creation is written to the journal as an intent
//! before the call and confirmed (with the provider's id) after it, in the
//! grow journal's format, so the job's file says what exists.
//! * **Giving back is not here.** A failed job gives back everything under
//! its reference with `Give::References` and proves it
//! with `Listing::Under` — the same two calls a purged
//! tenant goes through, so there is one teardown and not two.
//! * **What it laid is described exactly**, because the installer ADOPTS it
//! (`cargo xtask estate install --laid <file>`): titles, plans, sizes, tiers,
//! device addresses and both IPv4 addresses, per machine.
use std::collections::BTreeMap;
use crate::{CloudError, Zone};
/// The roles of an estate's machines, in the order they are laid.
pub const ROLES: [&str; 3] = ["front", "main", "twin"];
/// **The relay mark** (5.4): the label a one-machine plan carries when it lays ONE box of an
/// estate already laid, again (the estate guardian's `Lay` step, after the old box is fenced).
/// Its value is the machine's role, `main` or `twin`; the front is never laid again.
pub const RELAY_LABEL: &str = "relay";
/// The roles a relay may lay.
pub const RELAY_ROLES: [&str; 2] = ["main", "twin"];
/// **The added mark** (P3-FACTS, a plan change that adds machines): the label each machine of a
/// plan carries when a plan change lays it BESIDE an estate that stands (the Lake's `be-2`, its
/// backends going 1 → 2). Its value is the change's own name (`change-1`): the spend cap commits
/// the machines under `<reference>#<value>` ([`spend_key`]), never over what the estate's own lay
/// committed.
pub const ADDED_LABEL: &str = "added";
/// The step words this module writes to the journal.
pub mod step {
/// The lay began (set-wide intent) or finished (confirmed).
pub const LAY: &str = "lay";
/// One disk; `volume` is its title, `detail` its provider id when confirmed.
pub const DISK: &str = "lay-disk";
/// One server; `volume` is its hostname, `detail` its provider id when confirmed.
pub const SERVER: &str = "lay-server";
/// A server's plan change; `volume` is its provider id, `detail` the plan.
pub const PLAN: &str = "plan";
/// One machine's CLONE of the release medium (no-dwarf step 4, 2026-10-08); `volume` is
/// the clone's title ([`super::clone_title`]), `detail` the source medium on the intent
/// and the clone's provider id when confirmed.
pub const MEDIUM: &str = "lay-medium";
/// That clone given back after the hand-off (`Change::ForgetMedia`);
/// `volume` its title, `detail` its provider id, confirmed once it reads as gone.
pub const MEDIUM_GONE: &str = "lay-medium-gone";
/// One machine's metadata service turned OFF after the hand-off (its seed rode the
/// `user_data`; 2026-10-09): `volume` its hostname, `detail` its provider id;
/// `Failed` with the provider's words when it would not (the seed's bootstrap key is
/// spent once the box joined, so a refusal is recorded, not fatal).
pub const METADATA_OFF: &str = "lay-metadata-off";
/// The release's medium storage IMPORTED from its object (2026-10-09): `volume` its title
/// ([`super::MediaObject::storage_title`]), `detail` its provider id when confirmed. It is
/// the RELEASE's (shared by every order's clones), never given back with an order.
pub const MEDIUM_IMPORT: &str = "lay-medium-import";
/// One laid machine booted once from a KEEP medium (F4, a sold estate upgraded in place,
/// 2026-10-09): `volume` its hostname, `detail` its provider id; confirmed once it runs its
/// upgraded disk again.
pub const KEEP_BOOT: &str = "keep-boot";
/// One laid machine's inbound firewall WRITTEN, per its role (stinky S7, 2026-10-10: a
/// sold estate's servers were laid `"firewall": "on"` with no rules): `volume` its
/// hostname, `detail` the rule count when confirmed, the provider's words when failed.
pub const FIREWALL: &str = "lay-firewall";
/// A sold estate's lost FRONT restored from its daily backup (S8, 2026-10-11): `volume`
/// its hostname, `detail` the backup's provider id at the intent, the disk it was
/// restored onto when confirmed.
pub const RESTORE: &str = "restore-backup";
}
/// The label a release-medium clone carries beside the estate's reference (2026-10-09): a
/// clone takes no labels at the clone call, so it is labelled right after — the label search
/// and the orphan sweep find it by estate.
pub const CLONE_LABEL: &str = "release-clone";
/// **How long a whole keep boot may take** (audit 7, 2026-10-18): the keep medium imported and
/// cloned, the box stopped, the medium's run (the provider's own budget, 45 min at UpCloud),
/// the clone given back and the box started — each provider wait bounded on its own. The
/// shop's Upgrade call is held this long, and a front waits on it exactly as long: a front
/// that gave up first would report a failure while the shop still works. The one constant is
/// `monetize_product::KEEP_BOOT_WITHIN`, which the fronts read too (lane G9, A7 row 9).
pub use monetize_product::KEEP_BOOT_WITHIN;
/// **The title of a machine's release-medium clone**: the machine's title and
/// `-release-clone` — the suffix an orphan sweep finds a clone by when a crash left it
/// unrecorded.
pub fn clone_title(machine_title: &str) -> String {
format!("{machine_title}{CLONE_SUFFIX}")
}
/// The suffix every release-medium clone's title ends with ([`clone_title`]).
pub const CLONE_SUFFIX: &str = "-release-clone";
/// Pure: **the release clones this journal recorded and has not seen given back** —
/// `(title, provider id)`, in the order they were confirmed. ONLY these are ever given back
/// (`Change::ForgetMedia`): a storage the journal did not record is never
/// touched, whatever its title says.
pub fn journaled_clones(records: &[crate::grow::GrowRecord]) -> Vec<(String, String)> {
let mut out: Vec<(String, String)> = Vec::new();
for r in records
.iter()
.filter(|r| r.mark == crate::grow::Mark::Confirmed && !r.detail.is_empty())
{
match r.step.as_str() {
step::MEDIUM if !out.iter().any(|(_, id)| *id == r.detail) => {
out.push((r.volume.clone(), r.detail.clone()))
}
step::MEDIUM_GONE => out.retain(|(_, id)| *id != r.detail),
_ => {}
}
}
out
}
/// **What to lay**: the estate's machines, each with its disks. The first disk
/// of a machine is its system disk (blank, `virtio:0`); the rest are data
/// disks in order (`virtio:1`, `virtio:2`, …).
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct EstatePlan {
/// The order's reference, `estate/<company>`: the label value on every resource.
pub reference: String,
pub company: String,
pub zone: Zone,
pub machines: Vec<MachinePlan>,
/// **DEV-NARRATE's source** (lane N2, 2026-10-20): the ONE box's public IPv4. Set, every
/// machine of the estate admits `udp/51822` (nordisk-narrate's port) FROM THIS ADDRESS ONLY,
/// so the box that lays a test site hears its boot log; empty (the default, and always for a
/// sold estate or a serving site) keeps it closed. Set by the shop for a site that says
/// `[estates.<label>] narrate = true`, never by a provider.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub narrate_from: String,
/// **The product's shape the plan is checked against** (A4-FACTS): the roles its facts say an
/// estate of it lays ([`PlanShape`]), set by the shop from the product's facts before the
/// plan reaches a provider. `None` (an older shop's plan, a provider's own test): the shape
/// before A4-FACTS — a plan that names a main or a twin is exactly one front, one main and
/// one twin ([`EstatePlan::check`]).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub shape: Option<PlanShape>,
}
/// **The roles an estate of a product may lay** (A4-FACTS; the product's `Facts.roles`, as the
/// cloud reads them): what [`EstatePlan::check`] holds a plan to instead of Gunnar's three
/// machines. A product whose twin is optional lays a front and a main alone, and a plan change
/// lays its twin beside them later.
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct PlanShape {
pub roles: Vec<ShapeRole>,
}
/// One role of a [`PlanShape`] (the product's `RoleFacts`, as the cloud reads it).
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct ShapeRole {
pub role: String,
/// Laid n times: its first machine carries the role's name, the next ones `<role>-<n>`.
#[serde(default)]
pub many: bool,
/// May be left out of the estate.
#[serde(default)]
pub optional: bool,
/// **The ports this role opens to anyone, as the product declares them** (OX-WGDS5,
/// 2026-10-29): what the cloud's firewall admits from any address on top of the estate's own
/// lanes (the web, the relay, the installer's link). A product that declares none opens
/// nothing more; the cloud names no product here, it reads this list.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub open: Vec<OpenPort>,
}
/// **One port a role opens to any address** (a [`ShapeRole::open`] entry): `protocol` is `udp`
/// or `tcp`, `why` the product's words for it (the rule's comment).
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct OpenPort {
pub protocol: String,
pub port: u16,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub why: String,
}
impl PlanShape {
/// Pure: **the ports the machine `machine` opens to anyone** — its role's
/// [`ShapeRole::open`] (a numbered member's, its many-role's); none for a machine of no role.
pub fn open_of(&self, machine: &str) -> Vec<OpenPort> {
self.role_of(machine)
.map(|r| r.open.clone())
.unwrap_or_default()
}
/// Pure: **the role `machine` is laid as** — the role of that name, or the many-role
/// `<role>` of a numbered member `<role>-<n>` (n ≥ 2).
pub fn role_of(&self, machine: &str) -> Option<&ShapeRole> {
self.roles.iter().find(|r| r.role == machine).or_else(|| {
let (base, n) = machine.rsplit_once('-')?;
n.parse::<u32>().ok().filter(|n| *n >= 2)?;
self.roles.iter().find(|r| r.many && r.role == base)
})
}
/// The roles, as a sentence lists them.
fn names(&self) -> String {
self.roles
.iter()
.map(|r| r.role.as_str())
.collect::<Vec<_>>()
.join(", ")
}
/// Pure: **why `plan`'s machines are not an estate of this shape**, or `None`. Every machine
/// is one of the roles; a plan that lays the whole estate (no machine marked) lays every role
/// that is not optional; a relay ([`RELAY_LABEL`]) lays ONE machine of its role; a plan
/// change's machines ([`ADDED_LABEL`]) are each marked with the one change.
pub fn refusal(&self, plan: &EstatePlan) -> Option<String> {
if self.roles.is_empty() {
return Some("the product's shape names no role: nothing is laid by it".into());
}
if let Some(m) = plan
.machines
.iter()
.find(|m| self.role_of(&m.role).is_none())
{
return Some(format!(
"the plan lays a {:?}, which is not a role of this product ({})",
m.role,
self.names()
));
}
let relays: Vec<&MachinePlan> = plan
.machines
.iter()
.filter(|m| m.labels.contains_key(RELAY_LABEL))
.collect();
if !relays.is_empty() {
let ok = plan.machines.len() == 1
&& relays[0].labels.get(RELAY_LABEL) == Some(&relays[0].role);
return (!ok).then(|| format!("a relay lays ONE machine marked relay=<its role>; this plan marks {} of {} machine(s)", relays.len(), plan.machines.len()));
}
let added: Vec<&String> = plan
.machines
.iter()
.filter_map(|m| m.labels.get(ADDED_LABEL))
.collect();
if !added.is_empty() {
let one = added.len() == plan.machines.len()
&& added.iter().all(|a| *a == added[0] && !a.trim().is_empty());
return (!one).then(|| format!("a plan that lays machines beside a standing estate marks every one {ADDED_LABEL}=<the change>; this plan marks {} of {}", added.len(), plan.machines.len()));
}
let laid: Vec<&str> = plan.machines.iter().map(|m| m.role.as_str()).collect();
let missing: Vec<&str> = self
.roles
.iter()
.filter(|r| !r.optional && !laid.contains(&r.role.as_str()))
.map(|r| r.role.as_str())
.collect();
(!missing.is_empty()).then(|| {
format!(
"an estate of this product lays {}; this plan has no {}",
self.names(),
missing.join(", ")
)
})
}
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct MachinePlan {
/// `front` | `main` | `twin`.
pub role: String,
pub hostname: String,
pub title: String,
pub plan: String,
/// `disk,cdrom`: a blank system disk does not boot, so the box falls
/// through to the installer medium the boot lane attaches.
pub boot_order: String,
/// Labels beyond the reference (`role`, the estate's own labels).
#[serde(default)]
pub labels: BTreeMap<String, String>,
pub disks: Vec<DiskPlan>,
/// **The machine's network interfaces** — what is laid AND what is
/// priced: every public IPv4 address is a billed item at the provider
/// (UpCloud `ipv4_address`, €3.51 a month in se-sto1, measured
/// 2026-10-05). Absent in an older plan: a public and a utility IPv4,
/// what every estate machine has always been laid with.
#[serde(default = "default_interfaces")]
pub interfaces: Vec<NetInterface>,
/// **The seedless release medium this machine boots** (no operator machine in a sale,
/// 2026-10-08): the provider's id of the release's medium storage for this role's kind.
/// The lay CLONES it for this machine (journaled, [`step::MEDIUM`]) and attaches the clone
/// as the cdrom; `Change::ForgetMedia` gives the journaled clones back.
/// Empty: no medium (the kit attaches one).
#[serde(default)]
pub medium: String,
/// **This machine's pairing seed**, given to it as the provider's `user_data` (the box
/// reads it from the metadata on its first boot). It crosses the cloud guest's ABI with
/// the plan, set only for the lay call (never kept with a run); redacted in `Debug`.
#[serde(default, skip_serializing_if = "UserData::is_empty")]
pub user_data: UserData,
/// **The release medium as an OBJECT** (owner 2026-10-09: the release media live in
/// Object Storage): when [`Self::medium`] is empty, the lay imports this object into the
/// release's medium storage — once per release and zone, the provider fetching the bytes
/// itself — and clones THAT for this machine as before.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub medium_object: Option<MediaObject>,
/// **The release's TEMPLATE for this machine's kind** (owner 2026-10-12, option (a)): the
/// provider's id of a template made from the seedless release medium's installed disk,
/// before its first boot. Set: the lay CLONES it as the machine's system disk (disk 0)
/// and boots that disk — no medium, no installer pass; the seed still reaches the box as
/// its `user_data`. Empty: the medium path above.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub template: String,
}
/// **One release medium in Object Storage**: where the provider fetches it, and what its
/// bytes must be. `url` may be presigned (a credential while it lives): `Debug` prints it
/// without its query.
#[derive(Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub struct MediaObject {
pub url: String,
/// The bytes' sha256, hex: the import is refused unless the provider read exactly these.
pub sha256: String,
pub bytes: u64,
}
impl std::fmt::Debug for MediaObject {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"MediaObject({}?…, sha256 {}, {} bytes)",
self.url.split('?').next().unwrap_or(""),
self.sha256,
self.bytes
)
}
}
impl MediaObject {
/// The title of the release's medium storage for these bytes: `release-medium-<sha8>`.
pub fn storage_title(&self) -> String {
format!(
"release-medium-{}",
&self.sha256[..self.sha256.len().min(8)]
)
}
}
/// A machine's `user_data` (its pairing seed: a bootstrap key that joins one slot once).
/// `Debug` prints its length only.
#[derive(Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
pub struct UserData(pub String);
impl UserData {
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl std::fmt::Debug for UserData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "UserData({} bytes)", self.0.len())
}
}
/// One network interface of a laid machine.
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct NetInterface {
/// `public` | `utility` | `private`.
pub kind: String,
/// `IPv4` | `IPv6`.
pub family: String,
}
/// A public and a utility IPv4: what an estate machine is laid with.
pub fn default_interfaces() -> Vec<NetInterface> {
vec![
NetInterface {
kind: "public".into(),
family: "IPv4".into(),
},
NetInterface {
kind: "utility".into(),
family: "IPv4".into(),
},
]
}
/// **A box behind its estate's front** (owner 2026-10-23, the lean estate: one public IPv4 per
/// estate): a utility IPv4 only. The front is the estate's hub — the WireGuard relay the boxes
/// pair and follow over, and their one way out — so nothing dials main or the twin from outside.
pub fn behind_the_front_interfaces() -> Vec<NetInterface> {
vec![NetInterface {
kind: "utility".into(),
family: "IPv4".into(),
}]
}
impl MachinePlan {
/// **How many public IPv4 addresses this machine holds** — each one billed.
pub fn public_ipv4(&self) -> u64 {
self.interfaces
.iter()
.filter(|i| i.kind == "public" && i.family.eq_ignore_ascii_case("IPv4"))
.count() as u64
}
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct DiskPlan {
pub title: String,
pub gib: u64,
/// `standard` | `maxiops` | `hdd`.
pub tier: String,
#[serde(default)]
pub labels: BTreeMap<String, String>,
}
/// **What was laid**, exactly as the provider reports it.
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct LaidEstate {
pub reference: String,
pub company: String,
pub zone: Zone,
pub machines: Vec<LaidMachine>,
/// **DEV-NARRATE's source** (lane N2, 2026-10-20): the ONE box's public IPv4. Set, every
/// machine of the estate admits `udp/51822` (nordisk-narrate's port) FROM THIS ADDRESS ONLY,
/// so the box that laid a test site hears its boot log; empty (the default, and always for a
/// sold estate or a serving site) keeps it closed. Set by the shop for a site that says
/// `[estates.<label>] narrate = true`, never by a provider.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub narrate_from: String,
/// **The product's shape as the shop reads it NOW** ([`EstatePlan::shape`]; OX-WGDS5): every
/// firewall written again from this record opens each machine's declared ports
/// ([`PlanShape::open_of`]) and no other. The shop sets it before each write (an estate laid
/// before a product declared a port gets it on its next update); `None` opens none.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub shape: Option<PlanShape>,
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct LaidMachine {
pub role: String,
pub hostname: String,
pub title: String,
pub plan: String,
/// The provider's id for the server (an UpCloud server UUID).
pub server: String,
pub public_ipv4: String,
pub utility_ipv4: String,
pub disks: Vec<LaidDisk>,
/// The clone of the release medium this machine boots ([`MachinePlan::medium`]); empty
/// when none. Ejected and deleted once the estate is handed over.
#[serde(default)]
pub medium_clone: String,
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct LaidDisk {
pub title: String,
/// The provider's id for the disk (an UpCloud storage UUID).
pub id: String,
pub gib: u64,
pub tier: String,
/// `virtio:0` is the system disk.
pub address: String,
}
impl LaidEstate {
pub fn machine(&self, role: &str) -> Option<&LaidMachine> {
self.machines.iter().find(|m| m.role == role)
}
}
impl LaidMachine {
/// Every provider id the laid record names for this machine — the server first, then its
/// disks in order, then the release clone — as laid (an UpCloud uuid is bare here).
pub fn laid_ids(&self) -> Vec<&str> {
let mut out = vec![self.server.as_str()];
out.extend(self.disks.iter().map(|d| d.id.as_str()));
out.push(self.medium_clone.as_str());
out.retain(|i| !i.trim().is_empty());
out
}
}
/// **A laid id and a listed id are the same resource** when equal, or when the listed one is
/// the laid one under a kind prefix (`server/<uuid>`, `storage/<uuid>`): a provider lays with
/// its bare id and lists with its own scheme.
pub fn same_resource(listed: &str, laid: &str) -> bool {
!laid.is_empty()
&& (listed == laid
|| listed
.strip_suffix(laid)
.is_some_and(|p| p.ends_with('/') && !p[..p.len() - 1].contains('/')))
}
/// The spend cap's key for a lay: `<reference>` for an estate, `<reference>#<role>` for a
/// relay ([`RELAY_LABEL`]) — so a relay never settles over what its estate's lay committed,
/// and the fence of that box gives back exactly its own month — and `<reference>#<change>` for
/// the machines a plan change adds, every one marked [`ADDED_LABEL`] `= <change>`.
pub fn spend_key(plan: &EstatePlan) -> String {
let added: Vec<&String> = plan
.machines
.iter()
.filter_map(|m| m.labels.get(ADDED_LABEL))
.collect();
if !added.is_empty()
&& added.len() == plan.machines.len()
&& added.iter().all(|a| *a == added[0] && !a.trim().is_empty())
{
return relay_key(&plan.reference, added[0]);
}
match plan.machines.as_slice() {
[m] => match m.labels.get(RELAY_LABEL) {
Some(role) => relay_key(&plan.reference, role),
None => plan.reference.clone(),
},
_ => plan.reference.clone(),
}
}
/// `<reference>#<role>`: the relay of `role` in the estate's spend.
pub fn relay_key(reference: &str, role: &str) -> String {
format!("{reference}#{role}")
}
impl EstatePlan {
/// **Refuse a plan that is not an estate**, before anything is bought:
/// a bad company name, a reference that is not `estate/<company>`, not the
/// product's own shape when the plan names one ([`EstatePlan::shape`],
/// [`PlanShape::refusal`]); without one, not
/// exactly one front, one main and one twin (or ONE main or twin marked
/// [`RELAY_LABEL`] — a relay; or, for a product guest's own lifecycle that names none of
/// those roles, each of its own roles once), a machine with no system
/// disk, a disk of 0 GiB, or two resources with one title (the title is
/// what an adopting retry matches on).
pub fn check(&self) -> Result<(), CloudError> {
if let Some(why) = company_name_refusal(&self.company) {
return Err(CloudError::Refused(why));
}
if self.reference != reference_of(&self.company) {
return Err(CloudError::Refused(format!(
"the estate's reference must be {:?}, not {:?}: it is the label every resource carries",
reference_of(&self.company),
self.reference
)));
}
// **The product's own shape** (A4-FACTS), when the shop named it: its roles, never the
// three machines below.
if let Some(shape) = &self.shape {
if let Some(why) = shape.refusal(self) {
return Err(CloudError::Refused(why));
}
let mut roles: Vec<&str> = self.machines.iter().map(|m| m.role.as_str()).collect();
roles.sort_unstable();
if let Some(w) = roles.windows(2).find(|w| w[0] == w[1]) {
return Err(CloudError::Refused(format!(
"the role {:?} is laid twice; an estate lays each of its roles once",
w[0]
)));
}
return self.check_machines();
}
let marks: Vec<(&str, &str)> = self
.machines
.iter()
.filter_map(|m| {
m.labels
.get(RELAY_LABEL)
.map(|r| (m.role.as_str(), r.as_str()))
})
.collect();
if !marks.is_empty() {
let ok = self.machines.len() == 1
&& marks.len() == 1
&& marks[0].0 == marks[0].1
&& RELAY_ROLES.contains(&marks[0].0);
if !ok {
return Err(CloudError::Refused(format!("a relay lays ONE main or twin marked relay=<its role>; this plan marks {marks:?} over {} machine(s)", self.machines.len())));
}
}
let mut roles: Vec<&str> = self.machines.iter().map(|m| m.role.as_str()).collect();
roles.sort_unstable();
let mut want = ROLES.to_vec();
want.sort_unstable();
// The relay roles ([`RELAY_ROLES`], the ones the guardian lays again) come as the core's
// three-machine shape: a plan that names a main or a twin is exactly one front, one main
// and one twin ([`ROLES`]), so a relay that lost its mark is never laid as an estate.
// Any other plan names the product's own roles (A17; the Lake's `front`, `ingestor`,
// `be`, `fe`, `graph`, with `be-2`, … for a role laid n times): each is laid once.
let relay_shaped = roles.iter().any(|r| RELAY_ROLES.contains(r));
if marks.is_empty() && relay_shaped && roles != want {
return Err(CloudError::Refused(format!(
"an estate is exactly one front, one main and one twin; this plan has {roles:?}"
)));
}
if marks.is_empty() && !relay_shaped {
if roles.is_empty() {
return Err(CloudError::Refused(
"an estate has at least one machine; this plan has none".into(),
));
}
if let Some(w) = roles.windows(2).find(|w| w[0] == w[1]) {
return Err(CloudError::Refused(format!(
"the role {:?} is laid twice; an estate lays each of its roles once",
w[0]
)));
}
}
self.check_machines()
}
/// Every machine has a system disk, a plan, a hostname and a title; no disk is 0 GiB; no two
/// resources share a title.
fn check_machines(&self) -> Result<(), CloudError> {
let mut titles = std::collections::BTreeSet::new();
for m in &self.machines {
if m.disks.is_empty() {
return Err(CloudError::Refused(format!(
"{} has no system disk",
m.role
)));
}
if m.plan.is_empty() || m.hostname.is_empty() || m.title.is_empty() {
return Err(CloudError::Refused(format!(
"{} needs a plan, a hostname and a title",
m.role
)));
}
if !titles.insert(m.title.clone()) {
return Err(CloudError::Refused(format!(
"two resources are titled {:?}",
m.title
)));
}
for d in &m.disks {
if d.gib == 0 {
return Err(CloudError::Refused(format!(
"{}: the disk {:?} is 0 GiB",
m.role, d.title
)));
}
if !titles.insert(d.title.clone()) {
return Err(CloudError::Refused(format!(
"two resources are titled {:?}",
d.title
)));
}
}
}
Ok(())
}
/// Every disk of the plan, with its machine.
pub fn disks(&self) -> impl Iterator<Item = (&MachinePlan, usize, &DiskPlan)> {
self.machines
.iter()
.flat_map(|m| m.disks.iter().enumerate().map(move |(i, d)| (m, i, d)))
}
}
/// The order's reference for a company: `estate/<company>`.
pub fn reference_of(company: &str) -> String {
format!("estate/{company}")
}
/// Names no company may take: the serving estate's own names, the ONE `monetize` (OM-12) and
/// the test estates'. A product's own name is the shop's to refuse, from the products it links
/// (monetize-server `estate_order::product_name_refusal`, MONETIZE-FIX): the cloud names none.
pub const RESERVED: [&str; 4] = ["www", "git", "monetize", "blue"];
/// **Why `name` cannot be a company's estate name**, or `None` when it can.
///
/// The kit's instance rule (`private-gunnar-ops/xtask/src/instance.rs`), so
/// an order monetize accepts is an instance the kit can install: lowercase
/// letters and digits, starting with a letter, at most 16 characters; not a
/// name the serving estate uses, and not `test<N>` (the test estates').
pub fn company_name_refusal(name: &str) -> Option<String> {
if name.is_empty() || name.len() > 16 {
return Some(format!(
"a company name is 1 to 16 characters; {name:?} is {}",
name.len()
));
}
if !name.starts_with(|c: char| c.is_ascii_lowercase()) {
return Some(format!(
"a company name starts with a lowercase letter: {name:?}"
));
}
if !name
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
{
return Some(format!(
"a company name is lowercase letters and digits only: {name:?}"
));
}
if RESERVED.contains(&name) {
return Some(format!("{name:?} is reserved"));
}
if name
.strip_prefix("test")
.is_some_and(|rest| rest.chars().all(|c| c.is_ascii_digit()))
{
return Some(format!("{name:?} is reserved for the test estates"));
}
None
}
/// **One plan the provider sells**, as the provider describes it: what a
/// buyer sizes a machine from (cores, memory) and what it costs a month. Read
/// from the provider's own list (`Cloud::catalog`),
/// never from a constant.
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct PlanSpec {
pub name: String,
pub cores: u32,
pub memory_mib: u64,
/// What the server alone costs a month at this plan (disks apart).
pub price: crate::Price,
}
/// **The twin's size follows main's** (owner, 2026-10-04): the buyer never
/// sizes the twin. Its CPU and memory are an eighth of main's, never below
/// one core and 4 GiB (main 64 cores → twin 8 cores); the smallest plan in
/// the provider's list that meets both is chosen. `None`: no plan does.
///
/// **The floor is 4 GiB** (owner, 2026-10-13; it was 2): on a 2 GB twin
/// gunnar-server itself warns "this box cannot afford the handle-cache floor
/// … that is the 2 GB box's collapse" (`gunnar::limits`). It is the floor on
/// every size, for every NEW choice of a twin; a twin already laid smaller
/// keeps its plan until its estate's plan is changed.
pub const TWIN_FRACTION: u32 = 8;
pub const TWIN_FLOOR_CORES: u32 = 1;
pub const TWIN_FLOOR_MEMORY_MIB: u64 = 4096;
pub fn twin_plan_for<'a>(catalog: &'a [PlanSpec], main: &PlanSpec) -> Option<&'a PlanSpec> {
twin_plan_floored(catalog, main, TWIN_FLOOR_MEMORY_MIB)
}
/// [`twin_plan_for`] over a product's OWN memory floor (owner 2026-10-23, the lean Holger
/// estate: holger-server's twin runs in 1 GB; the 4 GiB floor is gunnar-server's). The eighth of
/// main and the one-core floor are everyone's.
pub fn twin_plan_floored<'a>(
catalog: &'a [PlanSpec],
main: &PlanSpec,
floor_memory_mib: u64,
) -> Option<&'a PlanSpec> {
let cores = main.cores.div_ceil(TWIN_FRACTION).max(TWIN_FLOOR_CORES);
let memory = (main.memory_mib / TWIN_FRACTION as u64).max(floor_memory_mib);
catalog
.iter()
.filter(|p| p.cores >= cores && p.memory_mib >= memory)
.min_by_key(|p| {
(
p.cores,
p.memory_mib,
p.price.minor_per_month,
p.name.clone(),
)
})
}
/// The refusal every default answers with: this provider cannot lay an estate.
pub fn unsupported(provider: &str) -> CloudError {
CloudError::Refused(format!(
"the {provider} cloud cannot lay an estate: it has no `lay_estate` implementation. Nothing was bought"
))
}
/// **One laid machine upgraded IN PLACE** (F4, 2026-10-09): booted once from a clone of the
/// release's KEEP medium (`korp.reinstall=keep`, `install_then=poweroff`): the ESP and rootfs
/// are rewritten, `/data` is kept byte for byte, and the box powers off; then it boots its disk
/// again (`Boot::Keep`). The medium is a storage id, or the object the
/// provider imports it from.
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct KeepBoot {
/// The estate's reference (`estate/<company>`): the clone is labelled with it.
pub reference: String,
pub zone: Zone,
pub machine: LaidMachine,
#[serde(default)]
pub medium: String,
#[serde(default)]
pub medium_object: Option<MediaObject>,
}
/// **A sold estate's lost FRONT restored from its provider backup** (S8, 2026-10-11;
/// `Boot::Restore`). The front was laid with the provider's own
/// daily backup of its one disk (S6, `simple_backup "0300,dailies"`); its disk holds the
/// estate's own keys, so a restore — unlike a re-lay from the release medium — needs nothing
/// trusted again. `machine` is the front AS LAID (its server, its disk); `plan` is the run's
/// front plan, what a server made again is made from when the old one is gone.
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct RestoreFromBackup {
/// The estate's reference (`estate/<company>`): what a disk or server made again is
/// labelled with.
pub reference: String,
pub zone: Zone,
pub machine: LaidMachine,
pub plan: MachinePlan,
/// [`EstatePlan::narrate_from`]: the estate's firewall written again after a restore keeps
/// DEV-NARRATE open from this address (empty: closed).
#[serde(default, skip_serializing_if = "String::is_empty")]
pub narrate_from: String,
/// [`LaidEstate::shape`]: the firewall written again after a restore opens the ports the
/// product declares (`None`: none).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub shape: Option<PlanShape>,
}
/// What `Boot::Restore` did.
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct Restored {
/// The machine as it stands now.
pub machine: LaidMachine,
/// The backup's provider id.
pub backup: String,
/// When the backup was taken (Unix ms; `0` when the provider did not say).
pub backup_created_ms: u64,
/// `true`: the server or its disk was MADE AGAIN (a new server, new addresses — DNS and
/// the estate's firewall follow); `false`: restored in place, the same server and disk.
pub moved: bool,
}
/// The refusal of an estate verb a provider does not implement.
pub fn unsupported_verb(provider: &str, verb: &str) -> CloudError {
CloudError::Refused(format!(
"the {provider} cloud has no `{verb}`: nothing was changed"
))
}
#[cfg(test)]
mod tests {
use super::*;
/// ★ **Only the clones the journal recorded are ever given back** (no-dwarf step 4,
/// 2026-10-08): a confirmed clone is listed until a confirmed give-back of the same id; an
/// intent with no id, a failed clone and a storage the journal never named are not.
#[test]
fn only_the_journaled_clones_are_given_back() {
use crate::grow::{GrowRecord, Mark};
let rec = |step: &str, mark: Mark, title: &str, id: &str| {
let mut r = GrowRecord::set_wide(1, 1, step, mark, id.to_string());
r.volume = title.to_string();
r
};
let front = clone_title("acme-front");
assert!(front.ends_with(CLONE_SUFFIX));
let records = vec![
rec(step::MEDIUM, Mark::Intent, &front, "01-release-front"),
rec(step::MEDIUM, Mark::Confirmed, &front, "c-front"),
rec(
step::MEDIUM,
Mark::Intent,
"acme-main-release-clone",
"01-release-gunnar",
),
rec(
step::MEDIUM,
Mark::Failed,
"acme-main-release-clone",
"QUOTA",
),
rec(
step::MEDIUM,
Mark::Confirmed,
"acme-twin-release-clone",
"c-twin",
),
rec(
step::MEDIUM,
Mark::Confirmed,
"acme-twin-release-clone",
"c-twin",
),
];
assert_eq!(
journaled_clones(&records),
vec![
(front.clone(), "c-front".to_string()),
("acme-twin-release-clone".to_string(), "c-twin".to_string())
]
);
let mut after = records.clone();
after.push(rec(step::MEDIUM_GONE, Mark::Intent, &front, "c-front"));
assert_eq!(
journaled_clones(&after).len(),
2,
"an intent to give back is not a give-back"
);
after.push(rec(step::MEDIUM_GONE, Mark::Confirmed, &front, "c-front"));
assert_eq!(
journaled_clones(&after),
vec![("acme-twin-release-clone".to_string(), "c-twin".to_string())]
);
}
/// ★ **The machines a plan change adds are their own spend** (P3-FACTS): a plan of machines
/// every one marked [`ADDED_LABEL`] `= change-1` is committed under `<reference>#change-1`,
/// never under the estate's own key (its lay's month); a plan that marks only some, or marks
/// them with two changes, is the estate's (and its check refuses nothing new).
#[test]
fn the_machines_a_plan_change_adds_are_spent_under_the_change_never_over_the_estate() {
let mut p = plan("acme");
assert_eq!(spend_key(&p), "estate/acme");
p.machines.retain(|m| m.role == "front");
p.machines[0].role = "be-2".into();
p.machines[0]
.labels
.insert(ADDED_LABEL.into(), "change-1".into());
assert_eq!(spend_key(&p), "estate/acme#change-1");
assert!(p.check().is_ok(), "{:?}", p.check());
let mut two = p.clone();
let mut more = two.machines[0].clone();
(more.role, more.title, more.hostname) =
("be-3".into(), "be-3 acme".into(), "be-3-acme".into());
more.disks[0].title = "be-3 system".into();
two.machines.push(more.clone());
assert_eq!(
spend_key(&two),
"estate/acme#change-1",
"every machine of the one change"
);
two.machines[1]
.labels
.insert(ADDED_LABEL.into(), "change-2".into());
assert_eq!(
spend_key(&two),
"estate/acme",
"two changes' marks: not one change's spend"
);
two.machines[1].labels.remove(ADDED_LABEL);
assert_eq!(
spend_key(&two),
"estate/acme",
"a machine unmarked: the estate's"
);
}
pub(crate) fn plan(company: &str) -> EstatePlan {
let m = |role: &str, disks: Vec<(&str, u64)>| MachinePlan {
role: role.into(),
hostname: format!("gunnar-{role}-{company}"),
title: format!("gunnar {role}-{company}"),
plan: "STARTER-1xCPU-2GB".into(),
boot_order: "disk,cdrom".into(),
labels: BTreeMap::from([("role".to_string(), role.to_string())]),
interfaces: default_interfaces(),
medium: String::new(),
medium_object: None,
template: String::new(),
user_data: UserData::default(),
disks: disks
.into_iter()
.map(|(t, gib)| DiskPlan {
title: format!("gunnar-{role}-{company} {t}"),
gib,
tier: "standard".into(),
labels: BTreeMap::new(),
})
.collect(),
};
EstatePlan {
reference: reference_of(company),
company: company.into(),
zone: Zone("se-sto1".into()),
machines: vec![
m("front", vec![("system", 10)]),
m("main", vec![("system", 10), ("data 1", 10)]),
m("twin", vec![("system", 10), ("data", 20)]),
],
narrate_from: String::new(),
shape: None,
}
}
#[test]
fn a_company_name_is_the_kits_instance_rule() {
for ok in ["acme", "a", "acme2", "abcdefghijklmnop"] {
assert_eq!(company_name_refusal(ok), None, "{ok}");
}
for bad in [
"",
"Acme",
"2acme",
"acme-corp",
"acme.rs",
"abcdefghijklmnopq",
"www",
"git",
"monetize",
"test2",
"test",
"blue",
] {
assert!(company_name_refusal(bad).is_some(), "{bad} must be refused");
}
}
/// OM-12 (ONE monetize): the core's own names are never sold as a company (a product's own
/// name is the shop's to refuse, from what it links: monetize-server `product_name_refusal`).
#[test]
fn the_cores_own_names_are_not_for_sale() {
for name in ["monetize", "www", "git"] {
assert!(
company_name_refusal(name).is_some(),
"{name} is the core's own name and must be refused"
);
}
}
/// **A product guest's own estate is laid by its own roles** (A17, the echo bench): a
/// lifecycle that names its own roles (`box`) is an estate when each role is laid once. A
/// plan that names a relay role (main or twin) is still exactly front, main and twin.
#[test]
fn a_product_guests_own_roles_are_an_estate_when_each_is_laid_once() {
let mut p = plan("acme");
p.machines.truncate(1);
p.machines[0].role = "box".into();
assert!(p.check().is_ok(), "{:?}", p.check());
let mut two = p.clone();
two.machines.push(two.machines[0].clone());
two.machines[1].title = "another".into();
for d in two.machines[1].disks.iter_mut() {
d.title = format!("{} 2", d.title);
}
assert!(
matches!(two.check(), Err(CloudError::Refused(w)) if w.contains("twice")),
"a role laid twice is not an estate"
);
let mut mixed = plan("acme");
mixed.machines[2].role = "box".into();
assert!(
matches!(mixed.check(), Err(CloudError::Refused(w)) if w.contains("one front, one main and one twin")),
"the relay roles stay exactly three"
);
let mut none = plan("acme");
none.machines.clear();
assert!(
matches!(none.check(), Err(CloudError::Refused(_))),
"no machine is no estate"
);
}
/// **A relay lays ONE gunnar box of an estate already laid, again** (5.4, the guardian): the
/// one machine carries `relay=<its role>`; a front, two machines, or an unmarked one-machine
/// plan is still not an estate.
#[test]
fn a_relay_plan_is_one_main_or_twin_marked_as_a_relay() {
let one = |role: &str, mark: Option<&str>| {
let mut p = plan("acme");
p.machines.retain(|m| m.role == role);
if let Some(r) = mark {
p.machines[0].labels.insert(RELAY_LABEL.into(), r.into());
}
p
};
assert!(one("twin", Some("twin")).check().is_ok());
assert!(one("main", Some("main")).check().is_ok());
assert!(
matches!(one("twin", None).check(), Err(CloudError::Refused(w)) if w.contains("one twin")),
"unmarked: not an estate"
);
assert!(
matches!(one("front", Some("front")).check(), Err(CloudError::Refused(w)) if w.contains("relay")),
"the front is never laid again"
);
assert!(
matches!(
one("twin", Some("main")).check(),
Err(CloudError::Refused(_))
),
"the mark names the machine's own role"
);
let mut two = plan("acme");
two.machines.retain(|m| m.role != "front");
two.machines[0]
.labels
.insert(RELAY_LABEL.into(), "main".into());
assert!(
matches!(two.check(), Err(CloudError::Refused(_))),
"a relay is one box"
);
let mut whole = plan("acme");
whole.machines[2]
.labels
.insert(RELAY_LABEL.into(), "twin".into());
assert!(
matches!(whole.check(), Err(CloudError::Refused(w)) if w.contains("relay")),
"a whole estate carries no relay mark"
);
}
#[test]
fn a_plan_is_exactly_front_main_and_twin_each_with_a_system_disk() {
assert!(plan("acme").check().is_ok());
let mut p = plan("acme");
p.machines.pop();
assert!(matches!(p.check(), Err(CloudError::Refused(w)) if w.contains("one twin")));
let mut p = plan("acme");
p.machines[1].disks.clear();
assert!(matches!(p.check(), Err(CloudError::Refused(w)) if w.contains("no system disk")));
let mut p = plan("acme");
p.machines[2].disks[1].gib = 0;
assert!(matches!(p.check(), Err(CloudError::Refused(w)) if w.contains("0 GiB")));
let mut p = plan("acme");
p.reference = "estate/other".into();
assert!(matches!(p.check(), Err(CloudError::Refused(w)) if w.contains("label")));
let mut p = plan("acme");
p.machines[2].disks[1].title = p.machines[1].disks[1].title.clone();
assert!(matches!(p.check(), Err(CloudError::Refused(w)) if w.contains("two resources")));
// A product's own roles beside a front (the Lake): laid; a role laid twice under one
// name is refused; a plan with a main or a twin is Gunnar's three, as ever.
let lake = |roles: &[&str]| {
let mut p = plan("acme");
let m = p.machines[0].clone();
p.machines = roles
.iter()
.map(|r| MachinePlan {
role: r.to_string(),
hostname: format!("lake-{r}-acme"),
title: format!("Lake {r} acme"),
disks: vec![DiskPlan {
title: format!("lake-{r}-acme system"),
gib: 20,
tier: "standard".into(),
labels: Default::default(),
}],
..m.clone()
})
.collect();
p
};
assert!(
lake(&["front", "ingestor", "be", "be-2", "fe"])
.check()
.is_ok(),
"{:?}",
lake(&["front", "ingestor", "be", "be-2", "fe"]).check()
);
assert!(
matches!(lake(&["front", "be", "be"]).check(), Err(CloudError::Refused(w)) if w.contains("laid twice"))
);
assert!(
matches!(lake(&["front", "main", "be"]).check(), Err(CloudError::Refused(w)) if w.contains("one twin"))
);
}
/// ★ **A plan that carries its product's shape is held to THAT shape** (A4-FACTS): the
/// roles its facts name, one of them optional (a Gunnar-shaped estate whose twin may be left
/// out), another laid n times; never Gunnar's three. A twin laid beside a standing estate by a
/// plan change (`added=change-1`) is an estate's machine of this shape too. Without a shape,
/// the rule before (exactly a front, a main and a twin) stands.
#[test]
fn a_shaped_plan_is_held_to_the_products_roles_not_gunnars_three() {
let shape = PlanShape {
roles: vec![
ShapeRole {
role: "front".into(),
..ShapeRole::default()
},
ShapeRole {
role: "main".into(),
..ShapeRole::default()
},
ShapeRole {
role: "twin".into(),
optional: true,
..ShapeRole::default()
},
ShapeRole {
role: "node".into(),
many: true,
optional: true,
..ShapeRole::default()
},
],
};
let shaped = |keep: &[&str]| {
let mut p = plan("acme");
p.machines.retain(|m| keep.contains(&m.role.as_str()));
p.shape = Some(shape.clone());
p
};
let mut twinless = plan("acme");
twinless.machines.retain(|m| m.role != "twin");
assert!(
matches!(twinless.check(), Err(CloudError::Refused(w)) if w.contains("one twin")),
"no shape: Gunnar's three, as before"
);
assert!(
shaped(&["front", "main"]).check().is_ok(),
"the twin is optional: {:?}",
shaped(&["front", "main"]).check()
);
assert!(shaped(&["front", "main", "twin"]).check().is_ok());
assert!(
matches!(shaped(&["front", "twin"]).check(), Err(CloudError::Refused(w)) if w.contains("this plan has no main")),
"main is not optional"
);
// A node laid n times; a role the product does not name is refused by name.
let mut nodes = shaped(&["front", "main"]);
for r in ["node", "node-2"] {
let mut m = nodes.machines[1].clone();
m.role = r.into();
m.title = format!("{r} acme");
m.disks
.iter_mut()
.for_each(|d| d.title = format!("{r} {}", d.title));
nodes.machines.push(m);
}
assert!(nodes.check().is_ok(), "{:?}", nodes.check());
let mut odd = nodes.clone();
odd.machines[3].role = "gate".into();
assert!(
matches!(odd.check(), Err(CloudError::Refused(w)) if w.contains("\"gate\", which is not a role of this product (front, main, twin, node)")),
"{:?}",
odd.check()
);
let mut twice = nodes.clone();
twice.machines[3].role = "node".into();
assert!(matches!(twice.check(), Err(CloudError::Refused(w)) if w.contains("laid twice")));
// The twin laid beside a standing estate: one machine, marked with the one change.
let mut add = shaped(&["twin"]);
add.machines[0]
.labels
.insert(ADDED_LABEL.into(), "change-1".into());
assert!(add.check().is_ok(), "{:?}", add.check());
let mut half = shaped(&["main", "twin"]);
half.machines[1]
.labels
.insert(ADDED_LABEL.into(), "change-1".into());
assert!(
matches!(half.check(), Err(CloudError::Refused(w)) if w.contains("marks every one added")),
"{:?}",
half.check()
);
assert!(
matches!(shaped(&["twin"]).check(), Err(CloudError::Refused(w)) if w.contains("no front, main")),
"an unmarked twin alone is no estate"
);
// A relay of the shape: ONE machine marked with its own role.
let mut relay = shaped(&["main"]);
relay.machines[0]
.labels
.insert(RELAY_LABEL.into(), "main".into());
assert!(relay.check().is_ok());
relay.machines[0]
.labels
.insert(RELAY_LABEL.into(), "twin".into());
assert!(matches!(relay.check(), Err(CloudError::Refused(w)) if w.contains("relay")));
// On the wire: absent when unnamed (an older provider reads the plan as before).
assert!(serde_json::to_value(plan("acme"))
.unwrap()
.get("shape")
.is_none());
let wire = serde_json::to_value(shaped(&["front", "main"])).unwrap();
assert_eq!(
wire["shape"]["roles"][2],
serde_json::json!({"role": "twin", "many": false, "optional": true})
);
assert_eq!(
serde_json::from_value::<EstatePlan>(wire).unwrap(),
shaped(&["front", "main"])
);
}
fn spec(name: &str, cores: u32, gib: u64) -> PlanSpec {
PlanSpec {
name: name.into(),
cores,
memory_mib: gib * 1024,
price: crate::Price {
minor_per_month: cores as u64 * 1000,
currency: monetize_pay::Currency::Eur,
},
}
}
#[test]
fn the_twin_is_an_eighth_of_main_never_below_one_core_and_4_gib() {
let catalog = vec![
spec("1x2", 1, 2),
spec("1x4", 1, 4),
spec("2x4", 2, 4),
spec("4x8", 4, 8),
spec("8x32", 8, 32),
spec("16x64", 16, 64),
spec("64x256", 64, 256),
];
let twin = |main: &str| {
twin_plan_for(&catalog, catalog.iter().find(|p| p.name == main).unwrap())
.map(|p| p.name.clone())
};
assert_eq!(
twin("64x256").as_deref(),
Some("8x32"),
"main 64 cores → twin 8 cores"
);
assert_eq!(
twin("16x64").as_deref(),
Some("4x8"),
"at least 2 cores AND 8 GiB: the 2x4 has too little memory"
);
assert_eq!(
twin("4x8").as_deref(),
Some("1x4"),
"the floor: one core, 4 GiB"
);
assert_eq!(
twin("1x2").as_deref(),
Some("1x4"),
"the floor holds under the smallest main too"
);
assert_eq!(
twin_plan_for(&catalog[..3], &spec("64x256", 64, 256)),
None,
"no plan is big enough: refused, never a smaller twin"
);
}
/// ★ **No twin under 4 GiB** (owner, 2026-10-13): on a 2 GB box gunnar-server itself says
/// "this box cannot afford the handle-cache floor … that is the 2 GB box's collapse"
/// (`gunnar::limits`), so 4 GiB is the twin's floor whatever main is — the smallest main
/// gets the one-core 4 GiB plan, never the 2 GiB one the provider also sells.
#[test]
fn no_twin_is_ever_laid_under_4_gib() {
let catalog = vec![
spec("1x1", 1, 1),
spec("1x2", 1, 2),
spec("1x4", 1, 4),
spec("2x4", 2, 4),
spec("4x8", 4, 8),
spec("4x16", 4, 16),
spec("8x32", 8, 32),
spec("16x64", 16, 64),
spec("64x256", 64, 256),
];
for main in &catalog {
let twin =
twin_plan_for(&catalog, main).unwrap_or_else(|| panic!("a twin for {}", main.name));
assert!(
twin.memory_mib >= 4 * 1024,
"the twin of a {} main is {} ({} MiB): under the 4 GiB floor",
main.name,
twin.name,
twin.memory_mib
);
}
let twin = |main: &str| {
twin_plan_for(&catalog, catalog.iter().find(|p| p.name == main).unwrap())
.map(|p| p.name.clone())
};
assert_eq!(
twin("4x8").as_deref(),
Some("1x4"),
"Mini's main: one core is enough, 4 GiB is the floor"
);
assert_eq!(twin("4x16").as_deref(), Some("1x4"));
assert_eq!(
twin("64x256").as_deref(),
Some("8x32"),
"a big main's twin is still an eighth of it"
);
assert_eq!(TWIN_FLOOR_MEMORY_MIB, 4 * 1024);
// A provider that sells nothing with 4 GiB is refused: never a 2 GiB twin instead.
assert_eq!(twin_plan_for(&catalog[..2], &spec("4x8", 4, 8)), None);
}
/// ★ **A product names its own twin floor** (owner 2026-10-23, the lean Holger estate): the
/// 4 GiB floor is Gunnar's (gunnar-server's handle cache); Holger's twin is a 1-core 1 GB
/// box under a 1-core 2 GB main. The floor is the product's; the eighth of main is everyone's.
#[test]
fn a_product_names_its_own_twin_floor() {
let catalog = vec![
spec("1x1", 1, 1),
spec("1x2", 1, 2),
spec("1x4", 1, 4),
spec("2x4", 2, 4),
spec("4x8", 4, 8),
spec("8x32", 8, 32),
spec("16x64", 16, 64),
];
let twin = |main: &str, floor: u64| {
twin_plan_floored(
&catalog,
catalog.iter().find(|p| p.name == main).unwrap(),
floor,
)
.map(|p| p.name.clone())
};
assert_eq!(
twin("1x2", 1024).as_deref(),
Some("1x1"),
"Holger's Mini: a 1 GB twin under a 2 GB main"
);
assert_eq!(
twin("16x64", 1024).as_deref(),
Some("4x8"),
"an eighth of main still holds over a low floor"
);
assert_eq!(
twin("1x2", TWIN_FLOOR_MEMORY_MIB).as_deref(),
Some("1x4"),
"Gunnar's floor: unchanged"
);
for main in &catalog {
assert_eq!(
twin_plan_floored(&catalog, main, TWIN_FLOOR_MEMORY_MIB),
twin_plan_for(&catalog, main),
"twin_plan_for is Gunnar's floor"
);
}
}
#[test]
fn the_disks_walk_every_machine_in_order() {
let p = plan("acme");
let walked: Vec<(String, usize)> = p.disks().map(|(m, i, _)| (m.role.clone(), i)).collect();
assert_eq!(
walked,
vec![
("front".into(), 0),
("main".into(), 0),
("main".into(), 1),
("twin".into(), 0),
("twin".into(), 1)
]
);
}
/// ★ **A machine opens what its role declares, and nothing else** (OX-WGDS5): the front of a
/// product that declares udp/51830 opens it, its other roles and a product that declares
/// nothing open none; a numbered member opens its many-role's ports.
#[test]
fn a_machine_opens_the_ports_its_role_declares() {
let wg = OpenPort {
protocol: "udp".into(),
port: 51830,
why: "a downstream's handshake".into(),
};
let role = |r: &str, open: Vec<OpenPort>| ShapeRole {
role: r.into(),
open,
..ShapeRole::default()
};
let declares = PlanShape {
roles: vec![
role("front", vec![wg.clone()]),
role("main", vec![]),
role("twin", vec![]),
],
};
assert_eq!(declares.open_of("front"), vec![wg.clone()]);
assert!(
declares.open_of("main").is_empty()
&& declares.open_of("twin").is_empty()
&& declares.open_of("nope").is_empty()
);
let plain = PlanShape {
roles: vec![
role("front", vec![]),
role("main", vec![]),
role("twin", vec![]),
],
};
assert!(
plain.open_of("front").is_empty(),
"declared nothing, opens nothing"
);
let many = PlanShape {
roles: vec![ShapeRole {
many: true,
..role("be", vec![wg.clone()])
}],
};
assert_eq!(many.open_of("be-2"), vec![wg.clone()]);
// The wire: an older shop's shape (no `open`) reads as none, and none is not written.
let old: ShapeRole = serde_json::from_str(r#"{"role":"front"}"#).unwrap();
assert!(old.open.is_empty());
assert_eq!(
serde_json::to_string(&old).unwrap(),
r#"{"role":"front","many":false,"optional":false}"#
);
}
}