94 lines
2.8 KiB
Python
94 lines
2.8 KiB
Python
"""UCI profile snapshots under /mmc/mk8/profiles/<name>/{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)
|