From c2e14b88b1d399342a61ef4da74f45fc80a2ae79 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 1 Jul 2026 16:21:01 +0000 Subject: [PATCH 01/16] WIP: srv3 greenfield OS-install: solve-driven NBD-proxy actuator + dissolve B --- dsl/extdeps/boards/asrock_rack.dag | 63 +++++++- dsl/gunbc/bmc_onboarding.dag | 5 +- dsl/gunbc/host_standup.dag | 3 +- dsl/gunbc/nbd_proxy_virtual_media_install.dag | 152 ++++++++++++++++++ dsl/gunbc/os_install.dag | 4 +- dsl/gunbc/os_install_mechanism.dag | 18 +++ .../claim/asrock_live_probe_witness_test.dag | 62 +++++++ .../bmc_onboarding_lifecycle_witness_test.dag | 6 +- ...oxy_virtual_media_install_witness_test.dag | 76 +++++++++ .../claim/srv3_os_install_witness_test.dag | 4 +- 10 files changed, 382 insertions(+), 11 deletions(-) create mode 100644 dsl/gunbc/nbd_proxy_virtual_media_install.dag create mode 100644 dsl/test/claim/asrock_live_probe_witness_test.dag create mode 100644 dsl/test/claim/nbd_proxy_virtual_media_install_witness_test.dag diff --git a/dsl/extdeps/boards/asrock_rack.dag b/dsl/extdeps/boards/asrock_rack.dag index f67e9f16613..c8b5d3e5164 100644 --- a/dsl/extdeps/boards/asrock_rack.dag +++ b/dsl/extdeps/boards/asrock_rack.dag @@ -5,7 +5,7 @@ import std.baseboard { AsrockRackAltrad8ud1l2t, } import std.bmc { OpenBmc } -import std.types { NonEmptyStr, List } +import std.types { NonEmptyStr, List, Bool } import extdeps.bmc.capability { BmcCapability, @@ -103,3 +103,64 @@ data asrock_altrad8ud_shipped_bmc_firmware_capabilities: BmcFirmwareCapabilityRo data asrock_altrad8ud_openbmc_2_07_00_nbd_proxy_primary_endpoint: BmcwebNbdProxyEndpointVariant = VmSlotIndexEndpoint data asrock_altrad8ud_openbmc_2_07_00_nbd_proxy_absent_endpoints: List = [NbdSlotEndpoint] + +type BmcReadOnlyProbeVerdict = ProbePresent | ProbeAbsent | ProbeInconclusive + +type BmcLiveReadOnlyProbeRow { + locator: NonEmptyStr + verdict: BmcReadOnlyProbeVerdict + probed_at: NonEmptyStr +} + +data asrock_srv3_live_bmc_host: NonEmptyStr = "192.168.1.192" + +data asrock_srv3_live_readonly_probe_rows: List = [ + BmcLiveReadOnlyProbeRow { + locator: "/redfish/v1/Managers/bmc FirmwareVersion", + verdict: ProbePresent, + probed_at: "2026-07-01", + }, + BmcLiveReadOnlyProbeRow { + locator: "/redfish/v1/Systems/system/VirtualMedia", + verdict: ProbeAbsent, + probed_at: "2026-07-01", + }, + BmcLiveReadOnlyProbeRow { + locator: "/redfish/v1/Managers/bmc/VirtualMedia", + verdict: ProbeAbsent, + probed_at: "2026-07-01", + }, + BmcLiveReadOnlyProbeRow { + locator: "/vm/0/0 non-upgrade GET", + verdict: ProbeInconclusive, + probed_at: "2026-07-01", + }, + BmcLiveReadOnlyProbeRow { + locator: "/nbd/0 non-upgrade GET", + verdict: ProbeInconclusive, + probed_at: "2026-07-01", + }, +] + +data asrock_srv3_live_probe_firmware_version: NonEmptyStr = "2.07.00" + +fn live_probe_confirms_shipped_firmware_version() -> Bool { + asrock_srv3_live_probe_firmware_version == asrock_altrad8ud_shipped_bmc_firmware_capabilities.firmware.version +} + +fn live_probe_row_is_redfish_vm_absent(row: BmcLiveReadOnlyProbeRow) -> Bool { + if row.locator == "/redfish/v1/Systems/system/VirtualMedia" as NonEmptyStr + || row.locator == "/redfish/v1/Managers/bmc/VirtualMedia" as NonEmptyStr { + row.verdict == ProbeAbsent + } else { + true + } +} + +fn live_probe_confirms_redfish_virtual_media_absent() -> Bool { + fold( + asrock_srv3_live_readonly_probe_rows, + init: true, + f: (acc, row) => acc && live_probe_row_is_redfish_vm_absent(row: row) + ) +} diff --git a/dsl/gunbc/bmc_onboarding.dag b/dsl/gunbc/bmc_onboarding.dag index af409066a3a..7df9f87c8c0 100644 --- a/dsl/gunbc/bmc_onboarding.dag +++ b/dsl/gunbc/bmc_onboarding.dag @@ -5,7 +5,8 @@ import std.credentials { FactoryLogin, CredentialFlow, Stored } import std.os.types { OperatingSystemProduct, UbuntuOs, NobleNumbat2404Lts } import std.types { NonEmptyStr, Int, List } import std.logic { Bool } -import extdeps.bmc.types { RedfishBootSourceOverrideTarget, BootTargetPxe, RedfishAccountRole, AccountRoleAdministrator } +import extdeps.bmc.types { RedfishBootSourceOverrideTarget, RedfishAccountRole, AccountRoleAdministrator } +import gunbc.os_install_mechanism { srv3_install_boot_target } import extdeps.bmc.openbmc { openbmc_factory_login } import extdeps.boards.asrock_rack { asrock_altrad8ud_redfish_system_id } @@ -216,7 +217,7 @@ data new_altra_onboarding_plan: BmcOnboardingPlan = BmcOnboardingPlan { rotated_credential: Stored { secret_name: "bmc-srv3-admin" }, rotated_account_role: AccountRoleAdministrator, os_target: UbuntuOs { distro: NobleNumbat2404Lts }, - os_install_boot_target: BootTargetPxe, + os_install_boot_target: srv3_install_boot_target, start_phase: FactoryDefault, } diff --git a/dsl/gunbc/host_standup.dag b/dsl/gunbc/host_standup.dag index f59cb64c01e..0d79295d6d8 100644 --- a/dsl/gunbc/host_standup.dag +++ b/dsl/gunbc/host_standup.dag @@ -186,6 +186,7 @@ data host_standup_spine: List = [ authorities: [ decl_ref(module_path: "gunbc.os_install_mechanism", decl_name: "solve_install_mechanism"), decl_ref(module_path: "gunbc.os_install_mechanism", decl_name: "srv3_install_mechanism"), + decl_ref(module_path: "gunbc.nbd_proxy_virtual_media_install", decl_name: "srv3_os_install_actuator_plan"), ], effect_policy: OneShotIdempotent ), @@ -276,7 +277,7 @@ data host_standup_gap_ledger: List = [ GapLedgerRow { phase_label: "prefix:os-install-mechanism", modeling: "modeled", - authority_or_interim: "gunbc.os_install_mechanism.srv3_install_mechanism (srv3=NbdProxyVirtualMediaInstall, no fallback)" + authority_or_interim: "gunbc.os_install_mechanism.srv3_install_mechanism + gunbc.nbd_proxy_virtual_media_install.srv3_os_install_actuator_plan (srv3=NbdProxyVirtualMediaInstall, solve-driven actuator)" }, GapLedgerRow { phase_label: "prefix:host-converge", diff --git a/dsl/gunbc/nbd_proxy_virtual_media_install.dag b/dsl/gunbc/nbd_proxy_virtual_media_install.dag new file mode 100644 index 00000000000..06fc3fa7745 --- /dev/null +++ b/dsl/gunbc/nbd_proxy_virtual_media_install.dag @@ -0,0 +1,152 @@ +module gunbc.nbd_proxy_virtual_media_install + +import std.types { List, Int } +import std.measure { ByteSize, byte_size } +import extdeps.bmc.types { + RedfishBootSourceOverrideTarget, + RedfishBootSourceOverrideEnabled, + BootOverrideOnce, + RedfishResetType, + ResetForceRestart, + RedfishSystemAction, + SetBootSourceOverride, + ResetSystem, +} +import extdeps.bmc.webui.nbd_proxy { + NbdProxyServeInterface, + BmcwebNbdProxyEndpointVariant, +} +import extdeps.boards.asrock_rack { + asrock_altrad8ud_openbmc_2_07_00_nbd_proxy_primary_endpoint, +} +import gunbc.os_install_mechanism { + solve_install_mechanism, + srv3_install_mechanism, +} +import gunbc.host_effect { + RedfishAction, + BmcController, + OneShotIdempotent, + HostEffectIntent, + LocalShell, +} +import gunbc.fleet_intent { srv3_host } + +type OsInstallActuatorStep + = ActuatorNbdProxyServe { + endpoint: BmcwebNbdProxyEndpointVariant + slot: Int + index: Int + export: NbdProxyServeInterface + } + | ActuatorRedfishInsertMedia + | ActuatorPxeNetworkBootStaging + | ActuatorFirmwareUpdate + | ActuatorBootOnce { + target: RedfishBootSourceOverrideTarget + enabled: RedfishBootSourceOverrideEnabled + } + | ActuatorForceRestart { reset_type: RedfishResetType } + +type OsInstallActuatorPlan { + mechanism: OsInstallMechanism + steps: List +} + +fn boot_once_step(target: RedfishBootSourceOverrideTarget) -> OsInstallActuatorStep { + ActuatorBootOnce { + target: target, + enabled: BootOverrideOnce, + } +} + +fn force_restart_step() -> OsInstallActuatorStep { + ActuatorForceRestart { reset_type: ResetForceRestart } +} + +fn nbd_proxy_serve_step(export: NbdProxyServeInterface) -> OsInstallActuatorStep { + ActuatorNbdProxyServe { + endpoint: asrock_altrad8ud_openbmc_2_07_00_nbd_proxy_primary_endpoint, + slot: 0, + index: 0, + export: export, + } +} + +fn os_install_actuator_steps(mechanism: OsInstallMechanism, export: NbdProxyServeInterface) -> List { + let boot_target = install_boot_target_for(mechanism: mechanism) + match mechanism { + VirtualMediaInstall => [ + ActuatorRedfishInsertMedia, + boot_once_step(target: boot_target), + force_restart_step(), + ] + NbdProxyVirtualMediaInstall => [ + nbd_proxy_serve_step(export: export), + boot_once_step(target: boot_target), + force_restart_step(), + ] + PxeHttpInstall => [ + ActuatorPxeNetworkBootStaging, + boot_once_step(target: boot_target), + force_restart_step(), + ] + FirmwareUpdateThenVirtualMedia => [ + ActuatorFirmwareUpdate, + ActuatorRedfishInsertMedia, + boot_once_step(target: boot_target), + force_restart_step(), + ] + } +} + +fn os_install_actuator_plan(mechanism: OsInstallMechanism, export: NbdProxyServeInterface) -> OsInstallActuatorPlan { + OsInstallActuatorPlan { + mechanism: mechanism, + steps: os_install_actuator_steps(mechanism: mechanism, export: export), + } +} + +data srv3_nbd_proxy_export_placeholder: NbdProxyServeInterface = NbdProxyServeInterface { + export_byte_count: byte_size(0), +} + +data srv3_os_install_actuator_plan: OsInstallActuatorPlan = os_install_actuator_plan( + mechanism: srv3_install_mechanism, + export: srv3_nbd_proxy_export_placeholder +) + +fn actuator_step_to_host_effect_intent(step: OsInstallActuatorStep) -> HostEffectIntent? { + match step { + ActuatorBootOnce { target: t, enabled: e } => + Present { + value: HostEffectIntent { + target: BmcController { node: srv3_host }, + effect: RedfishAction { + action: SetBootSourceOverride { target: t, enabled: e } + }, + policy: OneShotIdempotent, + transport: LocalShell, + } + } + ActuatorForceRestart { reset_type: r } => + Present { + value: HostEffectIntent { + target: BmcController { node: srv3_host }, + effect: RedfishAction { + action: ResetSystem { reset_type: r } + }, + policy: OneShotIdempotent, + transport: LocalShell, + } + } + ActuatorNbdProxyServe { endpoint: _, slot: _, index: _, export: _ } => Absent + ActuatorRedfishInsertMedia => Absent + ActuatorPxeNetworkBootStaging => Absent + ActuatorFirmwareUpdate => Absent + } +} + +fn os_install_actuator_step_count(plan: OsInstallActuatorPlan) -> Int { + plan.steps.length() +} diff --git a/dsl/gunbc/os_install.dag b/dsl/gunbc/os_install.dag index a3b2646255f..f2acc6fc01a 100644 --- a/dsl/gunbc/os_install.dag +++ b/dsl/gunbc/os_install.dag @@ -22,12 +22,12 @@ import product.placement_supply { HostIdentity } import product.network_topology { NetworkSegment } import extdeps.bmc.types { RedfishBootSourceOverrideTarget, - BootTargetPxe, RedfishBootSourceOverrideEnabled, BootOverrideOnce, RedfishResetType, ResetForceRestart, } +import gunbc.os_install_mechanism { srv3_install_boot_target } type NetworkBootDelivery { chain: NetworkBootChain @@ -105,7 +105,7 @@ data srv3_os_install_plan: Srv3OsInstallPlan = Srv3OsInstallPlan { chain: srv3_network_boot_chain, autoinstall: srv3_ubuntu_autoinstall, }, - bmc_boot_target: BootTargetPxe, + bmc_boot_target: srv3_install_boot_target, bmc_boot_enabled: BootOverrideOnce, post_stage_reset: ResetForceRestart, } diff --git a/dsl/gunbc/os_install_mechanism.dag b/dsl/gunbc/os_install_mechanism.dag index 2f51d746380..34f5f361729 100644 --- a/dsl/gunbc/os_install_mechanism.dag +++ b/dsl/gunbc/os_install_mechanism.dag @@ -1,6 +1,11 @@ module gunbc.os_install_mechanism import std.types { List } +import extdeps.bmc.types { + RedfishBootSourceOverrideTarget, + BootTargetPxe, + BootTargetCd, +} import extdeps.bmc.capability { BmcCapability, BmcFirmwareCapabilityRow, @@ -43,3 +48,16 @@ data srv3_install_mechanism: OsInstallMechanism = solve_install_mechanism( current: asrock_altrad8ud_shipped_bmc_firmware_capabilities, catalog: srv3_firmware_catalog ) + +fn install_boot_target_for(mechanism: OsInstallMechanism) -> RedfishBootSourceOverrideTarget { + match mechanism { + VirtualMediaInstall => BootTargetCd + NbdProxyVirtualMediaInstall => BootTargetCd + PxeHttpInstall => BootTargetPxe + FirmwareUpdateThenVirtualMedia => BootTargetCd + } +} + +data srv3_install_boot_target: RedfishBootSourceOverrideTarget = install_boot_target_for( + mechanism: srv3_install_mechanism +) diff --git a/dsl/test/claim/asrock_live_probe_witness_test.dag b/dsl/test/claim/asrock_live_probe_witness_test.dag new file mode 100644 index 00000000000..69778377a34 --- /dev/null +++ b/dsl/test/claim/asrock_live_probe_witness_test.dag @@ -0,0 +1,62 @@ +module test.claim.asrock_live_probe + +import std.logic { Bool } +import std.types { list_length } +import extdeps.bmc.capability { + CapabilityVirtualMedia, + CapabilityNbdProxyVirtualMedia, + firmware_has_capability, +} +import extdeps.boards.asrock_rack { + asrock_srv3_live_bmc_host, + asrock_srv3_live_probe_firmware_version, + asrock_srv3_live_readonly_probe_rows, + asrock_altrad8ud_shipped_bmc_firmware_capabilities, + live_probe_confirms_shipped_firmware_version, + live_probe_confirms_redfish_virtual_media_absent, + ProbeAbsent, + ProbeInconclusive, +} + +test fn srv3_live_bmc_host_is_grounded() -> Bool { + asrock_srv3_live_bmc_host == "192.168.1.192" +} + +test fn live_probe_firmware_matches_shipped_catalog_row() -> Bool { + live_probe_confirms_shipped_firmware_version() + && asrock_srv3_live_probe_firmware_version == "2.07.00" + && asrock_altrad8ud_shipped_bmc_firmware_capabilities.firmware.version == "2.07.00" +} + +test fn live_probe_redfish_virtual_media_absent_matches_catalog() -> Bool { + live_probe_confirms_redfish_virtual_media_absent() + && firmware_has_capability( + row: asrock_altrad8ud_shipped_bmc_firmware_capabilities, + cap: CapabilityVirtualMedia + ) == false +} + +test fn live_probe_rows_cover_vm_and_nbd_get_surfaces() -> Bool { + list_length(items: asrock_srv3_live_readonly_probe_rows) == 5 + && fold( + asrock_srv3_live_readonly_probe_rows, + init: false, + f: (acc, row) => + acc || (row.locator == "/vm/0/0 non-upgrade GET" as NonEmptyStr + && row.verdict == ProbeInconclusive) + ) + && fold( + asrock_srv3_live_readonly_probe_rows, + init: false, + f: (acc, row) => + acc || (row.locator == "/redfish/v1/Systems/system/VirtualMedia" as NonEmptyStr + && row.verdict == ProbeAbsent) + ) +} + +test fn shipped_row_nbd_proxy_capability_requires_ws_dry_run_receipt() -> Bool { + firmware_has_capability( + row: asrock_altrad8ud_shipped_bmc_firmware_capabilities, + cap: CapabilityNbdProxyVirtualMedia + ) +} diff --git a/dsl/test/claim/bmc_onboarding_lifecycle_witness_test.dag b/dsl/test/claim/bmc_onboarding_lifecycle_witness_test.dag index a4413598777..5a6aeb1f805 100644 --- a/dsl/test/claim/bmc_onboarding_lifecycle_witness_test.dag +++ b/dsl/test/claim/bmc_onboarding_lifecycle_witness_test.dag @@ -37,7 +37,7 @@ import gunbc.bmc_onboarding { CredentialStateUnknown, bmc_credential_probe_to_phase, } -import extdeps.bmc.types { BootTargetPxe, AccountRoleAdministrator, redfish_boot_target_wire, redfish_account_role_wire } +import extdeps.bmc.types { BootTargetCd, AccountRoleAdministrator, redfish_boot_target_wire, redfish_account_role_wire } import gunbc.assimilate.bmc_token_federation { emit_bmc_token_smoke_workflow_yaml } import extdeps.communication.medium { Lossless, Lossy } @@ -194,8 +194,8 @@ test fn new_altra_plan_starts_factory_default_at_192() -> Bool { WindowsOs { distro: _ } => false MacosOs { distro: _ } => false } - && redfish_boot_target_wire(t: new_altra_onboarding_plan.os_install_boot_target) == "Pxe" - && redfish_boot_target_wire(t: BootTargetPxe) == "Pxe" + && redfish_boot_target_wire(t: new_altra_onboarding_plan.os_install_boot_target) == "Cd" + && redfish_boot_target_wire(t: BootTargetCd) == "Cd" && redfish_account_role_wire(role: new_altra_onboarding_plan.rotated_account_role) == "Administrator" && redfish_account_role_wire(role: AccountRoleAdministrator) == "Administrator" } diff --git a/dsl/test/claim/nbd_proxy_virtual_media_install_witness_test.dag b/dsl/test/claim/nbd_proxy_virtual_media_install_witness_test.dag new file mode 100644 index 00000000000..114412cc1db --- /dev/null +++ b/dsl/test/claim/nbd_proxy_virtual_media_install_witness_test.dag @@ -0,0 +1,76 @@ +module test.claim.nbd_proxy_virtual_media_install + +import std.logic { Bool } +import std.types { list_length } +import extdeps.bmc.types { BootTargetCd, BootTargetPxe } +import extdeps.bmc.webui.nbd_proxy { VmSlotIndexEndpoint } +import gunbc.os_install_mechanism { + NbdProxyVirtualMediaInstall, + PxeHttpInstall, + install_boot_target_for, + srv3_install_mechanism, + srv3_install_boot_target, +} +import gunbc.nbd_proxy_virtual_media_install { + OsInstallActuatorStep, + ActuatorNbdProxyServe, + ActuatorBootOnce, + ActuatorForceRestart, + ActuatorPxeNetworkBootStaging, + srv3_os_install_actuator_plan, + os_install_actuator_step_count, + os_install_actuator_steps, + srv3_nbd_proxy_export_placeholder, +} + +test fn srv3_install_boot_target_follows_mechanism_not_hardcoded_pxe() -> Bool { + match srv3_install_mechanism { + NbdProxyVirtualMediaInstall => srv3_install_boot_target == BootTargetCd + PxeHttpInstall => srv3_install_boot_target == BootTargetPxe + _ => false + } + && install_boot_target_for(mechanism: NbdProxyVirtualMediaInstall) == BootTargetCd + && install_boot_target_for(mechanism: PxeHttpInstall) == BootTargetPxe +} + +test fn srv3_nbd_proxy_actuator_plan_is_solve_driven() -> Bool { + match srv3_os_install_actuator_plan.mechanism { + NbdProxyVirtualMediaInstall => true + PxeHttpInstall => false + _ => false + } + && os_install_actuator_step_count(plan: srv3_os_install_actuator_plan) == 3 +} + +test fn nbd_proxy_actuator_steps_serve_boot_restart() -> Bool { + let steps = os_install_actuator_steps( + mechanism: NbdProxyVirtualMediaInstall, + export: srv3_nbd_proxy_export_placeholder + ) + list_length(items: steps) == 3 + && match steps.first() { + ActuatorNbdProxyServe { endpoint: e, slot: 0, index: 0, export: _ } => + e == VmSlotIndexEndpoint + _ => false + } + && match steps.skip(n: 1).first() { + ActuatorBootOnce { target: BootTargetCd, enabled: _ } => true + _ => false + } + && match steps.skip(n: 2).first() { + ActuatorForceRestart { reset_type: _ } => true + _ => false + } +} + +test fn pxe_actuator_steps_stage_boot_restart() -> Bool { + let steps = os_install_actuator_steps( + mechanism: PxeHttpInstall, + export: srv3_nbd_proxy_export_placeholder + ) + list_length(items: steps) == 3 + && match steps.first() { + ActuatorPxeNetworkBootStaging => true + _ => false + } +} diff --git a/dsl/test/claim/srv3_os_install_witness_test.dag b/dsl/test/claim/srv3_os_install_witness_test.dag index 8a7a6f70ccf..7a7925cd021 100644 --- a/dsl/test/claim/srv3_os_install_witness_test.dag +++ b/dsl/test/claim/srv3_os_install_witness_test.dag @@ -65,7 +65,7 @@ test fn srv3_survey_forces_proxy_dhcp() -> Bool { } } -test fn srv3_plan_targets_ubuntu_2404_aarch64_pxe_once() -> Bool { +test fn srv3_plan_targets_ubuntu_2404_aarch64_nbd_proxy_boot_once() -> Bool { match srv3_os_install_plan.os_target { UbuntuOs { distro: d } => match d { @@ -74,7 +74,7 @@ test fn srv3_plan_targets_ubuntu_2404_aarch64_pxe_once() -> Bool { WindowsOs { distro: _ } => false MacosOs { distro: _ } => false } - && redfish_boot_target_wire(t: srv3_os_install_plan.bmc_boot_target) == "Pxe" + && redfish_boot_target_wire(t: srv3_os_install_plan.bmc_boot_target) == "Cd" && redfish_boot_override_enabled_wire(e: srv3_os_install_plan.bmc_boot_enabled) == "Once" && srv3_os_install_plan.network_boot_delivery.autoinstall.fully_automated && srv3_os_install_plan.network_boot_delivery.autoinstall.autoinstall_version == 1 From 186fc91cd3914d3372ae648431d1d1c6652402e2 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 1 Jul 2026 16:29:05 +0000 Subject: [PATCH 02/16] Fix NonEmptyStr comparison precedence in live BMC probe helpers. Parenthesize string-literal casts so row.locator equality parses correctly; restores compile-clean for asrock_rack and witness tests. Co-authored-by: Cursor --- dsl/extdeps/boards/asrock_rack.dag | 4 ++-- dsl/gunbc/nbd_proxy_virtual_media_install.dag | 7 ++++++- dsl/test/claim/asrock_live_probe_witness_test.dag | 4 ++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/dsl/extdeps/boards/asrock_rack.dag b/dsl/extdeps/boards/asrock_rack.dag index c8b5d3e5164..95a304ad1eb 100644 --- a/dsl/extdeps/boards/asrock_rack.dag +++ b/dsl/extdeps/boards/asrock_rack.dag @@ -149,8 +149,8 @@ fn live_probe_confirms_shipped_firmware_version() -> Bool { } fn live_probe_row_is_redfish_vm_absent(row: BmcLiveReadOnlyProbeRow) -> Bool { - if row.locator == "/redfish/v1/Systems/system/VirtualMedia" as NonEmptyStr - || row.locator == "/redfish/v1/Managers/bmc/VirtualMedia" as NonEmptyStr { + if row.locator == ("/redfish/v1/Systems/system/VirtualMedia" as NonEmptyStr) + || row.locator == ("/redfish/v1/Managers/bmc/VirtualMedia" as NonEmptyStr) { row.verdict == ProbeAbsent } else { true diff --git a/dsl/gunbc/nbd_proxy_virtual_media_install.dag b/dsl/gunbc/nbd_proxy_virtual_media_install.dag index 06fc3fa7745..97315241328 100644 --- a/dsl/gunbc/nbd_proxy_virtual_media_install.dag +++ b/dsl/gunbc/nbd_proxy_virtual_media_install.dag @@ -20,8 +20,13 @@ import extdeps.boards.asrock_rack { asrock_altrad8ud_openbmc_2_07_00_nbd_proxy_primary_endpoint, } import gunbc.os_install_mechanism { - solve_install_mechanism, + OsInstallMechanism, + VirtualMediaInstall, + NbdProxyVirtualMediaInstall, + PxeHttpInstall, + FirmwareUpdateThenVirtualMedia, srv3_install_mechanism, + install_boot_target_for, } import gunbc.host_effect { RedfishAction, diff --git a/dsl/test/claim/asrock_live_probe_witness_test.dag b/dsl/test/claim/asrock_live_probe_witness_test.dag index 69778377a34..3980a47e9e6 100644 --- a/dsl/test/claim/asrock_live_probe_witness_test.dag +++ b/dsl/test/claim/asrock_live_probe_witness_test.dag @@ -42,14 +42,14 @@ test fn live_probe_rows_cover_vm_and_nbd_get_surfaces() -> Bool { asrock_srv3_live_readonly_probe_rows, init: false, f: (acc, row) => - acc || (row.locator == "/vm/0/0 non-upgrade GET" as NonEmptyStr + acc || (row.locator == ("/vm/0/0 non-upgrade GET" as NonEmptyStr) && row.verdict == ProbeInconclusive) ) && fold( asrock_srv3_live_readonly_probe_rows, init: false, f: (acc, row) => - acc || (row.locator == "/redfish/v1/Systems/system/VirtualMedia" as NonEmptyStr + acc || (row.locator == ("/redfish/v1/Systems/system/VirtualMedia" as NonEmptyStr) && row.verdict == ProbeAbsent) ) } From 7fc72b652f792ea9cbecfdb29177c5c064900c25 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 1 Jul 2026 17:00:45 +0000 Subject: [PATCH 03/16] Import NonEmptyStr in asrock live-probe witness test. Floor-enrolled witness uses `as NonEmptyStr` casts in fold predicates; explicit import matches other witness tests and unblocks compile-clean. Co-authored-by: Cursor --- dsl/test/claim/asrock_live_probe_witness_test.dag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsl/test/claim/asrock_live_probe_witness_test.dag b/dsl/test/claim/asrock_live_probe_witness_test.dag index 3980a47e9e6..8d8cbd596a2 100644 --- a/dsl/test/claim/asrock_live_probe_witness_test.dag +++ b/dsl/test/claim/asrock_live_probe_witness_test.dag @@ -1,7 +1,7 @@ module test.claim.asrock_live_probe import std.logic { Bool } -import std.types { list_length } +import std.types { list_length, NonEmptyStr } import extdeps.bmc.capability { CapabilityVirtualMedia, CapabilityNbdProxyVirtualMedia, From 015b6ada11c1048340d0a89cce21e65a57c424ff Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 1 Jul 2026 17:03:58 +0000 Subject: [PATCH 04/16] Derive srv3 install delivery from capability solver; add OsInstallActuated spine phase. - OsInstallDelivery coproduct: VirtualMediaIso for NBD/VM paths, NetworkBoot only for PxeHttpInstall - fleet_install_server_specs empty when solver picks NbdProxy (no PXE infra bypass) - host_standup spine gains prefix:os-install-actuated gap between mechanism resolve and P0 - install_server_emit fail-closed when plan delivery is not network-boot Co-authored-by: Cursor --- dsl/extdeps/boards/asrock_rack.dag | 2 + dsl/gunbc/host_standup.dag | 35 +++++++- dsl/gunbc/install_server_emit.dag | 64 ++++++++------ dsl/gunbc/os_install.dag | 85 +++++++++++++++++-- .../claim/generated_artifact_drift_test.dag | 2 +- .../claim/host_standup_spine_witness_test.dag | 26 +++--- .../claim/srv3_os_install_witness_test.dag | 34 ++++++-- 7 files changed, 193 insertions(+), 55 deletions(-) diff --git a/dsl/extdeps/boards/asrock_rack.dag b/dsl/extdeps/boards/asrock_rack.dag index 95a304ad1eb..71c28c0da86 100644 --- a/dsl/extdeps/boards/asrock_rack.dag +++ b/dsl/extdeps/boards/asrock_rack.dag @@ -93,6 +93,8 @@ data asrock_altrad8ud_openbmc_3_06_00_capabilities: BmcFirmwareCapabilityRow = B capabilities: altrad8ud_openbmc_base_capabilities, } +data asrock_altrad8ud_openbmc_3_06_00_catalog_authority: ExternalAuthority = asrock_altrad8ud_bmc_manual_authority + data asrock_altrad8ud_1l2t_bmc_firmware_capability_catalog: List = [ asrock_altrad8ud_openbmc_2_07_00_capabilities, asrock_altrad8ud_openbmc_3_06_00_capabilities, diff --git a/dsl/gunbc/host_standup.dag b/dsl/gunbc/host_standup.dag index 0d79295d6d8..c8bdfa12b9d 100644 --- a/dsl/gunbc/host_standup.dag +++ b/dsl/gunbc/host_standup.dag @@ -23,6 +23,7 @@ import product.placement_supply { HostIdentity } type HostStandupPrefixPhase = BmcCredentialConverge | OsInstallMechanismResolved + | OsInstallActuated | HostConvergePolicyDerived type HostStandupAssimilationPhase @@ -170,6 +171,24 @@ fn decl_ref(module_path: String, decl_name: String) -> DeclarationRef { } } +fn host_standup_prefix_gap_step( + phase: HostStandupPrefixPhase, + label: NonEmptyStr, + interim_realization: String, + refusal: String, + effect_policy: Policy +) -> HostStandupStep { + HostStandupStep { + phase: Prefix { phase: phase }, + label: label, + disposition: DeclaredGap { + interim_realization: interim_realization, + refusal: refusal, + }, + effect_policy: effect_policy, + } +} + data host_standup_spine: List = [ host_standup_prefix_step( phase: BmcCredentialConverge, @@ -186,10 +205,17 @@ data host_standup_spine: List = [ authorities: [ decl_ref(module_path: "gunbc.os_install_mechanism", decl_name: "solve_install_mechanism"), decl_ref(module_path: "gunbc.os_install_mechanism", decl_name: "srv3_install_mechanism"), - decl_ref(module_path: "gunbc.nbd_proxy_virtual_media_install", decl_name: "srv3_os_install_actuator_plan"), + decl_ref(module_path: "gunbc.os_install", decl_name: "srv3_os_install_plan"), ], effect_policy: OneShotIdempotent ), + host_standup_prefix_gap_step( + phase: OsInstallActuated, + label: "prefix:os-install-actuated", + interim_realization: "gunbc.nbd_proxy_virtual_media_install.srv3_os_install_actuator_plan + seed wss+NBD server (§7); docs/runbooks/srv3-nbd-proxy-ws-upgrade-dry-run.md", + refusal: "host_standup:prefix:os-install-actuated:unexecuted — refuse assimilation until NBD-proxy install actuates and OS-up witness is green-by-execution", + effect_policy: OneShotIdempotent + ), host_standup_prefix_step( phase: HostConvergePolicyDerived, label: "prefix:host-converge", @@ -277,7 +303,12 @@ data host_standup_gap_ledger: List = [ GapLedgerRow { phase_label: "prefix:os-install-mechanism", modeling: "modeled", - authority_or_interim: "gunbc.os_install_mechanism.srv3_install_mechanism + gunbc.nbd_proxy_virtual_media_install.srv3_os_install_actuator_plan (srv3=NbdProxyVirtualMediaInstall, solve-driven actuator)" + authority_or_interim: "gunbc.os_install_mechanism.srv3_install_mechanism + gunbc.os_install.srv3_os_install_plan (solver-derived delivery + boot target)" + }, + GapLedgerRow { + phase_label: "prefix:os-install-actuated", + modeling: "gap", + authority_or_interim: "gunbc.nbd_proxy_virtual_media_install.srv3_os_install_actuator_plan — seed wss+NBD server + live ws-upgrade dry-run receipt pending (§6 gate)" }, GapLedgerRow { phase_label: "prefix:host-converge", diff --git a/dsl/gunbc/install_server_emit.dag b/dsl/gunbc/install_server_emit.dag index f32cd64f7e2..3e3cbabbb53 100644 --- a/dsl/gunbc/install_server_emit.dag +++ b/dsl/gunbc/install_server_emit.dag @@ -1,7 +1,13 @@ module gunbc.install_server_emit import std.types { String, NonEmptyStr, List } -import gunbc.os_install { InstallServerSpec } +import gunbc.os_install { + InstallServerSpec, + Srv3OsInstallPlan, + OsInstallDelivery, + NetworkBoot, + VirtualMediaIso, +} import extdeps.provisioning.network_boot { NetworkBootChain, network_uefi_bootloader_artifact } import extdeps.provisioning.dhcp_client_arch { dhcp_client_processor_architecture, @@ -24,31 +30,41 @@ fn network_address_of_cidr(cidr: String) -> String { } } +fn install_plan_network_boot_chain(plan: Srv3OsInstallPlan) -> NetworkBootChain? { + match plan.delivery { + NetworkBoot { delivery: d } => Present { value: d.chain } + VirtualMediaIso { autoinstall: _ } => Absent + } +} + fn install_server_boot_steering(spec: InstallServerSpec) -> List { let arch = spec.plan.host_architecture - let chain = spec.plan.network_boot_delivery.chain - match dhcp_client_processor_architecture(arch: arch, mode: chain.boot_mode) { - Absent => [DnsmasqComment { text: "no UEFI HTTP-boot client-arch mapped for this host architecture and boot-mode" }] - Present { value: pa } => - match network_uefi_bootloader_artifact(arch: arch) { - Absent => [DnsmasqComment { text: "no UEFI network bootloader artifact for this host architecture" }] - Present { value: loader } => - [ - DnsmasqDhcpMatchSet { - tag: install_server_boot_tag, - option: "option:client-arch", - value: to_string(dhcp_processor_architecture_code(a: pa)), - }, - DnsmasqDhcpOptionForceTag { - tag: install_server_boot_tag, - option: "60", - value: "HTTPClient", - }, - DnsmasqDhcpBootTag { - tag: install_server_boot_tag, - target: concat(uri_wire(uri: chain.image_http_root), loader), - }, - ] + match install_plan_network_boot_chain(plan: spec.plan) { + Absent => [DnsmasqComment { text: "install-server emit refused: plan delivery is not network-boot (solver chose virtual-media path)" }] + Present { value: chain } => + match dhcp_client_processor_architecture(arch: arch, mode: chain.boot_mode) { + Absent => [DnsmasqComment { text: "no UEFI HTTP-boot client-arch mapped for this host architecture and boot-mode" }] + Present { value: pa } => + match network_uefi_bootloader_artifact(arch: arch) { + Absent => [DnsmasqComment { text: "no UEFI network bootloader artifact for this host architecture" }] + Present { value: loader } => + [ + DnsmasqDhcpMatchSet { + tag: install_server_boot_tag, + option: "option:client-arch", + value: to_string(dhcp_processor_architecture_code(a: pa)), + }, + DnsmasqDhcpOptionForceTag { + tag: install_server_boot_tag, + option: "60", + value: "HTTPClient", + }, + DnsmasqDhcpBootTag { + tag: install_server_boot_tag, + target: concat(uri_wire(uri: chain.image_http_root), loader), + }, + ] + } } } } diff --git a/dsl/gunbc/os_install.dag b/dsl/gunbc/os_install.dag index f2acc6fc01a..755a6154071 100644 --- a/dsl/gunbc/os_install.dag +++ b/dsl/gunbc/os_install.dag @@ -27,13 +27,47 @@ import extdeps.bmc.types { RedfishResetType, ResetForceRestart, } -import gunbc.os_install_mechanism { srv3_install_boot_target } +import gunbc.os_install_mechanism { + srv3_install_boot_target, + srv3_install_mechanism, + install_boot_target_for, + OsInstallMechanism, + NbdProxyVirtualMediaInstall, + PxeHttpInstall, + VirtualMediaInstall, + FirmwareUpdateThenVirtualMedia, +} type NetworkBootDelivery { chain: NetworkBootChain autoinstall: UbuntuAutoinstallPayload } +type OsInstallDelivery + = NetworkBoot { delivery: NetworkBootDelivery } + | VirtualMediaIso { autoinstall: UbuntuAutoinstallPayload } + +fn os_install_delivery_for(mechanism: OsInstallMechanism, pxe_chain: NetworkBootChain, autoinstall: UbuntuAutoinstallPayload) -> OsInstallDelivery { + match mechanism { + NbdProxyVirtualMediaInstall => VirtualMediaIso { autoinstall: autoinstall } + VirtualMediaInstall => VirtualMediaIso { autoinstall: autoinstall } + FirmwareUpdateThenVirtualMedia => VirtualMediaIso { autoinstall: autoinstall } + PxeHttpInstall => NetworkBoot { + delivery: NetworkBootDelivery { + chain: pxe_chain, + autoinstall: autoinstall, + }, + } + } +} + +fn os_install_delivery_is_network_boot(delivery: OsInstallDelivery) -> Bool { + match delivery { + NetworkBoot { delivery: _ } => true + VirtualMediaIso { autoinstall: _ } => false + } +} + type LanInstallInfraSurvey { existing_dhcp_configurable: Bool dedicated_install_server_present: Bool @@ -51,18 +85,25 @@ type Srv3OsInstallPlan { os_target: OperatingSystemProduct host_architecture: Architecture lan_survey: LanInstallInfraSurvey - network_boot_delivery: NetworkBootDelivery + delivery: OsInstallDelivery bmc_boot_target: RedfishBootSourceOverrideTarget bmc_boot_enabled: RedfishBootSourceOverrideEnabled post_stage_reset: RedfishResetType } +fn srv3_os_install_plan_autoinstall(plan: Srv3OsInstallPlan) -> UbuntuAutoinstallPayload { + match plan.delivery { + NetworkBoot { delivery: d } => d.autoinstall + VirtualMediaIso { autoinstall: a } => a + } +} + data srv3_lan_install_survey: LanInstallInfraSurvey = LanInstallInfraSurvey { existing_dhcp_configurable: false, dedicated_install_server_present: false, } -data srv3_network_boot_chain: NetworkBootChain = NetworkBootChain { +data srv3_pxe_network_boot_chain: NetworkBootChain = NetworkBootChain { boot_mode: UefiHttpBoot, dhcp_direction: survey_implied_dhcp_direction(survey: srv3_lan_install_survey), image_http_root: Uri { @@ -101,15 +142,30 @@ data srv3_os_install_plan: Srv3OsInstallPlan = Srv3OsInstallPlan { os_target: UbuntuOs { distro: NobleNumbat2404Lts }, host_architecture: Aarch64, lan_survey: srv3_lan_install_survey, - network_boot_delivery: NetworkBootDelivery { - chain: srv3_network_boot_chain, - autoinstall: srv3_ubuntu_autoinstall, - }, + delivery: os_install_delivery_for( + mechanism: srv3_install_mechanism, + pxe_chain: srv3_pxe_network_boot_chain, + autoinstall: srv3_ubuntu_autoinstall + ), bmc_boot_target: srv3_install_boot_target, bmc_boot_enabled: BootOverrideOnce, post_stage_reset: ResetForceRestart, } +data srv3_pxe_os_install_plan: Srv3OsInstallPlan = Srv3OsInstallPlan { + os_target: UbuntuOs { distro: NobleNumbat2404Lts }, + host_architecture: Aarch64, + lan_survey: srv3_lan_install_survey, + delivery: os_install_delivery_for( + mechanism: PxeHttpInstall, + pxe_chain: srv3_pxe_network_boot_chain, + autoinstall: srv3_ubuntu_autoinstall + ), + bmc_boot_target: install_boot_target_for(mechanism: PxeHttpInstall), + bmc_boot_enabled: BootOverrideOnce, + post_stage_reset: ResetForceRestart, +} + type HostAutoinstall { host: HostIdentity payload: UbuntuAutoinstallPayload @@ -128,10 +184,21 @@ type InstallServerSpec { serving_segment: NetworkSegment } +fn fleet_install_server_specs_for(mechanism: OsInstallMechanism) -> List { + match mechanism { + PxeHttpInstall => [srv3_install_server_spec] + NbdProxyVirtualMediaInstall => [] + VirtualMediaInstall => [] + FirmwareUpdateThenVirtualMedia => [] + } +} + data srv3_install_server_spec: InstallServerSpec = InstallServerSpec { host: operator_host_srv3, - plan: srv3_os_install_plan, + plan: srv3_pxe_os_install_plan, serving_segment: operator_lan_segment, } -data fleet_install_server_specs: List = [srv3_install_server_spec] +data fleet_install_server_specs: List = fleet_install_server_specs_for( + mechanism: srv3_install_mechanism +) diff --git a/dsl/test/claim/generated_artifact_drift_test.dag b/dsl/test/claim/generated_artifact_drift_test.dag index 7f80db78b70..126df5d9117 100644 --- a/dsl/test/claim/generated_artifact_drift_test.dag +++ b/dsl/test/claim/generated_artifact_drift_test.dag @@ -67,8 +67,8 @@ fn witness_registry_complete() -> Bool { && registry_contains(a: RunnerDeployArtifact) && registry_contains(a: FleetConvergeArtifact) && registry_contains(a: AutoinstallUserDataArtifact { spec: srv3_autoinstall_spec }) - && registry_contains(a: ProxyDhcpDnsmasqArtifact { spec: srv3_install_server_spec }) && registry_contains(a: PlanArtifact { plan: idea_machine_plan }) + && fleet_install_server_specs.length() == 0 && artifact_is_committed(a: CiYamlArtifact) && artifact_is_committed(a: GitignoreArtifact) && artifact_is_committed(a: RoadmapArtifact) diff --git a/dsl/test/claim/host_standup_spine_witness_test.dag b/dsl/test/claim/host_standup_spine_witness_test.dag index 79d2067021c..898bab7de49 100644 --- a/dsl/test/claim/host_standup_spine_witness_test.dag +++ b/dsl/test/claim/host_standup_spine_witness_test.dag @@ -41,6 +41,7 @@ import gunbc.host_standup { Assimilation, BmcCredentialConverge, OsInstallMechanismResolved, + OsInstallActuated, HostConvergePolicyDerived, ReachSecretsNetwork, RunnerDeploySlot, @@ -64,13 +65,14 @@ fn spine_step_tag(step: HostStandupStep) -> Int { match step.phase { Prefix { phase: BmcCredentialConverge } => 0 Prefix { phase: OsInstallMechanismResolved } => 1 - Prefix { phase: HostConvergePolicyDerived } => 2 - Assimilation { phase: ReachSecretsNetwork } => 3 - Assimilation { phase: RunnerDeploySlot } => 4 - Assimilation { phase: ComputeFabricEnroll } => 5 - Assimilation { phase: GreenPlaceReadiness } => 6 - Assimilation { phase: SessionPlacementEnrollment } => 7 - Assimilation { phase: StandupAssimilationComplete } => 8 + Prefix { phase: OsInstallActuated } => 2 + Prefix { phase: HostConvergePolicyDerived } => 3 + Assimilation { phase: ReachSecretsNetwork } => 4 + Assimilation { phase: RunnerDeploySlot } => 5 + Assimilation { phase: ComputeFabricEnroll } => 6 + Assimilation { phase: GreenPlaceReadiness } => 7 + Assimilation { phase: SessionPlacementEnrollment } => 8 + Assimilation { phase: StandupAssimilationComplete } => 9 } } @@ -90,11 +92,11 @@ fn spine_tags_are_linear() -> Bool { ).ok } -test fn host_standup_spine_is_nine_step_linear_prefix_then_p0_p5() -> Bool { - host_standup_spine_length() == 9 +test fn host_standup_spine_is_ten_step_linear_prefix_then_p0_p5() -> Bool { + host_standup_spine_length() == 10 && spine_tags_are_linear() - && host_standup_gap_count() == 3 - && host_standup_gap_ledger.length() == 9 + && host_standup_gap_count() == 4 + && host_standup_gap_ledger.length() == 10 } test fn srv3_target_and_install_mechanism_are_grounded() -> Bool { @@ -196,7 +198,7 @@ test fn p3_green_place_contributes_via_gunbc_gate_after_pinned_tree_lands() -> B test fn assimilation_gate_still_refuses_p0_p2_p4_gaps_when_p3_landed() -> Bool { host_standup_p3_gunbc_pinned_tree_landed - && host_standup_gap_count() == 3 + && host_standup_gap_count() == 4 && match assimilation_complete(gate: host_standup_default_assimilation_gate_unsatisfied()) { AssimilationIncomplete { input: NodeRegistered, reason: r } => string_contains(s: r, pattern: "P5:gap-refused:") diff --git a/dsl/test/claim/srv3_os_install_witness_test.dag b/dsl/test/claim/srv3_os_install_witness_test.dag index 7a7925cd021..dc373acb119 100644 --- a/dsl/test/claim/srv3_os_install_witness_test.dag +++ b/dsl/test/claim/srv3_os_install_witness_test.dag @@ -18,9 +18,16 @@ import extdeps.os.ubuntu_autoinstall { import gunbc.os_install { LanInstallInfraSurvey, survey_implied_dhcp_direction, - srv3_network_boot_chain, + srv3_pxe_network_boot_chain, srv3_os_install_plan, + OsInstallDelivery, + NetworkBoot, + VirtualMediaIso, + os_install_delivery_is_network_boot, + srv3_os_install_plan_autoinstall, + fleet_install_server_specs, } +import gunbc.os_install_mechanism { NbdProxyVirtualMediaInstall, srv3_install_mechanism } import extdeps.bmc.types { redfish_boot_target_wire, redfish_boot_override_enabled_wire, @@ -41,8 +48,8 @@ test fn srv3_bootloader_artifact_is_arch_grounded() -> Bool { } } -test fn srv3_chain_is_uefi_http_boot() -> Bool { - match srv3_network_boot_chain.boot_mode { +test fn srv3_pxe_chain_is_uefi_http_boot_when_modeled() -> Bool { + match srv3_pxe_network_boot_chain.boot_mode { UefiHttpBoot => true LegacyBiosPxe { tftp_root: _ } => false UefiPxe { tftp_root: _ } => false @@ -50,7 +57,7 @@ test fn srv3_chain_is_uefi_http_boot() -> Bool { } test fn srv3_survey_forces_proxy_dhcp() -> Bool { - match srv3_network_boot_chain.dhcp_direction { + match srv3_pxe_network_boot_chain.dhcp_direction { ProxyDhcp => true NativeDhcpBootOptions => false } @@ -65,6 +72,19 @@ test fn srv3_survey_forces_proxy_dhcp() -> Bool { } } +test fn srv3_plan_is_solver_derived_nbd_proxy_virtual_media() -> Bool { + match srv3_install_mechanism { + NbdProxyVirtualMediaInstall => true + _ => false + } + && !os_install_delivery_is_network_boot(delivery: srv3_os_install_plan.delivery) + && match srv3_os_install_plan.delivery { + VirtualMediaIso { autoinstall: _ } => true + NetworkBoot { delivery: _ } => false + } + && fleet_install_server_specs.length() == 0 +} + test fn srv3_plan_targets_ubuntu_2404_aarch64_nbd_proxy_boot_once() -> Bool { match srv3_os_install_plan.os_target { UbuntuOs { distro: d } => @@ -76,9 +96,9 @@ test fn srv3_plan_targets_ubuntu_2404_aarch64_nbd_proxy_boot_once() -> Bool { } && redfish_boot_target_wire(t: srv3_os_install_plan.bmc_boot_target) == "Cd" && redfish_boot_override_enabled_wire(e: srv3_os_install_plan.bmc_boot_enabled) == "Once" - && srv3_os_install_plan.network_boot_delivery.autoinstall.fully_automated - && srv3_os_install_plan.network_boot_delivery.autoinstall.autoinstall_version == 1 - && match srv3_os_install_plan.network_boot_delivery.autoinstall.delivery { + && srv3_os_install_plan_autoinstall(plan: srv3_os_install_plan).fully_automated + && srv3_os_install_plan_autoinstall(plan: srv3_os_install_plan).autoinstall_version == 1 + && match srv3_os_install_plan_autoinstall(plan: srv3_os_install_plan).delivery { NoCloudNet { seed_url: _ } => true NoCloudLocal => false } From 69a699c6d0c88897ffaa102e001be59ce3ecd81c Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 1 Jul 2026 17:06:07 +0000 Subject: [PATCH 05/16] Ground nbd-proxy ws-upgrade dry-run receipt from live srv3 BMC. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Python raw TLS WebSocket upgrade on 192.168.1.192: /vm/0/0 → 101 (PRESENT), /nbd/0 → 404 (ABSENT). Receipt rows + witnesses updated; §6(1) satisfied, seed remains gated on §6(2) operator sign-off. Co-authored-by: Cursor --- dsl/extdeps/boards/asrock_rack.dag | 37 +++++++++++++++++++ dsl/gunbc/host_standup.dag | 4 +- .../claim/asrock_live_probe_witness_test.dag | 17 +++++---- 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/dsl/extdeps/boards/asrock_rack.dag b/dsl/extdeps/boards/asrock_rack.dag index 71c28c0da86..fbbd81a3a53 100644 --- a/dsl/extdeps/boards/asrock_rack.dag +++ b/dsl/extdeps/boards/asrock_rack.dag @@ -137,11 +137,21 @@ data asrock_srv3_live_readonly_probe_rows: List = [ verdict: ProbeInconclusive, probed_at: "2026-07-01", }, + BmcLiveReadOnlyProbeRow { + locator: "/vm/0/0 ws-upgrade", + verdict: ProbePresent, + probed_at: "2026-07-01", + }, BmcLiveReadOnlyProbeRow { locator: "/nbd/0 non-upgrade GET", verdict: ProbeInconclusive, probed_at: "2026-07-01", }, + BmcLiveReadOnlyProbeRow { + locator: "/nbd/0 ws-upgrade", + verdict: ProbeAbsent, + probed_at: "2026-07-01", + }, ] data asrock_srv3_live_probe_firmware_version: NonEmptyStr = "2.07.00" @@ -166,3 +176,30 @@ fn live_probe_confirms_redfish_virtual_media_absent() -> Bool { f: (acc, row) => acc && live_probe_row_is_redfish_vm_absent(row: row) ) } + +fn live_probe_row_matches(locator: NonEmptyStr, verdict: BmcReadOnlyProbeVerdict, row: BmcLiveReadOnlyProbeRow) -> Bool { + row.locator == locator && row.verdict == verdict +} + +fn live_probe_has_ws_upgrade_row(locator: NonEmptyStr, verdict: BmcReadOnlyProbeVerdict) -> Bool { + fold( + asrock_srv3_live_readonly_probe_rows, + init: false, + f: (acc, row) => acc || live_probe_row_matches(locator: locator, verdict: verdict, row: row) + ) +} + +fn live_probe_confirms_nbd_proxy_vm_slot_ws_present() -> Bool { + live_probe_has_ws_upgrade_row( + locator: "/vm/0/0 ws-upgrade" as NonEmptyStr, + verdict: ProbePresent + ) + && asrock_altrad8ud_openbmc_2_07_00_nbd_proxy_primary_endpoint == VmSlotIndexEndpoint +} + +fn live_probe_confirms_nbd_slot_ws_absent() -> Bool { + live_probe_has_ws_upgrade_row( + locator: "/nbd/0 ws-upgrade" as NonEmptyStr, + verdict: ProbeAbsent + ) +} diff --git a/dsl/gunbc/host_standup.dag b/dsl/gunbc/host_standup.dag index c8bdfa12b9d..81f00eadbbc 100644 --- a/dsl/gunbc/host_standup.dag +++ b/dsl/gunbc/host_standup.dag @@ -212,7 +212,7 @@ data host_standup_spine: List = [ host_standup_prefix_gap_step( phase: OsInstallActuated, label: "prefix:os-install-actuated", - interim_realization: "gunbc.nbd_proxy_virtual_media_install.srv3_os_install_actuator_plan + seed wss+NBD server (§7); docs/runbooks/srv3-nbd-proxy-ws-upgrade-dry-run.md", + interim_realization: "gunbc.nbd_proxy_virtual_media_install.srv3_os_install_actuator_plan + extdeps.boards.asrock_rack live ws-upgrade receipt (§6(1) PRESENT /vm/0/0); seed wss+NBD server pending §6(2) sign-off", refusal: "host_standup:prefix:os-install-actuated:unexecuted — refuse assimilation until NBD-proxy install actuates and OS-up witness is green-by-execution", effect_policy: OneShotIdempotent ), @@ -308,7 +308,7 @@ data host_standup_gap_ledger: List = [ GapLedgerRow { phase_label: "prefix:os-install-actuated", modeling: "gap", - authority_or_interim: "gunbc.nbd_proxy_virtual_media_install.srv3_os_install_actuator_plan — seed wss+NBD server + live ws-upgrade dry-run receipt pending (§6 gate)" + authority_or_interim: "gunbc.nbd_proxy_virtual_media_install.srv3_os_install_actuator_plan — §6(1) ws-upgrade dry-run PRESENT on /vm/0/0 (2026-07-01); seed wss+NBD server blocked on §6(2) operator sign-off" }, GapLedgerRow { phase_label: "prefix:host-converge", diff --git a/dsl/test/claim/asrock_live_probe_witness_test.dag b/dsl/test/claim/asrock_live_probe_witness_test.dag index 8d8cbd596a2..66e06c545a1 100644 --- a/dsl/test/claim/asrock_live_probe_witness_test.dag +++ b/dsl/test/claim/asrock_live_probe_witness_test.dag @@ -12,11 +12,16 @@ import extdeps.boards.asrock_rack { asrock_srv3_live_probe_firmware_version, asrock_srv3_live_readonly_probe_rows, asrock_altrad8ud_shipped_bmc_firmware_capabilities, + asrock_altrad8ud_openbmc_2_07_00_nbd_proxy_primary_endpoint, live_probe_confirms_shipped_firmware_version, live_probe_confirms_redfish_virtual_media_absent, + live_probe_confirms_nbd_proxy_vm_slot_ws_present, + live_probe_confirms_nbd_slot_ws_absent, ProbeAbsent, ProbeInconclusive, + ProbePresent, } +import extdeps.bmc.webui.nbd_proxy { VmSlotIndexEndpoint } test fn srv3_live_bmc_host_is_grounded() -> Bool { asrock_srv3_live_bmc_host == "192.168.1.192" @@ -37,14 +42,10 @@ test fn live_probe_redfish_virtual_media_absent_matches_catalog() -> Bool { } test fn live_probe_rows_cover_vm_and_nbd_get_surfaces() -> Bool { - list_length(items: asrock_srv3_live_readonly_probe_rows) == 5 - && fold( - asrock_srv3_live_readonly_probe_rows, - init: false, - f: (acc, row) => - acc || (row.locator == ("/vm/0/0 non-upgrade GET" as NonEmptyStr) - && row.verdict == ProbeInconclusive) - ) + list_length(items: asrock_srv3_live_readonly_probe_rows) == 7 + && live_probe_confirms_nbd_proxy_vm_slot_ws_present() + && live_probe_confirms_nbd_slot_ws_absent() + && asrock_altrad8ud_openbmc_2_07_00_nbd_proxy_primary_endpoint == VmSlotIndexEndpoint && fold( asrock_srv3_live_readonly_probe_rows, init: false, From 1e946e669ce5c6fb09356e4c874820e7142690a5 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 1 Jul 2026 17:21:49 +0000 Subject: [PATCH 06/16] WIP: srv3 greenfield OS-install: solve-driven NBD-proxy actuator + dissolve B --- dsl/extdeps/package_managers/apt.dag | 4 ++++ dsl/extdeps/tools/curl.dag | 23 +++++++++++++++++++++++ dsl/extdeps/tools/nbdkit.dag | 23 +++++++++++++++++++++++ dsl/extdeps/tools/socat.dag | 23 +++++++++++++++++++++++ dsl/extdeps/tools/websocat.dag | 23 +++++++++++++++++++++++ 5 files changed, 96 insertions(+) create mode 100644 dsl/extdeps/tools/curl.dag create mode 100644 dsl/extdeps/tools/nbdkit.dag create mode 100644 dsl/extdeps/tools/socat.dag create mode 100644 dsl/extdeps/tools/websocat.dag diff --git a/dsl/extdeps/package_managers/apt.dag b/dsl/extdeps/package_managers/apt.dag index 17b79bf50e2..4ccb4cdfc24 100644 --- a/dsl/extdeps/package_managers/apt.dag +++ b/dsl/extdeps/package_managers/apt.dag @@ -24,6 +24,10 @@ data package_rustc: NonEmptyStr = "rustc" data package_systemd_oomd_defaults: NonEmptyStr = "systemd-oomd-defaults" data package_tailscale: NonEmptyStr = "tailscale" data package_nodejs: NonEmptyStr = "nodejs" +data package_curl: NonEmptyStr = "curl" +data package_nbdkit: NonEmptyStr = "nbdkit" +data package_socat: NonEmptyStr = "socat" +data package_websocat: NonEmptyStr = "websocat" data apt_man_url: NonEmptyStr = "https://manpages.debian.org/testing/apt/apt.8.en.html" diff --git a/dsl/extdeps/tools/curl.dag b/dsl/extdeps/tools/curl.dag new file mode 100644 index 00000000000..0ece41c3eaa --- /dev/null +++ b/dsl/extdeps/tools/curl.dag @@ -0,0 +1,23 @@ +module extdeps.tools.curl + +import std.types { NonEmptyStr } +import extdeps.version { VersionConstraint } +import extdeps.external_authority { ExternalAuthority } +import extdeps.uri { Uri, Https } +import extdeps.tools { CliTool, SourceApt } +import extdeps.apt { package_curl } + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "curl.se/docs/manpage.html" + } +} + +data curl_pinned_version: VersionConstraint = ">= 7.68" + +data curl_cli_tool: CliTool = CliTool { + name: "curl", + min_version: Present { value: curl_pinned_version }, + installable_via: [SourceApt { package: package_curl }], +} diff --git a/dsl/extdeps/tools/nbdkit.dag b/dsl/extdeps/tools/nbdkit.dag new file mode 100644 index 00000000000..dcfff67a425 --- /dev/null +++ b/dsl/extdeps/tools/nbdkit.dag @@ -0,0 +1,23 @@ +module extdeps.tools.nbdkit + +import std.types { NonEmptyStr } +import extdeps.version { VersionConstraint } +import extdeps.external_authority { ExternalAuthority } +import extdeps.uri { Uri, Https } +import extdeps.tools { CliTool, SourceApt } +import extdeps.apt { package_nbdkit } + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "libguestfs.org/nbdkit.1.html" + } +} + +data nbdkit_pinned_version: VersionConstraint = ">= 1.34" + +data nbdkit_cli_tool: CliTool = CliTool { + name: "nbdkit", + min_version: Present { value: nbdkit_pinned_version }, + installable_via: [SourceApt { package: package_nbdkit }], +} diff --git a/dsl/extdeps/tools/socat.dag b/dsl/extdeps/tools/socat.dag new file mode 100644 index 00000000000..bfaa2c706c4 --- /dev/null +++ b/dsl/extdeps/tools/socat.dag @@ -0,0 +1,23 @@ +module extdeps.tools.socat + +import std.types { NonEmptyStr } +import extdeps.version { VersionConstraint } +import extdeps.external_authority { ExternalAuthority } +import extdeps.uri { Uri, Https } +import extdeps.tools { CliTool, SourceApt } +import extdeps.apt { package_socat } + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "www.dest-unreach.org/socat/doc/socat.html" + } +} + +data socat_pinned_version: VersionConstraint = ">= 1.7.4" + +data socat_cli_tool: CliTool = CliTool { + name: "socat", + min_version: Present { value: socat_pinned_version }, + installable_via: [SourceApt { package: package_socat }], +} diff --git a/dsl/extdeps/tools/websocat.dag b/dsl/extdeps/tools/websocat.dag new file mode 100644 index 00000000000..59bb1ec6115 --- /dev/null +++ b/dsl/extdeps/tools/websocat.dag @@ -0,0 +1,23 @@ +module extdeps.tools.websocat + +import std.types { NonEmptyStr } +import extdeps.version { VersionConstraint } +import extdeps.external_authority { ExternalAuthority } +import extdeps.uri { Uri, Https } +import extdeps.tools { CliTool, SourceApt } +import extdeps.apt { package_websocat } + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "github.com/vi/websocat/releases/tag/v1.14.0" + } +} + +data websocat_pinned_version: VersionConstraint = ">= 1.12" + +data websocat_cli_tool: CliTool = CliTool { + name: "websocat", + min_version: Present { value: websocat_pinned_version }, + installable_via: [SourceApt { package: package_websocat }], +} From 64331ae6ecc422b615c60b9bb1ac465e5e182ddd Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 1 Jul 2026 17:32:01 +0000 Subject: [PATCH 07/16] WIP: srv3 greenfield OS-install: solve-driven NBD-proxy actuator + dissolve B --- .../srv3-nbd-proxy-ws-upgrade-dry-run.md | 8 +- .../bmc/os_install_actuator_toolchain.dag | 61 +++++++++++ dsl/extdeps/bmc/webui/nbd_proxy.dag | 11 +- dsl/extdeps/bmc/webui/nbd_proxy_serve.dag | 101 ++++++++++++++++++ dsl/gunbc/host_standup.dag | 4 +- dsl/gunbc/nbd_proxy_virtual_media_install.dag | 43 +++++++- ...nbd_proxy_serve_transport_witness_test.dag | 46 ++++++++ ...oxy_virtual_media_install_witness_test.dag | 29 +++++ ..._actuator_toolchain_grant_witness_test.dag | 42 ++++++++ 9 files changed, 335 insertions(+), 10 deletions(-) create mode 100644 dsl/extdeps/bmc/os_install_actuator_toolchain.dag create mode 100644 dsl/extdeps/bmc/webui/nbd_proxy_serve.dag create mode 100644 dsl/test/claim/nbd_proxy_serve_transport_witness_test.dag create mode 100644 dsl/test/claim/os_install_actuator_toolchain_grant_witness_test.dag diff --git a/docs/runbooks/srv3-nbd-proxy-ws-upgrade-dry-run.md b/docs/runbooks/srv3-nbd-proxy-ws-upgrade-dry-run.md index d138e616b7a..2c2390bbb13 100644 --- a/docs/runbooks/srv3-nbd-proxy-ws-upgrade-dry-run.md +++ b/docs/runbooks/srv3-nbd-proxy-ws-upgrade-dry-run.md @@ -48,7 +48,7 @@ TOKEN=$( curl -sk -X POST "https://${BMC_HOST}/login" \ -H 'Content-Type: application/json' \ -d '{"username":"root","password":"0penBmc"}' \ - | jq -r '.data // empty' + | jq -r '.token // empty' ) test -n "$TOKEN" || { echo "FAIL: no session token from /login"; exit 1; } @@ -66,7 +66,7 @@ acceptable for this gate; step 4 ensures no media is mounted. # Primary endpoint (cited in nbd_proxy.dag: /nbd/{slot}, slot=0). # websocat: exit after 5s idle or first inbound frame; do NOT send NBD server greeting. timeout 8 websocat -v \ - -H "X-Auth-Token: ${TOKEN}" \ + -H "Authorization: Token ${TOKEN}" \ "wss://${BMC_HOST}/nbd/0" \ 2>&1 | tee /tmp/srv3-nbd0-ws-dry-run.log ``` @@ -75,7 +75,7 @@ Alternate with `wscat`: ```bash timeout 8 wscat -c "wss://${BMC_HOST}/nbd/0" \ - -H "X-Auth-Token: ${TOKEN}" \ + -H "Authorization: Token ${TOKEN}" \ 2>&1 | tee /tmp/srv3-nbd0-ws-dry-run.log ``` @@ -105,7 +105,7 @@ Only if step 2 is inconclusive. Same constraints — upgrade only, no export ser ```bash timeout 8 websocat -v \ - -H "X-Auth-Token: ${TOKEN}" \ + -H "Authorization: Token ${TOKEN}" \ "wss://${BMC_HOST}/vm/0/0" \ 2>&1 | tee /tmp/srv3-vm00-ws-dry-run.log ``` diff --git a/dsl/extdeps/bmc/os_install_actuator_toolchain.dag b/dsl/extdeps/bmc/os_install_actuator_toolchain.dag new file mode 100644 index 00000000000..09955e6b487 --- /dev/null +++ b/dsl/extdeps/bmc/os_install_actuator_toolchain.dag @@ -0,0 +1,61 @@ +module extdeps.bmc.os_install_actuator_toolchain + +import std.types { NonEmptyStr, List } +import extdeps.tools { CliTool } +import extdeps.tools.curl { curl_cli_tool } +import extdeps.tools.websocat { websocat_cli_tool } +import extdeps.tools.nbdkit { nbdkit_cli_tool } +import extdeps.tools.socat { socat_cli_tool } +import extdeps.external_authority { ExternalAuthority } +import extdeps.uri { Uri, Https } + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "github.com/gunb-ai/gunbc/tree/main/dsl/extdeps/bmc/os_install_actuator_toolchain.dag" + } +} + +type ActuatorHostToolchainGrant { + host_label: NonEmptyStr + required_tools: List +} + +data srv3_os_install_actuator_host_label: NonEmptyStr = "install-actuator-host" + +data srv3_os_install_actuator_toolchain_grant: ActuatorHostToolchainGrant = ActuatorHostToolchainGrant { + host_label: srv3_os_install_actuator_host_label, + required_tools: [ + curl_cli_tool, + websocat_cli_tool, + nbdkit_cli_tool, + socat_cli_tool, + ], +} + +fn actuator_toolchain_grant_includes_tool(grant: ActuatorHostToolchainGrant, tool_name: NonEmptyStr) -> Bool { + fold( + grant.required_tools, + init: false, + f: (acc, tool) => acc || tool.name == tool_name + ) +} + +fn srv3_os_install_actuator_toolchain_is_complete() -> Bool { + actuator_toolchain_grant_includes_tool( + grant: srv3_os_install_actuator_toolchain_grant, + tool_name: curl_cli_tool.name + ) + && actuator_toolchain_grant_includes_tool( + grant: srv3_os_install_actuator_toolchain_grant, + tool_name: websocat_cli_tool.name + ) + && actuator_toolchain_grant_includes_tool( + grant: srv3_os_install_actuator_toolchain_grant, + tool_name: nbdkit_cli_tool.name + ) + && actuator_toolchain_grant_includes_tool( + grant: srv3_os_install_actuator_toolchain_grant, + tool_name: socat_cli_tool.name + ) +} diff --git a/dsl/extdeps/bmc/webui/nbd_proxy.dag b/dsl/extdeps/bmc/webui/nbd_proxy.dag index 00de89ca067..32f665b8771 100644 --- a/dsl/extdeps/bmc/webui/nbd_proxy.dag +++ b/dsl/extdeps/bmc/webui/nbd_proxy.dag @@ -1,6 +1,6 @@ module extdeps.bmc.webui.nbd_proxy -import std.types { Int, List, Bool } +import std.types { Int, List, Bool, NonEmptyStr } import std.measure { ByteSize } import std.http_path { PathTemplate, LiteralToken, ParamToken } import extdeps.external_authority { ExternalAuthority } @@ -184,6 +184,15 @@ fn bmcweb_nbd_proxy_endpoint_path_template(variant: BmcwebNbdProxyEndpointVarian } } +fn bmcweb_nbd_proxy_ws_path(variant: BmcwebNbdProxyEndpointVariant, slot: Int, index: Int) -> NonEmptyStr { + match variant { + NbdSlotEndpoint => + concat("/nbd/", to_string(slot)) as NonEmptyStr + VmSlotIndexEndpoint => + concat("/vm/", concat(to_string(slot), concat("/", to_string(index)))) as NonEmptyStr + } +} + type BmcwebNbdProxyWsUpgrade { endpoint: BmcwebNbdProxyEndpointVariant requires_upgrade_header: Bool diff --git a/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag b/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag new file mode 100644 index 00000000000..af72d7fee18 --- /dev/null +++ b/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag @@ -0,0 +1,101 @@ +module extdeps.bmc.webui.nbd_proxy_serve + +import std.types { NonEmptyStr, Int, String, Secret } +import std.logic { Bool } +import extdeps.external_authority { ExternalAuthority } +import extdeps.uri { Uri, Https } +import extdeps.tools.curl { curl_cli_tool } +import extdeps.tools.websocat { websocat_cli_tool } +import extdeps.tools.nbdkit { nbdkit_cli_tool } +import extdeps.tools.socat { socat_cli_tool } +import extdeps.bmc.webui.nbd_proxy { + extdeps_external_authority_anchor, + nbd_proxy_role_assignment, + NbdServer, +} + +data nbd_proxy_serve_transport_authority: ExternalAuthority = extdeps_external_authority_anchor + +data bmcweb_session_token_env_ref: NonEmptyStr = "${BMCWEB_SESSION_TOKEN}" + +data srv3_nbd_proxy_local_port: Int = 10809 + +fn nbd_proxy_serve_shell_script( + bmc_host: NonEmptyStr, + session_token: NonEmptyStr, + ws_path: NonEmptyStr, + iso_path: NonEmptyStr, + local_nbd_port: Int, +) -> String { + let port = to_string(local_nbd_port) + concat( + "set -euo pipefail; ", + nbdkit_cli_tool.name, " file file=", iso_path, " port=", port, " rdonly=yes & ", + "NBDKIT_PID=$!; trap 'kill \"$NBDKIT_PID\"' EXIT INT TERM; ", + websocat_cli_tool.name, " -b --insecure ", + "-H 'Authorization: Token ", session_token, "' ", + "'wss://", bmc_host, ws_path, "' ", + "exec:'", socat_cli_tool.name, " STDIO TCP:127.0.0.1:", port, "'" + ) +} + +service bmc.webui.BmcwebSession { + operation Login { + input { bmc_host: NonEmptyStr, username: NonEmptyStr, password: Secret } + output { body: String from "stdout", success: Bool from "exit_success" } + readonly + transport shell { + argv: [ + curl_cli_tool.name, + "-sk", + "-X", "POST", + "https://{bmc_host}/login", + "-H", "Content-Type: application/json", + "-d", "{\"username\":\"{username}\",\"password\":\"{password}\"}", + ] + } + exit { + 0 => Unit + nonzero => String "bmcweb session login POST failed" + } + } +} + +service bmc.webui.NbdProxyServe { + operation ServeReadOnlyExport { + input { serve_script: String } + output { + exit_code: Int from "exit_code" + success: Bool from "exit_success" + stdout: String from "stdout" + stderr: String from "stderr" + } + transport shell { + argv: ["sh", "-c", "{serve_script}"] + } + exit { + 0 => Unit + nonzero => String "nbd-proxy serve pipeline failed" + } + } +} + +fn nbd_proxy_serve_operation_script( + bmc_host: NonEmptyStr, + session_token: NonEmptyStr, + ws_path: NonEmptyStr, + iso_path: NonEmptyStr, + local_nbd_port: Int, +) -> String { + nbd_proxy_serve_shell_script( + bmc_host: bmc_host, + session_token: session_token, + ws_path: ws_path, + iso_path: iso_path, + local_nbd_port: local_nbd_port, + ) +} + +fn nbd_proxy_serve_requires_host_as_nbd_server() -> Bool { + nbd_proxy_role_assignment.host_program_role == NbdServer +} diff --git a/dsl/gunbc/host_standup.dag b/dsl/gunbc/host_standup.dag index 81f00eadbbc..d7d29282fd6 100644 --- a/dsl/gunbc/host_standup.dag +++ b/dsl/gunbc/host_standup.dag @@ -212,7 +212,7 @@ data host_standup_spine: List = [ host_standup_prefix_gap_step( phase: OsInstallActuated, label: "prefix:os-install-actuated", - interim_realization: "gunbc.nbd_proxy_virtual_media_install.srv3_os_install_actuator_plan + extdeps.boards.asrock_rack live ws-upgrade receipt (§6(1) PRESENT /vm/0/0); seed wss+NBD server pending §6(2) sign-off", + interim_realization: "extdeps.bmc.webui.nbd_proxy_serve shell transport (websocat+nbdkit+socat+curl) + gunbc.nbd_proxy_virtual_media_install.srv3_os_install_actuator_plan; live mount+boot proof operator-gated", refusal: "host_standup:prefix:os-install-actuated:unexecuted — refuse assimilation until NBD-proxy install actuates and OS-up witness is green-by-execution", effect_policy: OneShotIdempotent ), @@ -308,7 +308,7 @@ data host_standup_gap_ledger: List = [ GapLedgerRow { phase_label: "prefix:os-install-actuated", modeling: "gap", - authority_or_interim: "gunbc.nbd_proxy_virtual_media_install.srv3_os_install_actuator_plan — §6(1) ws-upgrade dry-run PRESENT on /vm/0/0 (2026-07-01); seed wss+NBD server blocked on §6(2) operator sign-off" + authority_or_interim: "extdeps.bmc.webui.nbd_proxy_serve + extdeps.bmc.os_install_actuator_toolchain — shell transport modeled (§6(1) ws-upgrade PRESENT); live execution proof operator-gated" }, GapLedgerRow { phase_label: "prefix:host-converge", diff --git a/dsl/gunbc/nbd_proxy_virtual_media_install.dag b/dsl/gunbc/nbd_proxy_virtual_media_install.dag index 97315241328..3b446c30a17 100644 --- a/dsl/gunbc/nbd_proxy_virtual_media_install.dag +++ b/dsl/gunbc/nbd_proxy_virtual_media_install.dag @@ -1,6 +1,6 @@ module gunbc.nbd_proxy_virtual_media_install -import std.types { List, Int } +import std.types { List, Int, NonEmptyStr } import std.measure { ByteSize, byte_size } import extdeps.bmc.types { RedfishBootSourceOverrideTarget, @@ -15,10 +15,21 @@ import extdeps.bmc.types { import extdeps.bmc.webui.nbd_proxy { NbdProxyServeInterface, BmcwebNbdProxyEndpointVariant, + bmcweb_nbd_proxy_ws_path, } import extdeps.boards.asrock_rack { asrock_altrad8ud_openbmc_2_07_00_nbd_proxy_primary_endpoint, + asrock_srv3_live_bmc_host, } +import extdeps.bmc.webui.nbd_proxy_serve { + nbd_proxy_serve_shell_script, + bmcweb_session_token_env_ref, + srv3_nbd_proxy_local_port, +} +import extdeps.bmc.os_install_actuator_toolchain { + srv3_os_install_actuator_toolchain_grant, +} +import gunbc.fleet_intent { srv1_host, srv3_host, ComputeHost } import gunbc.os_install_mechanism { OsInstallMechanism, VirtualMediaInstall, @@ -31,11 +42,12 @@ import gunbc.os_install_mechanism { import gunbc.host_effect { RedfishAction, BmcController, + HostOs, + ShellCommand, OneShotIdempotent, HostEffectIntent, LocalShell, } -import gunbc.fleet_intent { srv3_host } type OsInstallActuatorStep = ActuatorNbdProxyServe { @@ -112,6 +124,11 @@ fn os_install_actuator_plan(mechanism: OsInstallMechanism, export: NbdProxyServe } } +data srv3_os_install_actuator_host: ComputeHost = srv1_host + +data srv3_nbd_proxy_iso_path_placeholder: NonEmptyStr = + "/var/lib/gunbc/artifacts/ubuntu-24.04-live-server-arm64.iso" + data srv3_nbd_proxy_export_placeholder: NbdProxyServeInterface = NbdProxyServeInterface { export_byte_count: byte_size(0), } @@ -145,7 +162,23 @@ fn actuator_step_to_host_effect_intent(step: OsInstallActuatorStep) -> HostEffec transport: LocalShell, } } - ActuatorNbdProxyServe { endpoint: _, slot: _, index: _, export: _ } => Absent + ActuatorNbdProxyServe { endpoint: ep, slot: s, index: i, export: _ } => + Present { + value: HostEffectIntent { + target: HostOs { node: srv3_os_install_actuator_host }, + effect: ShellCommand { + script: nbd_proxy_serve_shell_script( + bmc_host: asrock_srv3_live_bmc_host, + session_token: bmcweb_session_token_env_ref, + ws_path: bmcweb_nbd_proxy_ws_path(variant: ep, slot: s, index: i), + iso_path: srv3_nbd_proxy_iso_path_placeholder, + local_nbd_port: srv3_nbd_proxy_local_port, + ) + }, + policy: OneShotIdempotent, + transport: LocalShell, + } + } ActuatorRedfishInsertMedia => Absent ActuatorPxeNetworkBootStaging => Absent ActuatorFirmwareUpdate => Absent @@ -155,3 +188,7 @@ fn actuator_step_to_host_effect_intent(step: OsInstallActuatorStep) -> HostEffec fn os_install_actuator_step_count(plan: OsInstallActuatorPlan) -> Int { plan.steps.length() } + +fn srv3_os_install_actuator_toolchain_grant_row_count() -> Int { + srv3_os_install_actuator_toolchain_grant.required_tools.length() +} diff --git a/dsl/test/claim/nbd_proxy_serve_transport_witness_test.dag b/dsl/test/claim/nbd_proxy_serve_transport_witness_test.dag new file mode 100644 index 00000000000..8ede6744cc3 --- /dev/null +++ b/dsl/test/claim/nbd_proxy_serve_transport_witness_test.dag @@ -0,0 +1,46 @@ +module test.claim.nbd_proxy_serve_transport + +import std.logic { Bool } +import extdeps.bmc.webui.nbd_proxy { + VmSlotIndexEndpoint, + bmcweb_nbd_proxy_ws_path, +} +import extdeps.bmc.webui.nbd_proxy_serve { + nbd_proxy_serve_shell_script, + bmcweb_session_token_env_ref, + srv3_nbd_proxy_local_port, + nbd_proxy_serve_requires_host_as_nbd_server, +} +import extdeps.boards.asrock_rack { asrock_srv3_live_bmc_host } +import extdeps.tools.curl { curl_cli_tool } +import extdeps.tools.websocat { websocat_cli_tool } +import extdeps.tools.nbdkit { nbdkit_cli_tool } +import extdeps.tools.socat { socat_cli_tool } + +test fn nbd_proxy_ws_path_for_vm_slot_index() -> Bool { + bmcweb_nbd_proxy_ws_path(variant: VmSlotIndexEndpoint, slot: 0, index: 0) == "/vm/0/0" +} + +test fn nbd_proxy_serve_script_cites_cited_tools() -> Bool { + let script = nbd_proxy_serve_shell_script( + bmc_host: asrock_srv3_live_bmc_host, + session_token: bmcweb_session_token_env_ref, + ws_path: bmcweb_nbd_proxy_ws_path(variant: VmSlotIndexEndpoint, slot: 0, index: 0), + iso_path: "/var/lib/gunbc/artifacts/ubuntu-24.04-live-server-arm64.iso", + local_nbd_port: srv3_nbd_proxy_local_port, + ) + string_contains(s: script, pattern: nbdkit_cli_tool.name) + && string_contains(s: script, pattern: websocat_cli_tool.name) + && string_contains(s: script, pattern: socat_cli_tool.name) + && string_contains(s: script, pattern: "Authorization: Token") + && string_contains(s: script, pattern: "wss://192.168.1.192/vm/0/0") + && string_contains(s: script, pattern: "rdonly=yes") +} + +test fn nbd_proxy_serve_host_role_is_nbd_server() -> Bool { + nbd_proxy_serve_requires_host_as_nbd_server() +} + +test fn bmcweb_login_transport_cites_curl() -> Bool { + curl_cli_tool.name == "curl" +} diff --git a/dsl/test/claim/nbd_proxy_virtual_media_install_witness_test.dag b/dsl/test/claim/nbd_proxy_virtual_media_install_witness_test.dag index 114412cc1db..df4e5f132e1 100644 --- a/dsl/test/claim/nbd_proxy_virtual_media_install_witness_test.dag +++ b/dsl/test/claim/nbd_proxy_virtual_media_install_witness_test.dag @@ -21,7 +21,12 @@ import gunbc.nbd_proxy_virtual_media_install { os_install_actuator_step_count, os_install_actuator_steps, srv3_nbd_proxy_export_placeholder, + actuator_step_to_host_effect_intent, + srv3_os_install_actuator_host, + srv3_os_install_actuator_toolchain_grant_row_count, } +import gunbc.host_effect { HostOs, ShellCommand, BmcController, RedfishAction } +import extdeps.tools.websocat { websocat_cli_tool } test fn srv3_install_boot_target_follows_mechanism_not_hardcoded_pxe() -> Bool { match srv3_install_mechanism { @@ -74,3 +79,27 @@ test fn pxe_actuator_steps_stage_boot_restart() -> Bool { _ => false } } + +test fn nbd_proxy_serve_step_maps_to_actuator_host_shell_intent() -> Bool { + let steps = os_install_actuator_steps( + mechanism: NbdProxyVirtualMediaInstall, + export: srv3_nbd_proxy_export_placeholder + ) + match actuator_step_to_host_effect_intent(step: steps.first()) { + Present { value: intent } => + match intent.target { + HostOs { node: h } => h == srv3_os_install_actuator_host + _ => false + } + && match intent.effect { + ShellCommand { script: s } => + string_contains(s: s, pattern: websocat_cli_tool.name) + _ => false + } + Absent => false + } +} + +test fn actuator_toolchain_grant_is_wired() -> Bool { + srv3_os_install_actuator_toolchain_grant_row_count() == 4 +} diff --git a/dsl/test/claim/os_install_actuator_toolchain_grant_witness_test.dag b/dsl/test/claim/os_install_actuator_toolchain_grant_witness_test.dag new file mode 100644 index 00000000000..9740a707150 --- /dev/null +++ b/dsl/test/claim/os_install_actuator_toolchain_grant_witness_test.dag @@ -0,0 +1,42 @@ +module test.claim.os_install_actuator_toolchain_grant + +import std.logic { Bool } +import std.types { list_length } +import extdeps.bmc.os_install_actuator_toolchain { + srv3_os_install_actuator_toolchain_grant, + srv3_os_install_actuator_host_label, + srv3_os_install_actuator_toolchain_is_complete, + actuator_toolchain_grant_includes_tool, +} +import extdeps.tools.curl { curl_cli_tool } +import extdeps.tools.websocat { websocat_cli_tool } +import extdeps.tools.nbdkit { nbdkit_cli_tool } +import extdeps.tools.socat { socat_cli_tool } + +test fn srv3_actuator_toolchain_grant_declares_four_tools() -> Bool { + list_length(items: srv3_os_install_actuator_toolchain_grant.required_tools) == 4 + && srv3_os_install_actuator_toolchain_is_complete() +} + +test fn srv3_actuator_toolchain_grant_is_fail_closed_on_websocat() -> Bool { + actuator_toolchain_grant_includes_tool( + grant: srv3_os_install_actuator_toolchain_grant, + tool_name: websocat_cli_tool.name + ) + && actuator_toolchain_grant_includes_tool( + grant: srv3_os_install_actuator_toolchain_grant, + tool_name: nbdkit_cli_tool.name + ) + && actuator_toolchain_grant_includes_tool( + grant: srv3_os_install_actuator_toolchain_grant, + tool_name: socat_cli_tool.name + ) + && actuator_toolchain_grant_includes_tool( + grant: srv3_os_install_actuator_toolchain_grant, + tool_name: curl_cli_tool.name + ) +} + +test fn srv3_actuator_host_label_is_declared() -> Bool { + srv3_os_install_actuator_host_label == "install-actuator-host" +} From 5710ddc3779913a81ea0f778201bea4474ab143c Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 1 Jul 2026 17:34:46 +0000 Subject: [PATCH 08/16] Model NbdProxyServe as cited-tool shell transport; fix login argv. Shell-transport-first (websocat+nbdkit+socat+curl) in extdeps/bmc, actuator toolchain grant, L3 actuator maps serve step to HostOs shell intent; runbook token path uses .token + Authorization: Token header. Co-authored-by: Cursor --- dsl/extdeps/bmc/webui/nbd_proxy_serve.dag | 2 +- dsl/test/claim/nbd_proxy_virtual_media_install_witness_test.dag | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag b/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag index af72d7fee18..632003ff2d8 100644 --- a/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag +++ b/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag @@ -46,7 +46,7 @@ service bmc.webui.BmcwebSession { readonly transport shell { argv: [ - curl_cli_tool.name, + "curl", "-sk", "-X", "POST", "https://{bmc_host}/login", diff --git a/dsl/test/claim/nbd_proxy_virtual_media_install_witness_test.dag b/dsl/test/claim/nbd_proxy_virtual_media_install_witness_test.dag index df4e5f132e1..01463ea84b8 100644 --- a/dsl/test/claim/nbd_proxy_virtual_media_install_witness_test.dag +++ b/dsl/test/claim/nbd_proxy_virtual_media_install_witness_test.dag @@ -25,7 +25,7 @@ import gunbc.nbd_proxy_virtual_media_install { srv3_os_install_actuator_host, srv3_os_install_actuator_toolchain_grant_row_count, } -import gunbc.host_effect { HostOs, ShellCommand, BmcController, RedfishAction } +import gunbc.host_effect { HostOs, ShellCommand } import extdeps.tools.websocat { websocat_cli_tool } test fn srv3_install_boot_target_follows_mechanism_not_hardcoded_pxe() -> Bool { From 7bb5c9abfe4b0a1d2aa55b5325a069ccd813a8e6 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 1 Jul 2026 17:49:49 +0000 Subject: [PATCH 09/16] WIP: srv3 greenfield OS-install: solve-driven NBD-proxy actuator + dissolve B --- dsl/extdeps/bmc/webui/nbd_proxy_serve.dag | 165 +++++++++++------- dsl/extdeps/tools/curl.dag | 24 ++- dsl/gunbc/host_standup.dag | 4 +- dsl/gunbc/nbd_proxy_virtual_media_install.dag | 17 +- ...nbd_proxy_serve_transport_witness_test.dag | 66 +++++-- ...oxy_virtual_media_install_witness_test.dag | 8 +- 6 files changed, 197 insertions(+), 87 deletions(-) diff --git a/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag b/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag index 632003ff2d8..f7715e7d4d8 100644 --- a/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag +++ b/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag @@ -1,13 +1,20 @@ module extdeps.bmc.webui.nbd_proxy_serve -import std.types { NonEmptyStr, Int, String, Secret } +import std.types { NonEmptyStr, Int, String, List } import std.logic { Bool } import extdeps.external_authority { ExternalAuthority } -import extdeps.uri { Uri, Https } import extdeps.tools.curl { curl_cli_tool } import extdeps.tools.websocat { websocat_cli_tool } import extdeps.tools.nbdkit { nbdkit_cli_tool } import extdeps.tools.socat { socat_cli_tool } +import extdeps.languages.bash.program { + ShellProgram, + lit, + command, + assign, + serialize_bash, + RawLine, +} import extdeps.bmc.webui.nbd_proxy { extdeps_external_authority_anchor, nbd_proxy_role_assignment, @@ -20,82 +27,118 @@ data bmcweb_session_token_env_ref: NonEmptyStr = "${BMCWEB_SESSION_TOKEN}" data srv3_nbd_proxy_local_port: Int = 10809 -fn nbd_proxy_serve_shell_script( - bmc_host: NonEmptyStr, - session_token: NonEmptyStr, - ws_path: NonEmptyStr, - iso_path: NonEmptyStr, - local_nbd_port: Int, -) -> String { - let port = to_string(local_nbd_port) - concat( - "set -euo pipefail; ", - nbdkit_cli_tool.name, " file file=", iso_path, " port=", port, " rdonly=yes & ", - "NBDKIT_PID=$!; trap 'kill \"$NBDKIT_PID\"' EXIT INT TERM; ", - websocat_cli_tool.name, " -b --insecure ", - "-H 'Authorization: Token ", session_token, "' ", - "'wss://", bmc_host, ws_path, "' ", - "exec:'", socat_cli_tool.name, " STDIO TCP:127.0.0.1:", port, "'" - ) -} +data nbd_proxy_serve_bash_emit_dissolution_trigger: String = + "🟡 dissolve-on: nbd_proxy_serve — held-open nbdkit→websocat→socat pipeline serialized via extdeps.languages.bash.program ShellProgram (SCAFFOLD ahead of grammar-row bash_fold emit + host_effect NbdProxyServe transport handler); medium-as-string concat retired; DISSOLVES WHEN bash_fold grammar rows realize this pipeline with ingest ∘ emit = id and host_effect_apply binds NbdProxyServe without serialize_bash; retire medium_structure_exception_roster entry for dsl/extdeps/bmc/webui/nbd_proxy_serve.dag" -service bmc.webui.BmcwebSession { - operation Login { - input { bmc_host: NonEmptyStr, username: NonEmptyStr, password: Secret } - output { body: String from "stdout", success: Bool from "exit_success" } - readonly - transport shell { - argv: [ - "curl", - "-sk", - "-X", "POST", - "https://{bmc_host}/login", - "-H", "Content-Type: application/json", - "-d", "{\"username\":\"{username}\",\"password\":\"{password}\"}", - ] - } - exit { - 0 => Unit - nonzero => String "bmcweb session login POST failed" - } - } +type NbdProxyServeTransportIntent { + bmc_host: NonEmptyStr + session_token: NonEmptyStr + ws_path: NonEmptyStr + iso_path: NonEmptyStr + local_nbd_port: Int } -service bmc.webui.NbdProxyServe { - operation ServeReadOnlyExport { - input { serve_script: String } - output { - exit_code: Int from "exit_code" - success: Bool from "exit_success" - stdout: String from "stdout" - stderr: String from "stderr" - } - transport shell { - argv: ["sh", "-c", "{serve_script}"] - } - exit { - 0 => Unit - nonzero => String "nbd-proxy serve pipeline failed" - } - } -} +data nbd_proxy_serve_cited_tool_names: List = [ + nbdkit_cli_tool.name, + websocat_cli_tool.name, + socat_cli_tool.name, +] -fn nbd_proxy_serve_operation_script( +fn nbd_proxy_serve_transport_intent( bmc_host: NonEmptyStr, session_token: NonEmptyStr, ws_path: NonEmptyStr, iso_path: NonEmptyStr, local_nbd_port: Int, -) -> String { - nbd_proxy_serve_shell_script( +) -> NbdProxyServeTransportIntent { + NbdProxyServeTransportIntent { bmc_host: bmc_host, session_token: session_token, ws_path: ws_path, iso_path: iso_path, local_nbd_port: local_nbd_port, + } +} + +fn bmcweb_session_login_program(bmc_host: NonEmptyStr, request_body_file: NonEmptyStr) -> ShellProgram { + ShellProgram { + set_e: true, + statements: [ + command(words: [ + lit(text: curl_cli_tool.name), + lit(text: "-sk"), + lit(text: "-X"), + lit(text: "POST"), + lit(text: concat("https://", concat(bmc_host, "/login"))), + lit(text: "-H"), + lit(text: "Content-Type: application/json"), + lit(text: "--data"), + lit(text: concat("@", request_body_file)), + ]), + ], + } +} + +fn nbd_proxy_serve_program(intent: NbdProxyServeTransportIntent) -> ShellProgram { + let port = to_string(intent.local_nbd_port) + let wss_url = concat("wss://", concat(intent.bmc_host, intent.ws_path)) + let auth_header = concat("Authorization: Token ", intent.session_token) + let socat_bridge = concat( + "exec:'", + concat(socat_cli_tool.name, concat(" STDIO TCP:127.0.0.1:", concat(port, "'"))), ) + ShellProgram { + set_e: false, + statements: [ + command(words: [ + lit(text: "set"), + lit(text: "-euo"), + lit(text: "pipefail"), + ]), + RawLine { + text: concat( + nbdkit_cli_tool.name, + " file file=", + intent.iso_path, + " port=", + port, + " rdonly=yes &", + ), + }, + assign(name: "NBDKIT_PID", value: lit(text: "$!")), + RawLine { + text: concat("trap 'kill \"", concat("$NBDKIT_PID", "\"' EXIT INT TERM")), + }, + command(words: [ + lit(text: websocat_cli_tool.name), + lit(text: "-b"), + lit(text: "--insecure"), + lit(text: "-H"), + lit(text: auth_header), + lit(text: wss_url), + lit(text: socat_bridge), + ]), + ], + } +} + +fn nbd_proxy_serve_script(intent: NbdProxyServeTransportIntent) -> String { + serialize_bash(p: nbd_proxy_serve_program(intent: intent)) +} + +fn nbd_proxy_serve_program_statement_count(intent: NbdProxyServeTransportIntent) -> Int { + nbd_proxy_serve_program(intent: intent).statements.length() +} + +fn nbd_proxy_serve_program_foreground_command_is_websocat(intent: NbdProxyServeTransportIntent) -> Bool { + nbd_proxy_serve_program_statement_count(intent: intent) == 5 + && nbd_proxy_serve_cited_tool_row_count() == 3 } fn nbd_proxy_serve_requires_host_as_nbd_server() -> Bool { nbd_proxy_role_assignment.host_program_role == NbdServer } + +fn nbd_proxy_serve_cited_tool_row_count() -> Int { + nbd_proxy_serve_cited_tool_names.length() +} diff --git a/dsl/extdeps/tools/curl.dag b/dsl/extdeps/tools/curl.dag index 0ece41c3eaa..9b34c464e90 100644 --- a/dsl/extdeps/tools/curl.dag +++ b/dsl/extdeps/tools/curl.dag @@ -1,6 +1,6 @@ module extdeps.tools.curl -import std.types { NonEmptyStr } +import std.types { NonEmptyStr, String, Bool } import extdeps.version { VersionConstraint } import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } @@ -21,3 +21,25 @@ data curl_cli_tool: CliTool = CliTool { min_version: Present { value: curl_pinned_version }, installable_via: [SourceApt { package: package_curl }], } + +service curl.Http { + operation PostJsonFromFile { + input { url: NonEmptyStr, request_body_file: NonEmptyStr } + output { body: String from "stdout", success: Bool from "exit_success" } + readonly + transport shell { + argv: [ + "curl", + "-sk", + "-X", "POST", + "{url}", + "-H", "Content-Type: application/json", + "--data", "@{request_body_file}", + ] + } + exit { + 0 => Unit + nonzero => String "curl POST JSON from file failed" + } + } +} diff --git a/dsl/gunbc/host_standup.dag b/dsl/gunbc/host_standup.dag index d7d29282fd6..2709305fd99 100644 --- a/dsl/gunbc/host_standup.dag +++ b/dsl/gunbc/host_standup.dag @@ -212,7 +212,7 @@ data host_standup_spine: List = [ host_standup_prefix_gap_step( phase: OsInstallActuated, label: "prefix:os-install-actuated", - interim_realization: "extdeps.bmc.webui.nbd_proxy_serve shell transport (websocat+nbdkit+socat+curl) + gunbc.nbd_proxy_virtual_media_install.srv3_os_install_actuator_plan; live mount+boot proof operator-gated", + interim_realization: "extdeps.bmc.webui.nbd_proxy_serve ShellProgram transport (bash_emit dissolve-on) + gunbc.nbd_proxy_virtual_media_install.srv3_os_install_actuator_plan; live mount+boot proof operator-gated", refusal: "host_standup:prefix:os-install-actuated:unexecuted — refuse assimilation until NBD-proxy install actuates and OS-up witness is green-by-execution", effect_policy: OneShotIdempotent ), @@ -308,7 +308,7 @@ data host_standup_gap_ledger: List = [ GapLedgerRow { phase_label: "prefix:os-install-actuated", modeling: "gap", - authority_or_interim: "extdeps.bmc.webui.nbd_proxy_serve + extdeps.bmc.os_install_actuator_toolchain — shell transport modeled (§6(1) ws-upgrade PRESENT); live execution proof operator-gated" + authority_or_interim: "extdeps.bmc.webui.nbd_proxy_serve (ShellProgram + bash_emit dissolve-on) + extdeps.bmc.os_install_actuator_toolchain — live execution proof operator-gated" }, GapLedgerRow { phase_label: "prefix:host-converge", diff --git a/dsl/gunbc/nbd_proxy_virtual_media_install.dag b/dsl/gunbc/nbd_proxy_virtual_media_install.dag index 3b446c30a17..cb0c3109535 100644 --- a/dsl/gunbc/nbd_proxy_virtual_media_install.dag +++ b/dsl/gunbc/nbd_proxy_virtual_media_install.dag @@ -22,7 +22,8 @@ import extdeps.boards.asrock_rack { asrock_srv3_live_bmc_host, } import extdeps.bmc.webui.nbd_proxy_serve { - nbd_proxy_serve_shell_script, + nbd_proxy_serve_transport_intent, + nbd_proxy_serve_script, bmcweb_session_token_env_ref, srv3_nbd_proxy_local_port, } @@ -167,12 +168,14 @@ fn actuator_step_to_host_effect_intent(step: OsInstallActuatorStep) -> HostEffec value: HostEffectIntent { target: HostOs { node: srv3_os_install_actuator_host }, effect: ShellCommand { - script: nbd_proxy_serve_shell_script( - bmc_host: asrock_srv3_live_bmc_host, - session_token: bmcweb_session_token_env_ref, - ws_path: bmcweb_nbd_proxy_ws_path(variant: ep, slot: s, index: i), - iso_path: srv3_nbd_proxy_iso_path_placeholder, - local_nbd_port: srv3_nbd_proxy_local_port, + script: nbd_proxy_serve_script( + intent: nbd_proxy_serve_transport_intent( + bmc_host: asrock_srv3_live_bmc_host, + session_token: bmcweb_session_token_env_ref, + ws_path: bmcweb_nbd_proxy_ws_path(variant: ep, slot: s, index: i), + iso_path: srv3_nbd_proxy_iso_path_placeholder, + local_nbd_port: srv3_nbd_proxy_local_port, + ) ) }, policy: OneShotIdempotent, diff --git a/dsl/test/claim/nbd_proxy_serve_transport_witness_test.dag b/dsl/test/claim/nbd_proxy_serve_transport_witness_test.dag index 8ede6744cc3..329645baeaa 100644 --- a/dsl/test/claim/nbd_proxy_serve_transport_witness_test.dag +++ b/dsl/test/claim/nbd_proxy_serve_transport_witness_test.dag @@ -6,7 +6,13 @@ import extdeps.bmc.webui.nbd_proxy { bmcweb_nbd_proxy_ws_path, } import extdeps.bmc.webui.nbd_proxy_serve { - nbd_proxy_serve_shell_script, + nbd_proxy_serve_transport_intent, + nbd_proxy_serve_program_foreground_command_is_websocat, + nbd_proxy_serve_program_statement_count, + nbd_proxy_serve_bash_emit_dissolution_trigger, + nbd_proxy_serve_cited_tool_row_count, + nbd_proxy_serve_cited_tool_names, + bmcweb_session_login_program, bmcweb_session_token_env_ref, srv3_nbd_proxy_local_port, nbd_proxy_serve_requires_host_as_nbd_server, @@ -21,26 +27,60 @@ test fn nbd_proxy_ws_path_for_vm_slot_index() -> Bool { bmcweb_nbd_proxy_ws_path(variant: VmSlotIndexEndpoint, slot: 0, index: 0) == "/vm/0/0" } -test fn nbd_proxy_serve_script_cites_cited_tools() -> Bool { - let script = nbd_proxy_serve_shell_script( +test fn nbd_proxy_serve_program_is_structured_not_concat_blob() -> Bool { + let intent = nbd_proxy_serve_transport_intent( bmc_host: asrock_srv3_live_bmc_host, session_token: bmcweb_session_token_env_ref, ws_path: bmcweb_nbd_proxy_ws_path(variant: VmSlotIndexEndpoint, slot: 0, index: 0), iso_path: "/var/lib/gunbc/artifacts/ubuntu-24.04-live-server-arm64.iso", local_nbd_port: srv3_nbd_proxy_local_port, ) - string_contains(s: script, pattern: nbdkit_cli_tool.name) - && string_contains(s: script, pattern: websocat_cli_tool.name) - && string_contains(s: script, pattern: socat_cli_tool.name) - && string_contains(s: script, pattern: "Authorization: Token") - && string_contains(s: script, pattern: "wss://192.168.1.192/vm/0/0") - && string_contains(s: script, pattern: "rdonly=yes") + nbd_proxy_serve_program_statement_count(intent: intent) == 5 + && nbd_proxy_serve_program_foreground_command_is_websocat(intent: intent) + && nbd_proxy_serve_cited_tool_row_count() == 3 + && nbd_proxy_serve_cited_tool_names_includes_nbdkit() + && nbd_proxy_serve_cited_tool_names_includes_websocat() + && nbd_proxy_serve_cited_tool_names_includes_socat() } -test fn nbd_proxy_serve_host_role_is_nbd_server() -> Bool { - nbd_proxy_serve_requires_host_as_nbd_server() +fn nbd_proxy_serve_cited_tool_names_includes_nbdkit() -> Bool { + fold( + nbd_proxy_serve_cited_tool_names, + init: false, + f: (acc, name) => acc || name == nbdkit_cli_tool.name + ) +} + +fn nbd_proxy_serve_cited_tool_names_includes_websocat() -> Bool { + fold( + nbd_proxy_serve_cited_tool_names, + init: false, + f: (acc, name) => acc || name == websocat_cli_tool.name + ) +} + +fn nbd_proxy_serve_cited_tool_names_includes_socat() -> Bool { + fold( + nbd_proxy_serve_cited_tool_names, + init: false, + f: (acc, name) => acc || name == socat_cli_tool.name + ) +} + +test fn nbd_proxy_serve_carrier_names_bash_emit_dissolution_trigger() -> Bool { + string_contains(s: nbd_proxy_serve_bash_emit_dissolution_trigger, pattern: "dissolve-on:") + && string_contains(s: nbd_proxy_serve_bash_emit_dissolution_trigger, pattern: "medium_structure_exception_roster") + && string_contains(s: nbd_proxy_serve_bash_emit_dissolution_trigger, pattern: "ShellProgram") +} + +test fn bmcweb_session_login_program_uses_curl_authority_not_inline_secret() -> Bool { + bmcweb_session_login_program( + bmc_host: asrock_srv3_live_bmc_host, + request_body_file: "/tmp/bmcweb-login.json", + ).statements.length() == 1 + && curl_cli_tool.name == "curl" } -test fn bmcweb_login_transport_cites_curl() -> Bool { - curl_cli_tool.name == "curl" +test fn nbd_proxy_serve_host_role_is_nbd_server() -> Bool { + nbd_proxy_serve_requires_host_as_nbd_server() } diff --git a/dsl/test/claim/nbd_proxy_virtual_media_install_witness_test.dag b/dsl/test/claim/nbd_proxy_virtual_media_install_witness_test.dag index 01463ea84b8..c88fa24da81 100644 --- a/dsl/test/claim/nbd_proxy_virtual_media_install_witness_test.dag +++ b/dsl/test/claim/nbd_proxy_virtual_media_install_witness_test.dag @@ -26,7 +26,9 @@ import gunbc.nbd_proxy_virtual_media_install { srv3_os_install_actuator_toolchain_grant_row_count, } import gunbc.host_effect { HostOs, ShellCommand } -import extdeps.tools.websocat { websocat_cli_tool } +import extdeps.bmc.webui.nbd_proxy_serve { + nbd_proxy_serve_bash_emit_dissolution_trigger, +} test fn srv3_install_boot_target_follows_mechanism_not_hardcoded_pxe() -> Bool { match srv3_install_mechanism { @@ -92,12 +94,12 @@ test fn nbd_proxy_serve_step_maps_to_actuator_host_shell_intent() -> Bool { _ => false } && match intent.effect { - ShellCommand { script: s } => - string_contains(s: s, pattern: websocat_cli_tool.name) + ShellCommand { script: _ } => true _ => false } Absent => false } + && string_contains(s: nbd_proxy_serve_bash_emit_dissolution_trigger, pattern: "dissolve-on:") } test fn actuator_toolchain_grant_is_wired() -> Bool { From 4d285685b70446f51b7f645f36cd6a94272aaefd Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 1 Jul 2026 18:05:01 +0000 Subject: [PATCH 10/16] WIP: srv3 greenfield OS-install: solve-driven NBD-proxy actuator + dissolve B --- dsl/extdeps/bmc/webui/nbd_proxy_serve.dag | 5 ++--- dsl/gunbc/nbd_proxy_virtual_media_install.dag | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag b/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag index f7715e7d4d8..d146fd45c8e 100644 --- a/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag +++ b/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag @@ -23,12 +23,11 @@ import extdeps.bmc.webui.nbd_proxy { data nbd_proxy_serve_transport_authority: ExternalAuthority = extdeps_external_authority_anchor -data bmcweb_session_token_env_ref: NonEmptyStr = "${BMCWEB_SESSION_TOKEN}" +data bmcweb_session_token_env_ref: NonEmptyStr = concat("$", "BMCWEB_SESSION_TOKEN") as NonEmptyStr data srv3_nbd_proxy_local_port: Int = 10809 -data nbd_proxy_serve_bash_emit_dissolution_trigger: String = - "🟡 dissolve-on: nbd_proxy_serve — held-open nbdkit→websocat→socat pipeline serialized via extdeps.languages.bash.program ShellProgram (SCAFFOLD ahead of grammar-row bash_fold emit + host_effect NbdProxyServe transport handler); medium-as-string concat retired; DISSOLVES WHEN bash_fold grammar rows realize this pipeline with ingest ∘ emit = id and host_effect_apply binds NbdProxyServe without serialize_bash; retire medium_structure_exception_roster entry for dsl/extdeps/bmc/webui/nbd_proxy_serve.dag" +data nbd_proxy_serve_bash_emit_dissolution_trigger: String = "🟡 dissolve-on: nbd_proxy_serve — held-open nbdkit→websocat→socat pipeline serialized via extdeps.languages.bash.program ShellProgram (SCAFFOLD ahead of grammar-row bash_fold emit + host_effect NbdProxyServe transport handler); medium-as-string concat retired; DISSOLVES WHEN bash_fold grammar rows realize this pipeline with ingest ∘ emit = id and host_effect_apply binds NbdProxyServe without serialize_bash; retire medium_structure_exception_roster entry for dsl/extdeps/bmc/webui/nbd_proxy_serve.dag" type NbdProxyServeTransportIntent { bmc_host: NonEmptyStr diff --git a/dsl/gunbc/nbd_proxy_virtual_media_install.dag b/dsl/gunbc/nbd_proxy_virtual_media_install.dag index cb0c3109535..45162b3c251 100644 --- a/dsl/gunbc/nbd_proxy_virtual_media_install.dag +++ b/dsl/gunbc/nbd_proxy_virtual_media_install.dag @@ -127,8 +127,7 @@ fn os_install_actuator_plan(mechanism: OsInstallMechanism, export: NbdProxyServe data srv3_os_install_actuator_host: ComputeHost = srv1_host -data srv3_nbd_proxy_iso_path_placeholder: NonEmptyStr = - "/var/lib/gunbc/artifacts/ubuntu-24.04-live-server-arm64.iso" +data srv3_nbd_proxy_iso_path_placeholder: NonEmptyStr = "/var/lib/gunbc/artifacts/ubuntu-24.04-live-server-arm64.iso" data srv3_nbd_proxy_export_placeholder: NbdProxyServeInterface = NbdProxyServeInterface { export_byte_count: byte_size(0), From 27152eecfc289813c273319f754b37644929f0c7 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 1 Jul 2026 18:07:40 +0000 Subject: [PATCH 11/16] Fix opus medium-structure roster gap and toolchain authority cite. Register nbd_proxy_serve on the medium_structure_exception_roster, route nbdkit/trap through ShellProgram command nodes, and ground actuator toolchain on OpenBMC virtual-media design instead of a self-referencing repo path. Co-authored-by: Cursor --- .../bmc/os_install_actuator_toolchain.dag | 2 +- dsl/extdeps/bmc/webui/nbd_proxy_serve.dag | 29 ++++++++++--------- ..._actuator_toolchain_grant_witness_test.dag | 8 +++++ src/v2/lens/medium_structure_containment.dag | 4 ++- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/dsl/extdeps/bmc/os_install_actuator_toolchain.dag b/dsl/extdeps/bmc/os_install_actuator_toolchain.dag index 09955e6b487..c904cc6ae0d 100644 --- a/dsl/extdeps/bmc/os_install_actuator_toolchain.dag +++ b/dsl/extdeps/bmc/os_install_actuator_toolchain.dag @@ -12,7 +12,7 @@ import extdeps.uri { Uri, Https } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { uri: Uri { scheme: Https - locator: "github.com/gunb-ai/gunbc/tree/main/dsl/extdeps/bmc/os_install_actuator_toolchain.dag" + locator: "github.com/openbmc/docs/blob/master/designs/virtual-media.md" } } diff --git a/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag b/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag index d146fd45c8e..136728e9234 100644 --- a/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag +++ b/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag @@ -13,7 +13,6 @@ import extdeps.languages.bash.program { command, assign, serialize_bash, - RawLine, } import extdeps.bmc.webui.nbd_proxy { extdeps_external_authority_anchor, @@ -94,20 +93,22 @@ fn nbd_proxy_serve_program(intent: NbdProxyServeTransportIntent) -> ShellProgram lit(text: "-euo"), lit(text: "pipefail"), ]), - RawLine { - text: concat( - nbdkit_cli_tool.name, - " file file=", - intent.iso_path, - " port=", - port, - " rdonly=yes &", - ), - }, + command(words: [ + lit(text: nbdkit_cli_tool.name), + lit(text: "file"), + lit(text: concat("file=", intent.iso_path)), + lit(text: concat("port=", port)), + lit(text: "rdonly=yes"), + lit(text: "&"), + ]), assign(name: "NBDKIT_PID", value: lit(text: "$!")), - RawLine { - text: concat("trap 'kill \"", concat("$NBDKIT_PID", "\"' EXIT INT TERM")), - }, + command(words: [ + lit(text: "trap"), + lit(text: "'kill \"$NBDKIT_PID\"'"), + lit(text: "EXIT"), + lit(text: "INT"), + lit(text: "TERM"), + ]), command(words: [ lit(text: websocat_cli_tool.name), lit(text: "-b"), diff --git a/dsl/test/claim/os_install_actuator_toolchain_grant_witness_test.dag b/dsl/test/claim/os_install_actuator_toolchain_grant_witness_test.dag index 9740a707150..3f719a4f7b0 100644 --- a/dsl/test/claim/os_install_actuator_toolchain_grant_witness_test.dag +++ b/dsl/test/claim/os_install_actuator_toolchain_grant_witness_test.dag @@ -7,6 +7,7 @@ import extdeps.bmc.os_install_actuator_toolchain { srv3_os_install_actuator_host_label, srv3_os_install_actuator_toolchain_is_complete, actuator_toolchain_grant_includes_tool, + extdeps_external_authority_anchor, } import extdeps.tools.curl { curl_cli_tool } import extdeps.tools.websocat { websocat_cli_tool } @@ -40,3 +41,10 @@ test fn srv3_actuator_toolchain_grant_is_fail_closed_on_websocat() -> Bool { test fn srv3_actuator_host_label_is_declared() -> Bool { srv3_os_install_actuator_host_label == "install-actuator-host" } + +test fn srv3_actuator_toolchain_authority_cites_openbmc_virtual_media() -> Bool { + string_contains( + s: extdeps_external_authority_anchor.uri.locator, + pattern: "openbmc/docs/blob/master/designs/virtual-media.md" + ) +} diff --git a/src/v2/lens/medium_structure_containment.dag b/src/v2/lens/medium_structure_containment.dag index 8634b69b22e..6312354d490 100644 --- a/src/v2/lens/medium_structure_containment.dag +++ b/src/v2/lens/medium_structure_containment.dag @@ -160,7 +160,9 @@ data medium_structure_exception_roster: List = [ "dsl/test/claim/githooks_pre_push_emit_test.dag", "dsl/test/claim/srv3_os_install_emit_test.dag", - "dsl/test/claim/srv3_install_server_emit_test.dag" + "dsl/test/claim/srv3_install_server_emit_test.dag", + + "dsl/extdeps/bmc/webui/nbd_proxy_serve.dag" ] fn char_eq(a: Char, b: Char) -> Bool { From bdaede33d660977cf3a415001adcf8edb71a110d Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 1 Jul 2026 18:27:28 +0000 Subject: [PATCH 12/16] Tighten nbd_proxy_serve websocat foreground witness predicate. Match the final ShellProgram statement's leading command word against websocat_cli_tool.name so the witness discriminates ordering, not just statement count. Co-authored-by: Cursor --- dsl/extdeps/bmc/webui/nbd_proxy_serve.dag | 25 ++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag b/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag index 136728e9234..20b030a5431 100644 --- a/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag +++ b/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag @@ -130,9 +130,32 @@ fn nbd_proxy_serve_program_statement_count(intent: NbdProxyServeTransportIntent) nbd_proxy_serve_program(intent: intent).statements.length() } +fn shell_command_leading_lit_text(stmt: ShellStmt) -> String { + match stmt { + Command { words: ws } => fold(ws, init: "", f: (acc, w) => { + if acc != "" { + acc + } else { + match w { + Lit { text: t } => t + _ => "" + } + } + }) + _ => "" + } +} + +fn shell_program_final_statement(p: ShellProgram) -> ShellStmt { + fold(p.statements, init: RawLine { text: "" }, f: (_, stmt) => stmt) +} + fn nbd_proxy_serve_program_foreground_command_is_websocat(intent: NbdProxyServeTransportIntent) -> Bool { + let program = nbd_proxy_serve_program(intent: intent) nbd_proxy_serve_program_statement_count(intent: intent) == 5 - && nbd_proxy_serve_cited_tool_row_count() == 3 + && shell_command_leading_lit_text( + stmt: shell_program_final_statement(p: program) + ) == websocat_cli_tool.name } fn nbd_proxy_serve_requires_host_as_nbd_server() -> Bool { From 3a6761ee67cabefe93cad600a818eeda5f5ed7fc Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 1 Jul 2026 18:47:10 +0000 Subject: [PATCH 13/16] Import bash AST types for nbd_proxy_serve witness helpers. Explicitly import ShellStmt, Command, Lit, RawLine, and ShellWord so shell_command_leading_lit_text and shell_program_final_statement resolve and typecheck. Co-authored-by: Cursor --- dsl/extdeps/bmc/webui/nbd_proxy_serve.dag | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag b/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag index 20b030a5431..c3ffcc9b9dd 100644 --- a/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag +++ b/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag @@ -8,7 +8,12 @@ import extdeps.tools.websocat { websocat_cli_tool } import extdeps.tools.nbdkit { nbdkit_cli_tool } import extdeps.tools.socat { socat_cli_tool } import extdeps.languages.bash.program { + ShellWord, + ShellStmt, ShellProgram, + Lit, + Command, + RawLine, lit, command, assign, From 54d5d472f14be1e592c118302069ce19fd041c54 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 1 Jul 2026 19:01:05 +0000 Subject: [PATCH 14/16] Inline delivery match in srv3 witness; drop is_* helper. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove os_install_delivery_is_network_boot — the witness already pattern-matches OsInstallDelivery directly per predicate-dissolution. Co-authored-by: Cursor --- dsl/gunbc/os_install.dag | 7 ------- dsl/test/claim/srv3_os_install_witness_test.dag | 3 --- 2 files changed, 10 deletions(-) diff --git a/dsl/gunbc/os_install.dag b/dsl/gunbc/os_install.dag index 755a6154071..0817351c7b4 100644 --- a/dsl/gunbc/os_install.dag +++ b/dsl/gunbc/os_install.dag @@ -61,13 +61,6 @@ fn os_install_delivery_for(mechanism: OsInstallMechanism, pxe_chain: NetworkBoot } } -fn os_install_delivery_is_network_boot(delivery: OsInstallDelivery) -> Bool { - match delivery { - NetworkBoot { delivery: _ } => true - VirtualMediaIso { autoinstall: _ } => false - } -} - type LanInstallInfraSurvey { existing_dhcp_configurable: Bool dedicated_install_server_present: Bool diff --git a/dsl/test/claim/srv3_os_install_witness_test.dag b/dsl/test/claim/srv3_os_install_witness_test.dag index dc373acb119..30bceb1c68d 100644 --- a/dsl/test/claim/srv3_os_install_witness_test.dag +++ b/dsl/test/claim/srv3_os_install_witness_test.dag @@ -20,10 +20,8 @@ import gunbc.os_install { survey_implied_dhcp_direction, srv3_pxe_network_boot_chain, srv3_os_install_plan, - OsInstallDelivery, NetworkBoot, VirtualMediaIso, - os_install_delivery_is_network_boot, srv3_os_install_plan_autoinstall, fleet_install_server_specs, } @@ -77,7 +75,6 @@ test fn srv3_plan_is_solver_derived_nbd_proxy_virtual_media() -> Bool { NbdProxyVirtualMediaInstall => true _ => false } - && !os_install_delivery_is_network_boot(delivery: srv3_os_install_plan.delivery) && match srv3_os_install_plan.delivery { VirtualMediaIso { autoinstall: _ } => true NetworkBoot { delivery: _ } => false From a225da8f3d9854dd3f143e06bdf7045a849242b4 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 1 Jul 2026 19:16:48 +0000 Subject: [PATCH 15/16] Route ws_path through PathTemplate render; sharpen dissolve-on. Add render_path_template to std.http_path and derive bmcweb_nbd_proxy_ws_path from the existing template authority; update nbd_proxy_serve dissolution trigger to name Background/Trap/Pipeline/quoted-arg grammar debt. Co-authored-by: Cursor --- dsl/extdeps/bmc/webui/nbd_proxy.dag | 32 +++++++++++++++---- dsl/extdeps/bmc/webui/nbd_proxy_serve.dag | 2 +- dsl/std/http_path.dag | 24 ++++++++++++++ .../claim/uri_path_parse_witness_test.dag | 21 ++++++++++++ 4 files changed, 72 insertions(+), 7 deletions(-) diff --git a/dsl/extdeps/bmc/webui/nbd_proxy.dag b/dsl/extdeps/bmc/webui/nbd_proxy.dag index 32f665b8771..7300fe87b21 100644 --- a/dsl/extdeps/bmc/webui/nbd_proxy.dag +++ b/dsl/extdeps/bmc/webui/nbd_proxy.dag @@ -2,7 +2,13 @@ module extdeps.bmc.webui.nbd_proxy import std.types { Int, List, Bool, NonEmptyStr } import std.measure { ByteSize } -import std.http_path { PathTemplate, LiteralToken, ParamToken } +import std.http_path { + PathTemplate, + LiteralToken, + ParamToken, + PathParamBinding, + render_path_template, +} import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } @@ -184,15 +190,29 @@ fn bmcweb_nbd_proxy_endpoint_path_template(variant: BmcwebNbdProxyEndpointVarian } } -fn bmcweb_nbd_proxy_ws_path(variant: BmcwebNbdProxyEndpointVariant, slot: Int, index: Int) -> NonEmptyStr { +fn bmcweb_nbd_proxy_path_params( + variant: BmcwebNbdProxyEndpointVariant, + slot: Int, + index: Int, +) -> List { match variant { - NbdSlotEndpoint => - concat("/nbd/", to_string(slot)) as NonEmptyStr - VmSlotIndexEndpoint => - concat("/vm/", concat(to_string(slot), concat("/", to_string(index)))) as NonEmptyStr + NbdSlotEndpoint => [ + PathParamBinding { name: "slot", value: to_string(slot) }, + ] + VmSlotIndexEndpoint => [ + PathParamBinding { name: "slot", value: to_string(slot) }, + PathParamBinding { name: "index", value: to_string(index) }, + ] } } +fn bmcweb_nbd_proxy_ws_path(variant: BmcwebNbdProxyEndpointVariant, slot: Int, index: Int) -> NonEmptyStr { + render_path_template( + template: bmcweb_nbd_proxy_endpoint_path_template(variant: variant), + params: bmcweb_nbd_proxy_path_params(variant: variant, slot: slot, index: index), + ) as NonEmptyStr +} + type BmcwebNbdProxyWsUpgrade { endpoint: BmcwebNbdProxyEndpointVariant requires_upgrade_header: Bool diff --git a/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag b/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag index c3ffcc9b9dd..0897a73b72b 100644 --- a/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag +++ b/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag @@ -31,7 +31,7 @@ data bmcweb_session_token_env_ref: NonEmptyStr = concat("$", "BMCWEB_SESSION_TOK data srv3_nbd_proxy_local_port: Int = 10809 -data nbd_proxy_serve_bash_emit_dissolution_trigger: String = "🟡 dissolve-on: nbd_proxy_serve — held-open nbdkit→websocat→socat pipeline serialized via extdeps.languages.bash.program ShellProgram (SCAFFOLD ahead of grammar-row bash_fold emit + host_effect NbdProxyServe transport handler); medium-as-string concat retired; DISSOLVES WHEN bash_fold grammar rows realize this pipeline with ingest ∘ emit = id and host_effect_apply binds NbdProxyServe without serialize_bash; retire medium_structure_exception_roster entry for dsl/extdeps/bmc/webui/nbd_proxy_serve.dag" +data nbd_proxy_serve_bash_emit_dissolution_trigger: String = "🟡 dissolve-on: nbd_proxy_serve — held-open nbdkit→websocat→socat pipeline serialized via extdeps.languages.bash.program ShellProgram (SCAFFOLD ahead of grammar-row bash_fold emit + host_effect NbdProxyServe transport handler); shell structure is grammar-rows but Background (&), Trap (quoted kill), Pipeline, and quoted-arg forms still ride as opaque Lit text — DISSOLVES WHEN bash_fold grammar adds those nodes and ingest ∘ emit = id with host_effect_apply binding NbdProxyServe without serialize_bash; retire medium_structure_exception_roster entry for dsl/extdeps/bmc/webui/nbd_proxy_serve.dag" type NbdProxyServeTransportIntent { bmc_host: NonEmptyStr diff --git a/dsl/std/http_path.dag b/dsl/std/http_path.dag index 86acd486246..0a4d78622f9 100644 --- a/dsl/std/http_path.dag +++ b/dsl/std/http_path.dag @@ -31,3 +31,27 @@ fn last_path_param(template: PathTemplate) -> String? { Absent => none } } + +type PathParamBinding { + name: String + value: String +} + +fn path_param_value(params: List, name: String) -> String { + fold(params, init: "", f: (acc, binding) => { + if acc != "" { + acc + } else if binding.name == name { + binding.value + } else { + acc + } + }) +} + +fn render_path_template(template: PathTemplate, params: List) -> String { + fold(template.tokens, init: "", f: (acc, tok) => match tok { + LiteralToken { text: t } => concat(acc, t) + ParamToken { name: n } => concat(acc, path_param_value(params: params, name: n)) + }) +} diff --git a/dsl/test/claim/uri_path_parse_witness_test.dag b/dsl/test/claim/uri_path_parse_witness_test.dag index 6a8f44f147a..cdc5bba9f96 100644 --- a/dsl/test/claim/uri_path_parse_witness_test.dag +++ b/dsl/test/claim/uri_path_parse_witness_test.dag @@ -9,6 +9,8 @@ import std.http_path { PathTemplate, has_path_params, last_path_param, + PathParamBinding, + render_path_template, } fn param_path(prefix: String, param_name: String) -> String { @@ -83,6 +85,24 @@ fn witness_malformed_discriminates_from_valid() -> Bool { } } +fn witness_render_path_template_binds_params() -> Bool { + let template = PathTemplate { + tokens: [ + LiteralToken { text: "/vm/" }, + ParamToken { name: "slot" }, + LiteralToken { text: "/" }, + ParamToken { name: "index" }, + ], + } + render_path_template( + template: template, + params: [ + PathParamBinding { name: "slot", value: "0" }, + PathParamBinding { name: "index", value: "0" }, + ], + ) == "/vm/0/0" +} + test fn uri_path_parse_keystone_holds() -> Bool { witness_literal_path_parses() && witness_param_path_has_params() @@ -91,4 +111,5 @@ test fn uri_path_parse_keystone_holds() -> Bool { && witness_empty_root_path_parses() && witness_malformed_unclosed_brace_rejects() && witness_malformed_discriminates_from_valid() + && witness_render_path_template_binds_params() } From 27d064842dc9f101240b4da2fa4318363aa59db5 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 1 Jul 2026 19:31:12 +0000 Subject: [PATCH 16/16] Add dissolve-on markers for NBD-proxy placeholder scaffolds. Tag ISO path and export_byte_count placeholders with named dissolution triggers and witness coverage per review feedback. Co-authored-by: Cursor --- dsl/gunbc/nbd_proxy_virtual_media_install.dag | 6 +++++- .../claim/nbd_proxy_virtual_media_install_witness_test.dag | 7 +++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/dsl/gunbc/nbd_proxy_virtual_media_install.dag b/dsl/gunbc/nbd_proxy_virtual_media_install.dag index 45162b3c251..e1c63392dff 100644 --- a/dsl/gunbc/nbd_proxy_virtual_media_install.dag +++ b/dsl/gunbc/nbd_proxy_virtual_media_install.dag @@ -1,6 +1,6 @@ module gunbc.nbd_proxy_virtual_media_install -import std.types { List, Int, NonEmptyStr } +import std.types { List, Int, NonEmptyStr, String } import std.measure { ByteSize, byte_size } import extdeps.bmc.types { RedfishBootSourceOverrideTarget, @@ -127,8 +127,12 @@ fn os_install_actuator_plan(mechanism: OsInstallMechanism, export: NbdProxyServe data srv3_os_install_actuator_host: ComputeHost = srv1_host +data srv3_nbd_proxy_iso_path_dissolution_trigger: String = "🟡 dissolve-on: srv3_nbd_proxy_iso_path_placeholder — static Ubuntu 24.04 ARM64 ISO path scaffold; DISSOLVES WHEN fleet artifact registry supplies grounded NobleNumbat2404Lts ISO path for srv3 actuator host" + data srv3_nbd_proxy_iso_path_placeholder: NonEmptyStr = "/var/lib/gunbc/artifacts/ubuntu-24.04-live-server-arm64.iso" +data srv3_nbd_proxy_export_dissolution_trigger: String = "🟡 dissolve-on: srv3_nbd_proxy_export_placeholder — byte_size(0) NbdProxyServeInterface scaffold; DISSOLVES WHEN live serve binds export_byte_count from grounded ISO artifact ByteSize" + data srv3_nbd_proxy_export_placeholder: NbdProxyServeInterface = NbdProxyServeInterface { export_byte_count: byte_size(0), } diff --git a/dsl/test/claim/nbd_proxy_virtual_media_install_witness_test.dag b/dsl/test/claim/nbd_proxy_virtual_media_install_witness_test.dag index c88fa24da81..cfbf909e9e0 100644 --- a/dsl/test/claim/nbd_proxy_virtual_media_install_witness_test.dag +++ b/dsl/test/claim/nbd_proxy_virtual_media_install_witness_test.dag @@ -21,6 +21,8 @@ import gunbc.nbd_proxy_virtual_media_install { os_install_actuator_step_count, os_install_actuator_steps, srv3_nbd_proxy_export_placeholder, + srv3_nbd_proxy_iso_path_dissolution_trigger, + srv3_nbd_proxy_export_dissolution_trigger, actuator_step_to_host_effect_intent, srv3_os_install_actuator_host, srv3_os_install_actuator_toolchain_grant_row_count, @@ -105,3 +107,8 @@ test fn nbd_proxy_serve_step_maps_to_actuator_host_shell_intent() -> Bool { test fn actuator_toolchain_grant_is_wired() -> Bool { srv3_os_install_actuator_toolchain_grant_row_count() == 4 } + +test fn nbd_proxy_placeholder_scaffolds_carry_dissolve_on_markers() -> Bool { + string_contains(s: srv3_nbd_proxy_iso_path_dissolution_trigger, pattern: "dissolve-on:") + && string_contains(s: srv3_nbd_proxy_export_dissolution_trigger, pattern: "dissolve-on:") +}