Skip to content
Closed
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
145 changes: 124 additions & 21 deletions dag/gunbc/bmc/bmc_netboot_serve.dag
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,16 @@ import extdeps.filesystem.filesystem_io { Filesystem }
import gunbc.command_runner { run_shell_command }
import std.process { ProcessExit, ExitSuccess, exit_failure }
import std.disposition { Disposition, Scaffold, RealizationDispatch }
import std.decl_ref { DeclarationRef, WholeDeclaration }
import gunbc.os_install { srv4_ubuntu_autoinstall_on_iso }
import std.decl_ref { DeclarationRef, WholeDeclaration, decl_ref }
import gunbc.os_install { srv4_console_password_hash }
import extdeps.os.ubuntu_autoinstall { AutoinstallIdentity, NoCloudLocal, StoragePolicyDirectLayout }
import std.os.types { NobleNumbat2404Lts }
import gunbc.fleet_ssh_access { fleet_authorized_keys }
import gunbc.fleet_host_identity { operator_host_srv4 }
import gunbc.hostname_allocation { canonical_hostname_for_fleet_slot }
import product.host_identity { HostIdentity }
import gunbc.boot_artifact_delivery { BmcHostedServeLocation }
import std.roster_frontier { FrontierRow, frontier_row_decl }
import gunbc.os_install_emit {
autoinstall_user_data
AutoinstallUserData
Expand All @@ -34,27 +42,100 @@ import gunbc.os_install_emit {

type BmcNetbootServePlan {
bmc_host: NonEmptyStr
serve_port: Port
netboot_dir: NonEmptyStr
boot_interface: NonEmptyStr
serve: BmcHostedServeLocation
bulk_iso_url: Uri
autoinstall: UbuntuAutoinstallPayload
}

data srv4_bmc_netboot_serve_plan: BmcNetbootServePlan = BmcNetbootServePlan {
// THE PER-HOST ROSTER. One row per host whose controller serves its own bootstrap: the controller
// address, where on it the bootstrap is served and which host interface fetches it (the shared
// gunbc.boot_artifact_delivery location, not a second copy of its fields), and the console
// password hash the installed system's bootstrap account carries. Everything else in a plan is
// derived: the hostname from gunbc.hostname_allocation, the keys from the fleet, the install
// payload from one shape. A host with no row, or no allocated hostname, has no plan.
type BmcNetbootHostRow {
host: HostIdentity
bmc_host: NonEmptyStr
serve: BmcHostedServeLocation
console_password_hash: NonEmptyStr
}

data srv4_bmc_netboot_host_row: BmcNetbootHostRow = BmcNetbootHostRow {
host: operator_host_srv4,
bmc_host: "192.168.1.195",
serve_port: 8080,
netboot_dir: "/tmp/netboot",
boot_interface: "eth2",
bulk_iso_url: Uri {
scheme: Https,
locator: "cdimage.ubuntu.com/ubuntu/releases/24.04/release/ubuntu-24.04.3-live-server-arm64.iso",
},
autoinstall: srv4_ubuntu_autoinstall_on_iso,
serve: BmcHostedServeLocation { serve_port: 8080, serve_dir: "/tmp/netboot", host_boot_interface: "eth2" },
console_password_hash: srv4_console_password_hash,
}

// srv13-srv16 get rows at first contact, when their controller address and boot interface are
// observed and their console hash is minted; nothing here is authored before that.
data bmc_netboot_host_rows: List<BmcNetbootHostRow> = [srv4_bmc_netboot_host_row]

data bmc_netboot_bulk_iso_url: Uri = Uri {
scheme: Https,
locator: "cdimage.ubuntu.com/ubuntu/releases/24.04/release/ubuntu-24.04.3-live-server-arm64.iso",
}

fn bmc_netboot_autoinstall_of(row: BmcNetbootHostRow, hostname: NonEmptyStr) -> UbuntuAutoinstallPayload {
UbuntuAutoinstallPayload {
autoinstall_version: 1,
delivery: NoCloudLocal,
fully_automated: true,
target_distribution: NobleNumbat2404Lts,
identity: AutoinstallIdentity {
hostname: hostname,
username: "ubuntu",
realname: join(["gunbc fleet node ", hostname as String], "") as NonEmptyStr,
hashed_password: row.console_password_hash,
},
locale: "en_US.UTF-8",
keyboard_layout: "us",
install_openssh_server: true,
ssh_authorized_keys: fleet_authorized_keys,
ssh_password_auth: false,
storage_policy: StoragePolicyDirectLayout,
}
}

fn bmc_netboot_serve_plan_of(row: BmcNetbootHostRow, hostname: NonEmptyStr) -> BmcNetbootServePlan {
BmcNetbootServePlan {
bmc_host: row.bmc_host,
serve: row.serve,
bulk_iso_url: bmc_netboot_bulk_iso_url,
autoinstall: bmc_netboot_autoinstall_of(row: row, hostname: hostname),
}
}

type BmcNetbootPlanLookup
= BmcNetbootPlanned { plan: BmcNetbootServePlan }
| BmcNetbootHostNotRostered { host: HostIdentity }
| BmcNetbootHostnameUnallocated { host: HostIdentity }

fn bmc_netboot_row_for(host: HostIdentity) -> BmcNetbootHostRow? {
fold(bmc_netboot_host_rows, init: none, f: (acc, r) => match acc {
Present { value: _ } => acc
Absent => if (r.host as String) == (host as String) { Present { value: r } } else { none }
Comment on lines +116 to +117

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject duplicate hosts in the netboot roster

When the planned additional host rows are added, accidentally listing one host twice is silently accepted: after the first match this fold preserves it and ignores every later matching row. bmc_netboot_provision_for_host would consequently SSH, copy files, and launch the server using whichever BMC configuration appears first, even if another row contains the intended address or password. Build the list through duplicate-refusing admission such as std.allocation_roster, or return an ambiguity refusal before provisioning.

Useful? React with 👍 / 👎.

})
}

fn bmc_netboot_plan_for_host(host: HostIdentity) -> BmcNetbootPlanLookup {
match bmc_netboot_row_for(host: host) {
Absent => BmcNetbootHostNotRostered { host: host }
Present { value: row } =>
match canonical_hostname_for_fleet_slot(fleet_slot: host) {
Absent => BmcNetbootHostnameUnallocated { host: host }
Present { value: name } => BmcNetbootPlanned { plan: bmc_netboot_serve_plan_of(row: row, hostname: name as NonEmptyStr) }
}
}
}

// srv4's plan, kept as a value because the shell-boot rendering and its witnesses read it. It is
// built by the same constructor the lookup uses; a witness pins it equal to the lookup's answer,
// so it cannot drift from the allocated hostname.
data srv4_bmc_netboot_serve_plan: BmcNetbootServePlan = bmc_netboot_serve_plan_of(row: srv4_bmc_netboot_host_row, hostname: operator_host_srv4 as NonEmptyStr)

fn bmc_netboot_host_port(plan: BmcNetbootServePlan) -> NonEmptyStr {
join([plan.bmc_host as String, ":", to_string(plan.serve_port)], "") as NonEmptyStr
join([plan.bmc_host as String, ":", to_string(plan.serve.serve_port)], "") as NonEmptyStr
}

fn bmc_netboot_uri(plan: BmcNetbootServePlan, path: NonEmptyStr) -> Uri {
Expand Down Expand Up @@ -101,7 +182,7 @@ fn bmc_netboot_http_boot_entry(plan: BmcNetbootServePlan) -> UefiHttpBootEntry {
UefiHttpBootEntry {
description: "gunbc-bmc-netboot",
boot_file_uri: bmc_netboot_uri(plan: plan, path: "/grubaa64.efi"),
interface_name: plan.boot_interface,
interface_name: plan.serve.host_boot_interface,
}
}

Expand Down Expand Up @@ -139,7 +220,7 @@ fn bmc_netboot_artifact(name: NonEmptyStr) -> NonEmptyStr {
}

fn bmc_netboot_staged_path(plan: BmcNetbootServePlan, name: NonEmptyStr) -> NonEmptyStr {
join([plan.netboot_dir as String, "/", name as String], "") as NonEmptyStr
join([plan.serve.serve_dir as String, "/", name as String], "") as NonEmptyStr
}

// ---- THE SEMANTIC OPERATIONS (the shell-typed-invocation shape, applied to this lane) ----
Expand Down Expand Up @@ -295,8 +376,8 @@ fn bmc_netboot_op_prepared(plan: BmcNetbootServePlan, op: BmcNetbootOp) -> BmcNe
BmcNetbootStagingReady {
operations: [BmcNetbootPreparedOp {
disposition: BmcNetbootPreparedLaunchServe {
serve_root: plan.netboot_dir,
serve_port: plan.serve_port,
serve_root: plan.serve.serve_dir,
serve_port: plan.serve.serve_port,
busybox_path: bmc_netboot_staged_path(plan: plan, name: "busybox"),
},
}],
Expand Down Expand Up @@ -469,8 +550,12 @@ fn bmc_netboot_provision(plan: BmcNetbootServePlan) -> ProcessExit {
bmc_netboot_apply_staging(plan: plan, staging: bmc_netboot_provision_staging(plan: plan))
}

fn srv4_bmc_netboot_provision() -> ProcessExit {
bmc_netboot_provision(plan: srv4_bmc_netboot_serve_plan)
fn bmc_netboot_provision_for_host(host: HostIdentity) -> ProcessExit {
match bmc_netboot_plan_for_host(host: host) {
BmcNetbootPlanned { plan: p } => bmc_netboot_provision(plan: p)
BmcNetbootHostNotRostered { host: h } => exit_failure(reason: concat("bmc-netboot: no netboot roster row for ", h as String))
BmcNetbootHostnameUnallocated { host: h } => exit_failure(reason: concat("bmc-netboot: no hostname allocated for ", h as String))
}
}

data bmc_netboot_provision_scaffold: Disposition = Scaffold {
Expand All @@ -483,3 +568,21 @@ data bmc_netboot_provision_scaffold: Disposition = Scaffold {
}

data bmc_netboot_provision_dissolution_trigger: DissolutionCondition = unbound_dissolution(description: "🟡 dissolve-on: bmc_netboot_provision — the staging manifest IS the driver, and since 2026-09-30 it is ONE TYPED STAGING: provision folds its ops (BmcNetbootMakeDir / BmcNetbootPutArtifact / BmcNetbootPutText / BmcNetbootPutFromUri / BmcNetbootLaunchServe) through bmc_netboot_staging_of into one BmcNetbootStaging — the prepared operations, or the whole staging refused — and a refused staging runs nothing, so a bad argument cannot act while the ops around it are skipped past. Preparation also NARROWS the sources: the unrealizable uri source refuses as a staging verdict (the old provision ran its mkdir first and discovered the unrealizable put MID-FOLD, after the mkdir and every earlier scp had already executed against the BMC) and has no prepared constructor at all, so the realization match carries no dead arm. The three scattered run_shell_command call sites are gone (bmc_netboot_run_bmc, bmc_netboot_put_path, and the serve launch each reached the transport independently; that was three bridge sites duplicating one pattern): one executor, bmc_netboot_apply_staging, consumes the SEALED staged value (BmcNetbootPreparedOp is a sole_constructor minted only behind the relative-path verdict, so a hand-authored prepared op with a remote path its author chose is a compile-time refusal, proven by the domain witness's cross-module probe) and alone turns an arm into a run. The serve command's rendered-text surface (bmc_netboot_serve_command_text, BmcNetbootServeCommandTextBuilt/Refused) is deleted — the witness reads argv fields now, not scraped shell text. HISTORY, for the stale-citation discipline: this clause previously recorded the heredoc-bridge hop through command_runner's shell_exec_via_bash/retained_runtime, then its deletion when command_runner asked command_over_transport once and handed the result straight to shell.Exec.RunArgv, then the SshExec arm's RFC-4254 fold in extdeps.exec.command (cited IEEE 1003.1-2017 section 2.2.2) — each correction rewritten when it landed, not left standing. Remaining debt, scoped: command_runner_dissolution_trigger's own first clause — host_effect_apply has not yet bound ArgvCommand execution, so the executor's three arms (mkdir on the BMC, the scp transfers, the busybox-httpd launch) still lower through gunbc.command_runner.run_shell_command, which this module inherits rather than duplicates. DISSOLVES WHEN host_effect_apply binds ArgvCommand execution (mkdir / scp / busybox-httpd-launch) as typed staging effects, and dag/gunbc/bmc/bmc_netboot_serve.dag retires command_runner.run_shell_command in its favor — the sealed staged value is already the shape that binding would consume")

// What this provision does not yet establish, as counted rows rather than prose.
data bmc_netboot_serve_frontier_rows: List<FrontierRow> = [
frontier_row_decl(
ref: decl_ref(module_path: "gunbc.bmc_netboot_serve", decl_name: "bmc_netboot_autoinstall_of"),
reason: "the installed system's only account is the autoinstall identity's bootstrap user (ubuntu); the fleet runner user every runner host is assumed to have (briansrls, created by hand on srv4 per gunbc.plans.fleet_subsumption_manual_gaps) has no carrier, because extdeps.os.ubuntu_autoinstall UbuntuAutoinstallPayload models one identity and no further users",
dissolution: unbound_dissolution(
description: "dissolves when the autoinstall payload carries the fleet runner user (name, sudo group, fleet authorized keys) from one fleet authority and this constructor reads it, so a host installed through this plan answers ssh as the runner user with no hand step; adding the user only to srv4's row does not fire this"
)
),
frontier_row_decl(
ref: decl_ref(module_path: "gunbc.bmc_netboot_serve", decl_name: "bmc_netboot_provision_for_host"),
reason: "provision ends when the bootstrap is staged and served; nothing observes that the install completed and the host came back up from its own NVMe rather than re-entering the installer. gunbc.install_transport_qualification HostInstalledOsStanding and gunbc.host_reset_return HostReturnObservation are the homes such a readback would inhabit, and neither is produced for this route",
dissolution: unbound_dissolution(
description: "dissolves when this route's terminal reads back, from an observer that is not the subject, that the target host is running the installed system booted from its local disk (not the installer) after the one-shot UEFI Shell override was consumed, and records it as HostInstalledOsStanding InstalledOsRunning for that host; a controller's reset acknowledgement does not fire this"
)
),
]
10 changes: 5 additions & 5 deletions dag/gunbc/bmc/bmc_netboot_shell_boot.dag
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ data shell_boot_local_initrd: NonEmptyStr = "fs0:\\initrd"

fn bmc_tftpd_serve_command(plan: BmcNetbootServePlan) -> ArgvCommand {
busybox_tftpd_over_udpsvd_command(
busybox_bin: join([plan.netboot_dir as String, "/busybox"], "") as NonEmptyStr,
document_root: plan.netboot_dir,
busybox_bin: join([plan.serve.serve_dir as String, "/busybox"], "") as NonEmptyStr,
document_root: plan.serve.serve_dir,
)
}

Expand All @@ -50,10 +50,10 @@ fn shell_boot_kernel_cmdline(plan: BmcNetbootServePlan) -> List<KernelCmdlineArg

fn bmc_netboot_shell_boot_commands(plan: BmcNetbootServePlan) -> List<UefiShellCommand> {
[
Ifconfig { action: IfconfigSetDhcp { interface: plan.boot_interface } },
Ifconfig { action: IfconfigSetDhcp { interface: plan.serve.host_boot_interface } },
Ping { host: plan.bmc_host },
Tftp { interface: plan.boot_interface, server: plan.bmc_host, remote_file: "vmlinuz", local_file: shell_boot_local_kernel },
Tftp { interface: plan.boot_interface, server: plan.bmc_host, remote_file: "initrd", local_file: shell_boot_local_initrd },
Tftp { interface: plan.serve.host_boot_interface, server: plan.bmc_host, remote_file: "vmlinuz", local_file: shell_boot_local_kernel },
Tftp { interface: plan.serve.host_boot_interface, server: plan.bmc_host, remote_file: "initrd", local_file: shell_boot_local_initrd },
ExecuteEfi { path: shell_boot_local_kernel, cmdline: shell_boot_kernel_cmdline(plan: plan) },
]
}
Expand Down
8 changes: 6 additions & 2 deletions dag/gunbc/boot_artifact_delivery.dag
Original file line number Diff line number Diff line change
Expand Up @@ -235,10 +235,14 @@ data bmc_hosted_shell_boot_requirements_all: List<BmcHostedShellBootRequirement>
// from, which host interface the shell fetches over, and the digest of the
// bootstrap the serve was observed holding. The controller endpoint is not
// repeated here: it is the bound target's.
type BmcHostedShellBootParams {
type BmcHostedServeLocation {
serve_port: Port
serve_dir: NonEmptyStr
host_boot_interface: NonEmptyStr
}

type BmcHostedShellBootParams {
serve: BmcHostedServeLocation
artifact_digest: ContentHash
}

Expand Down Expand Up @@ -1146,7 +1150,7 @@ fn boot_delivery_candidate_standing_of(
data boot_artifact_delivery_frontier_rows: List<FrontierRow> = [
frontier_row_decl(
ref: decl_ref(module_path: "gunbc.boot_artifact_delivery", decl_name: "BmcHostedShellBootStanding"),
reason: "no producer in this tree mints BmcHostedShellBootEstablished; gunbc.bmc_netboot_serve stages and launches the controller-side serve for srv4 from its own constants, and the SOL send, the host's UEFI Shell reachability and the served bootstrap's digest are not yet observed into this standing, so BoundBootDeliveryEvidence.bmc_hosted_shell is constructed only by witnesses",
reason: "no producer in this tree mints BmcHostedShellBootEstablished; gunbc.bmc_netboot_serve stages and launches the controller-side serve for any host in its roster (srv4 is the only row), but observes nothing back, and the SOL send, the host's UEFI Shell reachability and the served bootstrap's digest are not yet observed into this standing, so BoundBootDeliveryEvidence.bmc_hosted_shell is constructed only by witnesses",
dissolution: unbound_dissolution(
description: "dissolves when one producer, given a bound target, observes the controller shell, reads back the served bootstrap's digest from the controller's serve, observes a SOL send and the host's UEFI Shell prompt for that target, and binds the result into BoundBootDeliveryEvidence.bmc_hosted_shell on a real boot attempt, with each unobserved requirement left in missing; a witness over supplied standings does not fire this"
)
Expand Down
2 changes: 2 additions & 0 deletions dag/gunbc/census_closure_frontier.dag
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import extdeps.cloudflare.r2 { cloudflare_r2_frontier_rows }
import gunbc.cloudflare.boot_origin { cloudflare_boot_origin_frontier_rows }
import gunbc.network_boot_delivery { network_boot_delivery_frontier_rows }
import gunbc.boot_artifact_delivery { boot_artifact_delivery_frontier_rows }
import gunbc.bmc_netboot_serve { bmc_netboot_serve_frontier_rows }
import gunbc.site_uefi_arm64_pxe_edge { site_uefi_arm64_pxe_edge_frontier_rows }
import product.supplier.cloudflare_r2 { cloudflare_r2_supplier_frontier_rows }
import extdeps.backblaze.b2 { backblaze_b2_frontier_rows }
Expand Down Expand Up @@ -140,6 +141,7 @@ fn census_closure_frontier_row_groups() -> List<List<FrontierRow>> {
cloudflare_boot_origin_frontier_rows,
network_boot_delivery_frontier_rows,
boot_artifact_delivery_frontier_rows,
bmc_netboot_serve_frontier_rows,
site_uefi_arm64_pxe_edge_frontier_rows,
cloudflare_r2_supplier_frontier_rows,
backblaze_b2_frontier_rows,
Expand Down
54 changes: 54 additions & 0 deletions dag/test/claim/bmc/bmc_netboot_serve_witness_test.dag
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,17 @@ import extdeps.exec.command {
scp_command,
}
import extdeps.tools.mkdir { mkdir_parents_command_at }
import extdeps.uri { Uri, Https }
import gunbc.boot_artifact_delivery { BmcHostedServeLocation }
import gunbc.os_install { srv4_ubuntu_autoinstall_on_iso }
import gunbc.fleet_host_identity { operator_host_srv4, operator_host_srv13 }
import gunbc.bmc_netboot_serve {
srv4_bmc_netboot_serve_plan,
BmcNetbootServePlan,
bmc_netboot_plan_for_host,
BmcNetbootPlanned,
BmcNetbootHostNotRostered,
BmcNetbootHostnameUnallocated,
bmc_transport,
bmc_path_resolved_mkdir_applet,
bmc_netboot_grub_cfg_text,
Expand Down Expand Up @@ -338,3 +347,48 @@ test fn staged_puts_carry_the_rendered_grub_and_seed() -> Bool {
})
grub_ok && seed_ok && list_length(items: puts) == 7
}

// THE LITERAL srv4 PLAN AS IT STOOD BEFORE THE ROSTER: every value spelled out, the install payload
// the seeded-ISO path still uses. The roster-derived plan must equal it as a value and render the
// same grub config and seed, so moving srv4 onto the roster changed no byte it serves.
data srv4_bmc_netboot_serve_plan_before_roster: BmcNetbootServePlan = BmcNetbootServePlan {
bmc_host: "192.168.1.195",
serve: BmcHostedServeLocation { serve_port: 8080, serve_dir: "/tmp/netboot", host_boot_interface: "eth2" },
bulk_iso_url: Uri {
scheme: Https,
locator: "cdimage.ubuntu.com/ubuntu/releases/24.04/release/ubuntu-24.04.3-live-server-arm64.iso",
},
autoinstall: srv4_ubuntu_autoinstall_on_iso,
}

fn seed_text(plan: BmcNetbootServePlan) -> String {
match bmc_netboot_user_seed_verdict(plan: plan) {
AutoinstallUserDataRendered { content: ud } => ud
AutoinstallUserDataRefused { reason: _ } => ""
}
}

test fn srv4_roster_plan_equals_the_literal_plan_it_replaced() -> Bool {
let derived = srv4_bmc_netboot_serve_plan
let before = srv4_bmc_netboot_serve_plan_before_roster
derived == before
&& bmc_netboot_grub_cfg_text(plan: derived) == bmc_netboot_grub_cfg_text(plan: before)
&& seed_text(plan: derived) != ""
&& seed_text(plan: derived) == seed_text(plan: before)
}

// The lookup the provision runs answers srv4 with exactly the pinned plan (so the allocated
// hostname and the plan's hostname agree), and refuses a named-but-unrostered host by name.
test fn lookup_plans_srv4_and_refuses_an_unrostered_host() -> Bool {
let srv4 = match bmc_netboot_plan_for_host(host: operator_host_srv4) {
BmcNetbootPlanned { plan: p } => p == srv4_bmc_netboot_serve_plan
BmcNetbootHostNotRostered { host: _ } => false
BmcNetbootHostnameUnallocated { host: _ } => false
}
let srv13 = match bmc_netboot_plan_for_host(host: operator_host_srv13) {
BmcNetbootHostNotRostered { host: h } => (h as String) == "srv13"
BmcNetbootPlanned { plan: _ } => false
BmcNetbootHostnameUnallocated { host: _ } => false
}
srv4 && srv13
}
Loading