"""UCI profile snapshots under /mmc/mk8/profiles//{pineapd,wireless,network}""" import os, re, time PROFILES_DIR = '/mmc/mk8/profiles' CONFIGS = ('pineapd', 'wireless', 'network') NAME_RE = re.compile(r'^[A-Za-z0-9._-]{1,64}$') def run_cmd(args, timeout=20, input_data=None): """Lazy import avoids a circular import with server.py; tests monkeypatch.""" from server import device_run return device_run(args, timeout=timeout, input_data=input_data) def _path(name): """Resolve a profile name to its directory. HTTP-supplied names are never trusted: reject anything but [A-Za-z0-9._-]{1,64} and explicitly refuse '.'/'..' so traversal can never escape PROFILES_DIR.""" if not isinstance(name, str) or not NAME_RE.match(name) \ or name in ('.', '..'): raise ValueError('invalid profile name') return os.path.join(PROFILES_DIR, name) def snapshot(name): dest = _path(name) try: os.makedirs(dest, exist_ok=True) wrote = False for cfg in CONFIGS: rc, out, err = run_cmd(['uci', 'export', cfg]) if rc != 0 or not (out or '').strip(): continue with open(os.path.join(dest, cfg + '.uci'), 'w') as f: f.write(out) wrote = True return wrote except OSError: return False def auto_name(op): return 'pre-%s-%d' % (op, int(time.time())) def list_profiles(): try: out = [] for d in os.listdir(PROFILES_DIR): try: if os.path.isdir(_path(d)): out.append(d) except ValueError: continue return sorted(out) except OSError: return [] def delete(name): import shutil shutil.rmtree(_path(name), ignore_errors=True) def restore(name): """Restore configs then commit once per config. Caller runs wifi reload / service restart as appropriate for the operation.""" src = _path(name) restored = [] if not os.path.isdir(src): return {'ok': False, 'restored': [], 'error': 'profile not found'} for cfg in CONFIGS: fpath = os.path.join(src, cfg + '.uci') if not os.path.isfile(fpath): continue with open(fpath) as f: text = f.read() rc, _, err = run_cmd(['uci', 'import', cfg], input_data=text) if rc != 0: return {'ok': False, 'restored': restored, 'error': 'import failed'} run_cmd(['uci', 'commit', cfg]) restored.append(cfg) return {'ok': True, 'restored': restored} LASTKNOWN_GOOD = 'lastknown-good' def promote_lastknown_good(): """Replace the lastknown-good profile with the live config.""" delete(LASTKNOWN_GOOD) return snapshot(LASTKNOWN_GOOD)