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).
177 lines
6.4 KiB
Python
177 lines
6.4 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)
|
|
A fourth, the password of the blancco Samba account in <network_share>,
|
|
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"(<business_location>)[^<]*(</business_location>)")
|
|
_WIRELESS = re.compile(
|
|
rb'(<wireless>\s*<enabled>[^<]*</enabled>\s*<ssid encrypted="false">)[^<]*'
|
|
rb'(</ssid>\s*<password encrypted="false">)[^<]*(</password>)')
|
|
# The report share login: the blancco Samba account (Tools > Share accounts).
|
|
_SHARE = re.compile(
|
|
rb'(<network_share>\s*<username encrypted="false">blancco</username>\s*'
|
|
rb'<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()
|
|
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 <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")
|
|
out, n = _SHARE.subn(lambda m: m.group(1) + share_pw + m.group(2), out)
|
|
if n != 1:
|
|
raise ValueError("preferences have no single <network_share> 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:]))
|