Tools > Share accounts sets the pxe-upload, blancco and clonezilla Samba passwords. Saving one runs smbpasswd and writes it everywhere it is used: startnet.cmd in boot.wim (pxe-upload), the served Blancco preferences and the live config.img (blancco), the iPXE menu's Clonezilla entries (clonezilla). Refused while a PC is imaging unless forced. Stored 0600 in UPDATES_DIR/shares/accounts.json, never shown back. - The stored values are the authority: the playbook only creates the Samba accounts, seeds the store from site-vars (pxe_upload_password, blancco_smb_password, clonezilla_smb_password) for accounts it does not hold, and runs "share_accounts apply" at the end of every run. - Every startnet save re-pins SHAREPASS, so a restored snapshot cannot bring an old password back; new menu blocks use %SHAREUSER%/%SHAREPASS%. - Shop floor unattend Order 4 reads the login from fetch-source.txt; Invoke-PostPpkg deletes that file once the PPKG is applied. - Upload-Image.ps1 asks for the password unless -Pass is given. - pxe_admin_password: the Linux pxe login. Required while the box still has the shipped "pxe"; set with chpasswd when it differs. - Blancco site settings now also reach the live config.img (BIOS path).
156 lines
6.3 KiB
Python
Executable File
156 lines
6.3 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 = os.environ.get("PXE_PASS") or "pxe" # the site's pxe_admin_password
|
|
|
|
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())
|