mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-10 18:34:18 +08:00
295 lines
11 KiB
JavaScript
295 lines
11 KiB
JavaScript
'use strict';
|
|
/* global hmUi */
|
|
'require view';
|
|
'require ui';
|
|
'require haproxy-manager.ui as hmUi';
|
|
|
|
function parseStatus(output) {
|
|
var status = { listeners: [] };
|
|
|
|
String(output || '').split(/\r?\n/).forEach(function(line) {
|
|
var fields = line.split('\t');
|
|
if (fields[0] == 'listener') {
|
|
status.listeners.push({
|
|
protocol: fields[1] || '',
|
|
address: fields[2] || '',
|
|
process: fields.slice(3).join('\t') || ''
|
|
});
|
|
}
|
|
else if (fields[0])
|
|
status[fields[0]] = fields.slice(1).join('\t');
|
|
});
|
|
|
|
return status;
|
|
}
|
|
|
|
function parseBackups(output) {
|
|
return String(output || '').split(/\r?\n/).map(function(line) {
|
|
var fields = line.split('\t');
|
|
return fields[0] == 'backup' ? { id: fields[1], latest: fields[2] == '1', files: fields[3] } : null;
|
|
}).filter(Boolean);
|
|
}
|
|
|
|
function parseIncidents(output) {
|
|
return String(output || '').split(/\r?\n/).map(function(line) {
|
|
var fields = line.split('\t');
|
|
return fields[0] == 'incident' ? {
|
|
id: fields[1],
|
|
latest: fields[2] == '1',
|
|
result: fields[3] || '',
|
|
action: fields[4] || '',
|
|
interface: fields[5] || '',
|
|
reason: fields[6] || ''
|
|
} : null;
|
|
}).filter(Boolean);
|
|
}
|
|
|
|
function parseFirewall(output) {
|
|
var result = { conflicts: '0', enabled: '0', policy: '' };
|
|
String(output || '').split(/\r?\n/).forEach(function(line) {
|
|
var fields = line.split('\t');
|
|
if (fields[0] && fields[0] != 'conflict' && fields[0] != 'port')
|
|
result[fields[0]] = fields.slice(1).join('\t');
|
|
});
|
|
return result;
|
|
}
|
|
|
|
function backupDate(id) {
|
|
var match = String(id || '').match(/^(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})$/);
|
|
return match ? '%s-%s-%s %s:%s:%s'.format(match[1], match[2], match[3], match[4], match[5], match[6]) : id;
|
|
}
|
|
|
|
function statusItem(label, value) {
|
|
return E('div', { 'class': 'hm-status-item' }, [
|
|
E('span', { 'class': 'hm-status-label' }, label),
|
|
E('span', { 'class': 'hm-status-value' }, value)
|
|
]);
|
|
}
|
|
|
|
return view.extend({
|
|
load: function() {
|
|
return Promise.all([
|
|
hmUi.exec('/usr/libexec/haproxy-manager/status', []).catch(function(err) {
|
|
return { stdout: '', stderr: err.message || String(err), code: 1 };
|
|
}),
|
|
hmUi.exec('/usr/libexec/haproxy-manager/backups', []).catch(function() { return { stdout: '' }; }),
|
|
hmUi.exec('/usr/libexec/haproxy-manager/firewall-plan', []).catch(function() { return { stdout: '' }; }),
|
|
hmUi.exec('/usr/libexec/haproxy-manager/incidents', []).catch(function() { return { stdout: '' }; })
|
|
]);
|
|
},
|
|
|
|
showIncident: function(incidentId) {
|
|
return hmUi.exec('/usr/libexec/haproxy-manager/incident', [ incidentId ]).then(function(result) {
|
|
ui.showModal(_('Incident diagnostics'), [
|
|
E('p', _('Diagnostic report for %s.').format(backupDate(incidentId))),
|
|
E('pre', { 'class': 'hm-incident-report' }, result.stdout || _('Not available')),
|
|
E('div', { 'class': 'right' }, E('button', {
|
|
'class': 'btn cbi-button cbi-button-action',
|
|
'type': 'button',
|
|
'click': ui.hideModal
|
|
}, _('Close')))
|
|
]);
|
|
}).catch(function(err) {
|
|
hmUi.notifyError(err);
|
|
});
|
|
},
|
|
|
|
showRestore: function(backupId) {
|
|
ui.showModal(_('Restore configuration?'), [
|
|
E('p', _('HAProxy, firewall, and LuCI settings will be restored from %s.').format(backupDate(backupId))),
|
|
E('div', { 'class': 'right' }, [
|
|
E('button', {
|
|
'class': 'btn cbi-button',
|
|
'type': 'button',
|
|
'click': ui.hideModal
|
|
}, _('Cancel')),
|
|
' ',
|
|
E('button', {
|
|
'class': 'btn cbi-button cbi-button-negative important',
|
|
'type': 'button',
|
|
'click': ui.createHandlerFn(this, function() {
|
|
return hmUi.exec('/usr/libexec/haproxy-manager/rollback', [ backupId ]).then(function(r) {
|
|
ui.hideModal();
|
|
hmUi.notify(r.stdout || _('Restored'), 'info');
|
|
hmUi.reloadAfterApply(3500);
|
|
}).catch(function(err) {
|
|
ui.hideModal();
|
|
hmUi.notifyError(err);
|
|
});
|
|
})
|
|
}, _('Restore'))
|
|
])
|
|
]);
|
|
},
|
|
|
|
render: function(data) {
|
|
var res = data[0];
|
|
var status = parseStatus(res.stdout);
|
|
var backups = parseBackups(data[1].stdout);
|
|
var firewall = parseFirewall(data[2].stdout);
|
|
var incidents = parseIncidents(data[3].stdout);
|
|
var running = status.service == 'running';
|
|
var autoRecovery = status.auto_recovery != '0';
|
|
var recoveryResults = {
|
|
'recovered': _('Recovered'),
|
|
'failed': _('Failed'),
|
|
'invalid-config': _('Invalid configuration'),
|
|
'backup-failed': _('Recovery point failed'),
|
|
'detected': _('In progress')
|
|
};
|
|
recoveryResults.reconciled = _('WAN address reconciled');
|
|
recoveryResults['rolled-back'] = _('Failed, configuration restored');
|
|
recoveryResults['rollback-failed'] = _('Failed, restore also failed');
|
|
recoveryResults['address-unavailable'] = _('WAN address unavailable');
|
|
recoveryResults['invalid-generated-config'] = _('Generated configuration is invalid');
|
|
recoveryResults['install-failed'] = _('Configuration installation failed');
|
|
var lastRecovery = status.last_incident ? '%s - %s%s'.format(
|
|
backupDate(status.last_incident),
|
|
recoveryResults[status.last_incident_result] || status.last_incident_result,
|
|
status.last_incident_interface ? ' (%s)'.format(status.last_incident_interface) : ''
|
|
) : _('No incidents recorded');
|
|
var firewallText = firewall.enabled != '1' ? _('Manual') : +firewall.conflicts > 0 ?
|
|
_('%d conflicts').format(+firewall.conflicts) : _('Managed');
|
|
var modes = {
|
|
generated: _('Generated routes'),
|
|
raw: _('Raw configuration'),
|
|
none: _('Not managed')
|
|
};
|
|
var listeners = status.listeners.map(function(listener) {
|
|
return E('tr', { 'class': 'tr' }, [
|
|
E('td', { 'class': 'td', 'data-title': _('Protocol') }, listener.protocol),
|
|
E('td', { 'class': 'td', 'data-title': _('Address') }, E('code', listener.address)),
|
|
E('td', { 'class': 'td', 'data-title': _('Process') }, listener.process)
|
|
]);
|
|
});
|
|
var backupRows = backups.map(function(backup) {
|
|
return E('tr', { 'class': 'tr' }, [
|
|
E('td', { 'class': 'td', 'data-title': _('Created') }, [
|
|
E('span', backupDate(backup.id)),
|
|
backup.latest ? E('span', { 'class': 'hm-badge hm-backup-latest' }, _('Latest')) : ''
|
|
]),
|
|
E('td', { 'class': 'td', 'data-title': _('Contents') }, _('%d configuration files').format(+backup.files || 0)),
|
|
E('td', { 'class': 'td cbi-section-actions' }, E('button', {
|
|
'class': 'btn cbi-button cbi-button-action',
|
|
'type': 'button',
|
|
'click': ui.createHandlerFn(this, function() { this.showRestore(backup.id); })
|
|
}, _('Restore')))
|
|
]);
|
|
}.bind(this));
|
|
var incidentRows = incidents.map(function(incident) {
|
|
var result = recoveryResults[incident.result] || incident.result || _('Not available');
|
|
var context = [ incident.action, incident.interface ].filter(Boolean).join(' / ');
|
|
return E('tr', { 'class': 'tr' }, [
|
|
E('td', { 'class': 'td', 'data-title': _('Created') }, [
|
|
E('span', backupDate(incident.id)),
|
|
incident.latest ? E('span', { 'class': 'hm-badge hm-backup-latest' }, _('Latest')) : ''
|
|
]),
|
|
E('td', { 'class': 'td', 'data-title': _('Result') }, result),
|
|
E('td', { 'class': 'td', 'data-title': _('Trigger') }, context || incident.reason || '-'),
|
|
E('td', { 'class': 'td cbi-section-actions' }, E('button', {
|
|
'class': 'btn cbi-button cbi-button-action',
|
|
'type': 'button',
|
|
'click': ui.createHandlerFn(this, function() { return this.showIncident(incident.id); })
|
|
}, _('View')))
|
|
]);
|
|
}.bind(this));
|
|
|
|
hmUi.ensureStyles();
|
|
|
|
if (!listeners.length) {
|
|
listeners.push(E('tr', { 'class': 'tr placeholder' }, [
|
|
E('td', { 'class': 'td', 'colspan': '3' }, _('No HAProxy or LuCI listeners detected.'))
|
|
]));
|
|
}
|
|
|
|
if (!backupRows.length) {
|
|
backupRows.push(E('tr', { 'class': 'tr placeholder' }, [
|
|
E('td', { 'class': 'td', 'colspan': '3' }, _('No recovery points yet.'))
|
|
]));
|
|
}
|
|
|
|
if (!incidentRows.length) {
|
|
incidentRows.push(E('tr', { 'class': 'tr placeholder' }, [
|
|
E('td', { 'class': 'td', 'colspan': '4' }, _('No incidents recorded'))
|
|
]));
|
|
}
|
|
|
|
return E('div', { 'class': 'cbi-map' }, [
|
|
E('h2', _('HAProxy Status')),
|
|
E('div', { 'class': 'cbi-map-descr' }, _('Service health, active listeners, and recovery points.')),
|
|
res.code && res.stderr ? E('div', { 'class': 'alert-message error' }, res.stderr) : '',
|
|
E('div', { 'class': 'hm-status-grid' }, [
|
|
statusItem(_('Service'), E('span', {
|
|
'class': 'hm-state %s'.format(running ? 'hm-state-on' : 'hm-state-off')
|
|
}, running ? _('Running') : _('Stopped'))),
|
|
statusItem(_('WAN address'), status.wan_ip || _('Not set')),
|
|
statusItem(_('HAProxy version'), status.version || _('Not available')),
|
|
statusItem(_('Automatic recovery'), E('span', {
|
|
'class': 'hm-state %s'.format(autoRecovery ? 'hm-state-on' : 'hm-state-off')
|
|
}, autoRecovery ? _('Enabled') : _('Disabled'))),
|
|
statusItem(_('Configuration mode'), modes[status.active_mode] || status.active_mode || modes.none),
|
|
statusItem(_('Last recovery'), lastRecovery),
|
|
statusItem(_('Firewall'), E('span', {
|
|
'class': 'hm-state %s'.format(+firewall.conflicts > 0 ? 'hm-state-danger' : firewall.enabled == '1' ? 'hm-state-on' : 'hm-state-off')
|
|
}, firewallText))
|
|
]),
|
|
E('h3', _('Active listeners')),
|
|
E('table', { 'class': 'table hm-listener-table' }, [
|
|
E('thead', {}, [ E('tr', { 'class': 'tr table-titles' }, [
|
|
E('th', { 'class': 'th' }, _('Protocol')),
|
|
E('th', { 'class': 'th' }, _('Address')),
|
|
E('th', { 'class': 'th' }, _('Process'))
|
|
]) ]),
|
|
E('tbody', {}, listeners)
|
|
]),
|
|
E('div', { 'class': 'hm-section-heading' }, [
|
|
E('h3', _('Recovery incidents')),
|
|
E('p', { 'class': 'cbi-section-descr' }, _('The seven latest automatic recovery reports are retained on the router.'))
|
|
]),
|
|
E('table', { 'class': 'table hm-incident-table' }, [
|
|
E('thead', {}, [ E('tr', { 'class': 'tr table-titles' }, [
|
|
E('th', { 'class': 'th' }, _('Created')),
|
|
E('th', { 'class': 'th' }, _('Result')),
|
|
E('th', { 'class': 'th' }, _('Trigger')),
|
|
E('th', { 'class': 'th' })
|
|
]) ]),
|
|
E('tbody', {}, incidentRows)
|
|
]),
|
|
E('div', { 'id': 'recovery', 'class': 'hm-section-heading' }, [
|
|
E('div', { 'class': 'hm-section-title-row' }, [
|
|
E('h3', _('Recovery points')),
|
|
E('button', {
|
|
'class': 'btn cbi-button cbi-button-add',
|
|
'type': 'button',
|
|
'click': ui.createHandlerFn(this, function() {
|
|
return hmUi.exec('/usr/libexec/haproxy-manager/backup', []).then(function() {
|
|
window.location.reload();
|
|
}).catch(function(err) { hmUi.notifyError(err); });
|
|
})
|
|
}, _('Create recovery point'))
|
|
]),
|
|
E('p', { 'class': 'cbi-section-descr' }, _('Restore HAProxy, firewall, and LuCI settings from a previous snapshot.'))
|
|
]),
|
|
E('table', { 'class': 'table hm-backup-table' }, [
|
|
E('thead', {}, [ E('tr', { 'class': 'tr table-titles' }, [
|
|
E('th', { 'class': 'th' }, _('Created')),
|
|
E('th', { 'class': 'th' }, _('Contents')),
|
|
E('th', { 'class': 'th' })
|
|
]) ]),
|
|
E('tbody', {}, backupRows)
|
|
]),
|
|
E('div', { 'class': 'cbi-page-actions hm-actions' }, [
|
|
E('button', {
|
|
'class': 'btn cbi-button cbi-button-action',
|
|
'type': 'button',
|
|
'click': function() { window.location.reload(); }
|
|
}, _('Refresh'))
|
|
])
|
|
]);
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|