Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions dag/extdeps/cloud_init/cloud_init.dag
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
module extdeps.cloud_init.cloud_init

import std.types { String, NonEmptyStr, Bool, List }
import extdeps.external_authority { ExternalAuthority }
import extdeps.uri { Uri, Https }

data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority {
uri: Uri {
scheme: Https
locator: "cloudinit.readthedocs.io/en/latest/howto/disable_cloud_init.html"
}
}

data cloud_init_authority_note: String = "cloud-init is a DISTINCT upstream from Ubuntu and from subiquity, and it gets its own module for the same reason extdeps.os.ubuntu_autoinstall already cites cloudinit.readthedocs.io separately for the NoCloud datasource rather than folding it into the subiquity anchor: they are different projects with different release cadences and different documentation, and a fact about cloud-init's lifecycle is not a fact about the distribution that ships it. What this module owns is narrow and load-bearing: how cloud-init is turned OFF, and what that does and does not imply about the files it wrote while it was on."

data cloud_init_disabled_marker_path: NonEmptyStr = "/etc/cloud/cloud-init.disabled"

data cloud_init_reenable_argv_shape: List<NonEmptyStr> = ["cloud-init", "clean", "--machine-id"]

type CloudInitDisposition
= CloudInitEnabled
| CloudInitDisabledByMarkerFile { path: NonEmptyStr, reason: NonEmptyStr }
| CloudInitDisabledByKernelCmdline

data ubuntu_live_installer_disable_note: String = "THE quirk, cited to the marker file's own contents as observed on srv4 2026-07-25: after a desktop/server ISO install, the Ubuntu live installer writes /etc/cloud/cloud-init.disabled containing verbatim 'Disabled by Ubuntu live installer after first boot.' followed by the re-enable instruction 'sudo cloud-init clean --machine-id'. This is STOCK, EXPECTED BEHAVIOUR on every ISO-installed Ubuntu host — it is not a fault, not a misconfiguration, and not something the fleet did. Recording it matters because its absence from the model cost real diagnostic time: on a host that had lost networking, finding cloud-init disabled reads like a smoking gun to anyone who does not know the installer always does this, and the wrong causal story (generator off, therefore config never applied) survives exactly as long as nobody checks who actually applies the config. Every ISO-installed fleet host carries this marker; a host WITHOUT it was installed by a different path and that difference is the interesting signal, not the marker itself."

data ubuntu_live_installer_disable_marker_text: NonEmptyStr = "Disabled by Ubuntu live installer after first boot."

data cloud_init_installer_config_paths: List<NonEmptyStr> = [
"/etc/cloud/cloud.cfg.d/90-installer-network.cfg",
"/etc/cloud/cloud.cfg.d/99-installer.cfg",
]

data cloud_init_is_a_writer_not_an_applier_note: String = "THE fact this module exists to make unfusable. cloud-init GENERATES /etc/netplan/50-cloud-init.yaml; it does not APPLY it. Applying is netplan's job (extdeps.netplan.netplan), performed at every boot by the renderer, and the renderer neither knows nor cares which process wrote the file or whether that process still runs. So disabling cloud-init freezes the file — no future regeneration, and hand edits become authoritative because nothing will overwrite them — and changes NOTHING about whether the network comes up. The tempting inference, 'the generator is disabled, therefore its output is not in effect', is FALSE, and it is false in the direction that produces a confident wrong diagnosis rather than a visible error. It was drawn live on srv4 (2026-07-25) against a host whose netplan was present, correct, and applied, and whose actual fault was a dead physical link. gunbc.host_network_diagnosis carries that inference as an executable RED control so it cannot be drawn again unnoticed."

fn cloud_init_is_disabled(d: CloudInitDisposition) -> Bool {
match d {
CloudInitEnabled => false
CloudInitDisabledByMarkerFile { path: _, reason: _ } => true
CloudInitDisabledByKernelCmdline => true
}
}

data cloud_init_generated_netplan_path: NonEmptyStr = "/etc/netplan/50-cloud-init.yaml"

type CloudInitGeneratedFileState
= RegeneratedEachBoot
| FrozenAtLastRun { because: CloudInitDisposition }

fn cloud_init_generated_file_state(d: CloudInitDisposition) -> CloudInitGeneratedFileState {
if cloud_init_is_disabled(d: d) {
FrozenAtLastRun { because: d }
} else {
RegeneratedEachBoot
}
}

data cloud_init_frozen_authority_note: String = "A frozen generated file is an AUTHORITY TRANSFER, not merely a stale artifact, and naming it that way is what makes it safe to edit. While cloud-init runs, /etc/netplan/50-cloud-init.yaml is derived and any hand edit is lost at the next boot. Once cloud-init is disabled, the same path is hand-authoritative — the generator that owned it will never run again — which is precisely the SelfEmitted-versus-SeedRetained distinction the self-host frontier draws for emitted sources, and the same question gunbc.generated_artifact answers for this repo's own generated files. The state must be derived from the disposition rather than assumed per-file, because the same path means opposite things on two hosts that differ only in whether an installer ran."
70 changes: 70 additions & 0 deletions dag/extdeps/netplan/netplan.dag
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
module extdeps.netplan.netplan

import std.types { String, NonEmptyStr, Bool, Int, List }
import extdeps.external_authority { ExternalAuthority }
import extdeps.uri { Uri, Https }

data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority {
uri: Uri {
scheme: Https
locator: "netplan.readthedocs.io/en/stable/netplan-yaml/"
}
}

data netplan_authority_note: String = "netplan was entirely unmodeled before this — zero occurrences corpus-wide — and its absence is why the fleet model can RECORD an address without being able to say anything about whether a host holds it. gunbc.fleet_intent_network carries srv1..srv4's addresses as NetworkEndpoint rows, which are topology facts: what the address IS, not that the host has it. netplan is the party that decides the latter, so the ensure side of host networking has nowhere to live until this module exists. This is the applier half of the writer/applier pair whose fusion produced a wrong diagnosis on srv4 (see extdeps.cloud_init.cloud_init)."

data netplan_config_dir: NonEmptyStr = "/etc/netplan"

type NetplanRenderer = SystemdNetworkd | NetworkManager

data netplan_reads_every_yaml_note: String = "THE applier fact, and the counterpart to cloud-init's writer fact. netplan reads EVERY *.yaml under /etc/netplan at boot, merges them in lexical order, and renders the result to its backend — with no knowledge of and no dependency on which process wrote any of them. A file whose generator has been uninstalled, disabled, or deleted is applied exactly as a hand-written one is. That is the whole content of the inference the srv4 diagnosis got wrong, stated positively so it can be consumed rather than merely warned about."

type NetplanConfigWriter
= WrittenByCloudInit
| WrittenByInstaller
| WrittenByOperator
| WriterUnknown

type NetplanConfigFile {
path: NonEmptyStr
writer: NetplanConfigWriter
}

fn netplan_file_is_applied(f: NetplanConfigFile) -> Bool {
true
}

data netplan_file_is_applied_note: String = "Deliberately total, and deliberately ignoring its argument's writer field. This looks like a function that should not exist until you notice it is the exact proposition the srv4 misdiagnosis denied: presence in /etc/netplan is sufficient for application, and the writer is irrelevant to it. Writing it as a total function makes the writer-dependent version unwritable in any consumer that routes through here, and gives the RED control in gunbc.host_network_diagnosis something concrete to contradict. If netplan ever gains a genuine non-application case — a malformed file the renderer rejects, say — this becomes a real fold with a typed refusal, and that is a change to THIS module rather than to every consumer that had rolled its own rule."

data netplan_link_state_note: String = "Link state is the axis netplan CANNOT influence, and separating it is what lets a diagnosis point at the right layer. An interface that netplan has configured is administratively UP with a real qdisc; whether it has CARRIER is a fact about the cable, the switch port and the PHY. The two are independently observable and were observed to disagree on srv4 (2026-07-25): enP3p3s0f1 read <NO-CARRIER,BROADCAST,MULTICAST,UP> with qdisc mq — configured and applied, no link partner — while its unused siblings read <BROADCAST,MULTICAST> with qdisc noop, which is what an interface no configuration mentions looks like. Those two shapes are easy to confuse by eye and mean opposite things about whether the model did its job, which is exactly why they are separate variants here rather than one 'down' flag."

type InterfaceAdminState
= AdminUpConfigured
| AdminDownUnconfigured

type InterfaceCarrier
= CarrierPresent
| CarrierAbsent
| CarrierUnobserved

type InterfaceObservation {
name: NonEmptyStr
mac: NonEmptyStr
admin: InterfaceAdminState
carrier: InterfaceCarrier
}

fn interface_is_configured(o: InterfaceObservation) -> Bool {
match o.admin {
AdminUpConfigured => true
AdminDownUnconfigured => false
}
}

fn interface_has_link(o: InterfaceObservation) -> Bool {
match o.carrier {
CarrierPresent => true
CarrierAbsent => false
CarrierUnobserved => false
}
}
Loading
Loading