Skip to main content

MachineConfig

Struct MachineConfig 

Source
pub struct MachineConfig {
Show 22 fields pub name: String, pub provider: String, pub vendor: Option<String>, pub nickname: Option<String>, pub location: Option<String>, pub server_type: Option<String>, pub hosts_mirrors: Vec<String>, pub mesh_tags: Vec<String>, pub region: Option<String>, pub zone: Option<String>, pub arch: Option<String>, pub bucket: Option<BucketSpec>, pub legacy_hostkey_fingerprint: Option<String>, pub ssh_keys: Vec<u64>, pub cloudflared: Option<String>, pub hosts_operator_bridge: bool, pub connect: Option<ConnectSpec>, pub allocatable: Option<NodeAllocatable>, pub taints: Vec<String>, pub sovereign_group: Option<String>, pub sovereign_role: Option<SovereignRole>, pub registration: MachineRegistration,
}
Expand description

Per-machine TOML from .yah/infra/machines/<name>.toml.

Two halves, split by provenance (R707-T1): everything here is declaration — operator intent under review and blame — except [registration], which carries what the fleet observed. See MachineRegistration for why the boundary is drawn there and what depends on it.

Fields§

§name: String§provider: String§vendor: Option<String>

Who the hardware actually comes from ("ovh", "vultr", "on-prem").

Deliberately not provider, which selects the auto-provision driver: a box we rented by hand and brought up over SSH is provider = "static" for its whole life, and writing the vendor there instead would flip it driver-backed and make validate demand location + server_type it has no answer for. The two axes genuinely differ — vendor is who bills you, provider is who yah can call an API against.

Worth recording because vendor-scoped policy is invisible in every other field and decides real work: outbound port 25, rDNS/PTR control, IP reputation, egress billing. It survived only in TOML prose until now, which made it ungreppable at exactly the moment you need it.

§nickname: Option<String>

Human label for the box ("gamer", "the GEEKOM"). Free-form and never matched on — name stays the identity everywhere. This is only so operators and agents can say which box they mean out loud.

§location: Option<String>

Provider DC code (e.g. Hetzner "hil"). Provisioning-only: required iff the provider has an auto-provision driver (provider_has_machine_driver); a BYO static node we brought up over SSH has no such code. Optional at load time so static machine.tomls omit it; MachineConfig::validate enforces presence at the right moment for driver-backed providers.

§server_type: Option<String>

Provider SKU/size (e.g. Hetzner "ccx13"). Provisioning-only, same optionality contract as location.

§hosts_mirrors: Vec<String>

Deprecated (R330-F16). A machine should describe itself (region, zone, provider, mesh_tags); which mirrors run on it is derived by the reconciler from each mirror’s required placement spec, not declared here. Now optional + omitted-when-empty so new machine.tomls leave it out. The legacy resolve_mirror_machine topology fallback still reads it until yubaba’s reverse-index supersedes the topology.toml path; once that lands, this field and its readers are removed wholesale.

§mesh_tags: Vec<String>§region: Option<String>

Canonical geo region label (latency axis), e.g. "us-west". F16’s three topology axes are orthogonal: region = geo (latency), zone = failure domain within a region (HA), provider = network/cost. region is distinct from location (the provider’s DC code, e.g. Hetzner "hil"): location is provider-scoped, region is our provider-neutral label. Optional for backward-compat; a machine without it never satisfies a required.regions constraint.

§zone: Option<String>

Failure-domain label within a region (HA axis), e.g. "hil". For single-DC Hetzner this typically mirrors location. F16 placement matches required.zones against this. Optional for backward-compat.

§arch: Option<String>

Declared CPU architecture ("x86_64" / "aarch64"). A machine has exactly one — it’s a first-class property of the box, not a reach detail and not a mesh tag. Drives the yubaba release triple. Optional only because there’s no provider API to probe it (static nodes declare it; a driver-backed provider may leave it unset until known).

§bucket: Option<BucketSpec>§legacy_hostkey_fingerprint: Option<String>

Legacy location, superseded by [registration].hostkey_fingerprint (R707-T1). Still deserialized so machine TOMLs written before the split keep parsing; never read directly — go through MachineConfig::hostkey_fingerprint, which prefers the registration block. MachineConfig::normalize folds this into registration, and MachineConfig::save normalizes before writing, so a load→save cycle migrates the file rather than dropping the value.

§ssh_keys: Vec<u64>

Provider-side SSH-key IDs (Hetzner: from GET /v1/ssh_keys) authorized for root at create time. Defaults to empty for backwards-compat with existing machine declarations; an empty list yields a Hetzner-emailed random root password (which the driver currently discards). Populate this when you want pre-mesh SSH access for bootstrap deploys or recovery.

§cloudflared: Option<String>

Cloudflare Tunnel ID this machine joins (e.g. abc123.cfargotunnel.com). None → no tunnel (mesh-only node, no public ingress). When set, yah cloud machine provision reads cloudflare-tunnel-token from the keys vault and injects the cloudflared install block into cloud-init so the new machine connects to CF edge on first boot.

§hosts_operator_bridge: bool

When true, this machine hosts operator-bridge workloads (Tailscale operator access to mesh-internal services). yah cloud machine provision will install tailscaled and run tailscale up during cloud-init via the {{OPERATOR_BRIDGE_BLOCK}} placeholder. Defaults to false for backward-compat with existing machine declarations.

§connect: Option<ConnectSpec>

BYO static-node reach descriptor. Static nodes have no provider API to probe, so how the camp reaches them (SSH user@host + the yubaba URL, which is loopback until the WireGuard mesh lands) is declared here. None for driver-backed providers (Hetzner/Vultr), whose address is resolved from the provider API / mesh at provision time.

§allocatable: Option<NodeAllocatable>

Static node capacity (R572-F3). Declares the node’s total hardware budget; F5’s scheduler subtracts committed workload requests from this to check whether a new workload fits. Absent means unconstrained.

§taints: Vec<String>

Placement taint keys (R572-F3). There is no toleration — a no-<archetype> taint is an absolute block, not a preference (W305/R742-T4; the pre-2026-08-11 “repel-unless-tolerate” wording here described an unless that was never built).

A key in this list influences placement in exactly one of two ways, and taint_effect is the authority on which:

  • repulsion — "no-server" / "no-appliance" / "no-job" reject workloads of that LifecycleArchetype outright;
  • affinity — a key in AFFINITY_TAINT_KEYS (today just "public-ip") that a workload names in yah.placement.requires-taint, which then requires this node.

Anything else is inert: it parses, it round-trips, and no scheduler decision can ever read it. yah cloud validate rejects such keys (validate::check_inert_taints) rather than letting them sit looking load-bearing — which is how no-voter spent months asserting a falsehood on three nodes. Facts about a node that are not placement inputs belong in mesh_tags or a comment.

§sovereign_group: Option<String>

Which consensus group this node belongs to — W305/R742-F1. None means standalone: in no group at all, which is us-west-002 and us-west-015.

Membership is not by itself quorum eligibility; that is sovereign_role, added by R605-F12 because us-west-003 is in prod’s blast radius and must never vote in it.

Not a placement input. It is deliberately absent from RequiredSpec::matches, and adding it there would be a category error: a sovereign group is a blast radius, not a filter. Nothing about “which quorum does this box vote in” should decide where a workload runs — that is what made the fleet express three unrelated properties through one taint list and get all three wrong (W305).

What it is for is refusal. judge_join answers “may this node join that node’s cluster”, and the answer is no unless both declare the same group. Before this field the only guard was a comment in three machine TOMLs saying “never run a raft join against this box from a shell pointed at prod” — habit, with no mechanism behind it, which is the same class of guard W257 §8 admitted to.

§Why sovereign_group and not raft_group

Raft is today’s mechanism (operator, 2026-08-10). A field named for the mechanism goes stale the day the mechanism is swapped, and every consumer that reads it inherits the lie. sovereign names what the group has — its own authority, its own upgrade cadence, its own destruction — which stays true under any consensus protocol.

Note the word already appears in this tree as prose (W267’s title, the IngressProvider::Passway doc comment’s “sovereign edge”). That is an adjective meaning “self-hosted, not SaaS”; this is the first time it carries structure.

§sovereign_role: Option<SovereignRole>

Whether this node may hold a seat in its group’s quorum — R605-F12. Meaningless without sovereign_group: a standalone box has no quorum to be eligible for.

None is “not written”, not a third role. Read it through sovereign_membership, which resolves the absence to SovereignRole::Voter — what declaring a group has always meant, so the six nodes stamped before this field keep their seats without an edit. The distinction is kept only so crate::validate::check_unroled_sovereign_members can tell an operator who chose voter from one who never considered the question; no join decision reads the Option directly.

§Why this is not a taint

It was, once: no-voter sat in taints on three nodes for months, read by nothing, and R742-T4 removed it because the taint list is a placement vocabulary and this is not a placement input (see taint_effect). Nor is it a second group label. It is a modifier on the membership this node already declares, which is why it lives beside the group and is judged with it in one predicate, workload_spec::sovereign::join_permitted.

§registration: MachineRegistration

[registration] — the observed half (R707-T1). Empty until the box has been attached / mesh-joined. See MachineRegistration.

Implementations§

Source§

impl MachineConfig

Source

pub fn sovereign_membership(&self) -> Membership<'_>

This node’s declared place in a sovereign group, as the shared join rule wants it — R605-F12.

The one place sovereign_role’s None is resolved. Absence means SovereignRole::Voter, which is what declaring a group meant before the role existed; resolving it here rather than at each call site is what keeps the camp-side and node-side gates from disagreeing about a node that never wrote the field.

Source

pub fn location(&self) -> &str

Provider DC code, or "" when omitted (static nodes). Most readers want a &str; the driver-backed provision/status paths still go through validate which guarantees presence for those.

Source

pub fn server_type(&self) -> &str

Provider SKU, or "" when omitted (static nodes).

Source

pub fn validate(&self) -> Result<()>

Enforce the provisioning-only-field contract: a machine whose provider has an auto-provision driver MUST declare location + server_type (the driver can’t create a server without them). Static nodes may omit both. Call this before any provision/diff that assumes a driver.

Source

pub fn inert_taints(&self) -> Vec<&str>

Declared taints that no placement decision can read (W305/R742-T4).

Deliberately not folded into validate: that guard runs on the provision/diff hot path and answers a different question (can the driver create this server). An inert taint is a lint — it never breaks an operation in flight, it just means the file is asserting something the scheduler will not honour. yah cloud validate is where the operator asks for that judgement; see crate::validate::check_inert_taints.

Source

pub fn hostkey_fingerprint(&self) -> Option<&str>

Yubaba’s TOFU’d hostkey fingerprint, from [registration] and falling back to the pre-R707-T1 top-level field. The only read path — a caller that reaches for legacy_hostkey_fingerprint directly sees None on every migrated machine.

Source

pub fn set_hostkey_fingerprint(&mut self, fingerprint: Option<String>)

Record (or clear) the observed hostkey fingerprint. Writes [registration] and drops any pre-R707-T1 top-level value, so the two locations can never disagree after a writeback.

Source

pub fn mesh_ipv4(&self) -> Option<&str>

Mesh (tailnet) IPv4 for this node, or None pre-mesh.

Prefers [registration].mesh_ipv4; falls back to the host of a legacy [connect].yubaba URL when that host is in the 100.64.0.0/10 CGNAT range the mesh uses. A loopback placeholder (http://127.0.0.1:7443, meaning “pre-mesh, reachable only through an SSH tunnel”) is not a mesh address and yields None.

Source

pub fn yubaba_url(&self) -> Option<String>

Base URL for this node’s yubaba, or None when no reach resolves.

Thin wrapper over reach for the many call sites that only branch on presence. Prefer reach anywhere the operator sees the outcome — a None here throws away a refusal that names exactly which address is missing.

Source

pub fn reach(&self) -> Result<String, String>

The one address automation dials for this node — mesh-only.

Err is a named refusal, not an absence: a node with no mesh address is unresolvable to every automated path, and R605-T10’s whole complaint is that this used to surface as a connect timeout against an address the caller has no route to.

Resolution order:

  1. A declared [connect].yubaba on a private host (10/8, 172.16/12, 192.168/16) is not dialed — see below.
  2. Any other declared [connect].yubaba wins verbatim. That includes the pre-mesh loopback placeholder (http://127.0.0.1:7443, “I have no mesh address; reach me through the SSH tunnel to ssh”), which is a genuine declaration and stays honoured.
  3. Otherwise [registration].mesh_ipv4 composed with [connect].yubaba_port.

Why a LAN literal loses (R605-T10, operator 2026-08-19). The LAN address is an emergency break-glass route, never an official one, and automation must ALWAYS assume the caller is not on that LAN — this camp sits on 192.168.22.0/22 with no route to the fleet’s 192.168.10.0/24 at all. Writing one into the field every resolver dials does not sit beside the mesh route, it overrides it: R707-T6 made a declared literal beat mesh_ipv4 outright, so us-west-011 (mesh-joined, healthy) was elected for every aarch64 build and then dialed at an address that answers only from inside bldg-2506.

What R707-T6 wanted is preserved elsewhere. Its forcing case was identity, not reach: the dev raft group advertises LAN addrs (192.168.10.11:7443, verified live off /raft/status 2026-08-27), and rollout::yubaba::membership_to_nodes has to map those back to declared machines. That match now runs against lan_endpoint, which is composed from the break-glass [connect].address metadata and is never dialed — so the two concerns the old precedence rule fused are split, and the literal can stop squatting a dialed field.

The LAN address itself STAYS in the machine TOML. It is useful metadata and the manual ssh path is entitled to it; it is only disconnected from every automated process.

Source

pub fn lan_endpoint(&self) -> Option<String>

The LAN host:port this node’s yubaba answers on, composed from the break-glass [connect].address metadata plus the declared port.

Identity only — never dial this. It exists so a raft membership entry that names a node by its LAN address can be mapped back to the declared machine (rollout::yubaba::membership_to_nodes) without that address having to live in a field a resolver reads. None when the machine is unprovisioned.

Source

pub fn normalize(&mut self)

Fold the pre-R707-T1 top-level hostkey_fingerprint into [registration], and lift a mesh IP out of a legacy [connect].yubaba URL. Idempotent; a machine already on the split shape is untouched.

save calls this, so writing a machine TOML migrates it rather than round-tripping the old shape back out.

Source

pub fn save(&self, cloud_dir: &Path) -> Result<()>

Persist to <cloud_dir>/machines/<name>.toml, creating the dir if needed.

⚠ Serializes the struct, so operator comments in the target file are lost. Pre-existing behaviour, not introduced here, but it is why registration writeback (yah cloud machine attach) goes through crate::state::MachineState and the comment-preserving path in the CLI rather than calling this on a hand-authored inventory file.

Trait Implementations§

Source§

impl Clone for MachineConfig

Source§

fn clone(&self) -> MachineConfig

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for MachineConfig

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl<'de> Deserialize<'de> for MachineConfig

Source§

fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl Serialize for MachineConfig

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Allocation for T
where T: RefUnwindSafe + Send + Sync,

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<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

Source§

impl<T> Downcast for T
where T: Any,

Source§

fn into_any(self: Box<T>) -> Box<dyn Any>

Convert Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.
Source§

fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>

Convert Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be further downcast into Rc<ConcreteType> where ConcreteType implements Trait.
Source§

fn as_any(&self) -> &(dyn Any + 'static)

Convert &Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &Any’s vtable from &Trait’s.
Source§

fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)

Convert &mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &mut Any’s vtable from &mut Trait’s.
Source§

impl<T> Downcast for T
where T: Any,

Source§

fn into_any(self: Box<T>) -> Box<dyn Any>

Converts Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>, which can then be downcast into Box<dyn ConcreteType> where ConcreteType implements Trait.
Source§

fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>

Converts Rc<Trait> (where Trait: Downcast) to Rc<Any>, which can then be further downcast into Rc<ConcreteType> where ConcreteType implements Trait.
Source§

fn as_any(&self) -> &(dyn Any + 'static)

Converts &Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &Any’s vtable from &Trait’s.
Source§

fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)

Converts &mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &mut Any’s vtable from &mut Trait’s.
Source§

impl<T> DowncastSend for T
where T: Any + Send,

Source§

fn into_any_send(self: Box<T>) -> Box<dyn Any + Send>

Converts Box<Trait> (where Trait: DowncastSend) to Box<dyn Any + Send>, which can then be downcast into Box<ConcreteType> where ConcreteType implements Trait.
Source§

impl<T> DowncastSync for T
where T: Any + Send + Sync,

Source§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Send + Sync> ⓘ

Convert Arc<Trait> (where Trait: Downcast) to Arc<Any>. Arc<Any> can then be further downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<T> DowncastSync for T
where T: Any + Send + Sync,

Source§

fn into_any_sync(self: Box<T>) -> Box<dyn Any + Send + Sync>

Converts Box<Trait> (where Trait: DowncastSync) to Box<dyn Any + Send + Sync>, which can then be downcast into Box<ConcreteType> where ConcreteType implements Trait.
Source§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Send + Sync> ⓘ

Converts Arc<Trait> (where Trait: DowncastSync) to Arc<Any>, which can then be downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<T> ErasedDestructor for T
where T: 'static,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> FromRef<T> for T
where T: Clone,

Source§

fn from_ref(input: &T) -> T

Converts to this type from a reference to the input type.
Source§

impl<T> FromRef<T> for T
where T: Clone,

Source§

fn from_ref(input: &T) -> T

Converts to this type from a reference to the input type.
Source§

impl<T> Fruit for T
where T: Send + Downcast,

Source§

impl<A, B, T> HttpServerConnExec<A, B> for T
where B: Body,

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
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> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

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

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> Serialize for T
where T: Serialize + ?Sized,

Source§

fn erased_serialize(&self, serializer: &mut dyn Serializer) -> Result<(), Error>

Source§

fn do_erased_serialize( &self, serializer: &mut dyn Serializer, ) -> Result<(), ErrorImpl>

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
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, <T as TryFrom<U>>::Error>

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.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more