Build/playbook re-run safety: corp access, netplan rollback, first-boot retry
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).
This commit is contained in:
@@ -38,18 +38,24 @@ Usage:
|
||||
|
||||
Exits non-zero when any ERROR is found, so it can gate a deploy.
|
||||
|
||||
Remote mode reads PXE_SERVER (default 172.16.9.1), PXE_USER (default pxe)
|
||||
and PXE_PASS (default pxe) from the environment. The password reaches
|
||||
sshpass through its environment (sshpass -e), never on a command line.
|
||||
|
||||
Requires: sshpass (remote mode only)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import xml.dom.minidom
|
||||
|
||||
PXE_HOST = "172.16.9.1"
|
||||
PXE_USER = "pxe"
|
||||
PXE_PASS = "pxe"
|
||||
PXE_HOST = os.environ.get("PXE_SERVER") or "172.16.9.1"
|
||||
PXE_USER = os.environ.get("PXE_USER") or "pxe"
|
||||
PXE_PASS = os.environ.get("PXE_PASS") or "pxe"
|
||||
IMAGE_BASE = "/srv/samba/winpeapps"
|
||||
UNATTEND_REL = "Deploy/FlatUnattendW10.xml"
|
||||
|
||||
@@ -76,15 +82,16 @@ ERROR, WARN = "ERROR", "WARN"
|
||||
|
||||
|
||||
def ssh_cmd(host, cmd):
|
||||
env = dict(os.environ, SSHPASS=PXE_PASS)
|
||||
return subprocess.run(
|
||||
["sshpass", "-p", PXE_PASS, "ssh", "-o", "StrictHostKeyChecking=no",
|
||||
["sshpass", "-e", "ssh", "-o", "StrictHostKeyChecking=no",
|
||||
"-o", "LogLevel=ERROR", f"{PXE_USER}@{host}", cmd],
|
||||
capture_output=True, text=True)
|
||||
capture_output=True, text=True, env=env)
|
||||
|
||||
|
||||
def read_remote_bytes(host, path):
|
||||
"""Base64 in transit so a BOM or CRLF survives the hop intact."""
|
||||
r = ssh_cmd(host, "base64 -w0 '%s' 2>/dev/null" % path)
|
||||
r = ssh_cmd(host, "base64 -w0 %s 2>/dev/null" % shlex.quote(path))
|
||||
if r.returncode != 0 or not r.stdout.strip():
|
||||
return None
|
||||
import base64
|
||||
@@ -165,9 +172,13 @@ def main():
|
||||
parser = argparse.ArgumentParser(description="Validate unattend answer files.")
|
||||
parser.add_argument("--server", default=PXE_HOST, help=f"PXE server (default: {PXE_HOST})")
|
||||
parser.add_argument("--image", help="single image type instead of all")
|
||||
parser.add_argument("--sha256", action="store_true",
|
||||
help="also print the sha256 of each file linted (deploy-unattend.sh compares it)")
|
||||
parser.add_argument("--local", nargs="+", metavar="FILE", help="lint local files instead")
|
||||
parser.add_argument("--quiet", action="store_true", help="suppress WARN lines")
|
||||
args = parser.parse_args()
|
||||
if args.image and not re.fullmatch(r"[A-Za-z0-9._-]+", args.image):
|
||||
sys.exit("ERROR: bad image type %r" % args.image)
|
||||
|
||||
findings = []
|
||||
targets = []
|
||||
@@ -180,7 +191,7 @@ def main():
|
||||
images = [args.image]
|
||||
else:
|
||||
r = ssh_cmd(args.server,
|
||||
"find '%s' -maxdepth 1 -mindepth 1 -type d -printf '%%f\\n'" % IMAGE_BASE)
|
||||
"find %s -maxdepth 1 -mindepth 1 -type d -printf '%%f\\n'" % shlex.quote(IMAGE_BASE))
|
||||
if r.returncode != 0:
|
||||
sys.exit("ERROR: cannot list %s: %s" % (IMAGE_BASE, r.stderr.strip()))
|
||||
images = sorted(n for n in r.stdout.split() if not n.startswith("_"))
|
||||
@@ -190,6 +201,9 @@ def main():
|
||||
|
||||
for name, data in targets:
|
||||
check(name, data, findings)
|
||||
if args.sha256 and data is not None:
|
||||
import hashlib
|
||||
print("sha256 %s %s" % (hashlib.sha256(data).hexdigest(), name))
|
||||
|
||||
print("Linted %d unattend file(s)\n" % len(targets))
|
||||
shown = [f for f in findings if not (args.quiet and f[0] == WARN)]
|
||||
|
||||
Reference in New Issue
Block a user