Adds the Blueforce Field OS provisioning stack on top of the existing fleet architecture, without rewriting the working systems (installer, diagnostics, Ansible, WireGuard/SSH/xRDP/RustDesk, Docker policies, monitoring, Docusaurus). ISO build (the previous builder was a skeleton that produced no medium): - boot layout is read from the base ISO at build time via `xorriso -report_el_torito as_mkisofs` and rebuilt with `-as mkisofs`; `-boot_image any replay` cannot work on Ubuntu live ISOs because the GRUB2 MBR lives in the source image's first 16 sectors and the EFI system partition is an appended partition, neither of which is a file in the ISO9660 tree - preflight now asserts El Torito boot metadata positively: xorriso exits 0 on a non-ISO input, which previously let an arbitrary file pass --check - artifact produced and verified: dist/Blueforce-Field-OS-1.0.0-amd64.iso, 6 481 917 952 bytes, SHA256 cb0cc56f...d431296, BIOS+UEFI entries preserved, /autoinstall.yaml at the media root, casper/minimal.squashfs byte-identical to the base - recorded baseline deviation: the verified medium on the build host is the Desktop flavour, so the manifest carries a baseline_deviation block instead of hiding it; Server remains one flag away Offline and identity: - offline APT contract with concrete package requirements, pin format and an audit table; builds fail closed while pins are UNPINNED - autoinstall keeps interactive-sections: [storage]: the operator confirms the target disk, and no automatic wipe is ever performed - firstboot terminal wizard for the 8-digit dealer id, three-state lifecycle (PROVISIONED_OFFLINE -> ENROLLED -> READY) with fail-closed gates, so a missing internet link or WireGuard handshake cannot fail provisioning but can never produce a false READY Fleet and migration: - five read-only Ansible playbooks for Field OS version, provisioning, enrollment, remote channels and offline readiness, all wave-scoped with a separate production override - Windows pre-migration inventory and data export extended to the full brief scope, read-only, never collecting credentials Tests: tests/run-all.sh -> 8/8 pass, exit 0. Boot acceptance (UEFI, Legacy BIOS, Secure Boot), air-gapped install, real enrollment endpoint and Windows rollback remain LAB work and are listed in IMPLEMENTATION-REPORT.md.
871 lines
38 KiB
Bash
Executable file
871 lines
38 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# Build the bootable Blueforce Field OS installation ISO from a local, verified upstream ISO.
|
|
#
|
|
# Output contract (written next to --output):
|
|
# <iso> dist/Blueforce-Field-OS-<version>-amd64.iso
|
|
# <iso>.sha256 SHA256 of the produced ISO
|
|
# <version>-manifest.yaml release manifest (field_os_version, base_os, ubuntu_version, ...)
|
|
#
|
|
# The build is flavour-agnostic: --upstream-iso may point at an Ubuntu Server or an
|
|
# Ubuntu Desktop ISO. The flavour is detected from the ISO volume label, recorded in
|
|
# the manifest as base_flavor, and never inferred from the file name alone.
|
|
#
|
|
# Safeguards that must not be weakened:
|
|
# * Nothing is downloaded. The upstream ISO and its SHA256 record must already exist
|
|
# locally, and the digest is always recomputed from the real file.
|
|
# * Device identity, dealer id, private keys and passwords are never embedded. The
|
|
# builder refuses to run when it finds them in the media inputs.
|
|
# * The media root carries autoinstall.yaml with `interactive-sections: [storage]`,
|
|
# so the operator still selects and confirms the target disk. No automatic wipe.
|
|
# * Existing boot files are never destroyed: every patched boot configuration keeps a
|
|
# pristine copy on the media as <name>.blueforce-orig.
|
|
#
|
|
# --check validates every precondition and writes nothing.
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
VERIFY_SCRIPT="$SCRIPT_DIR/verify-upstream-iso.sh"
|
|
VERSION_FILE="$SCRIPT_DIR/field-os-version"
|
|
PROVISIONING_DIR="$REPO_ROOT/provisioning"
|
|
AUTOINSTALL_DIR="$PROVISIONING_DIR/autoinstall"
|
|
FIRSTBOOT_DIR="$PROVISIONING_DIR/firstboot"
|
|
RELEASE_DIR="$PROVISIONING_DIR/release"
|
|
OFFLINE_REPO_DIR="$PROVISIONING_DIR/offline-repo"
|
|
OFFLINE_AUTODETECT="$OFFLINE_REPO_DIR/build"
|
|
DEFAULT_DIST="$REPO_ROOT/dist"
|
|
MEDIA_PREFIX="/blueforce-provisioning"
|
|
FIRSTBOOT_PATH="$MEDIA_PREFIX/firstboot"
|
|
ARCH="amd64"
|
|
REPRO_STAMP="2024010100000000"
|
|
|
|
# Identity and credential patterns that must never reach the media. Written with
|
|
# bracket expressions so this scanner cannot match its own source text.
|
|
IDENTITY_PATTERN='(BEGIN[[:space:]]+(OPENSSH|RSA|EC|DSA|PGP)[[:space:]]+PRIVATE[[:space:]]+KEY|WG_PRIVATE[[:space:]]+KEY|dealer[-_ ]?id[[:space:]]*[:=][[:space:]]*[0-9]{8}|BF-[0-9]{8}|bf-[0-9]{8})'
|
|
CREDENTIAL_PATTERN='(password|passwd|credential|api[_-]?key|access[_-]?token)[[:space:]]*[:=][[:space:]]*[^[:space:]<#]'
|
|
|
|
usage() {
|
|
cat <<'USAGE'
|
|
Usage: build-blueforce-iso.sh --upstream-iso PATH --checksum-file PATH [options]
|
|
|
|
Builds the bootable Blueforce Field OS installation medium from a local, SHA256
|
|
verified Ubuntu ISO (Server or Desktop flavour). No image is downloaded.
|
|
|
|
Required:
|
|
--upstream-iso PATH Local Ubuntu ISO used as the base medium
|
|
--checksum-file PATH SHA256SUMS record for that ISO (alias: --checksum)
|
|
|
|
Options:
|
|
--output PATH ISO path (default: dist/Blueforce-Field-OS-<version>-amd64.iso)
|
|
--version VERSION Field OS version (default: provisioning/iso/field-os-version)
|
|
--offline-repo DIR Built offline APT repository to embed (Packages, Packages.gz,
|
|
SHA256SUMS, packages.lock.tsv). Default: auto-detect
|
|
provisioning/offline-repo/build, else build without it.
|
|
--require-offline-repo Fail closed when no built offline repository is available
|
|
--volid LABEL ISO volume label (default: the upstream label, preserved so the
|
|
live-media discovery path stays untouched; recorded in the manifest)
|
|
--work-dir DIR Scratch directory (default: the output filesystem when it has
|
|
room, else $TMPDIR, else /var/tmp; needs about 2x the base ISO)
|
|
--keep-work Do not delete the scratch directory (debugging)
|
|
--reproducible Ask xorriso for fixed volume and file dates (byte-stable rebuilds)
|
|
--allow-remaster Deprecated no-op: this command always performs a real build
|
|
--check Validate every precondition, write nothing, exit 0/1
|
|
--help Show this help
|
|
|
|
Media layout produced inside the ISO:
|
|
/autoinstall.yaml assisted autoinstall (root of the medium)
|
|
/blueforce-provisioning/firstboot/ first boot payload (fixed contract path)
|
|
/blueforce-provisioning/autoinstall/ NoCloud seed copy of the same configuration
|
|
/blueforce-provisioning/offline-repo/ offline APT tooling, manifests and built repo
|
|
/blueforce-provisioning/release/ release and media metadata
|
|
USAGE
|
|
}
|
|
|
|
die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
|
|
ok() { printf 'OK: %s\n' "$*"; }
|
|
note() { printf '%s\n' "$*"; }
|
|
step() { printf '\n==> %s\n' "$*"; }
|
|
warn() { printf 'WARNING: %s\n' "$*" >&2; }
|
|
|
|
UPSTREAM=""
|
|
CHECKSUM_RECORD=""
|
|
OUTPUT=""
|
|
OUTPUT_DIR=""
|
|
ISO_NAME=""
|
|
SHA_FILE=""
|
|
MANIFEST=""
|
|
VERSION=""
|
|
OFFLINE_REPO=""
|
|
OFFLINE_MODE="none"
|
|
REQUIRE_OFFLINE=0
|
|
VOLID=""
|
|
VOLID_SOURCE="upstream-preserved"
|
|
CHECK_ONLY=0
|
|
REPRODUCIBLE=0
|
|
KEEP_WORK=0
|
|
OPT_WORK_DIR=""
|
|
WORK_DIR=""
|
|
WORK_ROOT=""
|
|
MEDIA=""
|
|
UP_IS_SIZE_KB=0
|
|
BASE_FLAVOR="unknown"
|
|
UBUNTU_VERSION="unknown"
|
|
UP_VOLID=""
|
|
UPSTREAM_SHA256=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--upstream-iso) UPSTREAM="${2:-}"; shift 2 ;;
|
|
--checksum|--checksum-file) CHECKSUM_RECORD="${2:-}"; shift 2 ;;
|
|
--output) OUTPUT="${2:-}"; shift 2 ;;
|
|
--version) VERSION="${2:-}"; shift 2 ;;
|
|
--offline-repo) OFFLINE_REPO="${2:-}"; shift 2 ;;
|
|
--require-offline-repo) REQUIRE_OFFLINE=1; shift ;;
|
|
--volid) VOLID="${2:-}"; VOLID_SOURCE="explicit"; shift 2 ;;
|
|
--work-dir) OPT_WORK_DIR="${2:-}"; shift 2 ;;
|
|
--keep-work) KEEP_WORK=1; shift ;;
|
|
--reproducible) REPRODUCIBLE=1; shift ;;
|
|
--allow-remaster) warn "--allow-remaster is deprecated and ignored: this command always builds a real ISO."; shift ;;
|
|
--check) CHECK_ONLY=1; shift ;;
|
|
--help|-h) usage; exit 0 ;;
|
|
*) printf 'Unknown argument: %s\n' "$1" >&2; usage >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
[[ -n "$UPSTREAM" ]] || { printf 'Missing required argument: --upstream-iso\n' >&2; usage >&2; exit 2; }
|
|
[[ -n "$CHECKSUM_RECORD" ]] || { printf 'Missing required argument: --checksum-file\n' >&2; usage >&2; exit 2; }
|
|
|
|
# ----------------------------------------------------------------- small tools --
|
|
|
|
free_kb() {
|
|
df -Pk -- "$1" 2>/dev/null | awk 'NR==2 {print $4}'
|
|
}
|
|
|
|
existing_ancestor() {
|
|
local path="$1"
|
|
while [[ ! -d "$path" && "$path" != "/" ]]; do
|
|
path="$(dirname "$path")"
|
|
done
|
|
printf '%s' "$path"
|
|
}
|
|
|
|
detect_flavor_from_label() {
|
|
local label="$1"
|
|
case "$label" in
|
|
*Server*) printf 'server' ;;
|
|
*Desktop*) printf 'desktop' ;;
|
|
*[Uu]buntu*) printf 'desktop' ;;
|
|
*) printf 'unknown' ;;
|
|
esac
|
|
}
|
|
|
|
version_from_text() {
|
|
printf '%s' "$1" | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' | head -n 1 || true
|
|
}
|
|
|
|
base_os_string() {
|
|
if [[ "$UBUNTU_VERSION" == "unknown" ]]; then
|
|
printf 'Ubuntu %s' "$BASE_FLAVOR"
|
|
else
|
|
# Title-case the flavour for display only; the machine-readable flavour stays
|
|
# in base_flavor, so nothing depends on this cosmetic spelling.
|
|
printf 'Ubuntu %s %s LTS' "$(printf '%s' "${BASE_FLAVOR:0:1}" | tr '[:lower:]' '[:upper:]')${BASE_FLAVOR:1}" "$UBUNTU_VERSION"
|
|
fi
|
|
}
|
|
|
|
# ------------------------------------------------------------------ parameters --
|
|
|
|
resolve_parameters() {
|
|
if [[ -z "$VERSION" ]]; then
|
|
[[ -r "$VERSION_FILE" ]] || die "Field OS version file is missing: $VERSION_FILE (pass --version)"
|
|
VERSION="$(tr -d '[:space:]' < "$VERSION_FILE")"
|
|
fi
|
|
[[ "$VERSION" =~ ^[0-9]+(\.[0-9]+){1,3}([-.+][0-9A-Za-z.-]+)?$ ]] \
|
|
|| die "Invalid Field OS version '${VERSION}': expected e.g. 1.0.0 or 1.1.0-rc1"
|
|
|
|
ISO_NAME="Blueforce-Field-OS-${VERSION}-${ARCH}.iso"
|
|
if [[ -z "$OUTPUT" ]]; then
|
|
OUTPUT="$DEFAULT_DIST/$ISO_NAME"
|
|
elif [[ "$OUTPUT" != /* ]]; then
|
|
OUTPUT="$PWD/$OUTPUT"
|
|
fi
|
|
[[ "$(basename "$OUTPUT")" == *.iso ]] || die "--output must name an .iso file: $OUTPUT"
|
|
OUTPUT_DIR="$(dirname "$OUTPUT")"
|
|
SHA_FILE="${OUTPUT}.sha256"
|
|
MANIFEST="$OUTPUT_DIR/${VERSION}-manifest.yaml"
|
|
|
|
case "$(basename "$UPSTREAM")" in
|
|
*"${ARCH}"*) : ;;
|
|
*i386*|*arm64*|*ppc64el*|*s390x*) die "Only ${ARCH} media is supported; got: $(basename "$UPSTREAM")" ;;
|
|
*) warn "Upstream ISO name does not mention ${ARCH}: $(basename "$UPSTREAM")" ;;
|
|
esac
|
|
}
|
|
|
|
require_media_inputs() {
|
|
local missing=() file
|
|
local -a required=(
|
|
"$AUTOINSTALL_DIR/autoinstall.yaml"
|
|
"$AUTOINSTALL_DIR/user-data"
|
|
"$AUTOINSTALL_DIR/meta-data"
|
|
"$FIRSTBOOT_DIR/bf-firstboot"
|
|
"$FIRSTBOOT_DIR/blueforce-firstboot.service"
|
|
"$RELEASE_DIR/manifest.yaml"
|
|
)
|
|
for file in "${required[@]}"; do
|
|
[[ -f "$file" ]] || missing+=("${file#"$REPO_ROOT"/}")
|
|
done
|
|
[[ -d "$OFFLINE_REPO_DIR" ]] || missing+=("${OFFLINE_REPO_DIR#"$REPO_ROOT"/}/")
|
|
[[ ${#missing[@]} -eq 0 ]] || die "Required provisioning inputs are missing: ${missing[*]}"
|
|
ok "provisioning inputs present (autoinstall, firstboot, offline-repo tooling, release manifest)"
|
|
}
|
|
|
|
resolve_offline_repo() {
|
|
local missing=() marker
|
|
if [[ -n "$OFFLINE_REPO" && "$OFFLINE_REPO" != "none" ]]; then
|
|
[[ -d "$OFFLINE_REPO" ]] || die "Offline repository directory not found: $OFFLINE_REPO"
|
|
elif [[ -z "$OFFLINE_REPO" && -d "$OFFLINE_AUTODETECT" ]]; then
|
|
OFFLINE_REPO="$OFFLINE_AUTODETECT"
|
|
else
|
|
OFFLINE_REPO=""
|
|
fi
|
|
|
|
if [[ -z "$OFFLINE_REPO" ]]; then
|
|
if [[ "$REQUIRE_OFFLINE" -eq 1 ]]; then
|
|
die "No built offline APT repository found (looked at ${OFFLINE_AUTODETECT#"$REPO_ROOT"/} and --offline-repo). Build one with provisioning/offline-repo/build-offline-repo.sh, then retry."
|
|
fi
|
|
OFFLINE_MODE="not-included"
|
|
warn "No built offline APT repository is embedded: the medium ships the offline tooling and manifests only."
|
|
warn "Package installation on the installed system stays OFFLINE BLOCKED until a repository is built and re-embedded."
|
|
return
|
|
fi
|
|
|
|
for marker in Packages Packages.gz SHA256SUMS packages.lock.tsv; do
|
|
[[ -f "$OFFLINE_REPO/$marker" ]] || missing+=("$marker")
|
|
done
|
|
[[ ${#missing[@]} -eq 0 ]] || die "Offline repository $OFFLINE_REPO is incomplete; missing: ${missing[*]}"
|
|
OFFLINE_MODE="embedded"
|
|
ok "offline APT repository will be embedded: $OFFLINE_REPO"
|
|
}
|
|
|
|
scan_media_inputs() {
|
|
local -a scan_dirs=("$AUTOINSTALL_DIR" "$FIRSTBOOT_DIR" "$RELEASE_DIR" "$OFFLINE_REPO_DIR")
|
|
local hits
|
|
hits="$(grep -RIlE --exclude='*.md' -- "$IDENTITY_PATTERN" "${scan_dirs[@]}" 2>/dev/null || true)"
|
|
[[ -z "$hits" ]] || die "Device identity found in media inputs; refusing to build:"$'\n'"$hits"
|
|
hits="$(grep -RIlE --exclude='*.md' -- "$CREDENTIAL_PATTERN" "${scan_dirs[@]}" 2>/dev/null || true)"
|
|
[[ -z "$hits" ]] || die "Credential-like value found in media inputs; refusing to build:"$'\n'"$hits"
|
|
ok "media inputs carry no device identity, private key or credential"
|
|
}
|
|
|
|
resolve_work_root() {
|
|
local need_kb=$(( UP_IS_SIZE_KB * 2 + 1048576 )) candidate available detail=""
|
|
local -a candidates=()
|
|
if [[ -n "$OPT_WORK_DIR" ]]; then
|
|
candidates=("$OPT_WORK_DIR")
|
|
elif [[ "$CHECK_ONLY" -eq 1 ]]; then
|
|
# Dry runs create no directories, so judge the filesystem the output will land on.
|
|
candidates=("$(existing_ancestor "$OUTPUT_DIR")" "${TMPDIR:-/tmp}" "/var/tmp")
|
|
else
|
|
# Real builds reuse the output directory, which keeps the scratch tree on the same
|
|
# filesystem as the artifact and inside a directory that already has to exist.
|
|
install -d "$OUTPUT_DIR" || die "Could not create the output directory: $OUTPUT_DIR"
|
|
candidates=("$OUTPUT_DIR" "${TMPDIR:-/tmp}" "/var/tmp")
|
|
fi
|
|
for candidate in "${candidates[@]}"; do
|
|
if [[ ! -d "$candidate" ]]; then
|
|
detail+=" ${candidate}: not a directory"$'\n'
|
|
continue
|
|
fi
|
|
available="$(free_kb "$candidate")"
|
|
if [[ -n "$available" && "$available" -ge "$need_kb" ]]; then
|
|
WORK_ROOT="$candidate"
|
|
return 0
|
|
fi
|
|
detail+=" ${candidate}: $(( ${available:-0} / 1024 )) MiB free"$'\n'
|
|
done
|
|
die "Not enough scratch space. This build extracts the base ISO and writes a new one, so it needs about $(( need_kb / 1024 )) MiB free. Checked:
|
|
${detail}Pass --work-dir with a directory that has room. Nothing is downloaded to satisfy this."
|
|
}
|
|
|
|
# ------------------------------------------------------------------- preflight --
|
|
|
|
preflight() {
|
|
step "Preconditions"
|
|
|
|
[[ -f "$UPSTREAM" ]] || die "Upstream ISO not found: $UPSTREAM"
|
|
[[ -s "$UPSTREAM" ]] || die "Upstream ISO is empty: $UPSTREAM"
|
|
UP_IS_SIZE_KB=$(( $(stat -c %s -- "$UPSTREAM") / 1024 ))
|
|
ok "upstream ISO present: $(basename "$UPSTREAM") ($(( UP_IS_SIZE_KB / 1024 )) MiB)"
|
|
[[ -f "$CHECKSUM_RECORD" ]] || die "Checksum record not found: $CHECKSUM_RECORD"
|
|
|
|
command -v xorriso >/dev/null 2>&1 \
|
|
|| die "xorriso is required to build bootable media and was not found in PATH. Install it (for example: apt-get install xorriso) and retry. Nothing was written."
|
|
ok "xorriso available: $(xorriso --version 2>/dev/null | awk 'NR==1 {print $2}')"
|
|
|
|
local boot_report
|
|
boot_report="$(xorriso -indev "$UPSTREAM" -report_el_torito plain 2>&1)" \
|
|
|| die "xorriso cannot read El Torito boot metadata from $(basename "$UPSTREAM"); refusing to build. Nothing was written."
|
|
# xorriso exits 0 even when the input is not an ISO at all: it prints
|
|
# "No ISO 9660 image at LBA 0" and reports nothing. Combined with a checksum
|
|
# record that happens to match, that would let a non-ISO file through the
|
|
# preflight, so assert positively that boot metadata was actually produced.
|
|
grep -qE 'El Torito boot img' <<<"$boot_report" \
|
|
|| die "$(basename "$UPSTREAM") is not usable as an installation ISO: no El Torito boot image was reported. Nothing was written."
|
|
grep -qE 'El Torito boot img.*BIOS' <<<"$boot_report" \
|
|
|| warn "upstream ISO reports no BIOS El Torito entry; the produced medium may not boot on Legacy BIOS"
|
|
grep -qE 'El Torito boot img.*(UEFI|EFI)' <<<"$boot_report" \
|
|
|| warn "upstream ISO reports no UEFI El Torito entry; the produced medium may not boot on UEFI"
|
|
ok "El Torito boot metadata readable from the upstream ISO"
|
|
|
|
UP_VOLID="$(sed -n 's/^Volume id[^:]*:[[:space:]]*//p' <<<"$boot_report" | head -n 1 | tr -d "\"'" || true)"
|
|
UP_VOLID="${UP_VOLID:-unknown}"
|
|
BASE_FLAVOR="$(detect_flavor_from_label "$UP_VOLID")"
|
|
UBUNTU_VERSION="$(version_from_text "$UP_VOLID")"
|
|
[[ -n "$UBUNTU_VERSION" ]] || UBUNTU_VERSION="unknown"
|
|
ok "base label '${UP_VOLID}' => base_flavor=${BASE_FLAVOR}, ubuntu_version=${UBUNTU_VERSION}"
|
|
if [[ "$BASE_FLAVOR" != "server" ]]; then
|
|
warn "Base medium is not the documented Server baseline (docs/24 K-01, docs/26)."
|
|
warn "The deviation is recorded in the manifest as base_flavor=${BASE_FLAVOR}; a Server ISO can be used with --upstream-iso."
|
|
fi
|
|
|
|
[[ -n "$VOLID" ]] || VOLID="$UP_VOLID"
|
|
[[ "$VOLID" != "unknown" ]] || VOLID="BLUEFORCE_FIELD_OS"
|
|
(( ${#VOLID} <= 32 )) || die "Volume label too long for ISO 9660 (max 32 characters): $VOLID"
|
|
ok "output volume label: '${VOLID}' (${VOLID_SOURCE})"
|
|
|
|
require_media_inputs
|
|
resolve_offline_repo
|
|
|
|
local -a existing=()
|
|
[[ -e "$OUTPUT" ]] && existing+=("$OUTPUT")
|
|
[[ -e "$SHA_FILE" ]] && existing+=("$SHA_FILE")
|
|
[[ -e "$MANIFEST" ]] && existing+=("$MANIFEST")
|
|
[[ ${#existing[@]} -eq 0 ]] || die "Refusing to overwrite existing artifacts: ${existing[*]}"
|
|
ok "no existing artifact would be overwritten"
|
|
|
|
scan_media_inputs
|
|
|
|
local verify_output
|
|
if ! verify_output="$("$VERIFY_SCRIPT" --iso "$UPSTREAM" --checksum "$CHECKSUM_RECORD" --check 2>&1)"; then
|
|
printf '%s\n' "$verify_output" >&2
|
|
die "Upstream ISO failed SHA256 verification against $CHECKSUM_RECORD"
|
|
fi
|
|
UPSTREAM_SHA256="$(sha256sum -- "$UPSTREAM" | awk '{print $1}')"
|
|
ok "upstream SHA256 recomputed from the real file: $UPSTREAM_SHA256"
|
|
|
|
resolve_work_root
|
|
ok "scratch directory will be created under: ${WORK_ROOT} (about $(( (UP_IS_SIZE_KB * 2 + 1048576) / 1024 )) MiB required)"
|
|
|
|
ok "output contract: $OUTPUT"
|
|
ok " ${SHA_FILE}"
|
|
ok " ${MANIFEST}"
|
|
}
|
|
|
|
# -------------------------------------------------------------- boot injection --
|
|
|
|
# Insert the `autoinstall` kernel argument into a boot configuration file without
|
|
# damaging the original: the pristine copy is kept on the medium as <name>.blueforce-orig.
|
|
# kind=grub -> `linux`/`linuxefi` lines get the argument right after the kernel path,
|
|
# ahead of the `---` separator.
|
|
# kind=isolinux -> `append` lines get the argument right after the keyword.
|
|
# Re-running is idempotent: lines that already request autoinstall are left untouched.
|
|
inject_autoinstall_arg() {
|
|
local file="$1" kind="$2" out="${1}.blueforce-inject.tmp" line changed=0
|
|
: > "$out"
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
if [[ "$kind" == "grub" ]] \
|
|
&& [[ "$line" =~ ^([[:space:]]*(linux|linuxefi)[[:space:]]+)([^[:space:]]+)(.*)$ ]] \
|
|
&& [[ "$line" != *autoinstall* ]]; then
|
|
printf '%s%s autoinstall%s\n' "${BASH_REMATCH[1]}" "${BASH_REMATCH[3]}" "${BASH_REMATCH[4]}" >> "$out"
|
|
changed=$((changed + 1))
|
|
continue
|
|
fi
|
|
if [[ "$kind" == "isolinux" ]] \
|
|
&& [[ "$line" =~ ^([[:space:]]*(append|APPEND)[[:space:]]+)(.*)$ ]] \
|
|
&& [[ "$line" != *autoinstall* ]]; then
|
|
printf '%sautoinstall %s\n' "${BASH_REMATCH[1]}" "${BASH_REMATCH[3]}" >> "$out"
|
|
changed=$((changed + 1))
|
|
continue
|
|
fi
|
|
printf '%s\n' "$line" >> "$out"
|
|
done < "$file"
|
|
|
|
if [[ "$changed" -gt 0 ]]; then
|
|
[[ -e "${file}.blueforce-orig" ]] || cp -p "$file" "${file}.blueforce-orig"
|
|
mv "$out" "$file"
|
|
else
|
|
rm -f "$out"
|
|
fi
|
|
printf '%d' "$changed"
|
|
}
|
|
|
|
inject_boot_configuration() {
|
|
step "Boot configuration"
|
|
local -a grub_files=(
|
|
"$MEDIA/boot/grub/grub.cfg"
|
|
"$MEDIA/boot/grub/loopback.cfg"
|
|
"$MEDIA/EFI/boot/grub.cfg"
|
|
)
|
|
local -a isolinux_files=(
|
|
"$MEDIA/isolinux/txt.cfg"
|
|
"$MEDIA/isolinux/isolinux.cfg"
|
|
"$MEDIA/isolinux/gtk.cfg"
|
|
)
|
|
local file changed grub_total=0 isolinux_total=0
|
|
for file in "${grub_files[@]}"; do
|
|
[[ -f "$file" ]] || continue
|
|
changed="$(inject_autoinstall_arg "$file" grub)"
|
|
grub_total=$((grub_total + changed))
|
|
ok "GRUB ${file#"$MEDIA"/}: ${changed} kernel line(s) now request autoinstall"
|
|
done
|
|
for file in "${isolinux_files[@]}"; do
|
|
[[ -f "$file" ]] || continue
|
|
changed="$(inject_autoinstall_arg "$file" isolinux)"
|
|
isolinux_total=$((isolinux_total + changed))
|
|
ok "isolinux ${file#"$MEDIA"/}: ${changed} append line(s) now request autoinstall"
|
|
done
|
|
|
|
[[ $((grub_total + isolinux_total)) -gt 0 ]] \
|
|
|| die "No recognised boot configuration (GRUB or isolinux) was found in the upstream ISO; refusing to write a medium that cannot start the installer."
|
|
if [[ "$grub_total" -eq 0 ]]; then
|
|
warn "No GRUB configuration was patched: automatic autoinstall start is not prepared for this base ISO."
|
|
fi
|
|
if [[ "$isolinux_total" -eq 0 ]]; then
|
|
note "note: this base ISO carries no isolinux configuration (Legacy BIOS boots through the El Torito GRUB image, which reads the patched GRUB configuration)."
|
|
fi
|
|
}
|
|
|
|
# --------------------------------------------------------------------- staging --
|
|
|
|
write_media_metadata() {
|
|
local release_file="$MEDIA$MEDIA_PREFIX/release/blueforce-release"
|
|
cat > "$release_file" <<EOF
|
|
# Blueforce Field OS release identity, installed as /etc/blueforce-release.
|
|
# Device identity, dealer id and provisioning state are written by the firstboot
|
|
# installer on the target, never by the installation medium.
|
|
FIELD_OS_VERSION=${VERSION}
|
|
BASE_OS=$(base_os_string)
|
|
BASE_FLAVOR=${BASE_FLAVOR}
|
|
UBUNTU_VERSION=${UBUNTU_VERSION}
|
|
BASE_ISO_FILENAME=$(basename "$UPSTREAM")
|
|
BASE_ISO_SHA256=${UPSTREAM_SHA256}
|
|
ARCHITECTURE=${ARCH}
|
|
FIRSTBOOT_PATH=${FIRSTBOOT_PATH}
|
|
AUTOINSTALL_CONFIG=/autoinstall.yaml
|
|
OFFLINE_REPO_STATUS=${OFFLINE_MODE}
|
|
EOF
|
|
chmod 0644 "$release_file"
|
|
|
|
cat > "$MEDIA$MEDIA_PREFIX/release/media-info.yaml" <<EOF
|
|
schema_version: 1
|
|
field_os_version: "${VERSION}"
|
|
base_os: "$(base_os_string)"
|
|
base_flavor: "${BASE_FLAVOR}"
|
|
base_iso_filename: "$(basename "$UPSTREAM")"
|
|
base_iso_sha256: "${UPSTREAM_SHA256}"
|
|
ubuntu_version: "${UBUNTU_VERSION}"
|
|
architecture: "${ARCH}"
|
|
iso_volid: "${VOLID}"
|
|
autoinstall_config: "/autoinstall.yaml"
|
|
interactive_sections: [storage]
|
|
apt_fallback: offline-install
|
|
firstboot_path: "${FIRSTBOOT_PATH}"
|
|
offline_repo_status: "${OFFLINE_MODE}"
|
|
build_timestamp: omitted-on-purpose
|
|
EOF
|
|
chmod 0644 "$MEDIA$MEDIA_PREFIX/release/media-info.yaml"
|
|
# Generated files get a fixed modification time so a rebuild from identical inputs
|
|
# differs only in what --reproducible pins down in xorriso.
|
|
touch -t "${REPRO_STAMP:0:8}${REPRO_STAMP:8:4}.${REPRO_STAMP:12:2}" "$release_file" \
|
|
"$MEDIA$MEDIA_PREFIX/release/media-info.yaml"
|
|
ok "release metadata written (no build timestamp is embedded in the medium)"
|
|
}
|
|
|
|
prepare_media() {
|
|
step "Staging media tree"
|
|
MEDIA="$WORK_DIR/media"
|
|
install -d "$MEDIA"
|
|
|
|
if ! xorriso -osirrox on -indev "$UPSTREAM" -extract / "$MEDIA" >"$WORK_DIR/extract.log" 2>&1; then
|
|
tail -n 15 "$WORK_DIR/extract.log" >&2
|
|
die "ISO filesystem extraction failed (log: $WORK_DIR/extract.log)"
|
|
fi
|
|
chmod -R u+w "$MEDIA"
|
|
ok "upstream tree extracted to the scratch directory"
|
|
|
|
install -d "$MEDIA$MEDIA_PREFIX/autoinstall" "$MEDIA$MEDIA_PREFIX/firstboot" \
|
|
"$MEDIA$MEDIA_PREFIX/offline-repo" "$MEDIA$MEDIA_PREFIX/release"
|
|
|
|
install -m 0644 "$AUTOINSTALL_DIR/autoinstall.yaml" "$MEDIA/autoinstall.yaml"
|
|
install -m 0644 "$AUTOINSTALL_DIR/autoinstall.yaml" "$MEDIA$MEDIA_PREFIX/autoinstall/autoinstall.yaml"
|
|
install -m 0644 "$AUTOINSTALL_DIR/user-data" "$MEDIA$MEDIA_PREFIX/autoinstall/user-data"
|
|
install -m 0644 "$AUTOINSTALL_DIR/meta-data" "$MEDIA$MEDIA_PREFIX/autoinstall/meta-data"
|
|
if [[ -f "$AUTOINSTALL_DIR/README.md" ]]; then
|
|
install -m 0644 "$AUTOINSTALL_DIR/README.md" "$MEDIA$MEDIA_PREFIX/autoinstall/README.md"
|
|
fi
|
|
ok "autoinstall configuration placed at /autoinstall.yaml and ${MEDIA_PREFIX}/autoinstall/"
|
|
|
|
install -m 0755 "$FIRSTBOOT_DIR/bf-firstboot" "$MEDIA$FIRSTBOOT_PATH/bf-firstboot"
|
|
install -m 0644 "$FIRSTBOOT_DIR/blueforce-firstboot.service" "$MEDIA$FIRSTBOOT_PATH/blueforce-firstboot.service"
|
|
if [[ -f "$FIRSTBOOT_DIR/README.md" ]]; then
|
|
install -m 0644 "$FIRSTBOOT_DIR/README.md" "$MEDIA$FIRSTBOOT_PATH/README.md"
|
|
fi
|
|
ok "firstboot payload placed at ${FIRSTBOOT_PATH}/"
|
|
|
|
cp -a "$OFFLINE_REPO_DIR/." "$MEDIA$MEDIA_PREFIX/offline-repo/"
|
|
rm -f "$MEDIA$MEDIA_PREFIX/offline-repo/packages/.gitkeep"
|
|
if [[ "$OFFLINE_MODE" == "embedded" ]]; then
|
|
install -d "$MEDIA$MEDIA_PREFIX/offline-repo/built"
|
|
cp -a "$OFFLINE_REPO/." "$MEDIA$MEDIA_PREFIX/offline-repo/built/"
|
|
ok "offline APT repository placed at ${MEDIA_PREFIX}/offline-repo/built/"
|
|
else
|
|
ok "offline APT tooling and manifests placed at ${MEDIA_PREFIX}/offline-repo/ (no built repository)"
|
|
fi
|
|
|
|
install -m 0644 "$RELEASE_DIR/manifest.yaml" "$MEDIA$MEDIA_PREFIX/release/manifest.yaml"
|
|
write_media_metadata
|
|
chmod -R a+rX "$MEDIA$MEDIA_PREFIX"
|
|
}
|
|
|
|
# --------------------------------------------------------------------- ISO out --
|
|
|
|
# Rebuild the boot structure from the base ISO's own layout.
|
|
#
|
|
# Why this is not `-boot_image any replay`:
|
|
# Ubuntu live ISOs carry boot information in two places that a replay cannot
|
|
# re-establish, because neither is a data file inside the ISO9660 tree:
|
|
# 1. the GRUB2 MBR boot code lives in the first 16 sectors of the *source
|
|
# image*, and
|
|
# 2. the EFI system partition is an APPENDED PARTITION, i.e. a byte range of
|
|
# the source image.
|
|
# Replaying them fails with:
|
|
# "Cannot enable EL Torito boot image #1 because it is not a data file in
|
|
# the ISO filesystem"
|
|
# "Cannot refer by GRUB2 MBR to data outside of ISO 9660 filesystem"
|
|
#
|
|
# `xorriso -report_el_torito as_mkisofs` emits the exact option set that
|
|
# reconstructs the layout, so it is read from the base ISO at build time rather
|
|
# than hard-coded. That keeps Server and Desktop flavours, and future ISO
|
|
# revisions, working without editing this script.
|
|
read_boot_layout_options() {
|
|
local lines="$WORK_DIR/boot-layout.lines" tokens="$1"
|
|
xorriso -indev "$UPSTREAM" -report_el_torito as_mkisofs >"$WORK_DIR/boot-layout.raw" 2>/dev/null || true
|
|
|
|
# Step 1: keep the mkisofs option lines only, and drop the options this script
|
|
# owns.
|
|
#
|
|
# `--modification-date` must be dropped: xorriso translates it into its own
|
|
# `-volume_date uuid`, which then rejects the value as "not an ECMA-119 time
|
|
# string". This script controls the date itself.
|
|
#
|
|
# Nothing else is rewritten. The `--interval:local_fs:...` operands inside
|
|
# `--grub2-mbr` and `-append_partition` already name the base ISO with the very
|
|
# path this build passed to xorriso, so they are used verbatim. Rewriting them
|
|
# (e.g. via sed or an extra gsub) reorders the interval fields and xorriso then
|
|
# fails with "Number text too short or too long in interval reader description
|
|
# string". If a future build ever reads and writes the base ISO under different
|
|
# paths, that rewrite has to replace the path operand in place, not prefix it.
|
|
awk '
|
|
/^-/ {
|
|
if ($0 ~ /^-V / || $0 == "-V" || $0 ~ /^-o / || $0 == "-o" || $0 ~ /^--modification-date[= ]/) next
|
|
print
|
|
}
|
|
' "$WORK_DIR/boot-layout.raw" >"$lines"
|
|
|
|
# Step 2: split each line into individual arguments.
|
|
#
|
|
# as_mkisofs quotes values that contain spaces or commas, and those quoted
|
|
# values must reach xorriso as ONE argument. Emitting one option per line and
|
|
# relying on the shell to strip the quotes does not work: the option and its
|
|
# value collapse into a single argument, and xorriso then reports
|
|
# "Unrecognized option '--grub2-mbr --interval:...'".
|
|
# So tokenise here: honour quotes while splitting, then drop the quote
|
|
# characters, and emit one argument per line for mapfile to collect.
|
|
awk '
|
|
{
|
|
line = $0; n = length(line); i = 1; token = ""; inq = 0
|
|
while (i <= n) {
|
|
c = substr(line, i, 1)
|
|
if (c == "'"'"'") { inq = !inq; i++; continue }
|
|
if (!inq && (c == " " || c == "\t")) {
|
|
if (token != "") { print token; token = "" }
|
|
i++; continue
|
|
}
|
|
token = token c; i++
|
|
}
|
|
if (token != "") print token
|
|
}
|
|
' "$lines" >"$tokens"
|
|
|
|
grep -q -- '-append_partition' "$tokens" || return 1
|
|
return 0
|
|
}
|
|
|
|
assemble_iso() {
|
|
step "Writing the ISO"
|
|
# The boot layout comes from the base ISO itself; see read_boot_layout_options.
|
|
if ! read_boot_layout_options "$WORK_DIR/boot-layout.opts"; then
|
|
die "could not read a usable boot layout from $(basename "$UPSTREAM"); refusing to write a medium that may not boot."
|
|
fi
|
|
|
|
local -a boot_opts=()
|
|
while IFS= read -r line; do
|
|
[[ -n "$line" ]] && boot_opts+=("$line")
|
|
done <"$WORK_DIR/boot-layout.opts"
|
|
step "Boot layout: ${#boot_opts[@]} option line(s) replayed from the base ISO"
|
|
|
|
local -a cmd=(xorriso -as mkisofs)
|
|
cmd+=(-V "$VOLID")
|
|
if [[ "$REPRODUCIBLE" -eq 1 ]]; then
|
|
# -as mkisofs does not know xorriso's -volume_date; the mkisofs-compatible
|
|
# spelling is --modification-date=<YYYYMMDDhhmmsscc>.
|
|
# File mtimes come from the extracted base ISO plus the files this script
|
|
# installs, and are already stable for identical inputs; --reproducible only
|
|
# has to pin the volume-level dates for a byte-stable rebuild.
|
|
cmd+=("--modification-date=$REPRO_STAMP")
|
|
fi
|
|
cmd+=("${boot_opts[@]}" -o "$OUTPUT" "$MEDIA")
|
|
|
|
# Diagnostic: record the exact argv before running it. This is what settles
|
|
# "the file looks right but xorriso still rejects an option" reports.
|
|
local -a trace_cmd=()
|
|
if [[ "${BF_ISO_TRACE:-0}" == "1" ]]; then
|
|
{ printf 'argv:'; printf ' %q' "${cmd[@]}"; printf '\n'; } >"$WORK_DIR/assemble-argv.txt"
|
|
fi
|
|
|
|
if ! "${cmd[@]}" >"$WORK_DIR/assemble.log" 2>&1; then
|
|
tail -n 20 "$WORK_DIR/assemble.log" >&2
|
|
[[ -s "$WORK_DIR/assemble-argv.txt" ]] && cat "$WORK_DIR/assemble-argv.txt" >&2
|
|
rm -f "$OUTPUT"
|
|
die "xorriso failed to write the output ISO; the incomplete file was removed."
|
|
fi
|
|
[[ -s "$OUTPUT" ]] || { rm -f "$OUTPUT"; die "xorriso reported success but produced no data."; }
|
|
ok "ISO written: $OUTPUT ($(( $(stat -c %s -- "$OUTPUT") / 1024 / 1024 )) MiB)"
|
|
}
|
|
|
|
extract_from_iso() {
|
|
local iso_path="$1" dest="$2"
|
|
rm -f "$dest"
|
|
xorriso -osirrox on -indev "$OUTPUT" -extract "$iso_path" "$dest" >/dev/null 2>&1
|
|
[[ -f "$dest" ]]
|
|
}
|
|
|
|
verify_output_iso() {
|
|
step "Verifying the produced medium"
|
|
local check_dir="$WORK_DIR/verify"
|
|
install -d "$check_dir"
|
|
|
|
xorriso -indev "$OUTPUT" -report_el_torito plain > "$WORK_DIR/boot-report.txt" 2>&1 || true
|
|
grep -qE 'El Torito boot img.*BIOS' "$WORK_DIR/boot-report.txt" \
|
|
|| { rm -f "$OUTPUT"; die "produced ISO lost its BIOS El Torito entry; the broken output was removed."; }
|
|
grep -qE 'El Torito boot img.*(UEFI|EFI)' "$WORK_DIR/boot-report.txt" \
|
|
|| { rm -f "$OUTPUT"; die "produced ISO lost its UEFI El Torito entry; the broken output was removed."; }
|
|
grep -qF "Volume id : '$VOLID'" "$WORK_DIR/boot-report.txt" \
|
|
|| warn "produced ISO reports a different volume label than requested (${VOLID})"
|
|
ok "El Torito boot entries preserved (BIOS + UEFI) and readable by xorriso"
|
|
|
|
extract_from_iso /autoinstall.yaml "$check_dir/autoinstall.yaml" \
|
|
|| { rm -f "$OUTPUT"; die "the produced ISO has no /autoinstall.yaml at the medium root."; }
|
|
grep -q 'interactive-sections: \[storage\]' "$check_dir/autoinstall.yaml" \
|
|
|| { rm -f "$OUTPUT"; die "medium autoinstall lost 'interactive-sections: [storage]'; the broken output was removed."; }
|
|
grep -q 'offline-install' "$check_dir/autoinstall.yaml" \
|
|
|| { rm -f "$OUTPUT"; die "medium autoinstall is missing the offline-install apt fallback."; }
|
|
grep -q "$MEDIA_PREFIX/firstboot" "$check_dir/autoinstall.yaml" \
|
|
|| { rm -f "$OUTPUT"; die "medium autoinstall does not place the firstboot payload from ${FIRSTBOOT_PATH}/."; }
|
|
grep -q 'blueforce-firstboot.service' "$check_dir/autoinstall.yaml" \
|
|
|| { rm -f "$OUTPUT"; die "medium autoinstall does not enable blueforce-firstboot.service."; }
|
|
ok "medium root /autoinstall.yaml verified (interactive storage, offline-install fallback, firstboot late-commands)"
|
|
|
|
extract_from_iso /boot/grub/grub.cfg "$check_dir/grub.cfg" \
|
|
|| { rm -f "$OUTPUT"; die "the produced ISO has no /boot/grub/grub.cfg; the broken output was removed."; }
|
|
grep -q 'autoinstall' "$check_dir/grub.cfg" \
|
|
|| { rm -f "$OUTPUT"; die "the medium GRUB configuration does not start autoinstall; the broken output was removed."; }
|
|
ok "medium GRUB configuration requests autoinstall on boot"
|
|
if extract_from_iso /boot/grub/grub.cfg.blueforce-orig "$check_dir/grub.cfg.orig"; then
|
|
ok "pristine GRUB configuration kept on the medium as grub.cfg.blueforce-orig"
|
|
fi
|
|
|
|
local -a expected=(
|
|
"$MEDIA_PREFIX/firstboot/bf-firstboot"
|
|
"$MEDIA_PREFIX/firstboot/blueforce-firstboot.service"
|
|
"$MEDIA_PREFIX/release/blueforce-release"
|
|
"$MEDIA_PREFIX/release/media-info.yaml"
|
|
"$MEDIA_PREFIX/release/manifest.yaml"
|
|
"$MEDIA_PREFIX/offline-repo/manifests/base-packages.txt"
|
|
)
|
|
local relative
|
|
for relative in "${expected[@]}"; do
|
|
extract_from_iso "$relative" "$check_dir/payload" \
|
|
|| { rm -f "$OUTPUT"; die "the produced ISO is missing ${relative}; the broken output was removed."; }
|
|
done
|
|
ok "firstboot, release and offline-repo payloads present in the produced ISO"
|
|
|
|
if [[ "$OFFLINE_MODE" == "embedded" ]]; then
|
|
extract_from_iso "$MEDIA_PREFIX/offline-repo/built/Packages" "$check_dir/Packages" \
|
|
|| { rm -f "$OUTPUT"; die "the offline APT index is missing from the produced ISO."; }
|
|
ok "offline APT repository index present in the produced ISO"
|
|
fi
|
|
}
|
|
|
|
# ------------------------------------------------------------------ artifacts --
|
|
|
|
package_version_from_offline_repo() {
|
|
local lock="" value name
|
|
[[ "$OFFLINE_MODE" == "embedded" ]] && lock="$OFFLINE_REPO/packages.lock.tsv"
|
|
if [[ -z "$lock" || ! -f "$lock" ]]; then
|
|
printf 'unknown'
|
|
return
|
|
fi
|
|
for name in "$@"; do
|
|
value="$(awk -F'\t' -v p="$name" 'NR > 1 && $1 == p {print $2; exit}' "$lock" | head -n 1 || true)"
|
|
if [[ -n "$value" ]]; then
|
|
printf '%s' "$value"
|
|
return
|
|
fi
|
|
done
|
|
printf 'unknown'
|
|
}
|
|
|
|
package_version_by_pattern() {
|
|
local pattern="$1" lock="" value
|
|
if [[ "$OFFLINE_MODE" != "embedded" ]]; then
|
|
printf 'unknown'
|
|
return
|
|
fi
|
|
lock="$OFFLINE_REPO/packages.lock.tsv"
|
|
[[ -f "$lock" ]] || { printf 'unknown'; return; }
|
|
value="$(awk -F'\t' -v pat="$pattern" 'NR > 1 && $1 ~ pat {print $2; exit}' "$lock" | head -n 1 || true)"
|
|
printf '%s' "${value:-unknown}"
|
|
}
|
|
|
|
write_dist_artifacts() {
|
|
step "Checksums and manifest"
|
|
local iso_sha256 build_date git_commit git_state
|
|
iso_sha256="$(sha256sum -- "$OUTPUT" | awk '{print $1}')"
|
|
printf '%s %s\n' "$iso_sha256" "$(basename "$OUTPUT")" > "$SHA_FILE"
|
|
ok "checksum written: $SHA_FILE"
|
|
|
|
build_date="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
git_commit="$(git -C "$REPO_ROOT" rev-parse --short HEAD 2>/dev/null || true)"
|
|
git_commit="${git_commit:-unknown}"
|
|
# Distinguish "clean", "dirty" and "cannot tell". If git is unavailable or the
|
|
# tree is not a repository, the status command fails and the previous form would
|
|
# silently report "clean" - a claim the build has no evidence for.
|
|
if ! git -C "$REPO_ROOT" rev-parse --git-dir >/dev/null 2>&1; then
|
|
git_state="unknown"
|
|
elif [[ -n "$(git -C "$REPO_ROOT" status --porcelain 2>/dev/null | head -n 1 || true)" ]]; then
|
|
git_state="dirty-worktree"
|
|
else
|
|
git_state="clean"
|
|
fi
|
|
|
|
local rustdesk_pkg docker_pkg xrdp_pkg wireguard_pkg meg_pkg installer_pkg packages_source
|
|
rustdesk_pkg="$(package_version_by_pattern '^(rustdesk|rustdesk-server)$')"
|
|
docker_pkg="$(package_version_from_offline_repo docker-ce docker.io docker)"
|
|
xrdp_pkg="$(package_version_from_offline_repo xrdp xorgxrdp gnome-session)"
|
|
wireguard_pkg="$(package_version_from_offline_repo wireguard wireguard-tools)"
|
|
meg_pkg="$(package_version_by_pattern '^meg')"
|
|
installer_pkg="$(package_version_by_pattern '^blueforce-installer')"
|
|
if [[ "$OFFLINE_MODE" == "embedded" ]]; then
|
|
packages_source="embedded offline APT lock file (packages.lock.tsv)"
|
|
else
|
|
packages_source="no built offline repository was embedded; every package version is reported as unknown on purpose"
|
|
fi
|
|
|
|
{
|
|
printf 'schema_version: 1\n'
|
|
printf 'field_os_version: "%s"\n' "$VERSION"
|
|
printf 'base_os: "%s"\n' "$(base_os_string)"
|
|
printf 'base_flavor: "%s"\n' "$BASE_FLAVOR"
|
|
printf 'base_iso_filename: "%s"\n' "$(basename "$UPSTREAM")"
|
|
printf 'ubuntu_version: "%s"\n' "$UBUNTU_VERSION"
|
|
printf 'ubuntu_iso_sha256: "%s"\n' "$UPSTREAM_SHA256"
|
|
printf 'git_commit: "%s"\n' "$git_commit"
|
|
printf 'git_worktree: "%s"\n' "$git_state"
|
|
printf 'build_date: "%s"\n' "$build_date"
|
|
printf 'architecture: "%s"\n' "$ARCH"
|
|
printf 'iso_volid: "%s"\n' "$VOLID"
|
|
printf 'iso_filename: "%s"\n' "$(basename "$OUTPUT")"
|
|
printf 'iso_sha256: "%s"\n' "$iso_sha256"
|
|
printf 'release_status: "lab-artifact"\n'
|
|
printf 'packages:\n'
|
|
printf ' rustdesk: "%s"\n' "$rustdesk_pkg"
|
|
printf ' docker: "%s"\n' "$docker_pkg"
|
|
printf ' xrdp: "%s"\n' "$xrdp_pkg"
|
|
printf ' wireguard: "%s"\n' "$wireguard_pkg"
|
|
printf ' meg_package_version: "%s"\n' "$meg_pkg"
|
|
printf ' blueforce_installer_version: "%s"\n' "$installer_pkg"
|
|
printf 'packages_source: "%s"\n' "$packages_source"
|
|
printf 'media:\n'
|
|
printf ' autoinstall_config: "/autoinstall.yaml"\n'
|
|
printf ' interactive_sections: [storage]\n'
|
|
printf ' apt_fallback: "offline-install"\n'
|
|
printf ' firstboot_path: "%s/"\n' "$FIRSTBOOT_PATH"
|
|
printf ' offline_repo_status: "%s"\n' "$OFFLINE_MODE"
|
|
printf ' boot: "El Torito BIOS + UEFI rebuilt from the base ISO boot layout; autoinstall requested on every kernel line"\n'
|
|
printf ' checksum_record: "%s"\n' "$(basename "$CHECKSUM_RECORD")"
|
|
printf 'safety:\n'
|
|
printf ' device_identity_embedded: false\n'
|
|
printf ' automatic_disk_wipe: false\n'
|
|
printf ' identity_collection_point: "firstboot (dealer ID is entered on the installed system)"\n'
|
|
if [[ "$BASE_FLAVOR" != "server" ]]; then
|
|
printf 'baseline_deviation:\n'
|
|
printf ' documented_baseline: "Ubuntu Server 26.04.1+ LTS (docs/24 K-01, docs/26, docs/32)"\n'
|
|
printf ' built_base: "%s"\n' "$(base_os_string)"
|
|
printf ' reason: "The only checksum-verified installation medium available on this build host is the %s ISO, and builds must not download another image."\n' "$BASE_FLAVOR"
|
|
printf ' impact: "Desktop media already ships GNOME, so the GNOME desktop layer needs no extra installation step on the target. Server media stays supported through --upstream-iso."\n'
|
|
printf ' decision_needed: "Accept the Desktop-based baseline for LAB, or rebuild against a Server ISO before production use."\n'
|
|
printf ' lab_required: true\n'
|
|
else
|
|
printf 'baseline_deviation: none\n'
|
|
fi
|
|
printf 'notes:\n'
|
|
printf ' - "The build date lives in this manifest only; the ISO itself carries no build timestamp."\n'
|
|
printf ' - "Boot acceptance (UEFI, Legacy BIOS, Secure Boot) and the autoinstall path on this flavour are LAB work; this artifact is not a production release."\n'
|
|
printf ' - "Device identity, dealer ID, private keys and passwords are never embedded; identity is collected at firstboot."\n'
|
|
} > "$MANIFEST"
|
|
ok "manifest written: $MANIFEST"
|
|
}
|
|
|
|
print_summary() {
|
|
step "Done"
|
|
note "Field OS version : $VERSION"
|
|
note "Base medium : $(base_os_string) [base_flavor=${BASE_FLAVOR}]"
|
|
note "Base ISO SHA256 : $UPSTREAM_SHA256"
|
|
note "ISO : $OUTPUT"
|
|
note "ISO checksum : $SHA_FILE"
|
|
note "Manifest : $MANIFEST"
|
|
note "Offline repo : $OFFLINE_MODE"
|
|
note ""
|
|
note "The medium boots into an operator-assisted install: storage stays interactive and"
|
|
note "device identity is entered at firstboot. UEFI/BIOS boot acceptance is still a LAB step."
|
|
}
|
|
|
|
main() {
|
|
resolve_parameters
|
|
preflight
|
|
if [[ "$CHECK_ONLY" -eq 1 ]]; then
|
|
step "Check passed"
|
|
note "All preconditions for $ISO_NAME are satisfied. Nothing was written."
|
|
exit 0
|
|
fi
|
|
|
|
WORK_DIR="$(mktemp -d "$WORK_ROOT/blueforce-iso.XXXXXX")" || die "Could not create a scratch directory under $WORK_ROOT"
|
|
cleanup() {
|
|
if [[ "$KEEP_WORK" -eq 1 ]]; then
|
|
printf 'Scratch directory kept: %s\n' "$WORK_DIR"
|
|
else
|
|
rm -rf -- "$WORK_DIR"
|
|
fi
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
prepare_media
|
|
inject_boot_configuration
|
|
assemble_iso
|
|
verify_output_iso
|
|
write_dist_artifacts
|
|
print_summary
|
|
}
|
|
|
|
main "$@"
|