6d50f3a804
Wrap the three install scripts in a shared inline style block (TTY/UTF-8/ NO_COLOR-aware) so the one-liner install looks and feels intentional: banner on start, timed step lines, braille spinner over silent apt/ systemctl calls with failure log dumps, single-line curl progress bars with size-prefixed headers, and a summary box at the end with live-image version + service state + next steps. install.sh defers banner/summary to proxmox-install.sh when VETTING_INSTALL_WRAPPED is set so the two scripts compose without duplication. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
455 lines
17 KiB
Bash
Executable File
455 lines
17 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# pxe-setup.sh — finish the PXE half of a vetting install.
|
|
#
|
|
# Run AFTER deploy/install.sh on the LXC (or wherever the orchestrator
|
|
# lives). Fetches pinned iPXE binaries, places the live image, and
|
|
# writes the pxe: block of /etc/vetting/vetting.yaml.
|
|
#
|
|
# dnsmasq runs in proxy-DHCP mode: it coexists with whatever DHCP
|
|
# server already serves your LAN (UniFi, pfSense, Asus, etc.) and
|
|
# only supplements the PXE options. No dedicated bridge, no VLAN,
|
|
# no cabling changes.
|
|
#
|
|
# Idempotent: safe to re-run with the same args. A second run with
|
|
# different args overwrites the pxe: block; pass --force to override
|
|
# a hand-edited block that differs from our args.
|
|
#
|
|
# Usage:
|
|
# sudo ./pxe-setup.sh \
|
|
# --interface eth0 \
|
|
# --subnet 192.168.1.0/24 \
|
|
# --orchestrator-url http://192.168.1.135:8080
|
|
#
|
|
# Optional:
|
|
# --tftp-root DIR default /var/lib/vetting/tftp
|
|
# --live-dir DIR default /var/lib/vetting/live
|
|
# --config PATH default /etc/vetting/vetting.yaml
|
|
# --bundle-dir DIR default: this script's dir (release tarball root)
|
|
# --force overwrite a customised pxe: block
|
|
set -euo pipefail
|
|
|
|
# ---- style helpers -----------------------------------------------------
|
|
# Identical block across proxmox-install.sh, install.sh, pxe-setup.sh.
|
|
# Inlined (not sourced) so the curl|bash entrypoint renders without a
|
|
# prior fetch. Respects NO_COLOR, non-TTY, and non-UTF-8 locales.
|
|
_color=0; _unicode=0
|
|
if [[ -t 2 && -z "${NO_COLOR:-}" && "${TERM:-dumb}" != "dumb" ]]; then
|
|
_color=1
|
|
fi
|
|
case "${LC_ALL:-}${LANG:-}" in
|
|
*UTF-8*|*utf8*|*UTF8*) _unicode=1 ;;
|
|
esac
|
|
if (( _color )); then
|
|
_B=$'\033[1m'; _D=$'\033[2m'; _R=$'\033[0m'
|
|
_C=$'\033[1;36m'; _G=$'\033[32m'; _Y=$'\033[33m'; _E=$'\033[31m'
|
|
else
|
|
_B=""; _D=""; _R=""; _C=""; _G=""; _Y=""; _E=""
|
|
fi
|
|
if (( _unicode )); then
|
|
_S_STEP="▸"; _S_OK="✓"; _S_WARN="⚠"; _S_FAIL="✗"; _S_INFO="·"
|
|
_B_TL="╭"; _B_TR="╮"; _B_BL="╰"; _B_BR="╯"; _B_H="─"; _B_V="│"
|
|
else
|
|
_S_STEP="-->"; _S_OK="[ok]"; _S_WARN="[!]"; _S_FAIL="[x]"; _S_INFO="*"
|
|
_B_TL="+"; _B_TR="+"; _B_BL="+"; _B_BR="+"; _B_H="-"; _B_V="|"
|
|
fi
|
|
_start_epoch="$(date +%s)"; _step_epoch=""; _quiet_log=""; _spin_pid=""
|
|
_fmt_dur() {
|
|
local s=$1
|
|
if (( s < 60 )); then printf '%ds' "$s"
|
|
elif (( s < 3600 )); then printf '%dm %ds' "$((s/60))" "$((s%60))"
|
|
else printf '%dh %dm' "$((s/3600))" "$(((s%3600)/60))"
|
|
fi
|
|
}
|
|
banner() {
|
|
local inner=" $1 " w line="" i=0
|
|
w=${#inner}
|
|
while (( i < w )); do line+="${_B_H}"; i=$((i+1)); done
|
|
printf '\n %s%s%s%s%s\n' "${_C}" "${_B_TL}" "${line}" "${_B_TR}" "${_R}" >&2
|
|
printf ' %s%s%s%s%s%s%s%s%s\n' "${_C}" "${_B_V}" "${_R}" "${_B}" "${inner}" "${_R}" "${_C}" "${_B_V}" "${_R}" >&2
|
|
printf ' %s%s%s%s%s\n\n' "${_C}" "${_B_BL}" "${line}" "${_B_BR}" "${_R}" >&2
|
|
}
|
|
step() {
|
|
_step_epoch="$(date +%s)"
|
|
printf '%s%s%s %s%s%s\n' "${_C}" "${_S_STEP}" "${_R}" "${_B}" "$1" "${_R}" >&2
|
|
}
|
|
ok() {
|
|
local tail=""
|
|
if [[ -n "${_step_epoch}" ]]; then
|
|
tail=" ${_D}($(_fmt_dur $(( $(date +%s) - _step_epoch ))))${_R}"
|
|
fi
|
|
printf ' %s%s%s %s%s\n' "${_G}" "${_S_OK}" "${_R}" "$1" "${tail}" >&2
|
|
_step_epoch=""
|
|
}
|
|
info() { printf ' %s%s %s%s\n' "${_D}" "${_S_INFO}" "$1" "${_R}" >&2; }
|
|
warn() { printf ' %s%s %s%s\n' "${_Y}" "${_S_WARN}" "$1" "${_R}" >&2; }
|
|
die() {
|
|
printf '\n%s%s %s%s\n' "${_E}" "${_S_FAIL}" "$1" "${_R}" >&2
|
|
if [[ -n "${_quiet_log:-}" && -s "${_quiet_log}" ]]; then
|
|
printf ' %s── last 40 lines of output ──%s\n' "${_D}" "${_R}" >&2
|
|
tail -n 40 "${_quiet_log}" | sed 's/^/ /' >&2
|
|
printf ' %sfull log: %s%s\n' "${_D}" "${_quiet_log}" "${_R}" >&2
|
|
fi
|
|
exit 1
|
|
}
|
|
_SPIN=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏")
|
|
_start_spin() {
|
|
(( _color && _unicode )) || return 0
|
|
local label="$1"
|
|
(
|
|
local i=0
|
|
while :; do
|
|
printf '\r %s%s%s %s ' "${_C}" "${_SPIN[i]}" "${_R}" "${label}" >&2
|
|
i=$(( (i+1) % ${#_SPIN[@]} ))
|
|
sleep 0.1
|
|
done
|
|
) &
|
|
_spin_pid=$!
|
|
}
|
|
_stop_spin() {
|
|
[[ -n "${_spin_pid}" ]] || return 0
|
|
kill "${_spin_pid}" 2>/dev/null || true
|
|
wait "${_spin_pid}" 2>/dev/null || true
|
|
_spin_pid=""
|
|
printf '\r\033[2K' >&2
|
|
}
|
|
# run_quiet "<label>" -- <cmd ...>
|
|
run_quiet() {
|
|
local label="$1"; shift
|
|
[[ "${1:-}" == "--" ]] && shift
|
|
local log start rc=0
|
|
log="$(mktemp -t vetting-install.XXXXXX)"
|
|
_quiet_log="${log}"
|
|
start="$(date +%s)"
|
|
_start_spin "${label}"
|
|
"$@" >"${log}" 2>&1 || rc=$?
|
|
_stop_spin
|
|
local dt=$(( $(date +%s) - start ))
|
|
if (( rc == 0 )); then
|
|
printf ' %s%s%s %s %s(%s)%s\n' "${_G}" "${_S_OK}" "${_R}" "${label}" "${_D}" "$(_fmt_dur "$dt")" "${_R}" >&2
|
|
rm -f "${log}"; _quiet_log=""
|
|
return 0
|
|
fi
|
|
printf ' %s%s %s failed (exit %d)%s\n' "${_E}" "${_S_FAIL}" "${label}" "${rc}" "${_R}" >&2
|
|
printf ' %s── last 40 lines of output ──%s\n' "${_D}" "${_R}" >&2
|
|
tail -n 40 "${log}" | sed 's/^/ /' >&2
|
|
printf ' %sfull log: %s%s\n' "${_D}" "${log}" "${_R}" >&2
|
|
exit "${rc}"
|
|
}
|
|
# curl_pretty --label LABEL --url URL -- [extra curl args, including -o PATH]
|
|
curl_pretty() {
|
|
local label="download" url=""
|
|
local args=()
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--label) label="$2"; shift 2 ;;
|
|
--url) url="$2"; args+=("$2"); shift 2 ;;
|
|
--) shift; args+=("$@"); break ;;
|
|
*) args+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
local size_str=""
|
|
if [[ -n "${url}" ]]; then
|
|
local size_bytes
|
|
size_bytes="$(curl -fsSLI --max-time 5 "${url}" 2>/dev/null \
|
|
| awk 'tolower($1) ~ /^content-length:/ {print $2}' \
|
|
| tr -d '\r' | tail -n1)"
|
|
if [[ "${size_bytes}" =~ ^[0-9]+$ ]]; then
|
|
local human
|
|
human="$(numfmt --to=iec --suffix=B --format='%.1f' "${size_bytes}" 2>/dev/null || echo "${size_bytes}B")"
|
|
size_str=" ${_D}(${human})${_R}"
|
|
fi
|
|
fi
|
|
printf '%s%s%s %sdownloading %s%s%s\n' "${_C}" "${_S_STEP}" "${_R}" "${_B}" "${label}" "${_R}" "${size_str}" >&2
|
|
local start rc=0
|
|
start="$(date +%s)"
|
|
if [[ -t 2 ]]; then
|
|
curl -fL --progress-bar "${args[@]}" || rc=$?
|
|
else
|
|
curl -fsSL "${args[@]}" || rc=$?
|
|
fi
|
|
local dt=$(( $(date +%s) - start ))
|
|
if (( rc == 0 )); then
|
|
printf ' %s%s%s %s ready %s(%s)%s\n' "${_G}" "${_S_OK}" "${_R}" "${label}" "${_D}" "$(_fmt_dur "$dt")" "${_R}" >&2
|
|
return 0
|
|
fi
|
|
printf ' %s%s %s download failed (exit %d)%s\n' "${_E}" "${_S_FAIL}" "${label}" "${rc}" "${_R}" >&2
|
|
exit "${rc}"
|
|
}
|
|
rule_open() {
|
|
local title="$1"
|
|
local dashes=$(( 46 - ${#title} - 4 ))
|
|
local tail="" i=0
|
|
(( dashes < 2 )) && dashes=2
|
|
while (( i < dashes )); do tail+="${_B_H}"; i=$((i+1)); done
|
|
printf '\n%s%s%s %s %s%s\n' "${_C}" "${_B_TL}" "${_B_H}" "${title}" "${tail}" "${_R}" >&2
|
|
}
|
|
rule_close() {
|
|
local line="" i=0
|
|
while (( i < 46 )); do line+="${_B_H}"; i=$((i+1)); done
|
|
printf '%s%s%s%s\n' "${_C}" "${_B_BL}" "${line:1}" "${_R}" >&2
|
|
}
|
|
total_elapsed() {
|
|
local dt=$(( $(date +%s) - _start_epoch ))
|
|
printf '%sinstalled in %s%s\n\n' "${_D}" "$(_fmt_dur "$dt")" "${_R}" >&2
|
|
}
|
|
# ---- end style helpers -------------------------------------------------
|
|
|
|
INTERFACE=""
|
|
SUBNET=""
|
|
ORCH_URL=""
|
|
TFTP_ROOT="/var/lib/vetting/tftp"
|
|
LIVE_DIR="/var/lib/vetting/live"
|
|
CONFIG="/etc/vetting/vetting.yaml"
|
|
BUNDLE_DIR=""
|
|
FORCE=0
|
|
SERVICE_USER="vetting"
|
|
|
|
# Resolve symlinks so `vetting-pxe-setup` (a symlink into /usr/local/sbin
|
|
# installed by install.sh) finds ipxe-shas.txt alongside the real script
|
|
# in /usr/local/share/vetting/.
|
|
SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
|
|
|
|
usage() {
|
|
sed -n '2,28p' "${BASH_SOURCE[0]}"
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--interface) INTERFACE="$2"; shift 2 ;;
|
|
--subnet) SUBNET="$2"; shift 2 ;;
|
|
--orchestrator-url) ORCH_URL="$2"; shift 2 ;;
|
|
--tftp-root) TFTP_ROOT="$2"; shift 2 ;;
|
|
--live-dir) LIVE_DIR="$2"; shift 2 ;;
|
|
--config) CONFIG="$2"; shift 2 ;;
|
|
--bundle-dir) BUNDLE_DIR="$2"; shift 2 ;;
|
|
--force) FORCE=1; shift ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
*) echo "unknown arg: $1" >&2; usage; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
if [[ $EUID -ne 0 ]]; then
|
|
die "pxe-setup.sh must be run as root (try: sudo $0 ...)"
|
|
fi
|
|
|
|
[[ -z "${INTERFACE}" ]] && die "--interface is required"
|
|
[[ -z "${SUBNET}" ]] && die "--subnet is required (e.g. 192.168.1.0/24)"
|
|
[[ -z "${ORCH_URL}" ]] && die "--orchestrator-url is required"
|
|
|
|
banner "TheWrightServer · Vetting · PXE"
|
|
|
|
# --- sanity checks -----------------------------------------------------
|
|
|
|
if ! ip link show "${INTERFACE}" >/dev/null 2>&1; then
|
|
die "interface ${INTERFACE} not found on host. Check \`ip link\` — the interface must exist *before* the orchestrator starts dnsmasq."
|
|
fi
|
|
|
|
# CIDR shape check — dnsmasq will re-validate, but catch the obvious
|
|
# errors before we write anything to disk.
|
|
if [[ ! "${SUBNET}" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$ ]]; then
|
|
die "--subnet must be CIDR form (e.g. 192.168.1.0/24), got '${SUBNET}'"
|
|
fi
|
|
|
|
if [[ ! -f "${CONFIG}" ]]; then
|
|
die "${CONFIG} not found — run deploy/install.sh first."
|
|
fi
|
|
|
|
if ! id -u "${SERVICE_USER}" >/dev/null 2>&1; then
|
|
die "${SERVICE_USER} user not found — run deploy/install.sh first."
|
|
fi
|
|
|
|
# Resolve the bundle dir. When pxe-setup.sh is run from a release
|
|
# tarball it sits alongside ipxe-shas.txt and a live-image/ subdir; when
|
|
# run from the repo tree it's deploy/pxe-setup.sh and the live image is
|
|
# under live-image/build/. Detect both.
|
|
if [[ -z "${BUNDLE_DIR}" ]]; then
|
|
BUNDLE_DIR="${SCRIPT_DIR}"
|
|
fi
|
|
SHAS_FILE="${BUNDLE_DIR}/ipxe-shas.txt"
|
|
if [[ ! -f "${SHAS_FILE}" ]]; then
|
|
die "${SHAS_FILE} not found — bundle is incomplete."
|
|
fi
|
|
|
|
# --- iPXE binaries: stage, verify, install ----------------------------
|
|
#
|
|
# Stage into a temp dir so a corrupt download never clobbers a known-
|
|
# good file in tftp_root. sha256sum -c must pass before we `install` —
|
|
# install(1) unlink-replaces, which avoids ETXTBSY and makes the whole
|
|
# operation atomic per file.
|
|
|
|
step "ensuring ${TFTP_ROOT} exists"
|
|
install -d -m 0755 -o "${SERVICE_USER}" -g "${SERVICE_USER}" "${TFTP_ROOT}"
|
|
info "tftp root: ${TFTP_ROOT}"
|
|
|
|
STAGE="$(mktemp -d)"
|
|
trap 'rm -rf "${STAGE}"' EXIT
|
|
|
|
need_fetch=0
|
|
for name in ipxe.efi undionly.kpxe; do
|
|
if [[ ! -f "${TFTP_ROOT}/${name}" ]]; then
|
|
need_fetch=1
|
|
break
|
|
fi
|
|
done
|
|
|
|
# Even if both files exist, re-verify against pinned SHAs. If they match
|
|
# we skip the fetch entirely; if not, re-download.
|
|
if (( ! need_fetch )); then
|
|
if ! ( cd "${TFTP_ROOT}" && sha256sum -c --status "${SHAS_FILE}" ); then
|
|
info "iPXE binaries in ${TFTP_ROOT} don't match pinned SHAs — re-fetching"
|
|
need_fetch=1
|
|
else
|
|
info "iPXE binaries already match pins — skipping fetch"
|
|
fi
|
|
fi
|
|
|
|
if (( need_fetch )); then
|
|
step "fetching iPXE binaries from boot.ipxe.org"
|
|
curl_pretty --label "ipxe.efi" --url "https://boot.ipxe.org/x86_64-efi/ipxe.efi" -- -o "${STAGE}/ipxe.efi"
|
|
curl_pretty --label "undionly.kpxe" --url "https://boot.ipxe.org/undionly.kpxe" -- -o "${STAGE}/undionly.kpxe"
|
|
|
|
step "verifying SHA256 against ${SHAS_FILE}"
|
|
if ! run_quiet "sha256sum -c" -- bash -c "cd '${STAGE}' && sha256sum -c '${SHAS_FILE}'"; then
|
|
die "iPXE SHA256 mismatch. Upstream binaries changed, or a MITM. To accept the new binaries, regenerate ${SHAS_FILE} after independently verifying the new checksums, then re-run."
|
|
fi
|
|
|
|
install -m 0644 -o "${SERVICE_USER}" -g "${SERVICE_USER}" \
|
|
"${STAGE}/ipxe.efi" "${TFTP_ROOT}/ipxe.efi"
|
|
install -m 0644 -o "${SERVICE_USER}" -g "${SERVICE_USER}" \
|
|
"${STAGE}/undionly.kpxe" "${TFTP_ROOT}/undionly.kpxe"
|
|
ok "iPXE binaries staged into ${TFTP_ROOT}"
|
|
fi
|
|
|
|
# --- live image: copy from bundle into live_dir -----------------------
|
|
|
|
# Accept two layouts: release tarball (${BUNDLE_DIR}/live-image/) or
|
|
# repo tree (${BUNDLE_DIR}/../live-image/build/).
|
|
LIVE_SRC=""
|
|
for cand in \
|
|
"${BUNDLE_DIR}/live-image" \
|
|
"${BUNDLE_DIR}/../live-image/build"; do
|
|
if [[ -f "${cand}/vmlinuz" && -f "${cand}/initrd.img" ]]; then
|
|
LIVE_SRC="${cand}"
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [[ -z "${LIVE_SRC}" ]]; then
|
|
# install.sh already stages vmlinuz + initrd.img into LIVE_DIR during
|
|
# the one-liner install, so a missing bundle/live-image/ is expected
|
|
# when pxe-setup.sh is run from /usr/local/sbin.
|
|
if [[ -f "${LIVE_DIR}/vmlinuz" && -f "${LIVE_DIR}/initrd.img" ]]; then
|
|
info "live image already staged in ${LIVE_DIR} (from install.sh)"
|
|
else
|
|
warn "no live image found under ${BUNDLE_DIR}/live-image, ${BUNDLE_DIR}/../live-image/build, or ${LIVE_DIR}."
|
|
warn "the orchestrator will fail PXE startup validation until vmlinuz + initrd.img land in ${LIVE_DIR}."
|
|
fi
|
|
else
|
|
step "staging live image from ${LIVE_SRC}"
|
|
install -d -m 0755 -o "${SERVICE_USER}" -g "${SERVICE_USER}" "${LIVE_DIR}"
|
|
install -m 0644 -o "${SERVICE_USER}" -g "${SERVICE_USER}" \
|
|
"${LIVE_SRC}/vmlinuz" "${LIVE_DIR}/vmlinuz"
|
|
install -m 0644 -o "${SERVICE_USER}" -g "${SERVICE_USER}" \
|
|
"${LIVE_SRC}/initrd.img" "${LIVE_DIR}/initrd.img"
|
|
ok "live image staged into ${LIVE_DIR}"
|
|
fi
|
|
|
|
# --- patch the pxe: block in vetting.yaml -----------------------------
|
|
#
|
|
# Replace the contents of the pxe: block in-place. Uses awk to walk
|
|
# line-by-line: when we hit `pxe:`, skip everything up to the next
|
|
# top-level key (a line starting with a non-whitespace letter + ":")
|
|
# or EOF, and emit our freshly-rendered block instead. Everything
|
|
# outside the pxe: block is passed through unchanged, so hand-tuned
|
|
# server:/database:/notifiers: blocks survive intact.
|
|
|
|
# extract_yaml_value <key> <config-path>: reads ` key: "value" # comment`
|
|
# from inside the pxe: block and prints the bare `value`. Empty or missing
|
|
# key → empty output. The production yaml ships default values like
|
|
# `interface: "" # e.g. "eth0"` — so we must
|
|
# strip the trailing comment BEFORE unquoting, or the comment's inner
|
|
# quotes get picked up.
|
|
extract_yaml_value() {
|
|
local key="$1" path="$2"
|
|
awk -v key="${key}" '
|
|
/^pxe:/ { in_pxe=1; next }
|
|
in_pxe && /^[A-Za-z_][A-Za-z0-9_]*:/ { in_pxe=0 }
|
|
in_pxe {
|
|
re = "^[[:space:]]+" key ":[[:space:]]*"
|
|
if ($0 ~ re) {
|
|
line = $0
|
|
sub(re, "", line)
|
|
# Quoted value: extract between the first pair of quotes.
|
|
if (match(line, /"[^"]*"/)) {
|
|
print substr(line, RSTART+1, RLENGTH-2)
|
|
exit
|
|
}
|
|
# Unquoted value: drop any trailing comment + whitespace.
|
|
sub(/[[:space:]]*#.*$/, "", line)
|
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", line)
|
|
print line
|
|
exit
|
|
}
|
|
}
|
|
' "${path}"
|
|
}
|
|
|
|
existing_iface="$(extract_yaml_value interface "${CONFIG}")"
|
|
existing_subnet="$(extract_yaml_value subnet "${CONFIG}")"
|
|
|
|
if [[ -n "${existing_iface}" && "${existing_iface}" != "${INTERFACE}" && ${FORCE} -eq 0 ]]; then
|
|
die "pxe.interface in ${CONFIG} is already set to ${existing_iface}, which differs from --interface ${INTERFACE}. Pass --force to overwrite."
|
|
fi
|
|
if [[ -n "${existing_subnet}" && "${existing_subnet}" != "${SUBNET}" && ${FORCE} -eq 0 ]]; then
|
|
die "pxe.subnet in ${CONFIG} is already ${existing_subnet}, which differs from --subnet ${SUBNET}. Pass --force to overwrite."
|
|
fi
|
|
|
|
step "patching pxe: block in ${CONFIG}"
|
|
new_block=$(cat <<EOF
|
|
pxe:
|
|
enabled: true
|
|
interface: "${INTERFACE}"
|
|
subnet: "${SUBNET}"
|
|
orchestrator_url: "${ORCH_URL}"
|
|
tftp_root: "${TFTP_ROOT}"
|
|
live_dir: "${LIVE_DIR}"
|
|
EOF
|
|
)
|
|
|
|
tmp_yaml="$(mktemp)"
|
|
# Pass the rendered block through awk ENVIRON so we don't have to
|
|
# quote-escape it into -v (which chokes on the embedded newlines).
|
|
NEW_BLOCK="${new_block}" awk '
|
|
BEGIN { skipping=0; emitted=0 }
|
|
/^pxe:/ { print ENVIRON["NEW_BLOCK"]; skipping=1; emitted=1; next }
|
|
skipping && /^[A-Za-z_][A-Za-z0-9_]*:/ { skipping=0 }
|
|
!skipping { print }
|
|
END {
|
|
if (!emitted) {
|
|
# No existing pxe: block — append one.
|
|
print ENVIRON["NEW_BLOCK"]
|
|
}
|
|
}
|
|
' "${CONFIG}" > "${tmp_yaml}"
|
|
|
|
# Preserve owner + mode from the original.
|
|
orig_mode="$(stat -c '%a' "${CONFIG}")"
|
|
orig_owner="$(stat -c '%U:%G' "${CONFIG}")"
|
|
install -m "${orig_mode}" -o "${orig_owner%:*}" -g "${orig_owner#*:}" \
|
|
"${tmp_yaml}" "${CONFIG}"
|
|
rm -f "${tmp_yaml}"
|
|
ok "pxe: block written"
|
|
|
|
rule_open "pxe configured"
|
|
while IFS= read -r ln; do
|
|
info "${ln}"
|
|
done <<<"${new_block}"
|
|
printf '\n' >&2
|
|
info "next:"
|
|
info " systemctl restart vetting"
|
|
info " journalctl -fu vetting"
|
|
info "the orchestrator refuses to start with clear errors if anything is still missing;"
|
|
info "you should see dnsmasq come up cleanly."
|
|
rule_close
|
|
total_elapsed
|