Skip to content
87 changes: 87 additions & 0 deletions dsl/gunbc/install_server_provision.dag
Original file line number Diff line number Diff line change
@@ -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 }
}
66 changes: 66 additions & 0 deletions dsl/test/claim/install_server_provision_witness_test.dag
Original file line number Diff line number Diff line change
@@ -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)
}
Loading