crates/monetize-cloud/src/server/mod.rs
//! **`server` — the iron this box already has.** The free cloud.
//!
//! The trait and this implementation live in monetize itself; the ones that
//! need a paid account live in the private implementation repository. That is
//! what lets the server and the console be run open source by people sharing
//! their servers for free.
//!
//! A self-hoster has one box, and its disk is already paid for. This provider
//! sells THAT: `Change::Ensure` answers [`Ensured::OnHand`] when the box
//! can cover the disk an order needs and [`CloudError::Refused`] when it cannot.
//! **It never answers `Provisioned`, because it never buys anything** — there is
//! no account, no credential, no socket, and no code path that could create a
//! resource. It is the operator's declaration of what the box gives away, made
//! into a ledger that refuses.
//!
//! # Two numbers, and why both
//!
//! * **`pool_gib` — the DECLARED allocation.** What the operator gives tenants
//! out of this box, in GiB, written in `[clouds.server]`. Sold capacity is
//! folded from the grants ([`pool::LedgerData`], the same arithmetic `upcloud`
//! and `sandbox` keep), so `free = pool_gib − Σ granted` is a number the
//! state file can be rebuilt to from the grants alone (rule 3).
//! **The pool follows the set** (L1, 2026-10-05): the declaration is kept
//! with the set size it was made against, and every GiB the set has grown
//! by since — the grow batcher's purchases — is added to it, re-measured at
//! every ask. Before, the pool was `pool_gib` for ever and a grown box
//! refused the room it had bought.
//! * **the PRODUCT's measurement — the wall.** Before a grant, the named
//! product is asked [`Product::servable`] — gunnar's `Admin.Capacity` — and if
//! it says the store cannot hold what is asked, the grant is refused whatever
//! the declared pool says. The product counts; monetize decides (rule 5).
//!
//! Why the measurement is the wall and not the pool: `servable_bytes` is *bytes
//! beyond what tenants already hold*. Folding sold GiB against it would subtract
//! every byte a sold tenant has since written TWICE — once because the product
//! no longer counts it as servable, once because the grant is still sold — and a
//! 300 GiB box would refuse its 151st GiB. An allocation must be a number that
//! does not move when a tenant writes; the operator's declaration is that number,
//! and the product's live answer is what stops it from being a lie about the
//! disk. A product that answers [`Servable::Unmeasured`] is *a measurement that
//! did not happen*, not a full disk: the declared pool decides alone and the
//! grant records that the product could not be asked.
//!
//! # Ceilings on what no tenant owns
//!
//! `[clouds.server] ceilings = { cpu_millicores = 4000, ram_bytes = … }` is the
//! DECLARED limit on the product-scope meters — what the box has of a thing a
//! tenant does not own. An order that would raise such a cap past the number is
//! refused by name at quote time (`RoomLeft::ceilings`). Declared because
//! measured is not here yet: when the product's `Admin.Capacity` reports cores
//! and RAM through [`Servable`], the measurement walls the declaration exactly as
//! it does for disk today.
//!
//! # What it is, on the console
//!
//! `Listing::Account` lists ONE thing: the product's own data set,
//! `kind = dataset`, sized from the product's `total_bytes` (or the declared
//! pool when unmeasured), `provenance = server`, attached to the product — so
//! the panel shows the box itself under `IRON · server` and never an orphan.
//! `Priced::Resource` is zero: your own iron has no per-order COST. What a
//! customer PAYS for it is the deployment's price list, which for the
//! open-source deployment is absent or zero; the transaction then runs at price
//! 0 with no payment vendor at all — see `monetize::transaction`'s free path.
//!
//! # State is persisted after every grant
//!
//! To `MONETIZE_SERVER_CLOUD_STATE`-shaped JSON (the registry defaults it to
//! `<data_dir>/server-pool.json`), rewritten atomically. `None` keeps it in
//! memory, which is right for a test and wrong for a deployment — a restart that
//! forgot its grants would sell the same gigabyte twice.
use crate::factory::{CloudBuild, CloudFactory};
use crate::pool::{self, Grant, LedgerData};
use crate::cloud::{Boot, Booted, Catalog, Change, Changed, Cloud, CloudManifest, Give, Listed, Listing, Priced, RoomLeft};
use crate::grow::Journal;
use crate::lay::{EstatePlan, LaidEstate};
use crate::{
fingerprint, CloudError, Ensured, Inventory, Owned, OwnedKind, Price, Reclaimed, Resource, ResourceId, ResourceStatus, Zone,
};
use monetize_pay::Currency;
use monetize_product::{bytes, Product, Servable};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
pub const PROVIDER: &str = "server";
/// Environment names this provider reads: **none**. There is no credential
/// because there is no account; the state path is the registry's to default.
/// The constant exists so the registry treats every cloud alike.
pub const CONFIG_ENVS: &[&str] = &[];
/// How a server links this box's iron: `[clouds.server] enabled = true,
/// pool_gib = N`, optionally `product = "<id>"`, `zone`, `currency`,
/// `ceilings = { cpu_millicores = …, ram_bytes = … }`. No `cap` — it cannot
/// spend (`spends: false`), and `cap::CannotSpend` holds it to that.
pub const FACTORY: CloudFactory = CloudFactory { name: PROVIDER, config_envs: CONFIG_ENVS, spends: false, build };
/// **How far a product's data set can still GROW, in bytes — answered by whoever
/// drives its growth** (g-dk-market / g-currencies, release-3 mock 2026-09-29).
///
/// This box NEVER buys, and that stays true: the room a reach stands for is
/// bought by the product's grow batcher (monetize-server `grow`, through the
/// capped cloud that holds the set's volumes), not by this provider. Without it
/// `default_cloud = "server"` sold only the box's CURRENT room — 37 GiB declared
/// on a 4×10 GiB set the batcher can enlarge — so after a fill every order was
/// withheld while the set could grow to cover it. A reach is added to BOTH
/// walls ([`Server::covers`]): the declared pool and the product's measurement.
///
/// A shared, late-bound slot: the server's state (which knows the growth, the
/// ceiling and what it still buys) exists only AFTER the clouds are built, so
/// the registry hands this box an empty slot and fills it once it can answer.
/// Empty — or an answer of `None` — is no reach: the old wall, unchanged.
#[derive(Clone, Default)]
pub struct GrowReach(Arc<std::sync::RwLock<Option<ReachFn>>>, Arc<std::sync::RwLock<Option<QueueFn>>>);
/// `product id → bytes the set can still grow by`, `None` when that cannot be
/// said (no growth configured, a sealed box, no ceiling to buy under).
pub type ReachFn = Arc<dyn Fn(&str) -> Option<u64> + Send + Sync>;
/// `product id → whether a grant past both walls is QUEUED` for the set's later
/// growth (round 5.3, owner 2026-10-08: "the customer must not know about a
/// cap! it must be queued and resized later"): `true` where a grow batcher
/// resizes the set once the room or the spend cap allows.
pub type QueueFn = Arc<dyn Fn(&str) -> bool + Send + Sync>;
impl GrowReach {
/// Who answers from now on (the last setter wins; the slot is shared).
pub fn answer_with(&self, f: ReachFn) {
*self.0.write().unwrap_or_else(|e| e.into_inner()) = Some(f);
}
/// Who answers [`GrowReach::queues`] from now on.
pub fn queue_with(&self, f: QueueFn) {
*self.1.write().unwrap_or_else(|e| e.into_inner()) = Some(f);
}
/// The reach of `product`'s set in bytes; 0 when nobody answers.
pub fn bytes(&self, product: &str) -> u64 {
let f = self.0.read().unwrap_or_else(|e| e.into_inner()).clone();
f.and_then(|f| f(product)).unwrap_or(0)
}
/// **Is a grant past the walls queued for `product`'s set's growth?**
/// `false` when nobody answers: the old wall, which refuses.
pub fn queues(&self, product: &str) -> bool {
let f = self.1.read().unwrap_or_else(|e| e.into_inner()).clone();
f.is_some_and(|f| f(product))
}
}
impl std::fmt::Debug for GrowReach {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(if self.0.read().map(|g| g.is_some()).unwrap_or(false) { "GrowReach(answered)" } else { "GrowReach(none)" })
}
}
/// The one reader of the block's keys. `pool_gib` is required; the product is
/// the one named or the only one linked; grants live at
/// `<data_dir>/server-pool.json`.
fn build(ctx: &CloudBuild<'_>) -> Result<Box<dyn Cloud>, String> {
build_with(ctx, GrowReach::default())
}
/// [`FACTORY`]'s build with the registry's [`GrowReach`] slot — what
/// monetize-server calls, so this box counts the set its batcher can grow.
pub fn build_with(ctx: &CloudBuild<'_>, reach: GrowReach) -> Result<Box<dyn Cloud>, String> {
let s = ctx.settings;
let pool_gib = s.get::<u64>("pool_gib").map_err(|e| ctx.err(e))?.ok_or_else(|| {
ctx.err(
"is enabled but declares no `pool_gib`. Write down how many GiB of this box tenants may be given, e.g. \
`pool_gib = 100`; 0 is a valid answer (every sale is refused; free plans without resources still work). \
The product's own measurement is the wall on top of it, never a substitute for it — see monetize_cloud::server",
)
})?;
let named = s.get::<String>("product").map_err(|e| ctx.err(e))?;
let product = match (&named, ctx.products) {
(Some(id), all) => all
.iter()
.find(|p| p.facts().id == id)
.cloned()
.ok_or_else(|| ctx.err(format!("product = {id:?}, but no [products.{id}] is linked into this server")))?,
(None, [only]) => only.clone(),
(None, []) => return Err(ctx.err("needs a product to measure through, and none is linked: add a [products.<id>]")),
(None, many) => {
return Err(ctx.err(format!(
"must name which product's disk this box is (`product = \"<product>\"`); {} are linked: {:?}",
many.len(),
many.iter().map(|p| p.facts().id).collect::<Vec<_>>()
)))
}
};
let mut sc = ServerConfig::new(product, pool_gib);
sc.reach = reach;
sc.state_path = Some(ctx.data_dir.join("server-pool.json"));
if let Some(v) = s.get::<String>("zone").map_err(|e| ctx.err(e))? {
sc.zone = Zone(v);
}
if let Some(v) = s.get::<Currency>("currency").map_err(|e| ctx.err(e))? {
sc.currency = v;
}
if let Some(v) = s.get::<std::collections::BTreeMap<String, u64>>("ceilings").map_err(|e| ctx.err(e))? {
sc.ceilings = v;
}
let inner = Server::new(sc).map_err(|e| ctx.err(e))?;
if let Some(refusal) = pool_past_the_disk(&inner) {
return Err(ctx.err(refusal));
}
eprintln!(
"monetize-server: [clouds.server] is THIS BOX — {} GiB declared for tenants, walled by {}'s own measurement, never buys (state {})",
inner.pool_gib(),
inner.product_id(),
inner.state_path().map(|p| p.display().to_string()).unwrap_or_default()
);
Ok(Box::new(inner))
}
/// **A declared pool bigger than the disk, refused BY NAME at start.**
///
/// The live front declared `pool_gib = 100` on a box whose product measures
/// 37.8 GiB (2026-09-18). Nothing broke, because `covers` overlays the
/// measurement on every grant and the measurement wins — but the config was
/// claiming nearly three times the disk, and every number derived from the
/// declaration alone (the free-GiB arithmetic, `inventory`'s fallback size, an
/// operator reading the file) was that big a lie. A declaration that the
/// product can CONTRADICT is a wall; one that it can contradict *while nobody
/// is told* is a trap, and the only moment anybody would have found out is when
/// the 38th GiB was sold.
///
/// So it is refused here, at start, before the process serves anything: the
/// operator edits one number in a file they were already wrong about. The two
/// silent alternatives were both worse — deriving the pool from the measurement
/// would make an operator's declaration mean nothing and would change the
/// number under them whenever a box was resized, and a warning would join the
/// other lines nobody reads.
///
/// **An unmeasured product is a WARNING and not a refusal.** A product that is
/// down at start, or one that predates `Admin.Capacity`, has said nothing about
/// the disk; refusing on that would take a deployment down because its
/// appliance was rebooting, and "the product did not answer" is not "the
/// operator is wrong". It is said out loud, naming the product's own reason, so
/// the declaration is known to be standing alone.
fn pool_past_the_disk(server: &Server) -> Option<String> {
let declared = server.pool_gib().saturating_mul(GIB);
match server.cfg.product.servable() {
// `total_bytes` is 0 from a product that measures what is LEFT but not
// what there is in total — a measurement that did not happen, and not a
// disk of no size.
Servable::Measured { total_bytes, .. } if total_bytes > 0 && declared > total_bytes => Some(format!(
"declares `pool_gib = {}` ({}) for tenants, and {} measures a store of {} in total — {} of it does not exist. \
The product's measurement already walls every grant, so nothing was ever handed out that the disk could not hold; \
what the declaration does is put a number nobody can honour into the free-GiB arithmetic and in front of whoever \
reads this file. Write down what the box really has, or give it the disk",
server.pool_gib(),
bytes(declared),
server.product_id(),
bytes(total_bytes),
bytes(declared - total_bytes)
)),
Servable::Measured { .. } => None,
Servable::Unmeasured(why) => {
eprintln!(
"monetize-server: WARNING [clouds.server] declares {} GiB for tenants and {} did not measure its store, so nothing \
has checked that the box has them: {why}",
server.pool_gib(),
server.product_id()
);
None
}
}
}
const GIB: u64 = 1 << 30;
#[derive(Clone)]
pub struct ServerConfig {
/// The product whose store this box's disk IS. Asked before every grant and
/// listed by `inventory`. There is no `server` without one: a pool with
/// nobody to measure it is a number nothing can contradict.
pub product: Arc<dyn Product>,
/// The product's id, read once from its facts when the box is configured.
pub product_id: &'static str,
/// GiB this box gives to tenants, declared by the operator. `0` is a valid
/// answer — it refuses every sale, the shape a box that only serves free
/// plans without resources wants.
pub pool_gib: u64,
/// Where the grants are remembered. `None` is in-memory: right for a test,
/// wrong for a deployment (see the module doc).
pub state_path: Option<PathBuf>,
/// What the zero price and the inventory are a number OF. Your own iron has
/// no price, but a `Price` without a currency is ambiguous, so it is named.
pub currency: Currency,
/// The one zone: the box. `this-box` unless the operator names it.
pub zone: Zone,
/// **Declared ceilings on product-scope meters**, by the product's own
/// meter name (`cpu_millicores`, `ram_bytes`): what this box HAS of a thing
/// no tenant owns. An order past one is refused by name at quote time.
/// Empty declares nothing, and then nothing is refused on their account —
/// the product's own `can_absorb` is still asked.
pub ceilings: std::collections::BTreeMap<String, u64>,
/// **How far the set can still grow** ([`GrowReach`]): added to both walls.
/// Empty by default — the box's current room alone.
pub reach: GrowReach,
}
impl ServerConfig {
pub fn new(product: Arc<dyn Product>, pool_gib: u64) -> ServerConfig {
let product_id = product.facts().id;
ServerConfig { product, product_id, pool_gib, state_path: None, currency: Currency::Eur, zone: Zone("this-box".to_string()), ceilings: Default::default(), reach: GrowReach::default() }
}
}
impl std::fmt::Debug for ServerConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ServerConfig")
.field("product", &self.product_id)
.field("pool_gib", &self.pool_gib)
.field("state_path", &self.state_path)
.field("currency", &self.currency)
.field("zone", &self.zone)
.field("ceilings", &self.ceilings)
.field("reach", &self.reach)
.finish()
}
}
#[derive(Default, serde::Serialize, serde::Deserialize)]
struct Stored {
version: u32,
/// The grants and the sold count, shared with `upcloud` and `sandbox` by
/// construction. Every grant here is `OnHand`; the type has no other arm
/// this provider can produce.
pool: LedgerData,
/// **What the declaration was made against**: the `pool_gib` in force and
/// the set's measured size when it was first seen. The pool follows the
/// set from there — see [`Server::grown_gib`]. Absent in a state file
/// written before it (2026-10-05): taken at the next measurement.
#[serde(default, skip_serializing_if = "Option::is_none")]
declared: Option<Declared>,
}
/// The set a `pool_gib` was declared against.
#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
struct Declared {
pool_gib: u64,
set_bytes: u64,
}
/// The box's own disk, sold from a declared pool and walled by the product's
/// own measurement. Never buys.
pub struct Server {
cfg: ServerConfig,
zones: Vec<Zone>,
/// Held for the whole of `ensure`: the pool arithmetic must not interleave.
state: Mutex<Stored>,
}
impl std::fmt::Debug for Server {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Server")
.field("product", &self.cfg.product_id)
.field("pool_gib", &self.cfg.pool_gib)
.field("sold_gib", &self.sold_gib())
.finish_non_exhaustive()
}
}
impl Server {
/// Refuses an empty zone, and a state file that exists but does not parse:
/// a provider that guessed over a corrupt grant list could sell the same
/// gigabyte twice, which is the one thing a pool exists to prevent.
pub fn new(cfg: ServerConfig) -> Result<Server, CloudError> {
if cfg.zone.0.trim().is_empty() {
return Err(CloudError::Refused("server cloud: zone is empty".into()));
}
let stored = match cfg.state_path.as_deref() {
Some(path) => Self::load(path)?,
None => Stored { version: 1, ..Default::default() },
};
let server = Server { zones: vec![cfg.zone.clone()], state: Mutex::new(stored), cfg };
// The set this declaration is made against, measured NOW — at start,
// so a growth before the first order is still counted as growth.
{
let mut st = server.state.lock().unwrap_or_else(|e| e.into_inner());
server.grown_gib(&mut st, &server.cfg.product.servable())?;
}
Ok(server)
}
/// **What the set has grown by since `pool_gib` was declared**, whole GiB
/// (L1, owner 2026-10-05: the pool was `pool_gib + reach − sold` for ever,
/// read once at install, so every GiB the grow batcher had already bought
/// was lost to it and test2's box refused room it had). The declaration
/// is kept with the set size it was made against; a new `pool_gib` is a
/// new declaration, made against the set as it is then. A product that
/// measures no total adds nothing and records nothing.
fn grown_gib(&self, st: &mut Stored, servable: &Servable) -> Result<u64, CloudError> {
let total = match servable {
Servable::Measured { total_bytes, .. } if *total_bytes > 0 => *total_bytes,
_ => return Ok(0),
};
match st.declared {
Some(d) if d.pool_gib == self.cfg.pool_gib => Ok(total.saturating_sub(d.set_bytes) / GIB),
_ => {
st.declared = Some(Declared { pool_gib: self.cfg.pool_gib, set_bytes: total });
self.persist(st)?;
Ok(0)
}
}
}
fn load(path: &Path) -> Result<Stored, CloudError> {
match std::fs::read(path) {
Ok(bytes) => serde_json::from_slice(&bytes).map_err(|e| {
CloudError::Unavailable(format!(
"server cloud: state {} does not parse ({e}); refusing to start rather than forget what this box has granted",
path.display()
))
}),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Stored { version: 1, ..Default::default() }),
Err(e) => Err(CloudError::Unavailable(format!("server cloud: state {}: {e}", path.display()))),
}
}
fn persist(&self, st: &Stored) -> Result<(), CloudError> {
let Some(path) = self.cfg.state_path.as_deref() else { return Ok(()) };
let bytes = serde_json::to_vec_pretty(st).map_err(|e| CloudError::Unavailable(format!("server cloud: state encode: {e}")))?;
pool::write_atomically(path, &bytes, "server cloud state")
}
pub fn product_id(&self) -> &'static str {
self.cfg.product_id
}
pub fn pool_gib(&self) -> u64 {
self.cfg.pool_gib
}
pub fn state_path(&self) -> Option<&Path> {
self.cfg.state_path.as_deref()
}
/// GiB granted out of the declared pool, folded from the grants: the sum
/// over every server name a plan ever used, because the box has ONE disk
/// however many names plans give it.
pub fn sold_gib(&self) -> u64 {
self.state.lock().unwrap_or_else(|e| e.into_inner()).pool.sold.values().sum()
}
/// `pool_gib − sold`, the declared side of the answer.
pub fn free_gib(&self) -> u64 {
self.cfg.pool_gib.saturating_sub(self.sold_gib())
}
/// Every grant, in key order. All `OnHand`, by construction.
pub fn grants(&self) -> Vec<(String, Grant)> {
self.state.lock().unwrap_or_else(|e| e.into_inner()).pool.grants.iter().map(|(k, g)| (k.clone(), g.clone())).collect()
}
/// The one id this provider knows: the product's data set.
fn dataset_id(&self) -> ResourceId {
ResourceId { provider: PROVIDER.into(), id: format!("dataset:{}", self.cfg.product_id) }
}
/// **Can this box cover `gib` more?** The declared side (pool minus
/// granted), then the measured side (the product's own word on the disk,
/// which the declaration cannot talk over; an unmeasured answer is not a
/// wall). Answers the free GiB before the grant. ONE writer for `ensure`
/// and `can_serve`, so a quote and the purchase it precedes cannot disagree.
fn covers(&self, st: &mut Stored, reference: &str, gib: u64) -> Result<u64, CloudError> {
let sold: u64 = st.pool.sold.values().sum();
// The product's measurement, read ONCE per ask: the set as it is now
// (what it has grown by since the declaration) and the wall below.
let servable = self.cfg.product.servable();
let grown_gib = self.grown_gib(st, &servable)?;
let grown = if grown_gib > 0 { format!(" + {grown_gib} GiB the set has grown by since") } else { String::new() };
// The set's growable reach (g-dk-market): what the grow batcher can
// still add under its ceiling, on BOTH walls. Whole GiB only on the
// declared side, so the pool never counts a fraction it cannot grant.
let reach = self.cfg.reach.bytes(self.cfg.product_id);
let reach_gib = reach / GIB;
let grows = if reach_gib > 0 { format!(" + {reach_gib} GiB the set can still grow by under its ceiling") } else { String::new() };
let free = self.cfg.pool_gib.saturating_add(grown_gib).saturating_add(reach_gib).saturating_sub(sold);
// ★ **QUEUED, not refused, where the set grows later** (round 5.3,
// owner 2026-10-08): the grant is recorded past both walls and the
// shortfall waits for the grow batcher's resize — the batcher grows
// the set for the queued purchases once the room or the spend cap
// allows, and tells the operator what waits. Said here for the log.
let short_of_disk = match &servable {
Servable::Measured { servable_bytes, .. } => servable_bytes.saturating_add(reach) < gib.saturating_mul(GIB),
Servable::Unmeasured(_) => false,
};
if (free < gib || short_of_disk) && self.cfg.reach.queues(self.cfg.product_id) {
eprintln!(
"server cloud: {gib} GiB for {reference} is past the room on hand ({free} GiB of the pool free{grown}{grows}): QUEUED for the set's resize — \
the grow batcher enlarges the set once the room or the spend cap allows"
);
return Ok(free);
}
if free < gib {
return Err(CloudError::Refused(format!(
"server cloud: this box cannot cover {gib} GiB for {reference}: {} GiB declared{grown}{grows}, {sold} GiB already granted, \
{free} GiB free — short {} GiB. `server` never buys; raise pool_gib only if the disk really has it, \
or serve this order from a cloud that does buy",
self.cfg.pool_gib,
gib - free
)));
}
match servable {
Servable::Measured { servable_bytes, .. } if servable_bytes.saturating_add(reach) < gib.saturating_mul(GIB) => Err(CloudError::Refused(format!(
"server cloud: {} measured that it can serve only {} more{grows}, and {gib} GiB is asked for {reference} — short {}. \
The declared pool ({} GiB, {free} GiB unsold) is not what the disk has; the product counts, and it says no",
self.cfg.product_id,
bytes(servable_bytes),
bytes(gib.saturating_mul(GIB) - servable_bytes.saturating_add(reach)),
self.cfg.pool_gib
))),
Servable::Measured { .. } | Servable::Unmeasured(_) => Ok(free),
}
}
fn only_disks(resource: &Resource) -> Result<(&str, u64), CloudError> {
match resource {
Resource::Disk { server, gib } => Ok((server.0.as_str(), *gib)),
other => Err(CloudError::Refused(format!(
"server cloud: this box is the only box — `server` sells no servers and no plan changes, and {} is one. \
An order needing it needs a cloud that sells iron",
fingerprint(other)
))),
}
}
}
/// The box's own iron, as its own: the wall it serves within, its one dataset row.
impl Server {
/// Zero. Your own iron has no per-order price; what it costs to run the box
/// is not a thing monetize measures.
fn own_price(&self, resource: &Resource) -> Result<Price, CloudError> {
Self::only_disks(resource)?;
Ok(Price { minor_per_month: 0, currency: self.cfg.currency })
}
/// `OnHand`, or `Refused` naming the shortfall. Idempotent on
/// `reference + fingerprint` like every provider: a retried attempt gets
/// the grant it already has, never a second one.
fn own_ensure(&self, reference: &str, resource: &Resource) -> Result<Ensured, CloudError> {
if reference.is_empty() || reference.len() > 255 {
return Err(CloudError::Refused("server cloud: reference must be 1–255 chars".into()));
}
let (server, gib) = Self::only_disks(resource)?;
if gib == 0 {
return Err(CloudError::Refused("server cloud: a disk of 0 GiB".into()));
}
let key = pool::Ledger::key(reference, &fingerprint(resource));
let mut st = self.state.lock().unwrap_or_else(|e| e.into_inner());
if let Some(Grant::OnHand { free_after, .. }) = st.pool.grant(&key) {
return Ok(Ensured::OnHand { pool_free_after: *free_after });
}
let free = self.covers(&mut st, reference, gib)?;
// A queued grant (past the walls) leaves nothing free.
let free_after = free.saturating_sub(gib);
st.pool.record(&key, Grant::OnHand { server: server.to_string(), gib, free_after });
self.persist(&st)?;
Ok(Ensured::OnHand { pool_free_after: free_after })
}
/// **The quote's question**: the disks in `delta` summed, against the same
/// two walls `ensure` checks — and no grant. A retried reference is not
/// special-cased here: a quote is asked before an order, never after.
fn own_can_serve(&self, delta: &[Resource]) -> Result<(), String> {
let mut gib = 0u64;
for r in delta {
gib = gib.saturating_add(Self::only_disks(r).map_err(|e| e.to_string())?.1);
}
if gib == 0 {
return Ok(());
}
let mut st = self.state.lock().unwrap_or_else(|e| e.into_inner());
self.covers(&mut st, "this order", gib).map(|_| ()).map_err(|e| e.to_string())
}
/// The pool's free GiB (declared + grown + growable − sold), never more
/// than the product measured it can serve (+ the reach) — the two walls
/// `covers` checks, as a size.
fn own_room_left_bytes(&self) -> Option<u64> {
let mut st = self.state.lock().unwrap_or_else(|e| e.into_inner());
let sold: u64 = st.pool.sold.values().sum();
let servable = self.cfg.product.servable();
let grown_gib = self.grown_gib(&mut st, &servable).ok()?;
let reach = self.cfg.reach.bytes(self.cfg.product_id);
let free = self.cfg.pool_gib.saturating_add(grown_gib).saturating_add(reach / GIB).saturating_sub(sold).saturating_mul(GIB);
Some(match servable {
Servable::Measured { servable_bytes, .. } => free.min(servable_bytes.saturating_add(reach)),
Servable::Unmeasured(_) => free,
})
}
/// The declared limits on what no tenant owns. See [`ServerConfig::ceilings`].
fn own_ceilings(&self) -> std::collections::BTreeMap<String, u64> {
self.cfg.ceilings.clone()
}
/// A no-op, and `Ok` whatever the id: on-hand iron is not returned to
/// anyone, because nobody was ever paid for it. The transaction never calls
/// this for an `OnHand` grant anyway; a caller that does gets the truth,
/// which is that there was nothing to give back.
fn own_destroy(&self, _id: &ResourceId) -> Result<(), CloudError> {
Ok(())
}
/// `Active` for the data set — the one thing this provider ever lists —
/// and `UnknownResource` for anything else, never a silent `Gone`.
fn own_status(&self, id: &ResourceId) -> Result<ResourceStatus, CloudError> {
if *id == self.dataset_id() {
Ok(ResourceStatus::Active)
} else {
Err(CloudError::UnknownResource(id.clone()))
}
}
/// The box itself: one `dataset` row, sized by the product's own
/// `total_bytes` (floored to GiB) when it measured, by the declared pool
/// when it could not. `provenance = server`, attached to the product,
/// `reference = None` because no order bought it. Nothing is foreign — there
/// is no account for anything else to be in.
fn own_inventory(&self) -> Result<Inventory, CloudError> {
let gib = match self.cfg.product.servable() {
Servable::Measured { total_bytes, .. } if total_bytes > 0 => total_bytes / GIB,
_ => self.cfg.pool_gib,
};
let owned = vec![Owned {
id: self.dataset_id(),
kind: OwnedKind::Dataset,
gib,
zone: self.cfg.zone.0.clone(),
attached_to: Some(self.cfg.product_id.to_string()),
reference: None,
monthly_minor: Some(0),
currency: self.cfg.currency,
provenance: PROVIDER.into(),
resource: None,
backup: None,
}];
Ok(Inventory { owned, foreign: 0, currency: self.cfg.currency })
}
}
/// **The box's own iron, in the nine ops.** It never buys: an ensure is `OnHand` from the
/// declared pool, walled by the product's own measurement; its room is that wall. It lays,
/// changes, boots and grows nothing, and says so by name.
impl Cloud for Server {
fn manifest(&self) -> CloudManifest {
CloudManifest { provider: PROVIDER.into(), zones: self.zones.clone(), settings: CONFIG_ENVS.iter().map(|s| s.to_string()).collect(), grow_refused: Some(crate::grow::unsupported(PROVIDER).to_string()) }
}
fn catalog(&self, _zone: &Zone) -> Result<Catalog, CloudError> {
Err(crate::lay::unsupported(PROVIDER))
}
fn price(&self, what: &Priced<'_>) -> Result<Price, CloudError> {
match what {
Priced::Resource(r) => self.own_price(r),
Priced::Plan(_) => Err(crate::lay::unsupported(PROVIDER)),
Priced::Grow(_) => Err(crate::grow::unsupported(PROVIDER)),
}
}
fn lay(&self, _plan: &EstatePlan, _journal: &Journal) -> Result<LaidEstate, CloudError> {
Err(crate::lay::unsupported(PROVIDER))
}
fn change(&self, change: &Change, _journal: &Journal) -> Result<Changed, CloudError> {
match change {
Change::Ensure { reference, resource, .. } => self.own_ensure(reference, resource).map(Changed::Ensured),
Change::Grow { .. } | Change::Settle { .. } => Err(crate::grow::unsupported(PROVIDER)),
// Nothing was laid here, so no medium was attached.
Change::ForgetMedia { .. } => Ok(Changed::Forgotten(Vec::new())),
Change::Retire { .. } => Err(crate::lay::unsupported_verb(PROVIDER, "retire_machine")),
Change::Plan { .. } => Err(crate::lay::unsupported_verb(PROVIDER, "change_plan")),
Change::LabelAdopted { .. } => Err(crate::lay::unsupported_verb(PROVIDER, "label_adopted")),
}
}
fn boot(&self, boot: &Boot, _journal: &Journal) -> Result<Booted, CloudError> {
let verb = match boot {
Boot::Keep(_) => "keep_boot",
Boot::Power { .. } => "set_power",
Boot::Restore(_) => "restore_from_backup",
};
Err(crate::lay::unsupported_verb(PROVIDER, verb))
}
fn firewall(&self, _laid: &LaidEstate) -> Result<(), CloudError> {
Err(crate::lay::unsupported_verb(PROVIDER, "set_estate_firewall"))
}
/// Nothing here was bought, so nothing is given back: the box's own data set is
/// spared by kind.
fn destroy(&self, what: &Give<'_>) -> Result<Reclaimed, CloudError> {
match what {
Give::Resource(id) => {
self.own_destroy(id)?;
Ok(Reclaimed { destroyed: vec![(*id).clone()], ..Reclaimed::default() })
}
Give::References(refs) => Ok(crate::release_for_by_rows(self.own_inventory()?, refs, |id| self.own_destroy(id))),
}
}
fn inventory(&self, what: &Listing<'_>) -> Result<Listed, CloudError> {
Ok(match what {
Listing::Account => Listed::Account(self.own_inventory()?),
Listing::Status(id) => Listed::Status(self.own_status(id)?),
Listing::Under(refs) => Listed::Under(crate::cloud::under(self.own_inventory()?, refs)),
Listing::Set(_) => return Err(crate::grow::unsupported(PROVIDER)),
Listing::Admits(_) => Listed::Admitted,
Listing::Room { delta, .. } => Listed::Room(RoomLeft { serve: self.own_can_serve(delta), left_bytes: self.own_room_left_bytes(), ceilings: self.own_ceilings() }),
})
}
}
#[cfg(test)]
mod tests;