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.
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: boolWhen 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 thatLifecycleArchetypeoutright; - affinity — a key in
AFFINITY_TAINT_KEYS(today just"public-ip") that a workload names inyah.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
impl MachineConfig
Sourcepub fn sovereign_membership(&self) -> Membership<'_>
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.
Sourcepub fn location(&self) -> &str
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.
Sourcepub fn server_type(&self) -> &str
pub fn server_type(&self) -> &str
Provider SKU, or "" when omitted (static nodes).
Sourcepub fn validate(&self) -> Result<()>
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.
Sourcepub fn inert_taints(&self) -> Vec<&str>
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.
Sourcepub fn hostkey_fingerprint(&self) -> Option<&str>
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.
Sourcepub fn set_hostkey_fingerprint(&mut self, fingerprint: Option<String>)
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.
Sourcepub fn mesh_ipv4(&self) -> Option<&str>
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.
Sourcepub fn yubaba_url(&self) -> Option<String>
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.
Sourcepub fn reach(&self) -> Result<String, String>
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:
- A declared
[connect].yubabaon a private host (10/8, 172.16/12, 192.168/16) is not dialed — see below. - Any other declared
[connect].yubabawins 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 tossh”), which is a genuine declaration and stays honoured. - Otherwise
[registration].mesh_ipv4composed 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.
Sourcepub fn lan_endpoint(&self) -> Option<String>
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.
Sourcepub fn normalize(&mut self)
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.
Sourcepub fn save(&self, cloud_dir: &Path) -> Result<()>
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
impl Clone for MachineConfig
Source§fn clone(&self) -> MachineConfig
fn clone(&self) -> MachineConfig
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreSource§impl Debug for MachineConfig
impl Debug for MachineConfig
Source§impl<'de> Deserialize<'de> for MachineConfig
impl<'de> Deserialize<'de> for MachineConfig
Source§fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
Auto Trait Implementations§
impl Freeze for MachineConfig
impl RefUnwindSafe for MachineConfig
impl Send for MachineConfig
impl Sync for MachineConfig
impl Unpin for MachineConfig
impl UnsafeUnpin for MachineConfig
impl UnwindSafe for MachineConfig
Blanket Implementations§
impl<T> Allocation for T
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> DeserializeOwned for Twhere
T: for<'de> Deserialize<'de>,
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
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>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
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)
fn as_any(&self) -> &(dyn Any + 'static)
&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)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&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 Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
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>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
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)
fn as_any(&self) -> &(dyn Any + 'static)
&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)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&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
impl<T> DowncastSend for T
Source§impl<T> DowncastSync for T
impl<T> DowncastSync for T
Source§impl<T> DowncastSync for T
impl<T> DowncastSync for T
impl<T> ErasedDestructor for Twhere
T: 'static,
impl<T> Fruit for T
impl<A, B, T> HttpServerConnExec<A, B> for Twhere
B: Body,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
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 moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
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