Development
This commit is contained in:
parent
3ee1a6f0de
commit
73d2df6be5
3 changed files with 86 additions and 97 deletions
|
|
@ -2,7 +2,7 @@ from flask import Blueprint, request, redirect, flash, session
|
||||||
from auth import require_level
|
from auth import require_level
|
||||||
from config_utils import (flush_pending_to_queue, get_dashboard_pending,
|
from config_utils import (flush_pending_to_queue, get_dashboard_pending,
|
||||||
revert_snapshot_to_config, queued_msg,
|
revert_snapshot_to_config, queued_msg,
|
||||||
_is_locked, _format_timing, _seconds_until_next_run)
|
_timing_status_msg)
|
||||||
|
|
||||||
bp = Blueprint('action_actions', __name__)
|
bp = Blueprint('action_actions', __name__)
|
||||||
|
|
||||||
|
|
@ -24,22 +24,7 @@ def actions_cardpending_applynow():
|
||||||
flash('No pending changes to apply.', 'info')
|
flash('No pending changes to apply.', 'info')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
flush_pending_to_queue()
|
flush_pending_to_queue()
|
||||||
if _is_locked():
|
flash(_timing_status_msg(None, 'Changes queued'), 'success')
|
||||||
msg = 'Changes queued. They are being applied now.'
|
|
||||||
else:
|
|
||||||
timing = _format_timing(_seconds_until_next_run())
|
|
||||||
if timing:
|
|
||||||
msg = f'Changes queued. They will be applied {timing}.'
|
|
||||||
else:
|
|
||||||
msg = 'Changes queued. The processing service is not running.'
|
|
||||||
flash(msg, 'success')
|
|
||||||
return redirect(_VIEW)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/action/queue_core_apply', methods=['POST'])
|
|
||||||
@require_level('administrator')
|
|
||||||
def queue_core_apply():
|
|
||||||
flash(queued_msg('core apply', action_label='Apply queued'), 'success')
|
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -255,26 +255,23 @@ def _lock_mtime():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _build_timing_msg(entry_ts, cmd, action_label='Configuration saved'):
|
def _timing_status_msg(entry_ts, action_label):
|
||||||
if not _apply_changes_immediately():
|
"""Return a flash message for a command already written to the queue."""
|
||||||
return f'{action_label}. Click Apply Now on the Configuration Changes card to apply.'
|
|
||||||
if _is_locked():
|
if _is_locked():
|
||||||
mtime = _lock_mtime()
|
mtime = _lock_mtime()
|
||||||
if entry_ts is not None and mtime and entry_ts < mtime:
|
if entry_ts is not None and mtime and entry_ts < mtime:
|
||||||
return f'{action_label}. Changes are being applied now.'
|
return f'{action_label}. Your changes are being applied now...'
|
||||||
return f'{action_label}. Changes will be applied on the next run.'
|
return f'{action_label}. Your changes will be applied on the next run.'
|
||||||
timing = _format_timing(_seconds_until_next_run())
|
timing = _format_timing(_seconds_until_next_run())
|
||||||
if timing:
|
if timing:
|
||||||
return f'{action_label}. Changes will be applied {timing}.'
|
return f'{action_label}. Your changes will be applied {timing}.'
|
||||||
if cmd is None:
|
|
||||||
return f'{action_label}. The processing service is not running.'
|
return f'{action_label}. The processing service is not running.'
|
||||||
parts = cmd.split()
|
|
||||||
cli_cmd = f'sudo python3 {parts[0]}.py --{parts[1]}' if len(parts) == 2 else cmd
|
|
||||||
install_cmd = 'sudo python3 install.py'
|
def _build_timing_msg(entry_ts, action_label='Configuration saved'):
|
||||||
from markupsafe import Markup
|
if not _apply_changes_immediately():
|
||||||
return Markup(f'{action_label}. The command processing service is not installed. '
|
return f'{action_label}. Visit Actions page to apply your changes.'
|
||||||
f'Run <strong>{install_cmd}</strong> to enable it, '
|
return _timing_status_msg(entry_ts, action_label)
|
||||||
f'or <strong>{cli_cmd}</strong> to apply manually.')
|
|
||||||
|
|
||||||
|
|
||||||
def queue_command(cmd, description=''):
|
def queue_command(cmd, description=''):
|
||||||
|
|
@ -287,7 +284,7 @@ def queued_msg(cmd=None, description='', action_label='Configuration saved'):
|
||||||
entry_ts = None
|
entry_ts = None
|
||||||
if cmd is not None:
|
if cmd is not None:
|
||||||
_entry_uuid, entry_ts = queue_command(cmd)
|
_entry_uuid, entry_ts = queue_command(cmd)
|
||||||
return _build_timing_msg(entry_ts, cmd, action_label)
|
return _build_timing_msg(entry_ts, action_label)
|
||||||
|
|
||||||
|
|
||||||
# Snapshot system ===================================================
|
# Snapshot system ===================================================
|
||||||
|
|
@ -434,7 +431,7 @@ def save_config_with_snapshot(new_config, path, key, operation, before, after,
|
||||||
else:
|
else:
|
||||||
_queue_pending_presigned(cmd, entry_uuid, entry_ts)
|
_queue_pending_presigned(cmd, entry_uuid, entry_ts)
|
||||||
|
|
||||||
return _build_timing_msg(entry_ts, cmd)
|
return _build_timing_msg(entry_ts)
|
||||||
|
|
||||||
|
|
||||||
# Misc ==============================================================
|
# Misc ==============================================================
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import json, re, subprocess, os, sys, html as html_mod
|
||||||
import sanitize
|
import sanitize
|
||||||
import validation as validate
|
import validation as validate
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from config_utils import config_hash, get_pending_entries, get_dashboard_pending, get_dashboard_done, load_snapshot_for_uuid, _seconds_until_next_run, _format_timing, _is_locked, _lock_mtime, WEB_APP_DISPLAY_NAME, CONFIGS_DIR, DATA_DIR
|
from config_utils import config_hash, get_pending_entries, get_dashboard_pending, get_dashboard_done, load_snapshot_for_uuid, queue_command, _apply_changes_immediately, _seconds_until_next_run, _format_timing, _is_locked, _lock_mtime, WEB_APP_DISPLAY_NAME, CONFIGS_DIR, DATA_DIR
|
||||||
|
|
||||||
bp = Blueprint('view_page', __name__)
|
bp = Blueprint('view_page', __name__)
|
||||||
|
|
||||||
|
|
@ -1649,7 +1649,7 @@ def render_layout(view_id, content_html, tokens):
|
||||||
cls = 'info-bar-warning info-bar-running'
|
cls = 'info-bar-warning info-bar-running'
|
||||||
else:
|
else:
|
||||||
timing = _format_timing(secs)
|
timing = _format_timing(secs)
|
||||||
text = f'{e(o_user)} has pending changes which will be applied {timing}.' if timing else f'{e(o_user)} has pending changes which will be applied on the next timer tick.'
|
text = f'{e(o_user)} has pending changes which will be applied {timing}.' if timing else f'{e(o_user)} has pending changes. The processing service is not running.'
|
||||||
cls = 'info-bar-warning'
|
cls = 'info-bar-warning'
|
||||||
other_bars += f'<div class="info-bar {cls}" data-apply-uuid="{e(o_uuid)}" data-apply-user="{e(o_user)}">{text}</div>\n'
|
other_bars += f'<div class="info-bar {cls}" data-apply-uuid="{e(o_uuid)}" data-apply-user="{e(o_user)}">{text}</div>\n'
|
||||||
|
|
||||||
|
|
@ -1663,56 +1663,45 @@ def render_layout(view_id, content_html, tokens):
|
||||||
if item.get('status') == 'problem':
|
if item.get('status') == 'problem':
|
||||||
sev = item.get('severity', 'error')
|
sev = item.get('severity', 'error')
|
||||||
text = e(item.get('detail', item.get('name', '')))
|
text = e(item.get('detail', item.get('name', '')))
|
||||||
tip = item.get('suggestion', '')
|
grouped.setdefault(sev, []).append(text)
|
||||||
grouped.setdefault(sev, []).append((text, tip))
|
|
||||||
for item in st.get('services', []):
|
for item in st.get('services', []):
|
||||||
if item.get('status') == 'problem':
|
if item.get('status') == 'problem':
|
||||||
name = item.get('name', '')
|
name = item.get('name', '')
|
||||||
utype = 'timer' if name.endswith('.timer') else 'service' if name.endswith('.service') else 'unit'
|
utype = 'timer' if name.endswith('.timer') else 'service' if name.endswith('.service') else 'unit'
|
||||||
exp_parts, act_parts, fix_parts = [], [], []
|
exp_parts, act_parts = [], []
|
||||||
if not item.get('active_ok'):
|
if not item.get('active_ok'):
|
||||||
exp_parts.append(item.get('expected_active', 'active'))
|
exp_parts.append(item.get('expected_active', 'active'))
|
||||||
act_parts.append(item.get('active', 'unknown'))
|
act_parts.append(item.get('active', 'unknown'))
|
||||||
fix_parts.append('activate')
|
|
||||||
if not item.get('enabled_ok'):
|
if not item.get('enabled_ok'):
|
||||||
exp_parts.append(item.get('expected_enabled', 'enabled'))
|
exp_parts.append(item.get('expected_enabled', 'enabled'))
|
||||||
act_parts.append(item.get('enabled', 'unknown'))
|
act_parts.append(item.get('enabled', 'unknown'))
|
||||||
fix_parts.append('enable')
|
|
||||||
detail = (f"The {utype} `{name}` is expected to be "
|
detail = (f"The {utype} `{name}` is expected to be "
|
||||||
f"{' and '.join(exp_parts)} but is {' and '.join(act_parts)}.")
|
f"{' and '.join(exp_parts)} but is {' and '.join(act_parts)}.")
|
||||||
tip = f"Run `sudo python3 core.py --apply` to {' and '.join(reversed(fix_parts))} it."
|
grouped.setdefault(item.get('severity', 'error'), []).append(e(detail))
|
||||||
sev = item.get('severity', 'error')
|
has_problems = any(items for items in grouped.values())
|
||||||
grouped.setdefault(sev, []).append((e(detail), tip))
|
fix_suffix = ''
|
||||||
|
if has_problems:
|
||||||
|
fix_uuid, fix_ts = queue_command('core apply')
|
||||||
|
if _apply_changes_immediately():
|
||||||
|
if _is_locked():
|
||||||
|
mtime = _lock_mtime()
|
||||||
|
fix_suffix = ('Fix is being applied now...' if fix_ts and mtime and fix_ts < mtime
|
||||||
|
else 'Fix will be applied on the next run.')
|
||||||
|
else:
|
||||||
|
timing = _format_timing(_seconds_until_next_run())
|
||||||
|
fix_suffix = (f'Fix will be applied {timing}.' if timing
|
||||||
|
else 'Fix pending. The processing service is not running.')
|
||||||
|
else:
|
||||||
|
fix_suffix = 'Fix pending. Visit Actions page ASAP to apply fix.'
|
||||||
for sev, items in grouped.items():
|
for sev, items in grouped.items():
|
||||||
if not items:
|
if not items:
|
||||||
continue
|
continue
|
||||||
cls = 'info-bar-danger' if sev == 'error' else 'info-bar-warning'
|
cls = 'info-bar-danger' if sev == 'error' else 'info-bar-warning'
|
||||||
seen_cmds, fix_cmds = set(), []
|
|
||||||
for _, tip in items:
|
|
||||||
if tip:
|
|
||||||
m = re.search(r'`([^`]+)`', tip)
|
|
||||||
cmd = m.group(1) if m else tip
|
|
||||||
if cmd not in seen_cmds:
|
|
||||||
seen_cmds.add(cmd)
|
|
||||||
fix_cmds.append(cmd)
|
|
||||||
problems_list = ('<ul style="margin:0.25em 0;padding-left:1.25em">'
|
problems_list = ('<ul style="margin:0.25em 0;padding-left:1.25em">'
|
||||||
+ ''.join(f'<li>{d}</li>' for d, _ in items)
|
+ ''.join(f'<li>{d}</li>' for d in items)
|
||||||
+ '</ul>')
|
+ '</ul>')
|
||||||
fix_html = ''
|
fix_html = (f'<div style="margin-top:0.5em" data-health-uuid="{e(fix_uuid)}">{e(fix_suffix)}</div>'
|
||||||
if fix_cmds:
|
if fix_suffix else '')
|
||||||
_queue_urls = {
|
|
||||||
'sudo python3 core.py --apply': '/action/queue_core_apply',
|
|
||||||
}
|
|
||||||
def _fix_item(c):
|
|
||||||
url = _queue_urls.get(c)
|
|
||||||
if url:
|
|
||||||
btn = (f'<form class="form-inline" method="post" action="{e(url)}">'
|
|
||||||
f'<button type="submit" class="btn-link">Click here</button></form>')
|
|
||||||
return f'<li>{btn} or manually run <code>{e(c)}</code></li>'
|
|
||||||
return f'<li>Run <code>{e(c)}</code></li>'
|
|
||||||
fix_items = ''.join(_fix_item(c) for c in fix_cmds)
|
|
||||||
fix_html = ('<div style="margin-top:0.5em"><strong>To fix:</strong></div>'
|
|
||||||
f'<ul style="margin:0.25em 0;padding-left:1.25em">{fix_items}</ul>')
|
|
||||||
content = ('<div style="width:100%">'
|
content = ('<div style="width:100%">'
|
||||||
'<div style="font-weight:600;margin-bottom:0.25em">Health check - problems found:</div>'
|
'<div style="font-weight:600;margin-bottom:0.25em">Health check - problems found:</div>'
|
||||||
+ problems_list + fix_html
|
+ problems_list + fix_html
|
||||||
|
|
@ -2555,44 +2544,27 @@ var validateEl;
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
function startApplyPoller(uuid, bar, mine) {
|
function timingPhrase(n) {
|
||||||
|
return n <= 5 ? 'momentarily'
|
||||||
|
: n < 60 ? 'in about ' + n + ' seconds'
|
||||||
|
: 'in about ' + Math.round(n / 60) + ' minute' + (Math.round(n / 60) !== 1 ? 's' : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function startPoller(uuid, handlers) {
|
||||||
var nextIn = null;
|
var nextIn = null;
|
||||||
var pollTimer = null;
|
var pollTimer = null;
|
||||||
var tickTimer = null;
|
var tickTimer = null;
|
||||||
|
|
||||||
function user() { return bar.getAttribute('data-apply-user') || ''; }
|
|
||||||
function esc(s) { return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
||||||
function setHtml(html) { bar.innerHTML = html; }
|
|
||||||
|
|
||||||
function updateCountdown() {
|
|
||||||
if (nextIn === null) {
|
|
||||||
setHtml(mine ? 'Configuration saved. The command processing service is not installed. Run <strong>core.py --install</strong> to enable it, or <strong>core.py --apply</strong> to apply manually.'
|
|
||||||
: esc(user()) + ' has pending changes. The command processing service is not installed.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var timing = nextIn <= 5 ? 'momentarily'
|
|
||||||
: nextIn < 60 ? 'in about ' + nextIn + ' seconds'
|
|
||||||
: 'in about ' + Math.round(nextIn / 60) + ' minute' + (Math.round(nextIn / 60) !== 1 ? 's' : '');
|
|
||||||
setHtml(mine ? 'Configuration saved. Changes will be applied ' + timing + '.'
|
|
||||||
: esc(user()) + ' has pending changes which will be applied ' + timing + '.');
|
|
||||||
}
|
|
||||||
|
|
||||||
function onStatus(data) {
|
function onStatus(data) {
|
||||||
if (data.status === 'complete') {
|
if (data.status === 'complete') {
|
||||||
bar.classList.remove('info-bar-running');
|
clearTimeout(pollTimer); clearTimeout(tickTimer);
|
||||||
setHtml(mine ? 'Changes have been applied.' : esc(user()) + '\'s changes have been applied.');
|
handlers.onComplete(); return;
|
||||||
clearTimeout(pollTimer);
|
|
||||||
clearTimeout(tickTimer);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (data.status === 'running') {
|
if (data.status === 'running') {
|
||||||
bar.classList.add('info-bar-running');
|
handlers.onRunning();
|
||||||
setHtml(mine ? 'Configuration saved. Changes are being applied now...'
|
|
||||||
: esc(user()) + '\'s changes are being applied now...');
|
|
||||||
} else {
|
} else {
|
||||||
bar.classList.remove('info-bar-running');
|
|
||||||
if (data.next_in !== null && data.next_in !== undefined) { nextIn = data.next_in; }
|
if (data.next_in !== null && data.next_in !== undefined) { nextIn = data.next_in; }
|
||||||
updateCountdown();
|
handlers.onPending(nextIn);
|
||||||
}
|
}
|
||||||
pollTimer = setTimeout(doPoll, 3000);
|
pollTimer = setTimeout(doPoll, 3000);
|
||||||
}
|
}
|
||||||
|
|
@ -2605,7 +2577,7 @@ function startApplyPoller(uuid, bar, mine) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function tick() {
|
function tick() {
|
||||||
if (nextIn !== null && nextIn > 0) { nextIn--; updateCountdown(); }
|
if (nextIn !== null && nextIn > 0) { nextIn--; handlers.onPending(nextIn); }
|
||||||
tickTimer = setTimeout(tick, 1000);
|
tickTimer = setTimeout(tick, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2613,6 +2585,31 @@ function startApplyPoller(uuid, bar, mine) {
|
||||||
tick();
|
tick();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function startApplyPoller(uuid, bar, mine) {
|
||||||
|
function user() { return bar.getAttribute('data-apply-user') || ''; }
|
||||||
|
function esc(s) { return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||||||
|
startPoller(uuid, {
|
||||||
|
onPending: function(nextIn) {
|
||||||
|
bar.classList.remove('info-bar-running');
|
||||||
|
bar.innerHTML = nextIn === null
|
||||||
|
? (mine ? 'Configuration saved. The processing service is not running.'
|
||||||
|
: esc(user()) + ' has pending changes. The processing service is not running.')
|
||||||
|
: (mine ? 'Configuration saved. Your changes will be applied ' + timingPhrase(nextIn) + '.'
|
||||||
|
: esc(user()) + ' has pending changes which will be applied ' + timingPhrase(nextIn) + '.');
|
||||||
|
},
|
||||||
|
onRunning: function() {
|
||||||
|
bar.classList.add('info-bar-running');
|
||||||
|
bar.innerHTML = mine ? 'Configuration saved. Your changes are being applied now...'
|
||||||
|
: esc(user()) + '\'s changes are being applied now...';
|
||||||
|
},
|
||||||
|
onComplete: function() {
|
||||||
|
bar.classList.remove('info-bar-running');
|
||||||
|
bar.innerHTML = mine ? 'Your changes have been applied.'
|
||||||
|
: esc(user()) + '\'s changes have been applied.';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
if (typeof APPLY_UUID !== 'undefined' && APPLY_UUID) {
|
if (typeof APPLY_UUID !== 'undefined' && APPLY_UUID) {
|
||||||
var bar = document.querySelector('.info-bar-flash.info-bar-success');
|
var bar = document.querySelector('.info-bar-flash.info-bar-success');
|
||||||
|
|
@ -2621,6 +2618,16 @@ function startApplyPoller(uuid, bar, mine) {
|
||||||
document.querySelectorAll('[data-apply-uuid]').forEach(function(bar) {
|
document.querySelectorAll('[data-apply-uuid]').forEach(function(bar) {
|
||||||
startApplyPoller(bar.getAttribute('data-apply-uuid'), bar, false);
|
startApplyPoller(bar.getAttribute('data-apply-uuid'), bar, false);
|
||||||
});
|
});
|
||||||
|
document.querySelectorAll('[data-health-uuid]').forEach(function(el) {
|
||||||
|
startPoller(el.getAttribute('data-health-uuid'), {
|
||||||
|
onPending: function(nextIn) {
|
||||||
|
el.textContent = nextIn === null ? 'Fix pending. The processing service is not running.'
|
||||||
|
: 'Fix will be applied ' + timingPhrase(nextIn) + '.';
|
||||||
|
},
|
||||||
|
onRunning: function() { el.textContent = 'Fix is being applied now...'; },
|
||||||
|
onComplete: function() { el.textContent = 'Fix has been applied.'; }
|
||||||
|
});
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue