Skip to content

Model the cloud-init/netplan writer-applier split: extdeps facts + a layer-locating host network diagnosis - #7255

Merged
briansrls merged 9 commits into
mainfrom
session/vivid-wolf-453-netcfg
Jul 26, 2026
Merged

briansrls merged 9 commits into
mainfrom
session/vivid-wolf-453-netcfg

Conversation

@briansrls

@briansrls briansrls commented Jul 25, 2026 •

Copy link
Copy Markdown
Contributor

Models the cloud-init / netplan writer-vs-applier split as cited extdeps facts, and turns it into a located network diagnosis that sits on the fleet status matrix as a counted axis.

Seeded by a wrong answer I gave during the srv4 outage (2026-07-25). srv4 dropped off the network; I found cloud-init disabled, read that as the netplan config never being applied, and built a causal story on it. The host's netplan was in fact present, correct and applied, and the real fault was a dead physical link — enP3p3s0f1 administratively UP with qdisc mq and no carrier, surviving a graceful power-down, a 20s de-energised hold and a power-up. Installer-disabled cloud-init is stock Ubuntu on every ISO-installed host.

The facts, where they belong

extdeps/os/cloud_init.dag (cited: cloudinit.readthedocs.io disable_cloud_init) owns the writer half — how cloud-init is turned off, and what that does and doesn't imply about files it wrote while on. The Ubuntu live installer's marker text is recorded verbatim. Disabling freezes the generated file, transferring authority to whoever edits it next; it does not void it.

extdeps/os/netplan.dag (cited: netplan.readthedocs.io netplan-yaml) owns the applier half. netplan was entirely unmodeled before this — zero occurrences corpus-wide — which is why the fleet model could record a host's address without being able to say anything about whether the host holds it. netplan_file_is_applied is deliberately total and deliberately ignores the writer field: presence in /etc/netplan is sufficient for application. That single proposition is what the misdiagnosis denied, written so it can be consumed rather than merely warned about.

The diagnosis

gunbc/host_network_diagnosis.dag folds three independently-observable facts into an outcome naming the layer at fault, and the generator's state is not an input to it at all. NetworkConfigAbsent is a modelling failure, ConfiguredLinkAbsent is physical (nothing in software will fix it, and saying so precisely is what stops hours going into config), LinkStateUnobserved refuses rather than guessing because carrier is only knowable from the host.

The forbidden inference is carried as an executable RED control rather than a comment: diagnose_via_generator_state_widen_sketch is the reasoning I actually performed, run against srv4's own readings, rejected by the gate while the authority is accepted on the same inputs.

On the status matrix

The fold is reachable as a per-host axis alongside swap-backing and toolchain-completeness: network_diagnosis_verdict projects to ObservationVerdict (layer stays on the diagnosis for consumers that must act), host_network_verdict routes through the read-back independence criterion via a new ip_link_carrier_read_probe (Inert — reading a carrier cannot bring a link up), and host_network_fleet_unobserved_count is the counted deficit. No producer exists yet, so every host refuses and the tally is the whole fleet — deliberately, because a counted deficit ranks for fixing and a masked one never does (§5).

The srv4 incident fixtures are not reachable from the host query. Answering a host with a once-off hand reading taken over a serial console would be the fabricated plausible output §5 forbids; they stay witness inputs.

Witnesses (green by execution, 8 conjuncts)

srv4 hinge (blames physical, names the interface and MAC an engineer needs, does not blame config) · RED control on the generator-state inference · installer-disable is normal and freezes rather than voids · the two down-looking shapes are not confused (qdisc noop unconfigured vs qdisc mq NO-CARRIER — reading the unused ports first is what produced the wrong story) · healthy host diagnoses healthy · unobserved carrier refuses both blames · the dry persistence bar discriminates · the axis cell refuses and counts, with a non-constant projection proven both directions.

The persistence bar is the dry half of the operator's subsumption ask: a host is subsumed when its config is present, applied, and proven to survive a power cycle. Worth naming what it would not have caught — srv4's fault was physical, so a persistence proof would have passed right up until the link died. Persistence catches applied-but-not-persisted; a liveness cell catches works-then-stops. Neither substitutes for the other.

srv4 itself still needs a human at the rack — host port MAC 48:21:0b:87:2d:64 and its switch port. Cable, switch port and failed PHY are indistinguishable remotely.

@gunbai-bot gunbai-bot Bot changed the title srvN subsumption Model the cloud-init/netplan writer-applier split: extdeps facts + a layer-locating host network diagnosis Jul 25, 2026
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review July 25, 2026 23:18
@gunbai-bot

gunbai-bot Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Review 42942 — finding confirmed and fixed in e1d11ce.

It was a real §3 fork and self-inflicted: this PR mints cloud_init_disabled_marker_path as the single authority in extdeps.os.cloud_init, and the srv4 fixture two lines below the netplan-path import — which did consume its authority — still carried the marker path as a bare literal. Two sources for one path, free to drift.

dag/gunbc/host_network_diagnosis.dag now imports and consumes it, so the fork is structurally gone rather than checked. Swept the rest of the diff for the same class while I was in there: every remaining /etc/... literal in the three new modules is a sole authority definition or prose.

host_network_diagnosis_witnesses re-run green by execution after the change.

— sent from vivid-wolf-453

@gunbai-bot

gunbai-bot Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Review 42951 — one finding fixed, one refuted by execution.

Finding 2 (dead std.upsert_decision import) — correct, and the fix is the one you named

You wrote: "either wire the diagnosis into that verdict fold (which would make the 'consumer' story concrete) or drop the import." Wiring was right, and it closed a gap I had against the operator's own bar for this work — prove it works in our install/subsumption workflow, dry at least for now.

Before this, diagnose_host_network had zero consumers outside its own module and its witness. An orphaned fold is the coverage-by-illusion tier DESIGN §6 names, and the dead import was the visible symptom of it. The network now sits on the same status axis as the swap-backing and toolchain-completeness axes, with the three parts those established:

  • network_diagnosis_verdict — HostNetworkDiagnosis → ObservationVerdict. Both decisive failures project to Drifted; the layer stays on the diagnosis, which is what a consumer that must act reads. LinkStateUnobserved projects to UnknownRefused, never to a decisive verdict — the ⊤-as-ignorance / ⊤-as-answer pair §5 names.
  • host_network_verdict — per host, routed through the read-back independence criterion via a new ip_link_carrier_read_probe (classified ProbeInert: reading a carrier cannot bring a link up), added to the roster so the inventory stays honest.
  • host_network_fleet_unobserved_count — the counted deficit. There is no live producer yet, so every host refuses and the tally is the whole fleet. That is deliberate: a counted deficit ranks for fixing, a masked one displaces nothing and never does (§5).

The srv4 incident fixtures are deliberately not reachable from the host query — answering a host with a once-off hand reading taken over a serial console would be the fabricated plausible output §5 forbids. They stay witness inputs.

New conjunct witness_network_axis_cell_refuses_and_counts; its last two conjuncts prove the projection is not constant (healthy → Converged, dead link → Drifted), so the all-refused tally is the producer's absence rather than a fold that refuses everything. The roster ratchet in the readback witness moved 4 → 5, kept as a deliberate visible edit rather than an unpinned list.

Finding 1 (witness has no imports, "will not compile") — refuted

Two receipts:

It runs, and it passes. Before and after this change:

$ claim_batch --source-root dag --source-root src/v2 \
    --entry dag/test/claim/host_network_diagnosis_witness_test.dag \
    --function host_network_diagnosis_witnesses
PASS host_network_diagnosis_witnesses

The premise is inverted. The review states "Every sibling witness test in dag/test/claim/*_witness_test.dag imports its dependencies explicitly." Measured:

dag/test/claim/*_witness_test.dag: 305 total, 164 with NO import block

Import-free is the majority pattern, not an anomaly. All four of my immediate siblings — host_swap_backing, readback_independence, materialized_secret, host_toolchain_components — have zero import blocks and each merged with green CI. Test-claim modules resolve bare references against the assembled closure; what they do require is that a variant in expression position be qualified (std.upsert_decision.Converged), which this file already does throughout.

I'd rather this finding had been right than not — an inert RED control is exactly the failure worth catching, and it is the one I went looking for on the consumer side, where it turned out to be real. It just wasn't here.

— sent from vivid-wolf-453

@gunbai-bot

gunbai-bot Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Re-homed both extdeps modules onto the refactored layout (operator flagged that these dirs moved recently). This invalidates the four approvals on the previous HEAD — apologies for the re-review, but the old paths were wrong and it is better caught now than after merge.

dag/extdeps/os/ is the OS distributions dir post-refactor (macos, ubuntu, ubuntu_autoinstall, windows). cloud-init and netplan are neither distributions nor Ubuntu-specific — they are distinct upstream projects with their own release cadences and their own documentation, which is the argument each module's own authority note already makes. Filing them under os/ was the pre-refactor habit, not a claim about what they are.

The layout's convention is one directory per upstream project, named for the project — extdeps/tmux/tmux.dag, extdeps/cron/cron.dag, extdeps/dhcp/v4.dag, extdeps/docker/docker.dag plus its surfaces. Single-file project dirs are ordinary there, so a project does not have to be large to get its own home.

dag/extdeps/os/cloud_init.dag -> dag/extdeps/cloud_init/cloud_init.dag
dag/extdeps/os/netplan.dag    -> dag/extdeps/netplan/netplan.dag

Module paths track directories, so these are renames, not moves: extdeps.os.cloud_init -> extdeps.cloud_init.cloud_init, extdeps.os.netplan -> extdeps.netplan.netplan. The sole importer (gunbc.host_network_diagnosis) and the prose cross-references inside both modules are repointed; zero stale module refs and zero stale path refs remain in dag/, src/ or docs/.

The gunbc and test/claim files were already correct and are untouched. I checked rather than assumed: the sibling axis modules that merged this week — host_swap_backing, host_toolchain_components, readback_independence, host_phase_status, host_standup — all sit at dag/gunbc/ root with their witnesses at dag/test/claim/ root, and gunbc/assimilate/ is specifically the BMC bootstrap lane rather than a home for host axes.

Also merged origin/main (7 commits) and rebuilt the seed locally before re-running, since main now carries the argv-materializer builtin rename and a stale binary would have been exercising the wrong seed.

Green by execution after the move: host_network_diagnosis_witnesses, readback_independence_witnesses.

— sent from vivid-wolf-453

@briansrls
briansrls merged commit 4331373 into main Jul 26, 2026
5 checks passed
@briansrls
briansrls deleted the session/vivid-wolf-453-netcfg branch July 26, 2026 03:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant