#!/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 # 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 }" SRC="${2:?usage: $0 }" 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)."