Skip to main content

Estate

Struct Estate 

Source
pub struct Estate {
    pub clock: Clock,
    pub timings: Timings,
    pub faults: Faults,
    pub zone: String,
    pub upload_base: String,
    pub dnat: Vec<Dnat>,
    pub maxiops_quota_gib: u64,
    pub engine: Arc<dyn GuestEngine>,
    /* private fields */
}

Fields§

§clock: Clock§timings: Timings§faults: Faults§zone: String§upload_base: String

What the mock says its own upload sessions live at. serve fills it in with the socket it bound, so a caller that FOLLOWS the direct_upload_url from the import reply reaches the mock instead of the internet. Empty renders the real provider’s shape (https://<zone>.img.upcloud.com/uploader/session/<uuid>), which is what a parser should be tested against.

§dnat: Vec<Dnat>

Every destination-NAT rule in the estate: the front’s :2222 to the appliance, and anything else a lay installs. Held here because the hairpin question (“can the box holding the rule use it”) can only be answered by something that knows all of them.

§maxiops_quota_gib: u64

The account’s MaxIOPS quota in GiB (behaviour 44). MEASURED for yvra.

§engine: Arc<dyn GuestEngine>

What is behind a server. VirtualGuest by default, which runs nothing and leaves every answer to the state machine below. A real engine (the draupnir-guest crate’s DraupnirGuest, injected by the mock-upcloud-kvm binary) boots a QEMU at every started, and the server’s state then follows that machine: see Estate::reap.

Implementations§

Source§

impl Estate

Source

pub fn new(clock: Clock, faults: Faults, seed: u64) -> Estate

Source

pub fn seed(&self) -> u64

Source

pub fn with_engine(self, engine: Arc<dyn GuestEngine>) -> Estate

The same estate with a real machine behind every server.

Source

pub fn reap(&mut self)

The server’s state follows its machine. A server that reads started while its QEMU is gone is a health line that cannot fail — and the installer powers the box off ~2–3 s after INSTALL-OK, so this is the ordinary case, not a crash. Only a server the engine has a machine for is touched (running is None otherwise), so the default engine changes nothing.

Source

pub fn next_correlation_id(&mut self) -> String

Source

pub fn tick(&mut self)

Move every object whose transition is due, then — when the clock is virtual — push time part-way toward the next one.

Part-way, not all the way, on purpose: a client that polls must poll several times, as it does against the real provider. Advancing straight to the next deadline would make every transition complete on the second read and silently excuse a client with no loop at all.

Source

pub fn run_to_quiet(&mut self)

Jump the virtual clock to the moment nothing is pending any more.

Only two callers, and both are honest about why: a restart is a stop and a start and the stop must COMPLETE in between, and a test that is not testing the poll loop says so by calling this. It is a no-op at real speed, where waiting is the only way through — which is the right asymmetry: virtual time may skip, real time may not.

Source

pub fn settle(&mut self)

Apply every due transition. Idempotent, and the only writer of a state that nobody asked for.

Source

pub fn resize_filesystem(&mut self, uuid: &str) -> Answer<String>

Behaviour 34 — POST /1.3/storage/{uuid}/resize, the door that actually left the 44 GB behind.

Behaviour 9 modelled the Resize Backup on a PLAN change, which is a door this estate never opens. The one it does open is this: cargo xtask grow grows the twin’s volume with PUT /storage/{uuid} (behaviour 22) and then asks the provider to grow the LAST PARTITION and the xfs inside it with POST /storage/{uuid}/resize. The provider takes a backup FIRST and hands it back in the reply as resize_backup — the whole storage object, uuid and all — and nothing in the estate deletes it afterwards. MEASURED 2026-09-20: a 44 GB Resize Backup on the live account whose origin was the twin’s data volume, billed at maxiops since 2026-09-19 21:54:06Z, and upcloud-orphans said “no orphans” over it.

The backup is the resized volume’s size (that is what 44 GB was: the twin’s volume after its growth), carries the volume’s labels unless Fault::ResizeBackupUnlabelled is armed, and its origin is the volume — or a gone uuid under Fault::OrphanResizeBackup. The volume itself goes maintenance for the resize and comes back online, so a caller that does not poll gets the same lesson every other write here teaches.

Refused on a volume attached to a server that is not stopped (SERVER_STATE_ILLEGAL; the estate stops the twin first and says so in its journal) and on a volume that is not online.

Source

pub fn server(&self, uuid: &str) -> Option<&Server>

Source

pub fn storage(&self, uuid: &str) -> Option<&Storage>

Source

pub fn was(&self, uuid: &str) -> Option<&'static str>

Source

pub fn servers_matching(&self, labels: &[(String, String)]) -> Vec<&Server>

GET /1.3/server, filtered by ?label=key=value (all must match).

Behaviour 5: a revoked credential answers this with 200 and ZERO rows. Not 401. A caller that treats “no rows” as “nothing to clean up” deletes nothing and reports success, and a caller that treats it as “nothing exists” creates a second copy of everything.

Source

pub fn storages_matching( &self, labels: &[(String, String)], private_only: bool, ) -> Vec<&Storage>

Source

pub fn create_storage( &mut self, title: &str, size_gib: u64, tier: &str, zone: &str, labels: Vec<Label>, ) -> Answer<String>

Source

pub fn modify_storage( &mut self, uuid: &str, size_gib: Option<u64>, title: Option<&str>, ) -> Answer<()>

Behaviour 22. A volume never shrinks. A growth is permanent and billed forever, so the refusal is by NAME and not a silent clamp: a clamp would let a caller believe it had shrunk something.

Source

pub fn delete_storage(&mut self, uuid: &str) -> Answer<()>

Source

pub fn attached_servers(&self, storage: &str) -> Vec<String>

Source

pub fn create_server( &mut self, title: &str, hostname: &str, plan: &str, zone: &str, labels: Vec<Label>, boot_disk_title: &str, boot_disk_gib: u64, ) -> Answer<String>

Source

pub fn configure_server( &mut self, uuid: &str, ifaces: Vec<Iface>, boot_order: Option<BootOrder>, firewall_on: bool, metadata: bool, timezone: &str, ) -> Answer<()>

The terraform create’s second half. create_server mints the machine every caller in this crate shares; this says the things only the terraform door asks for, in ONE place, so the create path is not forked into two machines that drift.

It is deliberately separate from modify_server: that one is the PUT /1.3/server/{uuid} a caller makes later and carries the stop/start behaviours. This is part of the create and makes no transition at all.

Source

pub fn rules(&self, uuid: &str) -> Option<&[Rule]>

The rule set of a machine, or the refusal the firewall door owes. The 403s (behaviours 1 and 35) stay in crate::http where they are the ANSWER rather than the state.

Source

pub fn set_rules(&mut self, uuid: &str, rules: Vec<Rule>) -> Answer<()>

Write a machine’s whole rule set. UpCloud’s firewall is a SET and not a list of independently addressable objects — a write replaces it — which is why upcloud_firewall_rules is one terraform resource per machine and not one per rule, and why this takes the whole vector.

Source

pub fn delete_server(&mut self, uuid: &str, with_storages: bool) -> Answer<()>

Behaviour 11. A server must be STOPPED before it can be deleted with its storages. A started one is refused, not queued.

Behaviour 7. The delete itself sits in maintenance for 60 s plus 65 s per attached member volume — an appliance carrying four of them is over five minutes, which is what was measured and what every caller’s timeout was too short for.

Source

pub fn start_server(&mut self, uuid: &str) -> Answer<()>

Behaviour 3. Out of stock, at poweron, with a 412. It was measured on fr-par-1 from 2026-09-09 and it did not clear for days, so the fault is sticky: a retry loop must not be able to outwait it.

Source

pub fn stop_server(&mut self, uuid: &str, hard: bool) -> Answer<()>

Source

pub fn modify_server( &mut self, uuid: &str, plan: Option<&str>, boot_order: Option<BootOrder>, labels: Option<Vec<Label>>, remote_access: Option<bool>, remote_access_password: Option<&str>, ) -> Answer<()>

PUT /1.3/server/{uuid} — the plan change, the boot order, the labels and the remote_access_enabled toggle, which is the whole of what this estate uses it for.

Source

pub fn rename_server( &mut self, uuid: &str, hostname: Option<&str>, title: Option<&str>, )

A server’s hostname and title, changed in place.

Source

pub fn attach( &mut self, server: &str, storage: &str, kind: &str, ) -> Answer<String>

Source

pub fn attach_at( &mut self, server: &str, storage: &str, kind: &str, want: Option<&str>, ) -> Answer<String>

An attach, at the address the caller asked for (behaviour 58). virtio:3 is honoured; virtio (or nothing) lets UpCloud pick the first FREE slot — REPORTED (monetize-impl grow.rs: address:"virtio" lets UpCloud pick). The mock used to ignore the request and take virtio:<count>, which collides after a middle detach and never puts a disk where a caller asked. Where it lands decides the guest’s name for it (Estate::guest_disk_names), and that decides the installer’s disk election.

Source

pub fn guest_disk_names(&self, server: &str) -> Vec<(String, String)>

The names the GUEST gives the disks (behaviour 58). virtio-blk devices are named in PCI slot order and the names are CONTIGUOUS: virtio:0, virtio:2, virtio:5 are vda, vdb, vdc. MEASURED as a rule the ladder depends on (gunnar machine.rs, holger HOLGER-PLAN: the ISO’s virtio copy is attached AFTER the data volume “so that it becomes vdc, not vdb”), and it is what korp-installer’s disk election reads. The IDE cdrom is not a Linux block device here: the kernel has no ATA.

Source

pub fn attach_at_create( &mut self, server: &str, storage: &str, kind: &str, want: Option<&str>, ) -> Answer<String>

A device named in the CREATE body (storage_devices entries with "action": "attach"). The server is in maintenance and has never run, so the cdrom’s hot-plug refusal in Estate::attach does not apply: this is how the provider takes an installer medium at create. The storage must exist and be online, exactly as for a later attach.

Source

pub fn detach(&mut self, server: &str, address: &str) -> Answer<()>

Behaviour 16, the half that refuses. A detach names an ADDRESS, and on a STARTED server an ide address is refused IDE_HOTPLUG_UNSUPPORTED, and a virtio one is the guest’s to allow (behaviour 60: a kernel without PCI hot-plug answers 511 HOTPLUG_FAILED). The only thing that takes a medium off a running box is Estate::eject.

Source

pub fn eject(&mut self, server: &str) -> Answer<()>

Behaviour 16, the half that works. cdrom/eject takes the MEDIUM out and leaves the device. MEASURED 200 on a STARTED server on 2026-09-14, and it ended an install loop: the next boot found no medium and fell through to the disk. It is the only never-loop primitive there is.

Source

pub fn load_cdrom(&mut self, server: &str, storage: &str) -> Answer<()>

cdrom/load — the other half of eject. Puts a storage into the existing cdrom device of an existing server, so a re-image with a DIFFERENT medium needs no detach and no re-create. With the CD first in the boot order, the next hypervisor start boots it (L50).

NOT MEASURED, and not in the ledger: the codes are UpCloud 1.3’s documented ones for this endpoint. What is modelled: it works on a started server like eject does (L52); a tray that already holds a medium is refused (CDROM_DEVICE_IN_USE, eject first); a server with no cdrom device is refused (CDROM_DEVICE_NOT_FOUND); the storage must exist and be online. The provider also requires the storage to be of type cdrom; the mock does not, because its uploaded media are normal storages that the ladder attaches as type: cdrom, which is MEASURED to work (L49).

Source

pub fn reach(&self, from: &str, dest: &str, port: u16) -> Answer<Reach>

Can from reach dest:port, from where it is standing?

There is no such thing here as “is that address up” — only “is it up from here”. Both of the asymmetries this models (a guest with no route off its prefix, and a box that cannot use its own DNAT) make the answer depend on the asker, and a reachability call that did not name one would be answering a question nobody has.

Source

pub fn inbound( &self, from_ip: &str, to: &str, proto: &str, port: u16, ) -> Answer<Reach>

The three paths a host key must answer identically, which is what tells a re-imaged machine from a hijacked name.

A re-image mints a new key, so REMOTE HOST IDENTIFICATION HAS CHANGED is expected and must not be trusted blindly. The check that makes it safe is that the SAME key answers on the name, on the front’s public address, and on the appliance’s OWN public address bypassing the DNAT. Fault::HijackedName makes the name path answer a different key, which is the case the whole check exists for — and a verifier that only looked at one path would accept it. Behaviour 62: can from_ip reach to:port INBOUND, through the provider’s firewall? Dropped is silence (the firewall), Refused is an RST (admitted, nothing listening — a server that is not started), Ok is a listener.

Source

pub fn udp_reply_arrives( &self, server: &str, from_ip: &str, from_port: u16, ) -> Answer<bool>

Behaviour 62, the UDP half: does the reply to this server’s own outbound query come back? The reply is an inbound packet FROM from_ip:from_port (53 for DNS, 123 for NTP) to an ephemeral port, and the firewall is stateless for UDP — so a rule set ending in a catch-all drop eats it, and a server with its firewall off gets it. This is the MECHANISM behind Fault::UdpInboundDropped, which stays the estate-wide default-ON approximation for callers that ask no server.

Source

pub fn host_key_via(&self, uuid: &str, path: HostKeyPath) -> Answer<String>

Source

pub fn relay(&mut self)

A lay of the estate: every address is reshuffled. Nothing else changes — this is what a xtask estate rebuild does to the ADDRESSES, and the point is that the next lay’s appliance is not on the last lay’s IP.

Source

pub fn lays(&self) -> u64

Source

pub fn start_import(&mut self, uuid: &str, source: &str) -> Answer<Import>

POST /1.3/storage/{uuid}/import — open a direct-upload session.

The session is prepared and nothing has moved yet. The URL it hands back is the mock’s own, so a caller that follows the reply (rather than building the URL itself) reaches the mock without being told to.

Source

pub fn upload(&mut self, uuid: &str, bytes: &[u8]) -> Answer<Import>

The PUT of the bytes to the session URL.

read_bytes and written_bytes are the REAL count and the digests are the REAL digests — the ladder compares sha256sum with the local file’s, so a made-up value here would make the verification pass without ever having run. Five seconds for a 43 MB ISO, and then the storage enters syncing for a hundred and something more.

Source

pub fn clone_storage(&mut self, uuid: &str, title: &str) -> Answer<String>

POST /1.3/storage/{uuid}/clone — the candidate fix.

The ladder uploads the same 43 MB medium twice per re-image, once as a CD-ROM and once as a virtio disk, because korp-installer probes virtio and nothing else. The second one could be a clone of the first: the same bytes, the same sha256, no second upload.

Behaviour 53, MEASURED 2026-09-20 (gunnar clone_probe.rs): the call is quick, then maintenanceonline in 47 s with NO syncing. This doc used to say the answer was not known; it was, one repository over. And the same probe says why it is still not the fix: a clone cannot start until its source is online, so import→clone is serial (161 s) where two parallel imports are ~115 s. Fault::CloneSyncsLikeImport keeps the old pessimistic guess, by name.

Source

pub fn all_servers(&self) -> impl Iterator<Item = &Server>

Everything, for the mock’s own /mock/estate view and for the tests.

Source

pub fn all_storages(&self) -> impl Iterator<Item = &Storage>

Auto Trait Implementations§

§

impl !Freeze for Estate

§

impl !RefUnwindSafe for Estate

§

impl !UnwindSafe for Estate

§

impl Send for Estate

§

impl Sync for Estate

§

impl Unpin for Estate

§

impl UnsafeUnpin for Estate

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.