H1 corp_admin_cidrs (UFW 22/9009 on the corp NIC from these only; refuses
when it would lock out the current SSH client) and corp_ssh_password_auth
(sshd drop-in, passwords only from the imaging subnet). Defaults keep the
current behaviour and print a loud warning.
H2 pxe-netplan-apply.sh: pre-flight refusal (pxe-MAC NIC holds the default
route, a DHCP lease or the route to an SSH client), run from the USB
before anything changes; post-apply checks (names, imaging IPv4, corp
IPv4 back within 30 s, SSH route) and a 120 s systemd-run auto-revert.
H3 restore() puts the previous NIC names back (two-phase rename via
temporary names, so swaps work) before generate + apply.
H4 a failed rename is fatal (restore + exit 1); the play drops the old
any-interface UFW rules only after verifying both NICs and their
per-interface SSH rules.
H5 first-boot checks the playbook exit code, retries at next boot (max 5),
writes /var/lib/pxe-firstboot.failed and /etc/issue + /etc/motd banners.
M1 build-usb.sh verifies iPXE files against the playbook pins, every wheel
against its own package's hashes, drops the unpinned pip fallback,
requires --site-vars or --no-site-vars.
M2 webapp restarts when the code, wheels or unit changed.
M3 ignore-carrier on the imaging NIC; ip_nonlocal_bind kept for the
apache/networkd boot race.
M4 menu suffix must match blancco_kver; missing kernel with no earlier
build is fatal; a live menu with the staged Blancco test entry is not
overwritten.
M5 build-proxmox-iso.sh is unsupported (hard exit); use build-usb --image.
M6 deploy-unattend.sh: password via SSHPASS env and sudo stdin only,
%q-quoted remote args, sha256 check of the live file; lint-unattend.py
reads PXE_SERVER/PXE_USER/PXE_PASS and uses sshpass -e.
M7 the playbook lints the unattend files before the force: yes copies
(lint-unattend.py ships on CIDATA as scripts/); pre-commit ACMR.
M8 Blancco initramfs build: no ignore_errors, pipefail, trap cleanup.
Also: Send-ImagingStage.ps1 deploy, carrier hook install tasks
(pxe-nic-carrier, networkd-dispatcher), L1 imaging-active hold
(allow_disruptive) and no iPXE downgrade below an Updates-page install,
L2 iso-volid, L3 kver pin read from the playbook, L4 cp -rL, L5 refuse
>= 4 GiB WinPE files, L6 PIP_DIR wipe guard, L7 per-package wheel hashes,
no online fallback, L8 check-updates.py (apt_pkg/cached compare, all
components, exit 2 on no debs / bad answers).
105 lines
4.2 KiB
Bash
Executable File
105 lines
4.2 KiB
Bash
Executable File
#!/bin/bash
|
|
#
|
|
# deploy-unattend.sh - the ONLY way to put an unattend answer file on a PXE
|
|
# server. A bad answer file does not fail at copy time: every bay imaged
|
|
# with it stops at "Windows could not parse or process unattend answer
|
|
# file" and has to be re-imaged. So this refuses to copy unless the
|
|
# linter passes, backs the live file up, swaps it in atomically, checks
|
|
# the live copy is byte-identical (sha256) to the linted one, and lints the
|
|
# copy on the server afterwards. Any check failing restores the backup.
|
|
#
|
|
# Usage: scripts/deploy-unattend.sh <image-type> <local FlatUnattendW10.xml>
|
|
# e.g. scripts/deploy-unattend.sh gea-engineer playbook/FlatUnattendW10-engineer.xml
|
|
#
|
|
# Env: PXE_SERVER (default 172.16.9.1), PXE_USER (default pxe); the
|
|
# remote password is read from PXE_PASS or prompted for. It never appears
|
|
# on a command line (local or remote): sshpass gets it from SSHPASS, sudo
|
|
# on the server from stdin.
|
|
|
|
set -euo pipefail
|
|
|
|
IMAGE="${1:?usage: $0 <image-type> <local unattend xml>}"
|
|
SRC="${2:?usage: $0 <image-type> <local unattend xml>}"
|
|
SERVER="${PXE_SERVER:-172.16.9.1}"
|
|
USER_="${PXE_USER:-pxe}"
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)"
|
|
LINT="$REPO_ROOT/scripts/lint-unattend.py"
|
|
DEST="/srv/samba/winpeapps/$IMAGE/Deploy/FlatUnattendW10.xml"
|
|
STAMP="$(date +%Y%m%d-%H%M%S)"
|
|
|
|
case "$IMAGE" in
|
|
*[!A-Za-z0-9._-]*|"") echo "bad image type: $IMAGE" >&2; exit 2 ;;
|
|
esac
|
|
case "$SERVER$USER_" in
|
|
*[!A-Za-z0-9._@:-]*) echo "bad PXE_SERVER / PXE_USER" >&2; exit 2 ;;
|
|
esac
|
|
[ -f "$SRC" ] || { echo "no such file: $SRC" >&2; exit 2; }
|
|
|
|
echo "== 1/4 lint $SRC"
|
|
if ! python3 "$LINT" --local "$SRC"; then
|
|
echo "REFUSED: the linter failed; nothing was copied." >&2
|
|
exit 1
|
|
fi
|
|
LOCAL_SHA=$(sha256sum <"$SRC" | cut -d' ' -f1)
|
|
|
|
if [ -z "${PXE_PASS:-}" ]; then
|
|
read -rs -p "password for $USER_@$SERVER: " PXE_PASS; echo
|
|
fi
|
|
export SSHPASS="$PXE_PASS" PXE_PASS PXE_USER="$USER_" PXE_SERVER="$SERVER"
|
|
SSH=(sshpass -e ssh -o StrictHostKeyChecking=no -o ConnectTimeout=8 "$USER_@$SERVER")
|
|
|
|
# remote SCRIPT ARGS...: run SCRIPT under bash on the server with ARGS. The
|
|
# first stdin line is the password; the script reads it into $P and hands
|
|
# it to "sudo -S" through a pipe (printf is a builtin: not in ps). Every
|
|
# word is %q-quoted for the remote shell.
|
|
remote() {
|
|
local script=$1; shift
|
|
local cmd
|
|
cmd="bash -c $(printf '%q' "$script") _$(printf ' %q' "$@")"
|
|
printf '%s\n' "$PXE_PASS" | "${SSH[@]}" "$cmd"
|
|
}
|
|
# Prologue every remote script starts with.
|
|
PRE='set -euo pipefail; IFS= read -r P; S() { printf "%s\n" "$P" | sudo -k -S -p "" "$@"; };'
|
|
|
|
echo "== 2/4 upload to $SERVER"
|
|
TMP=$(remote "$PRE"' mktemp /tmp/unattend.XXXXXX')
|
|
case "$TMP" in /tmp/unattend.*) ;; *) echo "remote mktemp failed: $TMP" >&2; exit 1 ;; esac
|
|
sshpass -e scp -q -o StrictHostKeyChecking=no "$SRC" "$USER_@$SERVER:$TMP"
|
|
|
|
echo "== 3/4 back up and swap $DEST"
|
|
OUT=$(remote "$PRE"'
|
|
DEST=$1 TMP=$2 STAMP=$3 WANT=$4
|
|
[ -f "$DEST" ] || { echo "no live file at $DEST" >&2; rm -f "$TMP"; exit 1; }
|
|
GOT=$(sha256sum <"$TMP" | cut -d" " -f1)
|
|
[ "$GOT" = "$WANT" ] || { echo "upload corrupted: $GOT != $WANT" >&2; rm -f "$TMP"; exit 1; }
|
|
S cp -a "$DEST" "$DEST.bak-$STAMP"
|
|
S install -m 644 -o root -g root "$TMP" "$DEST.new"
|
|
S mv -f "$DEST.new" "$DEST"
|
|
rm -f "$TMP"
|
|
echo "backup: $DEST.bak-$STAMP"
|
|
echo "live-sha256: $(sha256sum <"$DEST" | cut -d" " -f1)"' "$DEST" "$TMP" "$STAMP" "$LOCAL_SHA")
|
|
echo "$OUT"
|
|
|
|
rollback() {
|
|
echo "$1: restoring the backup." >&2
|
|
remote "$PRE"' S cp -a "$1.bak-$2" "$1"' "$DEST" "$STAMP"
|
|
exit 1
|
|
}
|
|
|
|
LIVE_SHA=$(sed -n 's/^live-sha256: //p' <<<"$OUT")
|
|
if [ "$LIVE_SHA" != "$LOCAL_SHA" ]; then
|
|
rollback "LIVE FILE sha256 $LIVE_SHA != linted copy $LOCAL_SHA"
|
|
fi
|
|
|
|
echo "== 4/4 lint the live copy"
|
|
if ! python3 "$LINT" --server "$SERVER" --image "$IMAGE" --sha256 >"${TMPDIR:-/tmp}/lint-live-$STAMP.log" 2>&1; then
|
|
cat "${TMPDIR:-/tmp}/lint-live-$STAMP.log" >&2
|
|
rollback "LIVE FILE FAILS LINT"
|
|
fi
|
|
cat "${TMPDIR:-/tmp}/lint-live-$STAMP.log"
|
|
if ! grep -q "^sha256 $LOCAL_SHA $IMAGE\$" "${TMPDIR:-/tmp}/lint-live-$STAMP.log"; then
|
|
rollback "the linter read back a different file than the one deployed"
|
|
fi
|
|
rm -f "${TMPDIR:-/tmp}/lint-live-$STAMP.log"
|
|
echo "Deployed $SRC to $IMAGE (sha256 $LOCAL_SHA)."
|