diff --git a/dsl/gunbc/install_server_provision.dag b/dsl/gunbc/install_server_provision.dag new file mode 100644 index 00000000000..16a001423c3 --- /dev/null +++ b/dsl/gunbc/install_server_provision.dag @@ -0,0 +1,87 @@ +module gunbc.install_server_provision + +import std.types { String, NonEmptyStr, Bool } +import std.resources { Network } +import std.disposition { Disposition, Scaffold, RealizationDispatch } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import extdeps.shell + +data shell_as_substrate_debt: Disposition = Scaffold { + dissolves_to: RealizationDispatch, + bind: DeclarationRef { + module_path: "gunbc.host_effect", + decl_name: "HostEffect", + field: WholeDeclaration + } +} + +data install_server_host: NonEmptyStr = "srv2-lan" + +data install_server_webroot: NonEmptyStr = "/srv/install" + +data install_server_iso_path: NonEmptyStr = "/srv/ubuntu.iso" + +data ubuntu_iso_mirror_base: NonEmptyStr = "https://cdimage.ubuntu.com/releases" + +data ubuntu_iso_series: NonEmptyStr = "24.04" + +data ubuntu_iso_channel: NonEmptyStr = "release" + +data ubuntu_iso_flavor: NonEmptyStr = "live-server" + +data ubuntu_iso_arch: NonEmptyStr = "arm64" + +fn iso_discovery_env() -> String { + join([ + concat("MIRROR=", ubuntu_iso_mirror_base), + concat("SERIES=", ubuntu_iso_series), + concat("CHANNEL=", ubuntu_iso_channel), + concat("FLAVOR=", ubuntu_iso_flavor), + concat("ARCH=", ubuntu_iso_arch) + ], "; ") +} + +fn iso_resolve_prelude() -> String { + concat(iso_discovery_env(), "; BASE=\"$MIRROR/$SERIES/$CHANNEL\"; PAT=\"ubuntu-$SERIES[.0-9]*-$FLAVOR-$ARCH[.]iso\"; ISO=$(curl -sL \"$BASE/\" | grep -oE \"$PAT\" | sort -V | tail -1); if [ -z \"$ISO\" ]; then echo NO_MATCHING_ISO_FAIL_CLOSED; exit 2; fi") +} + +fn host_exec(host: NonEmptyStr, command: String) -> String { + concat(concat(concat("ssh ", host), " 'set -e; "), concat(command, "'")) +} + +fn step_apt_install() -> String { + "sudo DEBIAN_FRONTEND=noninteractive apt-get update -y; sudo DEBIAN_FRONTEND=noninteractive apt-get install -y dnsmasq grub-efi-arm64-signed rsync; echo APT_DONE" +} + +fn step_disable_stock_dnsmasq() -> String { + "if systemctl list-unit-files dnsmasq.service --no-legend 2>/dev/null | grep -q dnsmasq; then sudo systemctl disable --now dnsmasq; fi; echo DNSMASQ_SERVICE_DISABLED" +} + +fn step_make_webroot() -> String { + "sudo install -d /srv/install /srv/install/autoinstall; echo WEBROOT_READY" +} + +fn step_resolve_and_fetch_iso() -> String { + concat(iso_resolve_prelude(), "; echo RESOLVED_URL=$BASE/$ISO; if systemctl is-active --quiet srv3-isodl; then echo ISO_FETCH_IN_PROGRESS; exit 0; fi; if [ -f /srv/ubuntu.iso ]; then echo ISO_FILE_PRESENT $(du -h /srv/ubuntu.iso | cut -f1); exit 0; fi; sudo systemd-run --unit=srv3-isodl --collect wget -q -O /srv/ubuntu.iso \"$BASE/$ISO\"; echo DOWNLOAD_STARTED") +} + +fn step_verify_iso() -> String { + concat(iso_resolve_prelude(), "; cd /tmp; curl -sLO \"$BASE/SHA256SUMS\"; LINE=$(grep \"[* ]$ISO$\" SHA256SUMS); if [ -z \"$LINE\" ]; then echo NO_CHECKSUM_LINE_FAIL_CLOSED; exit 4; fi; WANT=$(echo \"$LINE\" | cut -d\" \" -f1); GOT=$(sha256sum /srv/ubuntu.iso | cut -d\" \" -f1); if [ \"$WANT\" != \"$GOT\" ]; then echo ISO_CHECKSUM_MISMATCH_FAIL_CLOSED; exit 3; fi; echo ISO_VERIFIED $GOT") +} + +pattern provision_prep_and_fetch() -> { apt: String, webroot: String, fetch: String, ok: Bool } + uses net: Network +{ + node a: shell.Exec.Run(script: host_exec(host: install_server_host, command: step_apt_install())) + node d: shell.Exec.Run(script: host_exec(host: install_server_host, command: step_disable_stock_dnsmasq())) + node w: shell.Exec.Run(script: host_exec(host: install_server_host, command: step_make_webroot())) + node f: shell.Exec.Run(script: host_exec(host: install_server_host, command: step_resolve_and_fetch_iso())) + return { apt: a.stdout, webroot: w.stdout, fetch: f.stdout, ok: a.success && d.success && w.success && f.success } +} + +pattern provision_verify_iso() -> { out: String, ok: Bool } + uses net: Network +{ + node v: shell.Exec.Run(script: host_exec(host: install_server_host, command: step_verify_iso())) + return { out: v.stdout, ok: v.success } +} diff --git a/dsl/test/claim/install_server_provision_witness_test.dag b/dsl/test/claim/install_server_provision_witness_test.dag new file mode 100644 index 00000000000..e06b5b22c44 --- /dev/null +++ b/dsl/test/claim/install_server_provision_witness_test.dag @@ -0,0 +1,66 @@ +module test.claim.install_server_provision_witness + +import std.logic { Bool } +import gunbc.install_server_provision { + iso_resolve_prelude, + host_exec, + step_apt_install, + step_disable_stock_dnsmasq, + step_resolve_and_fetch_iso, + step_verify_iso, + install_server_host, + ubuntu_iso_series, + ubuntu_iso_flavor, + ubuntu_iso_arch +} + +test fn iso_url_is_discovered_not_pinned() -> Bool { + let s = iso_resolve_prelude() + string_contains(s: s, pattern: "sort -V | tail -1") + && string_contains(s: s, pattern: "NO_MATCHING_ISO_FAIL_CLOSED") + && string_contains(s: s, pattern: "exit 2") + && (string_contains(s: s, pattern: "24.04.4") == false) + && (string_contains(s: s, pattern: "24.04.2") == false) +} + +test fn iso_url_carries_modeled_parts() -> Bool { + let s = iso_resolve_prelude() + string_contains(s: s, pattern: ubuntu_iso_series) + && string_contains(s: s, pattern: ubuntu_iso_flavor) + && string_contains(s: s, pattern: ubuntu_iso_arch) +} + +test fn host_exec_applies_fail_closed_preamble() -> Bool { + let s = host_exec(host: install_server_host, command: step_apt_install()) + string_contains(s: s, pattern: "set -e") + && string_contains(s: s, pattern: "ssh ") +} + +test fn apt_step_does_not_fail_open() -> Bool { + let s = step_apt_install() + string_contains(s: s, pattern: "apt-get install -y dnsmasq") + && (string_contains(s: s, pattern: "|| true") == false) + && (string_contains(s: s, pattern: "/tmp/isrv-apt.log") == false) +} + +test fn steps_are_transport_agnostic() -> Bool { + (string_contains(s: step_apt_install(), pattern: "ssh ") == false) + && (string_contains(s: step_resolve_and_fetch_iso(), pattern: "ssh ") == false) + && (string_contains(s: step_verify_iso(), pattern: "ssh ") == false) +} + +test fn fetch_distinguishes_presence_from_completeness() -> Bool { + let f = step_resolve_and_fetch_iso() + let v = step_verify_iso() + string_contains(s: f, pattern: "ISO_FETCH_IN_PROGRESS") + && string_contains(s: f, pattern: "ISO_FILE_PRESENT") + && string_contains(s: v, pattern: "ISO_CHECKSUM_MISMATCH_FAIL_CLOSED") + && string_contains(s: v, pattern: "sha256sum") +} + +test fn dnsmasq_disable_is_construction_gated() -> Bool { + let s = step_disable_stock_dnsmasq() + string_contains(s: s, pattern: "list-unit-files dnsmasq.service") + && string_contains(s: s, pattern: "systemctl disable --now dnsmasq") + && (string_contains(s: s, pattern: "|| true") == false) +}