Everything West Jefferson specific moved out to a separate repo (pxe-site-wj): NTLARS/UDC/heat-treat backups keyed by WJ machine numbers, CMM and Wax/Trace bay lists, WJ OpenText profiles, site-config.json, the WJ preinstall list, and the WJ shopfloor PC types (Collections, NoCollections, Common, Keyence, CMM, Genspect, HeatTreat, Wax/Trace, Display, PartMarker) with GE-Enforce, machine numbers and the desktop layout. The part-marker bench tools moved to their own repo. Dead scripts removed: build-proxmox-iso, deploy-bios, sync_hardware_models, mirror-from-gold, pull-bios, startnet-template.cmd. Shopfloor is one generic type, gea-shopfloor-standard, with today's timing: - PPKG at first logon (unattend Order 10, unchanged). - Install-AppList.ps1 runs C:\PreInstall\preinstall.json during setup. - A new SYSTEM task, GE Shopfloor Post-PPKG, runs postppkg.json once after the post-PPKG reboot. It also clears AutoLogonCount after the ShopFloor handover (the Users-level monitor can be denied that HKLM write) and removes the Intune Sync task when done. - Both lists ship empty. A site adds entries and installers. - WinPE choice 3 goes straight to the PPKG menu. Staging, fetch and heal carry only the generic payload. The webapp's shopfloor sub-menu editor is gone, and the imaging tiles name the new stages. - The playbook deploys the empty lists only if missing. It now deploys the PowerShell 7 MSI the shopfloor unattend installs, which it never did before. Blancco site settings move into the webapp (Updates > Blancco releases): business location, licensing Wi-Fi SSID and password. They are rendered into the served preferences.xml and applied to every staged release, and are seeded from site-vars (site_name, blancco_wifi_ssid, blancco_wifi_password) only while none exist. The repo XML carries no site values; the plaintext Wi-Fi password is gone from it. New site_name and site_timezone settings: report and evidence-pack site name, server clock and webapp time zone. No more West Jefferson defaults in webapp/config.py or the dashboard title. Sync-BPRT.ps1 gets the server address filled in at download instead of WJ's GE address. USB on Windows: - scripts/Build-PxeUsb.ps1 runs the same checks as build-usb.sh. It writes one FAT32 CIDATA partition (ISO files plus patched grub.cfg plus payload) with diskpart and reads every file back by SHA-256. - Tested with Windows PowerShell 5.1 in the win11 VM, and the layout installs and first-boots in the pxetest VM. - scripts/stage-usb-share.sh stages the build folder on a share. - site-content/ is optional for both builders. KB docs/kb/How_To_Deploy_PXE_Server_New_Site.html: new-site deployment, Windows only, with a site-vars.yml guide. Checks: webapp tests pass (5 suites); the unattend lint and playbook syntax checks pass; every .ps1 parses; there are no WJ hosts, IPs, names or PC types in playbook/, webapp/, scripts/ or autoinstall/.
156 lines
6.2 KiB
Python
Executable File
156 lines
6.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
share-drift.py - report where the repo and the live share disagree.
|
|
|
|
WHY
|
|
The share is production. The repo is meant to describe it. On 2026-08-06 an
|
|
unattend edit made directly on the share broke every shopfloor, standard and
|
|
engineer build for a day - while the repo copy was fine the whole time. Nobody
|
|
could have known, because nothing compared them.
|
|
|
|
Worse, drift runs both directions. Some live files are AHEAD of the repo
|
|
(hand-edits nobody committed) and some repo files are ahead of live (fixes never
|
|
deployed). Blindly pushing the repo over the share would have overwritten a
|
|
working 17 KB unattend with a stale 12 KB one.
|
|
|
|
So this tool REPORTS by default and never writes. Each pair is classified:
|
|
|
|
git-owned the repo is the source of truth. Safe to push.
|
|
unreconciled the two have diverged and nobody has decided which wins. NOT safe
|
|
to push. Reconcile by hand, then move the pair to git-owned.
|
|
|
|
Usage:
|
|
./share-drift.py # status table
|
|
./share-drift.py --diff # show the actual differences
|
|
./share-drift.py --only unattend # filter by substring
|
|
|
|
Exit non-zero if any git-owned pair differs, so it can gate a deploy. An
|
|
unreconciled pair that differs is reported but does not fail - that is a known
|
|
state, not a regression.
|
|
"""
|
|
|
|
import argparse
|
|
import base64
|
|
import difflib
|
|
import hashlib
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
PXE_HOST = "172.16.9.1"
|
|
PXE_USER = "pxe"
|
|
PXE_PASS = "pxe"
|
|
|
|
GIT_OWNED, UNRECONCILED = "git-owned", "unreconciled"
|
|
|
|
# (repo path, live path, ownership)
|
|
PAIRS = [
|
|
("playbook/shopfloor-setup/run-enrollment.ps1",
|
|
"/srv/samba/enrollment/scripts/run-enrollment.ps1", GIT_OWNED),
|
|
("playbook/scripts/preflight.ps1",
|
|
"/srv/samba/enrollment/scripts/preflight.ps1", GIT_OWNED),
|
|
("playbook/shopfloor-setup/Run-ShopfloorSetup.ps1",
|
|
"/srv/samba/enrollment/shopfloor-setup/Run-ShopfloorSetup.ps1", GIT_OWNED),
|
|
("playbook/shopfloor-setup/Verify-And-Heal-Staging.ps1",
|
|
"/srv/samba/enrollment/shopfloor-setup/Verify-And-Heal-Staging.ps1", GIT_OWNED),
|
|
("playbook/shopfloor-setup/Fetch-StagingPayload.ps1",
|
|
"/srv/samba/enrollment/shopfloor-setup/Fetch-StagingPayload.ps1", GIT_OWNED),
|
|
("playbook/shopfloor-setup/Shopfloor/Install-AppList.ps1",
|
|
"/srv/samba/enrollment/shopfloor-setup/Shopfloor/Install-AppList.ps1", GIT_OWNED),
|
|
("playbook/shopfloor-setup/Shopfloor/Register-PostPpkgTask.ps1",
|
|
"/srv/samba/enrollment/shopfloor-setup/Shopfloor/Register-PostPpkgTask.ps1", GIT_OWNED),
|
|
("playbook/shopfloor-setup/Shopfloor/Invoke-PostPpkg.ps1",
|
|
"/srv/samba/enrollment/shopfloor-setup/Shopfloor/Invoke-PostPpkg.ps1", GIT_OWNED),
|
|
("playbook/shopfloor-setup/BIOS/models.txt",
|
|
"/srv/samba/winpeapps/_shared/BIOS/models.txt", GIT_OWNED),
|
|
("playbook/shopfloor-setup/BIOS/check-bios.cmd",
|
|
"/srv/samba/winpeapps/_shared/BIOS/check-bios.cmd", GIT_OWNED),
|
|
|
|
# All three unattends are reconciled. The live files were ahead of the repo
|
|
# and the LIVE side won, because these are what boot machines - pushing the
|
|
# repo over them is precisely the 2026-08-06 outage.
|
|
#
|
|
# Engineer has its own repo file. It used to be compared against the
|
|
# standard unattend, which is a different answer file, so the pair reported
|
|
# DIFFERS permanently - and a light that is always red is one nobody reads.
|
|
("playbook/FlatUnattendW10-shopfloor.xml",
|
|
"/srv/samba/winpeapps/gea-shopfloor/Deploy/FlatUnattendW10.xml", GIT_OWNED),
|
|
("playbook/FlatUnattendW10.xml",
|
|
"/srv/samba/winpeapps/gea-standard/Deploy/FlatUnattendW10.xml", GIT_OWNED),
|
|
("playbook/FlatUnattendW10-engineer.xml",
|
|
"/srv/samba/winpeapps/gea-engineer/Deploy/FlatUnattendW10.xml", GIT_OWNED),
|
|
]
|
|
|
|
|
|
def ssh(cmd):
|
|
return subprocess.run(
|
|
["sshpass", "-p", PXE_PASS, "ssh", "-o", "StrictHostKeyChecking=no",
|
|
"-o", "LogLevel=ERROR", f"{PXE_USER}@{PXE_HOST}", cmd],
|
|
capture_output=True, text=True)
|
|
|
|
|
|
def live_bytes(path):
|
|
"""base64 in transit so BOM and CRLF survive the hop unchanged."""
|
|
r = ssh("base64 -w0 '%s' 2>/dev/null" % path)
|
|
if r.returncode != 0 or not r.stdout.strip():
|
|
return None
|
|
return base64.b64decode(r.stdout.strip())
|
|
|
|
|
|
def repo_bytes(path):
|
|
try:
|
|
with open(os.path.join(REPO, path), "rb") as f:
|
|
return f.read()
|
|
except OSError:
|
|
return None
|
|
|
|
|
|
def short(b):
|
|
return hashlib.md5(b).hexdigest()[:10] if b is not None else "MISSING"
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description="Report repo vs live share drift.")
|
|
ap.add_argument("--diff", action="store_true", help="show the actual differences")
|
|
ap.add_argument("--only", help="only pairs whose paths contain this substring")
|
|
args = ap.parse_args()
|
|
|
|
blocking = 0
|
|
known = 0
|
|
print("%-13s %-44s %-11s %-11s" % ("OWNERSHIP", "LIVE PATH", "REPO", "LIVE"))
|
|
print("-" * 84)
|
|
|
|
for repo_path, live_path, owner in PAIRS:
|
|
if args.only and args.only not in repo_path and args.only not in live_path:
|
|
continue
|
|
rb, lb = repo_bytes(repo_path), live_bytes(live_path)
|
|
same = rb is not None and lb is not None and rb == lb
|
|
tag = "same" if same else "DIFFERS"
|
|
if not same:
|
|
if owner == GIT_OWNED:
|
|
blocking += 1
|
|
else:
|
|
known += 1
|
|
print("%-13s %-44s %-11s %-11s %s"
|
|
% (owner, live_path.replace("/srv/samba/", ""), short(rb), short(lb), tag))
|
|
|
|
if args.diff and not same and rb is not None and lb is not None:
|
|
a = rb.decode("utf-8", "replace").replace("\r\n", "\n").splitlines()
|
|
b = lb.decode("utf-8", "replace").replace("\r\n", "\n").splitlines()
|
|
for line in list(difflib.unified_diff(a, b, "repo", "live", lineterm="", n=1))[:40]:
|
|
print(" " + line)
|
|
print()
|
|
|
|
print("-" * 84)
|
|
print("%d git-owned pair(s) out of sync, %d known-unreconciled" % (blocking, known))
|
|
if blocking:
|
|
print("FAILED: a git-owned file differs from the share. Deploy it or commit the live version.")
|
|
return 1
|
|
print("PASSED: everything git-owned matches the share.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|