feat(deploy): reliability API routes, atomic releases, version single-source
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
"""UCI profile snapshots under /mmc/mk8/profiles/<name>/{pineapd,wireless,network}"""
|
||||
import os, time
|
||||
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):
|
||||
@@ -12,6 +13,12 @@ def run_cmd(args, timeout=20, input_data=None):
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -38,8 +45,14 @@ def auto_name(op):
|
||||
|
||||
def list_profiles():
|
||||
try:
|
||||
return sorted(d for d in os.listdir(PROFILES_DIR)
|
||||
if os.path.isdir(_path(d)))
|
||||
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 []
|
||||
|
||||
|
||||
@@ -4863,6 +4863,60 @@ def h_rfplan_post(ctx):
|
||||
return (200, result) if ok else (502, result)
|
||||
|
||||
|
||||
def h_profiles_get(ctx):
|
||||
import mk8_profiles
|
||||
return 200, {'profiles': mk8_profiles.list_profiles()}
|
||||
|
||||
|
||||
def h_profile_save(ctx):
|
||||
import mk8_events
|
||||
import mk8_profiles
|
||||
body = ctx.body or {}
|
||||
name = (body.get('name') or '').strip()
|
||||
if not name:
|
||||
return 400, {'error': 'profile name is required'}
|
||||
try:
|
||||
ok = bool(mk8_profiles.snapshot(name))
|
||||
except ValueError as exc:
|
||||
return 400, {'error': str(exc)}
|
||||
except OSError:
|
||||
ok = False
|
||||
try:
|
||||
mk8_events.log_event('profile_save', sev='info' if ok else 'warn',
|
||||
msg='profile %s %s'
|
||||
% (name, 'saved' if ok else 'save failed'),
|
||||
meta={'name': name, 'ok': ok})
|
||||
except Exception:
|
||||
pass
|
||||
return (200, {'ok': True}) if ok else (502, {'ok': False})
|
||||
|
||||
|
||||
def h_profile_restore(ctx):
|
||||
import mk8_events
|
||||
import mk8_gate
|
||||
import mk8_profiles
|
||||
body = ctx.body or {}
|
||||
name = (body.get('name') or '').strip()
|
||||
if not name:
|
||||
return 400, {'error': 'profile name is required'}
|
||||
mk8_gate.enter('restore_profile')
|
||||
try:
|
||||
result = mk8_profiles.restore(name)
|
||||
except ValueError as exc:
|
||||
return 400, {'error': str(exc)}
|
||||
device_run(['wifi', 'reload'], timeout=45)
|
||||
ok = bool(result.get('ok'))
|
||||
try:
|
||||
mk8_events.log_event('profile_restore',
|
||||
sev='info' if ok else 'warn',
|
||||
msg='profile %s %s' % (name, 'restored' if ok
|
||||
else 'restore failed'),
|
||||
meta=result)
|
||||
except Exception:
|
||||
pass
|
||||
return (200, result) if ok else (502, result)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Startup environment check: run once at service startup (and via
|
||||
# ``server.py --env-check`` on the payload screen) to make the device match
|
||||
@@ -7247,6 +7301,9 @@ ROUTER.add('GET', r'/api/mode', h_mode_get)
|
||||
ROUTER.add('POST', r'/api/mode/release', h_mode_release)
|
||||
ROUTER.add('GET', r'/api/rfplan', h_rfplan_get)
|
||||
ROUTER.add('POST', r'/api/rfplan/role', h_rfplan_post)
|
||||
ROUTER.add('GET', r'/api/reliability/profiles', h_profiles_get)
|
||||
ROUTER.add('POST', r'/api/reliability/profile', h_profile_save)
|
||||
ROUTER.add('POST', r'/api/reliability/restore', h_profile_restore)
|
||||
|
||||
|
||||
def serve():
|
||||
|
||||
@@ -269,7 +269,7 @@
|
||||
<script src="js/xterm-addon-fit.min.js"></script>
|
||||
<script src="js/terminal.js?v=20260820-4"></script>
|
||||
<script src="js/pager.js?v=20260820-4"></script>
|
||||
<script src="js/views.js?v=20260822-1"></script>
|
||||
<script src="js/views.js?v=20260822-2"></script>
|
||||
<script src="js/app.js?v=20260822-1"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3920,6 +3920,43 @@ views.settings = (root) => {
|
||||
overlay.appendChild(restoreBtn);
|
||||
loadOverlay();
|
||||
|
||||
const profiles = settingsCard(box, 'Reliability Profiles',
|
||||
'Snapshots of the PineAP, wireless, and network UCI config. Save one before risky changes; restoring rolls the config back and reloads the radios.');
|
||||
const profBody = h('div', { class: 'settings-table-wrap', text: 'Loading…' });
|
||||
profiles.appendChild(profBody);
|
||||
function loadProfiles() {
|
||||
PagerAPI.get('/api/reliability/profiles').then((r) => {
|
||||
const names = (r.data || {}).profiles || [];
|
||||
profBody.innerHTML = '';
|
||||
if (!names.length) {
|
||||
profBody.appendChild(h('div', { class: 'empty', text: 'No saved profiles yet.' }));
|
||||
return;
|
||||
}
|
||||
profBody.appendChild(table([
|
||||
{ label: 'Profile', key: 'name' },
|
||||
{ label: '', render: (row) => {
|
||||
const doRestore = btn('Restore', () => runAction(doRestore,
|
||||
() => PagerAPI.post('/api/reliability/restore', { name: row.name })
|
||||
.then(() => App.toast('Profile restored; radios reloaded')), 'Restoring…'), 'ghost');
|
||||
return doRestore;
|
||||
} }
|
||||
], names.map((name) => ({ name }))));
|
||||
}).catch(() => { profBody.textContent = 'Unable to load profiles.'; });
|
||||
}
|
||||
const profName = h('input', { placeholder: 'e.g. pre-evilwpa', maxlength: '64',
|
||||
autocomplete: 'off' });
|
||||
const profSave = btn('Save Profile', () => {
|
||||
const name = profName.value.trim();
|
||||
if (!name) { App.toast('Profile name is required', 'error'); return; }
|
||||
return runAction(profSave, () => PagerAPI.post('/api/reliability/profile', { name })
|
||||
.then(() => { profName.value = ''; App.toast('Config profile saved'); loadProfiles(); }), 'Saving…');
|
||||
});
|
||||
profiles.appendChild(h('div', { class: 'settings-form-grid' },
|
||||
h('label', {}, 'Profile Name', profName)));
|
||||
profiles.appendChild(h('div', { class: 'settings-actions' }, profSave,
|
||||
btn('Refresh', () => { loadProfiles(); }, 'ghost')));
|
||||
loadProfiles();
|
||||
|
||||
settingsCard(box, 'Button Script', 'The Mark VII button script has no safe Pager equivalent. Pager buttons remain managed by the native input and payload-launcher system.');
|
||||
|
||||
const resources = settingsCard(box, 'Resources');
|
||||
|
||||
Reference in New Issue
Block a user