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/.
166 lines
5.8 KiB
Python
166 lines
5.8 KiB
Python
"""Site settings in the Blancco preferences: set in the webapp, not in XML.
|
|
|
|
Three values differ per site and are filled into our preferences.xml:
|
|
business_location printed on every erasure report (the site)
|
|
wifi_ssid the Wi-Fi Blancco joins to license erasures in the GE
|
|
wifi_password Blancco cloud (the imaging LAN has no internet)
|
|
|
|
They live in UPDATES_DIR/blancco/site-settings.json (0600, root). The repo's
|
|
playbook/blancco-preferences.xml carries none of them; the playbook installs
|
|
it as WEB_ROOT/blancco/preferences.repo.xml and this module renders the
|
|
served WEB_ROOT/blancco/preferences.xml from it. The same values are applied
|
|
whenever a new Blancco release is staged (blancco_update.apply_prefs), so an
|
|
update keeps them.
|
|
|
|
The playbook seeds the settings from site-vars.yml (site_name,
|
|
blancco_wifi_ssid, blancco_wifi_password) only while no settings file exists:
|
|
after install the webapp page (Updates > Blancco releases) is the place to
|
|
change them, and a playbook re-run never overwrites them.
|
|
|
|
python3 -m services.blancco_prefs render [--seed]
|
|
--seed reads BLANCCO_SEED_LOCATION / _SSID / _PASSWORD from the
|
|
environment (not argv: the password stays out of ps).
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
from xml.sax.saxutils import escape
|
|
|
|
import config
|
|
from services import updates_common as uc
|
|
|
|
FIELDS = ("business_location", "wifi_ssid", "wifi_password")
|
|
DEFAULTS = {"business_location": "", "wifi_ssid": "INTERNETACCESS", "wifi_password": ""}
|
|
|
|
_LOCATION = re.compile(rb"(<business_location>)[^<]*(</business_location>)")
|
|
_WIRELESS = re.compile(
|
|
rb'(<wireless>\s*<enabled>[^<]*</enabled>\s*<ssid encrypted="false">)[^<]*'
|
|
rb'(</ssid>\s*<password encrypted="false">)[^<]*(</password>)')
|
|
|
|
|
|
def settings_path():
|
|
return os.path.join(uc.state_dir("blancco"), "site-settings.json")
|
|
|
|
|
|
def repo_prefs_path():
|
|
return os.path.join(config.WEB_ROOT, "blancco", "preferences.repo.xml")
|
|
|
|
|
|
def served_prefs_path():
|
|
return os.path.join(config.WEB_ROOT, "blancco", "preferences.xml")
|
|
|
|
|
|
def load():
|
|
data = uc.read_json(settings_path(), None)
|
|
out = dict(DEFAULTS)
|
|
if isinstance(data, dict):
|
|
for k in FIELDS:
|
|
if isinstance(data.get(k), str):
|
|
out[k] = data[k]
|
|
return out
|
|
|
|
|
|
def exists():
|
|
return os.path.isfile(settings_path())
|
|
|
|
|
|
def _clean(value, field):
|
|
value = (value or "").strip()
|
|
if len(value) > 128:
|
|
raise ValueError(f"{field} is too long (128 characters at most)")
|
|
if any(ord(c) < 32 for c in value):
|
|
raise ValueError(f"{field} has control characters")
|
|
return value
|
|
|
|
|
|
def save(business_location, wifi_ssid, wifi_password=None):
|
|
"""Store the settings. wifi_password None (or '') keeps the stored one."""
|
|
cur = load()
|
|
new = {
|
|
"business_location": _clean(business_location, "Business location"),
|
|
"wifi_ssid": _clean(wifi_ssid, "Wi-Fi network"),
|
|
"wifi_password": cur["wifi_password"] if not wifi_password else _clean(wifi_password, "Wi-Fi password"),
|
|
}
|
|
if not new["wifi_ssid"]:
|
|
raise ValueError("Wi-Fi network (SSID) cannot be empty")
|
|
path = settings_path()
|
|
tmp = path + ".tmp"
|
|
fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
with os.fdopen(fd, "w") as f:
|
|
json.dump(new, f, indent=2)
|
|
os.replace(tmp, path)
|
|
return new
|
|
|
|
|
|
def apply(xml, settings=None):
|
|
"""Our preferences (bytes) with the site settings filled in. Values are
|
|
XML-escaped; the replacement is a function, so nothing in them is read
|
|
as a regex group reference."""
|
|
s = settings or load()
|
|
loc = escape(s["business_location"] or "Unnamed site").encode("utf-8")
|
|
ssid = escape(s["wifi_ssid"]).encode("utf-8")
|
|
pw = escape(s["wifi_password"]).encode("utf-8")
|
|
out, n = _LOCATION.subn(lambda m: m.group(1) + loc + m.group(2), xml)
|
|
if n != 1:
|
|
raise ValueError("preferences have no single <business_location> element")
|
|
out, n = _WIRELESS.subn(lambda m: m.group(1) + ssid + m.group(2) + pw + m.group(3), out)
|
|
if n != 1:
|
|
raise ValueError("preferences have no single <wireless> ssid/password block")
|
|
return out
|
|
|
|
|
|
def render():
|
|
"""Write the served preferences.xml from the repo copy + settings, when it
|
|
changes. Returns True if the file was written."""
|
|
with open(repo_prefs_path(), "rb") as f:
|
|
base = f.read()
|
|
data = apply(base)
|
|
# Same guard the playbook and the Blancco page apply: well-formed XML.
|
|
import xml.etree.ElementTree as ET
|
|
ET.fromstring(data)
|
|
path = served_prefs_path()
|
|
try:
|
|
with open(path, "rb") as f:
|
|
if f.read() == data:
|
|
return False
|
|
except OSError:
|
|
pass
|
|
tmp = path + ".tmp"
|
|
with open(tmp, "wb") as f:
|
|
f.write(data)
|
|
os.chmod(tmp, 0o644)
|
|
os.replace(tmp, path)
|
|
return True
|
|
|
|
|
|
def public():
|
|
"""For the page: the settings without the password."""
|
|
s = load()
|
|
return {"business_location": s["business_location"], "wifi_ssid": s["wifi_ssid"],
|
|
"wifi_password_set": bool(s["wifi_password"]), "configured": exists()}
|
|
|
|
|
|
def main(argv):
|
|
if not argv or argv[0] != "render":
|
|
print(__doc__)
|
|
return 2
|
|
if "--seed" in argv[1:] and not exists():
|
|
save(os.environ.get("BLANCCO_SEED_LOCATION", ""),
|
|
os.environ.get("BLANCCO_SEED_SSID", "") or DEFAULTS["wifi_ssid"],
|
|
os.environ.get("BLANCCO_SEED_PASSWORD", ""))
|
|
print("seeded site settings from site-vars")
|
|
s = load()
|
|
changed = render()
|
|
print("CHANGED" if changed else "UNCHANGED")
|
|
if not s["business_location"]:
|
|
print("WARNING: business_location is empty: reports say 'Unnamed site'")
|
|
if not s["wifi_password"]:
|
|
print(f"WARNING: no Wi-Fi password for '{s['wifi_ssid']}': Blancco cannot license erasures")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv[1:]))
|