"""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) A fourth, the password of the blancco Samba account in , comes from services/share_accounts.py (Tools > Share accounts). 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"()[^<]*()") _WIRELESS = re.compile( rb'(\s*[^<]*\s*)[^<]*' rb'(\s*)[^<]*()') # The report share login: the blancco Samba account (Tools > Share accounts). _SHARE = re.compile( rb'(\s*blancco\s*' rb')[^<]*()') 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() from services import share_accounts share_pw = escape(share_accounts.load()["blancco"]).encode("utf-8") 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 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 ssid/password block") out, n = _SHARE.subn(lambda m: m.group(1) + share_pw + m.group(2), out) if n != 1: raise ValueError("preferences have no single login for blancco") 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:]))