mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-10 18:34:18 +08:00
🎈 Sync 2026-09-09 01:06:03
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
#
|
||||
# Copyright (C) 2026 HAProxy Manager contributors
|
||||
#
|
||||
# This is free software, licensed under the MIT License.
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_VERSION:=0.6.1
|
||||
PKG_RELEASE:=1
|
||||
PKG_LICENSE:=MIT
|
||||
PKG_MAINTAINER:=Philipp Shklyaev <job@umk0.ru>
|
||||
LUCI_MAINTAINER:=$(PKG_MAINTAINER)
|
||||
|
||||
LUCI_TITLE:=LuCI HAProxy Manager
|
||||
LUCI_DESCRIPTION:=Manage Web and TCP HAProxy services, firewall access, and recovery points.
|
||||
LUCI_DEPENDS:=+luci-base +rpcd +rpcd-mod-file +haproxy
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
define Package/luci-app-haproxy-manager/conffiles
|
||||
/etc/config/haproxy_manager
|
||||
endef
|
||||
|
||||
define Package/luci-app-haproxy-manager/postinst
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||
/usr/libexec/haproxy-manager/migrate >/dev/null 2>&1 || logger -t haproxy-manager "Configuration migration failed"
|
||||
rm -f /tmp/luci-indexcache.* 2>/dev/null || true
|
||||
rm -rf /tmp/luci-modulecache/ 2>/dev/null || true
|
||||
/etc/init.d/rpcd reload >/dev/null 2>&1 || true
|
||||
/usr/libexec/haproxy-manager/recover package install >/dev/null 2>&1 &
|
||||
}
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Package/luci-app-haproxy-manager/prerm
|
||||
#!/bin/sh
|
||||
case "$${PKG_UPGRADE:-0}:$${1:-}" in
|
||||
1:*|*:upgrade) exit 0 ;;
|
||||
esac
|
||||
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||
/usr/libexec/haproxy-manager/uninstall >/dev/null 2>&1 ||
|
||||
logger -t haproxy-manager "Package runtime cleanup failed"
|
||||
}
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Package/luci-app-haproxy-manager/postrm
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||
rm -f /tmp/luci-indexcache.* 2>/dev/null || true
|
||||
rm -rf /tmp/luci-modulecache/ 2>/dev/null || true
|
||||
/etc/init.d/rpcd reload >/dev/null 2>&1 || true
|
||||
}
|
||||
exit 0
|
||||
endef
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
@@ -0,0 +1,305 @@
|
||||
.hm-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin: 0 0 1.25rem;
|
||||
}
|
||||
|
||||
.hm-filter {
|
||||
appearance: none;
|
||||
box-sizing: border-box;
|
||||
width: min(100%, 28rem);
|
||||
min-height: 2.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--border-color-medium, rgba(128, 128, 128, 0.45));
|
||||
border-radius: 4px;
|
||||
outline: 0;
|
||||
background: var(--background-color-high, rgba(128, 128, 128, 0.06));
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.hm-filter:focus {
|
||||
border-color: var(--primary-color-medium, currentColor);
|
||||
box-shadow: 0 0 0 2px rgba(128, 128, 128, 0.18);
|
||||
}
|
||||
|
||||
.hm-route-count {
|
||||
white-space: nowrap;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.hm-endpoint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.hm-endpoint-value,
|
||||
.hm-destination {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.hm-port-summary {
|
||||
opacity: 0.72;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.hm-port-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.hm-port-list code,
|
||||
.hm-destination code {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.hm-destination {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.hm-badge,
|
||||
.hm-state {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: max-content;
|
||||
min-height: 1.6rem;
|
||||
padding: 0 0.45rem;
|
||||
border: 1px solid rgba(128, 128, 128, 0.45);
|
||||
border-radius: 4px;
|
||||
font-size: 0.82em;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.hm-state-off {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.hm-state-danger {
|
||||
border-color: var(--error-color, #b94a48);
|
||||
color: var(--error-color, #b94a48);
|
||||
}
|
||||
|
||||
.hm-filter-empty {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.cbi-section-table-row[hidden],
|
||||
.hm-filter-empty[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.hm-route-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.hm-service-cell {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.hm-mobile-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hm-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.hm-notification {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.hm-incident-report {
|
||||
box-sizing: border-box;
|
||||
max-height: min(60vh, 36rem);
|
||||
margin: 1rem 0;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.hm-inline-alert {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.hm-status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 1px;
|
||||
margin: 1.25rem 0 2rem;
|
||||
border: 1px solid rgba(128, 128, 128, 0.35);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hm-status-item {
|
||||
min-width: 0;
|
||||
padding: 1rem;
|
||||
background: var(--background-color-low, rgba(128, 128, 128, 0.08));
|
||||
}
|
||||
|
||||
.hm-status-label {
|
||||
display: block;
|
||||
margin-bottom: 0.35rem;
|
||||
font-size: 0.85em;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.hm-status-value {
|
||||
display: block;
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 1.05em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hm-listener-table code {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.hm-section-heading {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.hm-section-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.hm-section-title-row h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hm-section-heading > p {
|
||||
margin: 0.25rem 0 0.75rem;
|
||||
}
|
||||
|
||||
.hm-backup-latest {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.hm-backup-table .cbi-section-actions {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.hm-raw-editor {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: 32rem;
|
||||
font-family: monospace;
|
||||
line-height: 1.45;
|
||||
tab-size: 2;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.hm-status-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.hm-toolbar {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.hm-filter {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hm-status-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.hm-section-title-row {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.hm-section-title-row .cbi-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hm-actions > .btn,
|
||||
.hm-actions > .cbi-button {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.hm-mobile-only {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.cbi-section-table-row > .td[data-name="_status"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.hm-route-table .cbi-section-table-row[data-sid] {
|
||||
display: grid !important;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 0.75rem 1rem;
|
||||
padding: 1rem !important;
|
||||
}
|
||||
|
||||
.hm-route-table .cbi-section-table-row[data-sid] > .td {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: auto !important;
|
||||
min-width: 0 !important;
|
||||
padding: 0 !important;
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
.hm-route-table .cbi-section-table-row[data-sid] > .td::before {
|
||||
display: none !important;
|
||||
content: none !important;
|
||||
}
|
||||
|
||||
.hm-route-table .cbi-section-table-row[data-sid] > .td[data-name="_service"],
|
||||
.hm-route-table .cbi-section-table-row[data-sid] > .td[data-name="_endpoint"] {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.hm-route-table .cbi-section-table-row[data-sid] > .td[data-name="_destination"] {
|
||||
align-self: center;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.hm-route-table .cbi-section-table-row[data-sid] > .cbi-section-actions {
|
||||
align-self: center;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.hm-route-table .cbi-section-table-row[data-sid] > .td[data-name="_firewall"] {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.hm-route-table .cbi-section-table-row[data-sid] > .cbi-section-actions > div {
|
||||
display: flex;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.hm-raw-editor {
|
||||
min-height: 24rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require fs';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
|
||||
var callUciCommit = rpc.declare({
|
||||
object: 'uci',
|
||||
method: 'commit',
|
||||
params: [ 'config' ],
|
||||
reject: true
|
||||
});
|
||||
var APPLY_LOCK_BUSY = 75;
|
||||
|
||||
function recoveryId(output) {
|
||||
var path = String(output || '').trim();
|
||||
var id = path.split('/').pop();
|
||||
|
||||
if (!/^\d{8}-\d{6}$/.test(id))
|
||||
throw new Error(_('Unable to create a recovery point.'));
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
ensureStyles: function() {
|
||||
if (document.querySelector('link[data-haproxy-manager-style]'))
|
||||
return;
|
||||
|
||||
document.head.appendChild(E('link', {
|
||||
'rel': 'stylesheet',
|
||||
'href': L.resource('haproxy-manager/style.css'),
|
||||
'data-haproxy-manager-style': '1'
|
||||
}));
|
||||
},
|
||||
|
||||
notify: function(message, level) {
|
||||
ui.addNotification(null, E('pre', { 'class': 'hm-notification' }, String(message || '').trim()), level || 'info');
|
||||
},
|
||||
|
||||
notifyError: function(error) {
|
||||
this.notify(error && error.message ? error.message : String(error), 'danger');
|
||||
},
|
||||
|
||||
exec: function(path, args) {
|
||||
return fs.exec(path, args || []).then(function(result) {
|
||||
if (!result.code)
|
||||
return result;
|
||||
|
||||
var message = String(result.stderr || result.stdout ||
|
||||
_('Command failed with code %d.').format(result.code)).trim();
|
||||
var error = new Error(message);
|
||||
error.code = result.code;
|
||||
error.stdout = result.stdout;
|
||||
error.stderr = result.stderr;
|
||||
throw error;
|
||||
});
|
||||
},
|
||||
|
||||
commitAndApply: function() {
|
||||
var backupId;
|
||||
var committed = false;
|
||||
|
||||
return this.exec('/usr/libexec/haproxy-manager/backup', []).then(function(result) {
|
||||
backupId = recoveryId(result.stdout);
|
||||
return callUciCommit('haproxy_manager');
|
||||
}).then(function() {
|
||||
committed = true;
|
||||
return this.exec('/usr/libexec/haproxy-manager/apply', [ '--backup', backupId ]);
|
||||
}.bind(this)).catch(function(error) {
|
||||
if (!committed || !backupId || error.code !== APPLY_LOCK_BUSY)
|
||||
throw error;
|
||||
|
||||
return this.exec('/usr/libexec/haproxy-manager/rollback', [ backupId ]).catch(function() {
|
||||
return null;
|
||||
}).then(function() {
|
||||
throw error;
|
||||
});
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
saveAndApply: function(map) {
|
||||
return map.save(null, true).then(function() {
|
||||
return this.commitAndApply();
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
notifyApplied: function() {
|
||||
this.notify(_('Changes saved and applied.'), 'info');
|
||||
},
|
||||
|
||||
reloadAfterApply: function(delay) {
|
||||
window.setTimeout(function() {
|
||||
window.location.reload();
|
||||
}, delay || 2500);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
'use strict';
|
||||
/* global hmUi */
|
||||
'require view';
|
||||
'require fs';
|
||||
'require ui';
|
||||
'require haproxy-manager.ui as hmUi';
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
fs.read('/etc/haproxy.cfg').catch(function() {
|
||||
return '';
|
||||
})
|
||||
]);
|
||||
},
|
||||
|
||||
applyRaw: function(textarea) {
|
||||
ui.showModal(_('Apply raw configuration?'), [
|
||||
E('p', _('The file will be validated and backed up before HAProxy restarts.')),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button',
|
||||
'type': 'button',
|
||||
'click': ui.hideModal
|
||||
}, _('Cancel')),
|
||||
' ',
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-apply',
|
||||
'type': 'button',
|
||||
'click': ui.createHandlerFn(this, function() {
|
||||
return fs.write('/tmp/haproxy-manager-raw.cfg', textarea.value).then(function() {
|
||||
return hmUi.exec('/usr/libexec/haproxy-manager/apply-raw-file', [ '/tmp/haproxy-manager-raw.cfg' ]);
|
||||
}).then(function(r) {
|
||||
ui.hideModal();
|
||||
hmUi.notify(r.stdout || _('Applied'), 'info');
|
||||
}).catch(function(err) {
|
||||
ui.hideModal();
|
||||
hmUi.notifyError(err);
|
||||
});
|
||||
})
|
||||
}, _('Apply'))
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var cfg = data[0];
|
||||
var textarea = E('textarea', {
|
||||
'id': 'haproxy-raw-config',
|
||||
'class': 'cbi-input-textarea hm-raw-editor',
|
||||
'spellcheck': 'false',
|
||||
'aria-label': _('Raw HAProxy configuration')
|
||||
}, cfg || '');
|
||||
|
||||
hmUi.ensureStyles();
|
||||
|
||||
return E('div', { 'class': 'cbi-map' }, [
|
||||
E('h2', _('Raw HAProxy Config')),
|
||||
E('div', { 'class': 'cbi-map-descr' }, _('Expert editor for /etc/haproxy.cfg.')),
|
||||
E('div', { 'class': 'alert-message warning' }, _('Generated routes can overwrite manual changes to this file.')),
|
||||
textarea,
|
||||
E('div', { 'class': 'cbi-page-actions hm-actions' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'type': 'button',
|
||||
'click': ui.createHandlerFn(this, function() {
|
||||
return fs.write('/tmp/haproxy-manager-raw.cfg', textarea.value).then(function() {
|
||||
return hmUi.exec('/usr/libexec/haproxy-manager/validate', [ '/tmp/haproxy-manager-raw.cfg' ]);
|
||||
}).then(function(r) {
|
||||
hmUi.notify(r.stdout || _('Config is valid'), 'info');
|
||||
}).catch(function(err) {
|
||||
hmUi.notifyError(err);
|
||||
});
|
||||
})
|
||||
}, _('Validate')),
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-apply',
|
||||
'type': 'button',
|
||||
'click': ui.createHandlerFn(this, function() { this.applyRaw(textarea); })
|
||||
}, _('Apply raw config'))
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,464 @@
|
||||
'use strict';
|
||||
/* global hmUi */
|
||||
'require view';
|
||||
'require dom';
|
||||
'require form';
|
||||
'require ui';
|
||||
'require haproxy-manager.ui as hmUi';
|
||||
|
||||
function listValue(value) {
|
||||
if (Array.isArray(value))
|
||||
return value;
|
||||
if (value == null || value === '')
|
||||
return [];
|
||||
return String(value).trim().split(/\s+/);
|
||||
}
|
||||
|
||||
function routeKind(map, sectionId) {
|
||||
return map.data.get('haproxy_manager', sectionId, 'kind') ||
|
||||
map.data.get('haproxy_manager', sectionId, 'protocol') || 'web';
|
||||
}
|
||||
|
||||
function kindLabel(kind) {
|
||||
switch (kind) {
|
||||
case 'web': return _('Web');
|
||||
case 'ssh': return _('SSH');
|
||||
case 'rdp': return _('Remote Desktop');
|
||||
case 'custom': return _('Custom TCP');
|
||||
case 'http': return _('HTTP only');
|
||||
case 'https': return _('HTTPS only');
|
||||
case 'both': return _('Legacy Web');
|
||||
default: return _('TCP');
|
||||
}
|
||||
}
|
||||
|
||||
function portMapIsValid(value) {
|
||||
var match = String(value || '').match(/^(\d+):(\d+)$/);
|
||||
return !!match && +match[1] >= 1 && +match[1] <= 65535 && +match[2] >= 1 && +match[2] <= 65535;
|
||||
}
|
||||
|
||||
function parseFirewall(output) {
|
||||
var result = { ports: {}, conflicts: [], conflict_count: '0', enabled: '0', policy: '', managed_rule: '0' };
|
||||
|
||||
String(output || '').split(/\r?\n/).forEach(function(line) {
|
||||
var fields = line.split('\t');
|
||||
if (fields[0] == 'port')
|
||||
result.ports[fields[1]] = true;
|
||||
else if (fields[0] == 'conflict')
|
||||
result.conflicts.push({ section: fields[1], name: fields[2], ports: fields[3] });
|
||||
else if (fields[0] == 'conflicts')
|
||||
result.conflict_count = fields[1] || '0';
|
||||
else if (fields[0])
|
||||
result[fields[0]] = fields.slice(1).join('\t');
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function specContains(spec, port) {
|
||||
return String(spec || '').split(/[\s,]+/).some(function(token) {
|
||||
var range = token.split('-').map(Number);
|
||||
return range.length == 1 ? range[0] == port : port >= range[0] && port <= range[1];
|
||||
});
|
||||
}
|
||||
|
||||
function routePorts(map, sectionId, kind) {
|
||||
var main = function(option, fallback) {
|
||||
return map.data.get('haproxy_manager', 'main', option) || fallback;
|
||||
};
|
||||
|
||||
switch (kind) {
|
||||
case 'web':
|
||||
return [
|
||||
map.data.get('haproxy_manager', sectionId, 'web_http') == '0' ? null : main('http_port', '80'),
|
||||
map.data.get('haproxy_manager', sectionId, 'web_https') == '0' ? null : main('https_port', '443')
|
||||
].filter(Boolean);
|
||||
case 'ssh':
|
||||
return [map.data.get('haproxy_manager', sectionId, 'ssh_listen_port') || '22'];
|
||||
case 'rdp':
|
||||
return [map.data.get('haproxy_manager', sectionId, 'rdp_listen_port') || '3389'];
|
||||
case 'custom':
|
||||
return listValue(map.data.get('haproxy_manager', sectionId, 'port_map')).map(function(item) {
|
||||
return item.split(':')[0];
|
||||
});
|
||||
case 'http': return [main('http_port', '80')];
|
||||
case 'https': return [main('https_port', '443')];
|
||||
case 'both': return [main('http_port', '80'), main('https_port', '443')];
|
||||
default:
|
||||
return [map.data.get('haproxy_manager', sectionId, 'listen_port') ||
|
||||
map.data.get('haproxy_manager', sectionId, 'backend_port')].filter(Boolean);
|
||||
}
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
hmUi.exec('/usr/libexec/haproxy-manager/firewall-plan', []).catch(function() {
|
||||
return { stdout: '' };
|
||||
})
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var m, s, o;
|
||||
var firewall = parseFirewall(data[0].stdout);
|
||||
|
||||
hmUi.ensureStyles();
|
||||
|
||||
m = new form.Map('haproxy_manager', _('HAProxy Services'),
|
||||
_('Publish Web, SSH, Remote Desktop, and custom TCP services from one place.'));
|
||||
|
||||
s = m.section(form.GridSection, 'route', _('Services'));
|
||||
s.anonymous = true;
|
||||
s.addremove = true;
|
||||
s.sortable = true;
|
||||
s.nodescriptions = true;
|
||||
s.addbtntitle = _('Add service');
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enabled'));
|
||||
o.default = '1';
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'name', _('Service name'));
|
||||
o.placeholder = _('My service');
|
||||
o.rmempty = true;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'kind', _('Service type'));
|
||||
o.value('web', _('Web (HTTP + HTTPS)'));
|
||||
o.value('ssh', _('SSH'));
|
||||
o.value('rdp', _('Remote Desktop'));
|
||||
o.value('custom', _('Custom TCP'));
|
||||
o.default = 'web';
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'host', _('Domain'));
|
||||
o.placeholder = 'example.org';
|
||||
o.datatype = 'hostname';
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
o.depends('kind', 'web');
|
||||
|
||||
o = s.option(form.Value, 'backend_host', _('Destination host'));
|
||||
o.placeholder = '192.0.2.10';
|
||||
o.datatype = 'or(ipaddr,hostname)';
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'web_http', _('Publish HTTP'));
|
||||
o.default = '1';
|
||||
o.modalonly = true;
|
||||
o.depends('kind', 'web');
|
||||
|
||||
o = s.option(form.Value, 'backend_http_port', _('Destination HTTP port'));
|
||||
o.datatype = 'port';
|
||||
o.default = '80';
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
o.depends({ kind: 'web', web_http: '1' });
|
||||
|
||||
o = s.option(form.Flag, 'web_https', _('Publish HTTPS'));
|
||||
o.default = '1';
|
||||
o.modalonly = true;
|
||||
o.depends('kind', 'web');
|
||||
|
||||
o = s.option(form.Value, 'backend_https_port', _('Destination HTTPS port'));
|
||||
o.datatype = 'port';
|
||||
o.default = '443';
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
o.depends({ kind: 'web', web_https: '1' });
|
||||
|
||||
o = s.option(form.Value, 'ssh_listen_port', _('Public SSH port'));
|
||||
o.datatype = 'port';
|
||||
o.default = '22';
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
o.depends('kind', 'ssh');
|
||||
|
||||
o = s.option(form.Value, 'ssh_backend_port', _('Destination SSH port'));
|
||||
o.datatype = 'port';
|
||||
o.default = '22';
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
o.depends('kind', 'ssh');
|
||||
|
||||
o = s.option(form.Value, 'rdp_listen_port', _('Public RDP port'));
|
||||
o.datatype = 'port';
|
||||
o.default = '3389';
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
o.depends('kind', 'rdp');
|
||||
|
||||
o = s.option(form.Value, 'rdp_backend_port', _('Destination RDP port'));
|
||||
o.datatype = 'port';
|
||||
o.default = '3389';
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
o.depends('kind', 'rdp');
|
||||
|
||||
o = s.option(form.DynamicList, 'port_map', _('TCP port mappings'));
|
||||
o.placeholder = '8443:443';
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
o.depends('kind', 'custom');
|
||||
o.validate = function(sectionId, value) {
|
||||
return portMapIsValid(value) || _('Use the public:destination format, for example 8443:443.');
|
||||
};
|
||||
|
||||
o = s.option(form.DummyValue, '_status', _('Status'));
|
||||
o.modalonly = false;
|
||||
o.textvalue = function(sectionId) {
|
||||
var enabled = m.data.get('haproxy_manager', sectionId, 'enabled') != '0';
|
||||
return E('span', {
|
||||
'class': 'hm-state %s'.format(enabled ? 'hm-state-on' : 'hm-state-off')
|
||||
}, enabled ? _('On') : _('Off'));
|
||||
};
|
||||
|
||||
o = s.option(form.DummyValue, '_service', _('Service'));
|
||||
o.modalonly = false;
|
||||
o.textvalue = function(sectionId) {
|
||||
var kind = routeKind(m, sectionId);
|
||||
var name = m.data.get('haproxy_manager', sectionId, 'name') ||
|
||||
m.data.get('haproxy_manager', sectionId, 'host') || kindLabel(kind);
|
||||
var enabled = m.data.get('haproxy_manager', sectionId, 'enabled') != '0';
|
||||
|
||||
return E('div', { 'class': 'hm-service-cell' }, [
|
||||
E('div', { 'class': 'hm-route-name' }, [
|
||||
E('strong', name),
|
||||
E('span', {
|
||||
'class': 'hm-state hm-mobile-only %s'.format(enabled ? 'hm-state-on' : 'hm-state-off')
|
||||
}, enabled ? _('On') : _('Off'))
|
||||
]),
|
||||
E('span', { 'class': 'hm-badge' }, kindLabel(kind))
|
||||
]);
|
||||
};
|
||||
|
||||
o = s.option(form.DummyValue, '_endpoint', _('Public endpoint'));
|
||||
o.modalonly = false;
|
||||
o.textvalue = function(sectionId) {
|
||||
var kind = routeKind(m, sectionId);
|
||||
var ports = routePorts(m, sectionId, kind);
|
||||
var host = m.data.get('haproxy_manager', sectionId, 'host');
|
||||
|
||||
if (kind == 'web' || kind == 'http' || kind == 'https' || kind == 'both') {
|
||||
return E('div', { 'class': 'hm-endpoint' }, [
|
||||
E('span', { 'class': 'hm-endpoint-value' }, host || _('Not set')),
|
||||
E('span', { 'class': 'hm-port-summary' }, ports.map(function(port) { return ':' + port; }).join(' + '))
|
||||
]);
|
||||
}
|
||||
|
||||
return E('div', { 'class': 'hm-port-list' }, ports.map(function(port) {
|
||||
return E('code', ':' + port);
|
||||
}));
|
||||
};
|
||||
|
||||
o = s.option(form.DummyValue, '_destination', _('Destination'));
|
||||
o.modalonly = false;
|
||||
o.textvalue = function(sectionId) {
|
||||
var kind = routeKind(m, sectionId);
|
||||
var host = m.data.get('haproxy_manager', sectionId, 'backend_host') || _('Not set');
|
||||
var mappings = [];
|
||||
|
||||
if (kind == 'web') {
|
||||
if (m.data.get('haproxy_manager', sectionId, 'web_http') != '0')
|
||||
mappings.push(_('HTTP') + ' :' + (m.data.get('haproxy_manager', sectionId, 'backend_http_port') || '80'));
|
||||
if (m.data.get('haproxy_manager', sectionId, 'web_https') != '0')
|
||||
mappings.push(_('HTTPS') + ' :' + (m.data.get('haproxy_manager', sectionId, 'backend_https_port') || '443'));
|
||||
}
|
||||
else if (kind == 'ssh')
|
||||
mappings.push(':' + (m.data.get('haproxy_manager', sectionId, 'ssh_backend_port') || '22'));
|
||||
else if (kind == 'rdp')
|
||||
mappings.push(':' + (m.data.get('haproxy_manager', sectionId, 'rdp_backend_port') || '3389'));
|
||||
else if (kind == 'custom')
|
||||
mappings = listValue(m.data.get('haproxy_manager', sectionId, 'port_map')).map(function(item) { return ':' + item.split(':')[1]; });
|
||||
else
|
||||
mappings.push(':' + (m.data.get('haproxy_manager', sectionId, 'backend_port') || ''));
|
||||
|
||||
return E('div', { 'class': 'hm-destination' }, [
|
||||
E('code', host),
|
||||
E('span', mappings.join(' / '))
|
||||
]);
|
||||
};
|
||||
|
||||
o = s.option(form.DummyValue, '_firewall', _('Firewall'));
|
||||
o.modalonly = false;
|
||||
o.textvalue = function(sectionId) {
|
||||
var kind = routeKind(m, sectionId);
|
||||
var ports = routePorts(m, sectionId, kind).map(Number);
|
||||
var conflict = firewall.conflicts.some(function(item) {
|
||||
return ports.some(function(port) { return specContains(item.ports, port); });
|
||||
});
|
||||
var state = firewall.enabled != '1' ? _('Manual') : conflict ? _('Conflict') :
|
||||
(firewall.managed_rule == '1' || firewall.policy == 'ACCEPT' ? _('Ready') : _('Pending'));
|
||||
|
||||
return E('span', {
|
||||
'class': 'hm-state %s'.format(conflict ? 'hm-state-danger' : firewall.enabled == '1' ? 'hm-state-on' : 'hm-state-off')
|
||||
}, state);
|
||||
};
|
||||
|
||||
function applied(reload) {
|
||||
hmUi.notifyApplied();
|
||||
if (reload)
|
||||
hmUi.reloadAfterApply();
|
||||
}
|
||||
|
||||
function applyMap(reload) {
|
||||
return hmUi.saveAndApply(m).then(function() {
|
||||
applied(reload);
|
||||
}).catch(function(error) {
|
||||
hmUi.notifyError(error);
|
||||
});
|
||||
}
|
||||
|
||||
var baseHandleDrop = s.handleDrop;
|
||||
var baseHandleTouchEnd = s.handleTouchEnd;
|
||||
|
||||
s.handleModalSave = function(modalMap, ev) {
|
||||
var mapNode = this.getActiveModalMap();
|
||||
var activeMap = dom.findClassInstance(mapNode);
|
||||
|
||||
return activeMap.save(null, true).then(function() {
|
||||
return hmUi.commitAndApply();
|
||||
}).then(function() {
|
||||
return this.handleModalCancel(modalMap, ev, true);
|
||||
}.bind(this)).then(function() {
|
||||
applied(true);
|
||||
}).catch(function(error) {
|
||||
hmUi.notifyError(error);
|
||||
});
|
||||
};
|
||||
|
||||
s.handleRemove = function(sectionId) {
|
||||
ui.showModal(_('Delete service?'), [
|
||||
E('p', _('The service will be removed from HAProxy and the firewall immediately.')),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button',
|
||||
'click': ui.hideModal
|
||||
}, _('Cancel')),
|
||||
' ',
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-negative important',
|
||||
'click': ui.createHandlerFn(this, function() {
|
||||
ui.hideModal();
|
||||
m.data.remove('haproxy_manager', sectionId);
|
||||
return applyMap(true);
|
||||
})
|
||||
}, _('Delete and apply'))
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
function applyReorder() {
|
||||
window.setTimeout(function() {
|
||||
applyMap(true);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
s.handleDrop = function(ev) {
|
||||
var shouldApply = !!(ev.currentTarget && ev.currentTarget.matches &&
|
||||
ev.currentTarget.matches('.drag-over-above, .drag-over-below'));
|
||||
var result = baseHandleDrop.call(this, ev);
|
||||
if (shouldApply)
|
||||
applyReorder();
|
||||
return result;
|
||||
};
|
||||
|
||||
s.handleTouchEnd = function(ev) {
|
||||
var row = ev.target && ev.target.closest ? ev.target.closest('.tr') : null;
|
||||
var shouldApply = !!(document.querySelector('.touchsort-element') && row &&
|
||||
row.parentNode.querySelector('.drag-over-above, .drag-over-below'));
|
||||
var result = baseHandleTouchEnd.call(this, ev);
|
||||
if (shouldApply)
|
||||
applyReorder();
|
||||
return result;
|
||||
};
|
||||
|
||||
return m.render().then(function(node) {
|
||||
var filterInput = E('input', {
|
||||
'id': 'haproxy-route-filter',
|
||||
'class': 'cbi-input-text hm-filter',
|
||||
'type': 'search',
|
||||
'placeholder': _('Filter services'),
|
||||
'aria-label': _('Filter services')
|
||||
});
|
||||
var countNode = E('span', { 'class': 'hm-route-count' });
|
||||
var emptyNode = E('p', {
|
||||
'class': 'alert-message notice hm-filter-empty',
|
||||
'hidden': ''
|
||||
}, _('No matching services'));
|
||||
var toolbar = E('div', { 'class': 'hm-toolbar' }, [ filterInput, countNode ]);
|
||||
var section = node.querySelector('.cbi-section');
|
||||
|
||||
function updateFilter() {
|
||||
var query = filterInput.value.trim().toLowerCase();
|
||||
var rows = node.querySelectorAll('.cbi-section-table-row[data-sid]');
|
||||
var visible = 0;
|
||||
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var match = !query || rows[i].textContent.toLowerCase().indexOf(query) > -1;
|
||||
rows[i].hidden = !match;
|
||||
visible += match ? 1 : 0;
|
||||
}
|
||||
|
||||
countNode.textContent = _('%d services').format(visible);
|
||||
emptyNode.hidden = visible > 0 || !query;
|
||||
}
|
||||
|
||||
filterInput.addEventListener('input', updateFilter);
|
||||
|
||||
if (section) {
|
||||
var table = section.querySelector('.table');
|
||||
if (table)
|
||||
table.classList.add('hm-route-table');
|
||||
section.insertBefore(toolbar, table || section.firstChild);
|
||||
section.appendChild(emptyNode);
|
||||
}
|
||||
|
||||
if (firewall.conflicts.length) {
|
||||
node.insertBefore(E('div', { 'class': 'alert-message warning hm-inline-alert' }, [
|
||||
E('strong', _('Firewall conflict detected.')),
|
||||
' ',
|
||||
E('a', { 'href': L.url('admin/services/haproxy-manager/settings') }, _('Review firewall settings'))
|
||||
]), node.firstChild.nextSibling);
|
||||
}
|
||||
|
||||
node.appendChild(E('div', { 'class': 'cbi-page-actions hm-actions' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'type': 'button',
|
||||
'click': ui.createHandlerFn(this, function() {
|
||||
return hmUi.exec('/usr/libexec/haproxy-manager/generate', [ '/tmp/haproxy-manager-preview.cfg' ]).then(function() {
|
||||
return hmUi.exec('/usr/libexec/haproxy-manager/validate', [ '/tmp/haproxy-manager-preview.cfg' ]);
|
||||
}).then(function() {
|
||||
return hmUi.exec('/usr/libexec/haproxy-manager/firewall-plan', []);
|
||||
}).then(function(r) {
|
||||
var plan = parseFirewall(r.stdout);
|
||||
hmUi.notify(plan.conflicts.length ?
|
||||
_('Configuration is valid. Firewall conflicts: %d').format(plan.conflicts.length) :
|
||||
_('Configuration and firewall are ready.'), plan.conflicts.length ? 'warning' : 'info');
|
||||
}).catch(function(err) {
|
||||
hmUi.notifyError(err);
|
||||
});
|
||||
})
|
||||
}, _('Check configuration')),
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-apply',
|
||||
'type': 'button',
|
||||
'click': ui.createHandlerFn(this, function() {
|
||||
return applyMap(true);
|
||||
})
|
||||
}, _('Synchronize now'))
|
||||
]));
|
||||
|
||||
updateFilter();
|
||||
return node;
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
'use strict';
|
||||
/* global hmUi */
|
||||
'require view';
|
||||
'require form';
|
||||
'require ui';
|
||||
'require haproxy-manager.ui as hmUi';
|
||||
|
||||
return view.extend({
|
||||
render: function() {
|
||||
var m, s, o;
|
||||
|
||||
hmUi.ensureStyles();
|
||||
|
||||
m = new form.Map('haproxy_manager', _('HAProxy Settings'),
|
||||
_('Configure shared Web entry ports, router access, and firewall automation.'));
|
||||
|
||||
s = m.section(form.NamedSection, 'main', 'settings', _('HAProxy service'));
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enable managed configuration'));
|
||||
o.default = '0';
|
||||
|
||||
o = s.option(form.Value, 'wan_interface', _('WAN network interface'));
|
||||
o.default = 'wan';
|
||||
o.datatype = 'uciname';
|
||||
o.rmempty = false;
|
||||
o.description = _('The selected logical interface is monitored for address changes. In multi-WAN setups, select the interface that receives public connections.');
|
||||
|
||||
o = s.option(form.Value, 'wan_bind_ip', _('WAN bind address'));
|
||||
o.placeholder = _('Automatic');
|
||||
o.default = 'auto';
|
||||
o.datatype = 'or(ipaddr,hostname)';
|
||||
o.rmempty = false;
|
||||
|
||||
s = m.section(form.NamedSection, 'main', 'settings', _('Web entry ports'));
|
||||
s.anonymous = true;
|
||||
s.description = _('Every Web service shares these public ports. SSH, Remote Desktop, and Custom TCP services use the ports configured on each service.');
|
||||
|
||||
o = s.option(form.Value, 'http_port', _('Public HTTP port'));
|
||||
o.datatype = 'port';
|
||||
o.default = '80';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'https_port', _('Public HTTPS port'));
|
||||
o.datatype = 'port';
|
||||
o.default = '443';
|
||||
o.rmempty = false;
|
||||
|
||||
s = m.section(form.NamedSection, 'main', 'settings', _('Router access'));
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.Flag, 'manage_uhttpd_bind', _('Keep LuCI on the LAN address'));
|
||||
o.default = '0';
|
||||
o.description = _('Recommended when HAProxy uses public ports 80 or 443. LuCI remains available from the local network.');
|
||||
|
||||
o = s.option(form.Value, 'lan_bind_ip', _('LuCI LAN address'));
|
||||
o.placeholder = _('Automatic');
|
||||
o.default = 'auto';
|
||||
o.datatype = 'or(ipaddr,hostname)';
|
||||
o.depends('manage_uhttpd_bind', '1');
|
||||
|
||||
s = m.section(form.NamedSection, 'main', 'settings', _('Firewall automation'));
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.Flag, 'manage_firewall', _('Open HAProxy ports on WAN automatically'));
|
||||
o.default = '0';
|
||||
o.description = _('Creates and updates only firewall rules owned by HAProxy Manager.');
|
||||
|
||||
o = s.option(form.Value, 'firewall_zone', _('WAN firewall zone'));
|
||||
o.default = 'wan';
|
||||
o.rmempty = false;
|
||||
o.depends('manage_firewall', '1');
|
||||
|
||||
o = s.option(form.ListValue, 'firewall_conflict_mode', _('Existing port-forward conflicts'));
|
||||
o.value('warn', _('Stop and show conflicts'));
|
||||
o.value('disable', _('Disable conflicting forwards during apply'));
|
||||
o.default = 'warn';
|
||||
o.rmempty = false;
|
||||
o.depends('manage_firewall', '1');
|
||||
|
||||
s = m.section(form.NamedSection, 'main', 'settings', _('Recovery'));
|
||||
s.anonymous = true;
|
||||
s.description = _('A restorable snapshot is created before migration and every apply.');
|
||||
|
||||
o = s.option(form.Flag, 'auto_recover', _('Recover HAProxy after networking becomes ready'));
|
||||
o.default = '1';
|
||||
o.description = _('Reconciles generated listeners after WAN address changes, retries a stopped service, and retains diagnostics for the seven latest incidents. Raw configurations are never overwritten.');
|
||||
|
||||
o = s.option(form.Flag, 'webhook_enabled', _('Send recovery webhook notifications'));
|
||||
o.default = '0';
|
||||
o.description = _('Sends only the incident identifier, result, action, interface, and reason. Configuration, addresses, and diagnostic logs are never included.');
|
||||
|
||||
o = s.option(form.Value, 'webhook_url', _('Webhook URL'));
|
||||
o.password = true;
|
||||
o.rmempty = false;
|
||||
o.depends('webhook_enabled', '1');
|
||||
o.validate = function(sectionId, value) {
|
||||
return /^https?:\/\/[^/\s]+(?:\/\S*)?$/.test(value || '') || _('Enter an HTTP or HTTPS URL without spaces.');
|
||||
};
|
||||
|
||||
return m.render().then(function(node) {
|
||||
var recoverySection = node.querySelectorAll('.cbi-section');
|
||||
var lastSection = recoverySection.length ? recoverySection[recoverySection.length - 1] : null;
|
||||
|
||||
if (lastSection) {
|
||||
lastSection.appendChild(E('p', [
|
||||
E('a', {
|
||||
'class': 'btn cbi-button',
|
||||
'href': L.url('admin/services/haproxy-manager/status') + '#recovery'
|
||||
}, _('Open recovery'))
|
||||
]));
|
||||
}
|
||||
|
||||
node.appendChild(E('div', { 'class': 'cbi-page-actions hm-actions' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-apply',
|
||||
'type': 'button',
|
||||
'click': ui.createHandlerFn(this, function() {
|
||||
return hmUi.saveAndApply(m).then(function() {
|
||||
hmUi.notifyApplied();
|
||||
hmUi.reloadAfterApply();
|
||||
}).catch(function(err) {
|
||||
hmUi.notifyError(err);
|
||||
});
|
||||
})
|
||||
}, _('Save settings'))
|
||||
]));
|
||||
|
||||
return node;
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,294 @@
|
||||
'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
|
||||
});
|
||||
@@ -0,0 +1,499 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Language: es\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "Command failed with code %d."
|
||||
msgstr "El comando terminó con el código %d."
|
||||
|
||||
msgid "%d configuration files"
|
||||
msgstr "%d archivos de configuración"
|
||||
|
||||
msgid "%d conflicts"
|
||||
msgstr "%d conflictos"
|
||||
|
||||
msgid "%d services"
|
||||
msgstr "%d servicios"
|
||||
|
||||
msgid "A restorable snapshot is created before migration and every apply."
|
||||
msgstr "Se crea un punto de restauración antes de la migración y de cada aplicación."
|
||||
|
||||
msgid "Active listeners"
|
||||
msgstr "Escuchas activas"
|
||||
|
||||
msgid "Add service"
|
||||
msgstr "Añadir servicio"
|
||||
|
||||
msgid "Address"
|
||||
msgstr "Dirección"
|
||||
|
||||
msgid "Applied"
|
||||
msgstr "Aplicado"
|
||||
|
||||
msgid "Apply"
|
||||
msgstr "Aplicar"
|
||||
|
||||
msgid "Apply changes"
|
||||
msgstr "Aplicar cambios"
|
||||
|
||||
msgid "Apply raw config"
|
||||
msgstr "Aplicar configuración"
|
||||
|
||||
msgid "Apply raw configuration?"
|
||||
msgstr "¿Aplicar la configuración manual?"
|
||||
|
||||
msgid "Automatic"
|
||||
msgstr "Automático"
|
||||
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
|
||||
msgid "Check configuration"
|
||||
msgstr "Comprobar configuración"
|
||||
|
||||
msgid "Config is valid"
|
||||
msgstr "La configuración es válida"
|
||||
|
||||
msgid "Configuration and firewall are ready."
|
||||
msgstr "La configuración y el cortafuegos están listos."
|
||||
|
||||
msgid "Configuration is valid. Firewall conflicts: %d"
|
||||
msgstr "La configuración es válida. Conflictos del cortafuegos: %d"
|
||||
|
||||
msgid "Configure shared Web entry ports, router access, and firewall automation."
|
||||
msgstr "Configure los puertos Web compartidos, el acceso al router y la automatización del cortafuegos."
|
||||
|
||||
msgid "Conflict"
|
||||
msgstr "Conflicto"
|
||||
|
||||
msgid "Contents"
|
||||
msgstr "Contenido"
|
||||
|
||||
msgid "Create recovery point"
|
||||
msgstr "Crear punto de restauración"
|
||||
|
||||
msgid "Created"
|
||||
msgstr "Creado"
|
||||
|
||||
msgid "Creates and updates only firewall rules owned by HAProxy Manager."
|
||||
msgstr "Crea y actualiza únicamente las reglas del cortafuegos gestionadas por HAProxy Manager."
|
||||
|
||||
msgid "Custom TCP"
|
||||
msgstr "TCP personalizado"
|
||||
|
||||
msgid "Destination"
|
||||
msgstr "Destino"
|
||||
|
||||
msgid "Destination host"
|
||||
msgstr "Host de destino"
|
||||
|
||||
msgid "Destination HTTP port"
|
||||
msgstr "Puerto HTTP de destino"
|
||||
|
||||
msgid "Destination HTTPS port"
|
||||
msgstr "Puerto HTTPS de destino"
|
||||
|
||||
msgid "Destination RDP port"
|
||||
msgstr "Puerto RDP de destino"
|
||||
|
||||
msgid "Destination SSH port"
|
||||
msgstr "Puerto SSH de destino"
|
||||
|
||||
msgid "Disable conflicting forwards during apply"
|
||||
msgstr "Desactivar redirecciones conflictivas al aplicar"
|
||||
|
||||
msgid "Domain"
|
||||
msgstr "Dominio"
|
||||
|
||||
msgid "Enable managed configuration"
|
||||
msgstr "Activar configuración administrada"
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr "Activado"
|
||||
|
||||
msgid "Every Web service shares these public ports. SSH, Remote Desktop, and Custom TCP services use the ports configured on each service."
|
||||
msgstr "Todos los servicios Web comparten estos puertos públicos. SSH, Escritorio remoto y TCP personalizado usan los puertos configurados en cada servicio."
|
||||
|
||||
msgid "Existing port-forward conflicts"
|
||||
msgstr "Conflictos con redirecciones existentes"
|
||||
|
||||
msgid "Expert editor for /etc/haproxy.cfg."
|
||||
msgstr "Editor avanzado de /etc/haproxy.cfg."
|
||||
|
||||
msgid "Filter services"
|
||||
msgstr "Filtrar servicios"
|
||||
|
||||
msgid "Firewall"
|
||||
msgstr "Cortafuegos"
|
||||
|
||||
msgid "Firewall automation"
|
||||
msgstr "Automatización del cortafuegos"
|
||||
|
||||
msgid "Firewall conflict detected."
|
||||
msgstr "Se detectó un conflicto del cortafuegos."
|
||||
|
||||
msgid "Generated routes can overwrite manual changes to this file."
|
||||
msgstr "Las rutas generadas pueden sobrescribir los cambios manuales de este archivo."
|
||||
|
||||
msgid "Grant access to LuCI HAProxy Manager"
|
||||
msgstr "Conceder acceso a LuCI HAProxy Manager"
|
||||
|
||||
msgid "HAProxy"
|
||||
msgstr "HAProxy"
|
||||
|
||||
msgid "HAProxy service"
|
||||
msgstr "Servicio HAProxy"
|
||||
|
||||
msgid "HAProxy Services"
|
||||
msgstr "Servicios HAProxy"
|
||||
|
||||
msgid "HAProxy Settings"
|
||||
msgstr "Ajustes de HAProxy"
|
||||
|
||||
msgid "HAProxy Status"
|
||||
msgstr "Estado de HAProxy"
|
||||
|
||||
msgid "HAProxy version"
|
||||
msgstr "Versión de HAProxy"
|
||||
|
||||
msgid "HAProxy, firewall, and LuCI settings will be restored from %s."
|
||||
msgstr "La configuración de HAProxy, del cortafuegos y de LuCI se restaurará desde %s."
|
||||
|
||||
msgid "HTTP"
|
||||
msgstr "HTTP"
|
||||
|
||||
msgid "HTTP only"
|
||||
msgstr "Solo HTTP"
|
||||
|
||||
msgid "HTTPS"
|
||||
msgstr "HTTPS"
|
||||
|
||||
msgid "HTTPS only"
|
||||
msgstr "Solo HTTPS"
|
||||
|
||||
msgid "Keep LuCI on the LAN address"
|
||||
msgstr "Mantener LuCI en la dirección LAN"
|
||||
|
||||
msgid "Latest"
|
||||
msgstr "Más reciente"
|
||||
|
||||
msgid "Legacy Web"
|
||||
msgstr "Web heredado"
|
||||
|
||||
msgid "LuCI LAN address"
|
||||
msgstr "Dirección LAN de LuCI"
|
||||
|
||||
msgid "Managed"
|
||||
msgstr "Gestionado"
|
||||
|
||||
msgid "Manual"
|
||||
msgstr "Manual"
|
||||
|
||||
msgid "My service"
|
||||
msgstr "Mi servicio"
|
||||
|
||||
msgid "No HAProxy or LuCI listeners detected."
|
||||
msgstr "No se detectaron escuchas de HAProxy o LuCI."
|
||||
|
||||
msgid "No matching services"
|
||||
msgstr "No hay servicios coincidentes"
|
||||
|
||||
msgid "No recovery points yet."
|
||||
msgstr "Aún no hay puntos de restauración."
|
||||
|
||||
msgid "Not available"
|
||||
msgstr "No disponible"
|
||||
|
||||
msgid "Not set"
|
||||
msgstr "Sin definir"
|
||||
|
||||
msgid "Off"
|
||||
msgstr "Desactivado"
|
||||
|
||||
msgid "On"
|
||||
msgstr "Activado"
|
||||
|
||||
msgid "Open HAProxy ports on WAN automatically"
|
||||
msgstr "Abrir automáticamente los puertos de HAProxy en WAN"
|
||||
|
||||
msgid "Open recovery"
|
||||
msgstr "Abrir recuperación"
|
||||
|
||||
msgid "Pending"
|
||||
msgstr "Pendiente"
|
||||
|
||||
msgid "Process"
|
||||
msgstr "Proceso"
|
||||
|
||||
msgid "Protocol"
|
||||
msgstr "Protocolo"
|
||||
|
||||
msgid "Public endpoint"
|
||||
msgstr "Punto de entrada público"
|
||||
|
||||
msgid "Public HTTP port"
|
||||
msgstr "Puerto HTTP público"
|
||||
|
||||
msgid "Public HTTPS port"
|
||||
msgstr "Puerto HTTPS público"
|
||||
|
||||
msgid "Public RDP port"
|
||||
msgstr "Puerto RDP público"
|
||||
|
||||
msgid "Public SSH port"
|
||||
msgstr "Puerto SSH público"
|
||||
|
||||
msgid "Publish HTTP"
|
||||
msgstr "Publicar HTTP"
|
||||
|
||||
msgid "Publish HTTPS"
|
||||
msgstr "Publicar HTTPS"
|
||||
|
||||
msgid "Publish Web, SSH, Remote Desktop, and custom TCP services from one place."
|
||||
msgstr "Publique servicios Web, SSH, Escritorio remoto y TCP personalizado desde un solo lugar."
|
||||
|
||||
msgid "Raw Config"
|
||||
msgstr "Configuración"
|
||||
|
||||
msgid "Raw HAProxy Config"
|
||||
msgstr "Configuración de HAProxy"
|
||||
|
||||
msgid "Raw HAProxy configuration"
|
||||
msgstr "Configuración de HAProxy"
|
||||
|
||||
msgid "Ready"
|
||||
msgstr "Listo"
|
||||
|
||||
msgid "Recommended when HAProxy uses public ports 80 or 443. LuCI remains available from the local network."
|
||||
msgstr "Recomendado cuando HAProxy usa los puertos públicos 80 o 443. LuCI seguirá disponible desde la red local."
|
||||
|
||||
msgid "Recovery"
|
||||
msgstr "Recuperación"
|
||||
|
||||
msgid "Recovery points"
|
||||
msgstr "Puntos de restauración"
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr "Actualizar"
|
||||
|
||||
msgid "Remote Desktop"
|
||||
msgstr "Escritorio remoto"
|
||||
|
||||
msgid "Restore"
|
||||
msgstr "Restaurar"
|
||||
|
||||
msgid "Restore configuration?"
|
||||
msgstr "¿Restaurar la configuración?"
|
||||
|
||||
msgid "Restore HAProxy, firewall, and LuCI settings from a previous snapshot."
|
||||
msgstr "Restaure la configuración de HAProxy, del cortafuegos y de LuCI desde una instantánea anterior."
|
||||
|
||||
msgid "Restored"
|
||||
msgstr "Restaurado"
|
||||
|
||||
msgid "Review firewall settings"
|
||||
msgstr "Revisar ajustes del cortafuegos"
|
||||
|
||||
msgid "Router access"
|
||||
msgstr "Acceso al router"
|
||||
|
||||
msgid "Running"
|
||||
msgstr "En ejecución"
|
||||
|
||||
msgid "Save and apply"
|
||||
msgstr "Guardar y aplicar"
|
||||
|
||||
msgid "Save settings"
|
||||
msgstr "Guardar ajustes"
|
||||
|
||||
msgid "Service"
|
||||
msgstr "Servicio"
|
||||
|
||||
msgid "Service health, active listeners, and recovery points."
|
||||
msgstr "Estado del servicio, listeners activos y puntos de restauración."
|
||||
|
||||
msgid "Service name"
|
||||
msgstr "Nombre del servicio"
|
||||
|
||||
msgid "Service type"
|
||||
msgstr "Tipo de servicio"
|
||||
|
||||
msgid "Services"
|
||||
msgstr "Servicios"
|
||||
|
||||
msgid "Settings"
|
||||
msgstr "Ajustes"
|
||||
|
||||
msgid "Settings saved"
|
||||
msgstr "Ajustes guardados"
|
||||
|
||||
msgid "SSH"
|
||||
msgstr "SSH"
|
||||
|
||||
msgid "Status"
|
||||
msgstr "Estado"
|
||||
|
||||
msgid "Stop and show conflicts"
|
||||
msgstr "Detener y mostrar conflictos"
|
||||
|
||||
msgid "Stopped"
|
||||
msgstr "Detenido"
|
||||
|
||||
msgid "TCP"
|
||||
msgstr "TCP"
|
||||
|
||||
msgid "TCP port mappings"
|
||||
msgstr "Asignaciones de puertos TCP"
|
||||
|
||||
msgid "The file will be validated and backed up before HAProxy restarts."
|
||||
msgstr "El archivo se validará y guardará antes de reiniciar HAProxy."
|
||||
|
||||
msgid "Use the public:destination format, for example 8443:443."
|
||||
msgstr "Use el formato público:destino, por ejemplo 8443:443."
|
||||
|
||||
msgid "Validate"
|
||||
msgstr "Validar"
|
||||
|
||||
msgid "WAN address"
|
||||
msgstr "Dirección WAN"
|
||||
|
||||
msgid "WAN bind address"
|
||||
msgstr "Dirección de enlace WAN"
|
||||
|
||||
msgid "WAN firewall zone"
|
||||
msgstr "Zona WAN del cortafuegos"
|
||||
|
||||
msgid "Web"
|
||||
msgstr "Web"
|
||||
|
||||
msgid "Web (HTTP + HTTPS)"
|
||||
msgstr "Web (HTTP + HTTPS)"
|
||||
|
||||
msgid "Web entry ports"
|
||||
msgstr "Puertos de entrada Web"
|
||||
msgid "Changes saved and applied."
|
||||
msgstr "Cambios guardados y aplicados."
|
||||
|
||||
msgid "Delete and apply"
|
||||
msgstr "Eliminar y aplicar"
|
||||
|
||||
msgid "Delete service?"
|
||||
msgstr "¿Eliminar el servicio?"
|
||||
|
||||
msgid "Synchronize now"
|
||||
msgstr "Sincronizar ahora"
|
||||
|
||||
msgid "The service will be removed from HAProxy and the firewall immediately."
|
||||
msgstr "El servicio se eliminará inmediatamente de HAProxy y del cortafuegos."
|
||||
|
||||
msgid "Unable to create a recovery point."
|
||||
msgstr "No se pudo crear un punto de recuperación."
|
||||
|
||||
msgid "Automatic recovery"
|
||||
msgstr "Recuperación automática"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "Desactivada"
|
||||
|
||||
msgid "Failed"
|
||||
msgstr "Falló"
|
||||
|
||||
msgid "If HAProxy stops before a listener address exists, retry it after a network interface comes up and retain diagnostics for the seven latest incidents."
|
||||
msgstr "Si HAProxy se detiene antes de que exista una dirección de escucha, reintentar tras activarse una interfaz de red y conservar el diagnóstico de los siete incidentes más recientes."
|
||||
|
||||
msgid "In progress"
|
||||
msgstr "En curso"
|
||||
|
||||
msgid "Invalid configuration"
|
||||
msgstr "Configuración no válida"
|
||||
|
||||
msgid "Last recovery"
|
||||
msgstr "Última recuperación"
|
||||
|
||||
msgid "No incidents recorded"
|
||||
msgstr "No se registraron incidentes"
|
||||
|
||||
msgid "Recover HAProxy after networking becomes ready"
|
||||
msgstr "Recuperar HAProxy cuando la red esté lista"
|
||||
|
||||
msgid "Recovered"
|
||||
msgstr "Recuperado"
|
||||
|
||||
msgid "Recovery point failed"
|
||||
msgstr "Error al crear el punto de recuperación"
|
||||
|
||||
msgid "WAN network interface"
|
||||
msgstr "Interfaz de red WAN"
|
||||
|
||||
msgid "Close"
|
||||
msgstr "Cerrar"
|
||||
|
||||
msgid "Configuration mode"
|
||||
msgstr "Modo de configuración"
|
||||
|
||||
msgid "Diagnostic report for %s."
|
||||
msgstr "Informe de diagnóstico de %s."
|
||||
|
||||
msgid "Enter an HTTP or HTTPS URL without spaces."
|
||||
msgstr "Introduzca una URL HTTP o HTTPS sin espacios."
|
||||
|
||||
msgid "Failed, configuration restored"
|
||||
msgstr "Error; se restauró la configuración"
|
||||
|
||||
msgid "Failed, restore also failed"
|
||||
msgstr "Error; la restauración también falló"
|
||||
|
||||
msgid "Generated routes"
|
||||
msgstr "Rutas generadas"
|
||||
|
||||
msgid "Incident diagnostics"
|
||||
msgstr "Diagnóstico del incidente"
|
||||
|
||||
msgid "Not managed"
|
||||
msgstr "No gestionado"
|
||||
|
||||
msgid "Raw configuration"
|
||||
msgstr "Configuración manual"
|
||||
|
||||
msgid "Reconciles generated listeners after WAN address changes, retries a stopped service, and retains diagnostics for the seven latest incidents. Raw configurations are never overwritten."
|
||||
msgstr "Sincroniza los listeners generados tras cambios de la dirección WAN, vuelve a iniciar un servicio detenido y conserva el diagnóstico de los siete incidentes más recientes. Las configuraciones manuales nunca se sobrescriben."
|
||||
|
||||
msgid "Recovery incidents"
|
||||
msgstr "Incidentes de recuperación"
|
||||
|
||||
msgid "Result"
|
||||
msgstr "Resultado"
|
||||
|
||||
msgid "Send recovery webhook notifications"
|
||||
msgstr "Enviar notificaciones webhook de recuperación"
|
||||
|
||||
msgid "Sends only the incident identifier, result, action, interface, and reason. Configuration, addresses, and diagnostic logs are never included."
|
||||
msgstr "Solo envía el identificador del incidente, el resultado, la acción, la interfaz y el motivo. Nunca incluye la configuración, las direcciones ni los registros de diagnóstico."
|
||||
|
||||
msgid "The selected logical interface is monitored for address changes. In multi-WAN setups, select the interface that receives public connections."
|
||||
msgstr "Se supervisan los cambios de dirección de la interfaz lógica seleccionada. En configuraciones multi-WAN, seleccione la interfaz que recibe conexiones públicas."
|
||||
|
||||
msgid "The seven latest automatic recovery reports are retained on the router."
|
||||
msgstr "El router conserva los siete informes de recuperación automática más recientes."
|
||||
|
||||
msgid "Trigger"
|
||||
msgstr "Desencadenante"
|
||||
|
||||
msgid "View"
|
||||
msgstr "Ver"
|
||||
|
||||
msgid "WAN address reconciled"
|
||||
msgstr "Dirección WAN sincronizada"
|
||||
|
||||
msgid "WAN address unavailable"
|
||||
msgstr "Dirección WAN no disponible"
|
||||
|
||||
msgid "Webhook URL"
|
||||
msgstr "URL del webhook"
|
||||
msgid "Generated configuration is invalid"
|
||||
msgstr "La configuración generada no es válida"
|
||||
|
||||
msgid "Configuration installation failed"
|
||||
msgstr "No se pudo instalar la configuración"
|
||||
@@ -0,0 +1,499 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Language: ja\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "Command failed with code %d."
|
||||
msgstr "コマンドはコード %d で終了しました。"
|
||||
|
||||
msgid "%d configuration files"
|
||||
msgstr "%d 個の設定ファイル"
|
||||
|
||||
msgid "%d conflicts"
|
||||
msgstr "%d 件の競合"
|
||||
|
||||
msgid "%d services"
|
||||
msgstr "%d 個のサービス"
|
||||
|
||||
msgid "A restorable snapshot is created before migration and every apply."
|
||||
msgstr "移行前および変更を適用するたびに復元ポイントを作成します。"
|
||||
|
||||
msgid "Active listeners"
|
||||
msgstr "有効なリスナー"
|
||||
|
||||
msgid "Add service"
|
||||
msgstr "サービスを追加"
|
||||
|
||||
msgid "Address"
|
||||
msgstr "アドレス"
|
||||
|
||||
msgid "Applied"
|
||||
msgstr "適用しました"
|
||||
|
||||
msgid "Apply"
|
||||
msgstr "適用"
|
||||
|
||||
msgid "Apply changes"
|
||||
msgstr "変更を適用"
|
||||
|
||||
msgid "Apply raw config"
|
||||
msgstr "設定を適用"
|
||||
|
||||
msgid "Apply raw configuration?"
|
||||
msgstr "手動設定を適用しますか?"
|
||||
|
||||
msgid "Automatic"
|
||||
msgstr "自動"
|
||||
|
||||
msgid "Cancel"
|
||||
msgstr "キャンセル"
|
||||
|
||||
msgid "Check configuration"
|
||||
msgstr "設定を確認"
|
||||
|
||||
msgid "Config is valid"
|
||||
msgstr "設定は有効です"
|
||||
|
||||
msgid "Configuration and firewall are ready."
|
||||
msgstr "設定とファイアウォールの準備が完了しました。"
|
||||
|
||||
msgid "Configuration is valid. Firewall conflicts: %d"
|
||||
msgstr "設定は有効です。ファイアウォールの競合: %d"
|
||||
|
||||
msgid "Configure shared Web entry ports, router access, and firewall automation."
|
||||
msgstr "共有 Web ポート、ルーターへのアクセス、ファイアウォールの自動化を設定します。"
|
||||
|
||||
msgid "Conflict"
|
||||
msgstr "競合"
|
||||
|
||||
msgid "Contents"
|
||||
msgstr "内容"
|
||||
|
||||
msgid "Create recovery point"
|
||||
msgstr "復元ポイントを作成"
|
||||
|
||||
msgid "Created"
|
||||
msgstr "作成日時"
|
||||
|
||||
msgid "Creates and updates only firewall rules owned by HAProxy Manager."
|
||||
msgstr "HAProxy Manager が所有するファイアウォールルールのみを作成、更新します。"
|
||||
|
||||
msgid "Custom TCP"
|
||||
msgstr "カスタム TCP"
|
||||
|
||||
msgid "Destination"
|
||||
msgstr "転送先"
|
||||
|
||||
msgid "Destination host"
|
||||
msgstr "転送先ホスト"
|
||||
|
||||
msgid "Destination HTTP port"
|
||||
msgstr "転送先 HTTP ポート"
|
||||
|
||||
msgid "Destination HTTPS port"
|
||||
msgstr "転送先 HTTPS ポート"
|
||||
|
||||
msgid "Destination RDP port"
|
||||
msgstr "転送先 RDP ポート"
|
||||
|
||||
msgid "Destination SSH port"
|
||||
msgstr "転送先 SSH ポート"
|
||||
|
||||
msgid "Disable conflicting forwards during apply"
|
||||
msgstr "適用時に競合する転送を無効化"
|
||||
|
||||
msgid "Domain"
|
||||
msgstr "ドメイン"
|
||||
|
||||
msgid "Enable managed configuration"
|
||||
msgstr "管理対象の設定を有効化"
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr "有効"
|
||||
|
||||
msgid "Every Web service shares these public ports. SSH, Remote Desktop, and Custom TCP services use the ports configured on each service."
|
||||
msgstr "すべての Web サービスはこれらの公開ポートを共有します。SSH、リモートデスクトップ、カスタム TCP は各サービスに設定したポートを使用します。"
|
||||
|
||||
msgid "Existing port-forward conflicts"
|
||||
msgstr "既存のポート転送との競合"
|
||||
|
||||
msgid "Expert editor for /etc/haproxy.cfg."
|
||||
msgstr "/etc/haproxy.cfg の上級者向けエディターです。"
|
||||
|
||||
msgid "Filter services"
|
||||
msgstr "サービスを絞り込み"
|
||||
|
||||
msgid "Firewall"
|
||||
msgstr "ファイアウォール"
|
||||
|
||||
msgid "Firewall automation"
|
||||
msgstr "ファイアウォールの自動化"
|
||||
|
||||
msgid "Firewall conflict detected."
|
||||
msgstr "ファイアウォールの競合を検出しました。"
|
||||
|
||||
msgid "Generated routes can overwrite manual changes to this file."
|
||||
msgstr "生成されたルートにより、このファイルの手動変更が上書きされる場合があります。"
|
||||
|
||||
msgid "Grant access to LuCI HAProxy Manager"
|
||||
msgstr "LuCI HAProxy Manager へのアクセスを許可"
|
||||
|
||||
msgid "HAProxy"
|
||||
msgstr "HAProxy"
|
||||
|
||||
msgid "HAProxy service"
|
||||
msgstr "HAProxy サービス"
|
||||
|
||||
msgid "HAProxy Services"
|
||||
msgstr "HAProxy サービス"
|
||||
|
||||
msgid "HAProxy Settings"
|
||||
msgstr "HAProxy 設定"
|
||||
|
||||
msgid "HAProxy Status"
|
||||
msgstr "HAProxy ステータス"
|
||||
|
||||
msgid "HAProxy version"
|
||||
msgstr "HAProxy バージョン"
|
||||
|
||||
msgid "HAProxy, firewall, and LuCI settings will be restored from %s."
|
||||
msgstr "HAProxy、ファイアウォール、LuCI の設定を %s から復元します。"
|
||||
|
||||
msgid "HTTP"
|
||||
msgstr "HTTP"
|
||||
|
||||
msgid "HTTP only"
|
||||
msgstr "HTTP のみ"
|
||||
|
||||
msgid "HTTPS"
|
||||
msgstr "HTTPS"
|
||||
|
||||
msgid "HTTPS only"
|
||||
msgstr "HTTPS のみ"
|
||||
|
||||
msgid "Keep LuCI on the LAN address"
|
||||
msgstr "LuCI を LAN アドレスに限定"
|
||||
|
||||
msgid "Latest"
|
||||
msgstr "最新"
|
||||
|
||||
msgid "Legacy Web"
|
||||
msgstr "旧形式 Web"
|
||||
|
||||
msgid "LuCI LAN address"
|
||||
msgstr "LuCI LAN アドレス"
|
||||
|
||||
msgid "Managed"
|
||||
msgstr "管理中"
|
||||
|
||||
msgid "Manual"
|
||||
msgstr "手動"
|
||||
|
||||
msgid "My service"
|
||||
msgstr "マイサービス"
|
||||
|
||||
msgid "No HAProxy or LuCI listeners detected."
|
||||
msgstr "HAProxy または LuCI のリスナーが見つかりません。"
|
||||
|
||||
msgid "No matching services"
|
||||
msgstr "一致するサービスはありません"
|
||||
|
||||
msgid "No recovery points yet."
|
||||
msgstr "復元ポイントはまだありません。"
|
||||
|
||||
msgid "Not available"
|
||||
msgstr "利用不可"
|
||||
|
||||
msgid "Not set"
|
||||
msgstr "未設定"
|
||||
|
||||
msgid "Off"
|
||||
msgstr "オフ"
|
||||
|
||||
msgid "On"
|
||||
msgstr "オン"
|
||||
|
||||
msgid "Open HAProxy ports on WAN automatically"
|
||||
msgstr "WAN の HAProxy ポートを自動的に開く"
|
||||
|
||||
msgid "Open recovery"
|
||||
msgstr "復元を開く"
|
||||
|
||||
msgid "Pending"
|
||||
msgstr "適用待ち"
|
||||
|
||||
msgid "Process"
|
||||
msgstr "プロセス"
|
||||
|
||||
msgid "Protocol"
|
||||
msgstr "プロトコル"
|
||||
|
||||
msgid "Public endpoint"
|
||||
msgstr "公開エンドポイント"
|
||||
|
||||
msgid "Public HTTP port"
|
||||
msgstr "公開 HTTP ポート"
|
||||
|
||||
msgid "Public HTTPS port"
|
||||
msgstr "公開 HTTPS ポート"
|
||||
|
||||
msgid "Public RDP port"
|
||||
msgstr "公開 RDP ポート"
|
||||
|
||||
msgid "Public SSH port"
|
||||
msgstr "公開 SSH ポート"
|
||||
|
||||
msgid "Publish HTTP"
|
||||
msgstr "HTTP を公開"
|
||||
|
||||
msgid "Publish HTTPS"
|
||||
msgstr "HTTPS を公開"
|
||||
|
||||
msgid "Publish Web, SSH, Remote Desktop, and custom TCP services from one place."
|
||||
msgstr "Web、SSH、リモートデスクトップ、カスタム TCP サービスを一か所から公開します。"
|
||||
|
||||
msgid "Raw Config"
|
||||
msgstr "生設定"
|
||||
|
||||
msgid "Raw HAProxy Config"
|
||||
msgstr "HAProxy 生設定"
|
||||
|
||||
msgid "Raw HAProxy configuration"
|
||||
msgstr "HAProxy 生設定"
|
||||
|
||||
msgid "Ready"
|
||||
msgstr "準備完了"
|
||||
|
||||
msgid "Recommended when HAProxy uses public ports 80 or 443. LuCI remains available from the local network."
|
||||
msgstr "HAProxy が公開ポート 80 または 443 を使用する場合に推奨します。LuCI はローカルネットワークから利用できます。"
|
||||
|
||||
msgid "Recovery"
|
||||
msgstr "復元"
|
||||
|
||||
msgid "Recovery points"
|
||||
msgstr "復元ポイント"
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr "更新"
|
||||
|
||||
msgid "Remote Desktop"
|
||||
msgstr "リモートデスクトップ"
|
||||
|
||||
msgid "Restore"
|
||||
msgstr "復元"
|
||||
|
||||
msgid "Restore configuration?"
|
||||
msgstr "設定を復元しますか?"
|
||||
|
||||
msgid "Restore HAProxy, firewall, and LuCI settings from a previous snapshot."
|
||||
msgstr "以前のスナップショットから HAProxy、ファイアウォール、LuCI の設定を復元します。"
|
||||
|
||||
msgid "Restored"
|
||||
msgstr "復元しました"
|
||||
|
||||
msgid "Review firewall settings"
|
||||
msgstr "ファイアウォール設定を確認"
|
||||
|
||||
msgid "Router access"
|
||||
msgstr "ルーターへのアクセス"
|
||||
|
||||
msgid "Running"
|
||||
msgstr "実行中"
|
||||
|
||||
msgid "Save and apply"
|
||||
msgstr "保存して適用"
|
||||
|
||||
msgid "Save settings"
|
||||
msgstr "設定を保存"
|
||||
|
||||
msgid "Service"
|
||||
msgstr "サービス"
|
||||
|
||||
msgid "Service health, active listeners, and recovery points."
|
||||
msgstr "サービス状態、待受ポート、復元ポイントを表示します。"
|
||||
|
||||
msgid "Service name"
|
||||
msgstr "サービス名"
|
||||
|
||||
msgid "Service type"
|
||||
msgstr "サービス種別"
|
||||
|
||||
msgid "Services"
|
||||
msgstr "サービス"
|
||||
|
||||
msgid "Settings"
|
||||
msgstr "設定"
|
||||
|
||||
msgid "Settings saved"
|
||||
msgstr "設定を保存しました"
|
||||
|
||||
msgid "SSH"
|
||||
msgstr "SSH"
|
||||
|
||||
msgid "Status"
|
||||
msgstr "ステータス"
|
||||
|
||||
msgid "Stop and show conflicts"
|
||||
msgstr "停止して競合を表示"
|
||||
|
||||
msgid "Stopped"
|
||||
msgstr "停止中"
|
||||
|
||||
msgid "TCP"
|
||||
msgstr "TCP"
|
||||
|
||||
msgid "TCP port mappings"
|
||||
msgstr "TCP ポートマッピング"
|
||||
|
||||
msgid "The file will be validated and backed up before HAProxy restarts."
|
||||
msgstr "HAProxy の再起動前にファイルを検証し、バックアップします。"
|
||||
|
||||
msgid "Use the public:destination format, for example 8443:443."
|
||||
msgstr "公開:転送先 の形式を使用してください。例: 8443:443"
|
||||
|
||||
msgid "Validate"
|
||||
msgstr "検証"
|
||||
|
||||
msgid "WAN address"
|
||||
msgstr "WAN アドレス"
|
||||
|
||||
msgid "WAN bind address"
|
||||
msgstr "WAN バインドアドレス"
|
||||
|
||||
msgid "WAN firewall zone"
|
||||
msgstr "WAN ファイアウォールゾーン"
|
||||
|
||||
msgid "Web"
|
||||
msgstr "Web"
|
||||
|
||||
msgid "Web (HTTP + HTTPS)"
|
||||
msgstr "Web (HTTP + HTTPS)"
|
||||
|
||||
msgid "Web entry ports"
|
||||
msgstr "Web 公開ポート"
|
||||
msgid "Changes saved and applied."
|
||||
msgstr "変更を保存して適用しました。"
|
||||
|
||||
msgid "Delete and apply"
|
||||
msgstr "削除して適用"
|
||||
|
||||
msgid "Delete service?"
|
||||
msgstr "サービスを削除しますか?"
|
||||
|
||||
msgid "Synchronize now"
|
||||
msgstr "今すぐ同期"
|
||||
|
||||
msgid "The service will be removed from HAProxy and the firewall immediately."
|
||||
msgstr "サービスは HAProxy とファイアウォールから直ちに削除されます。"
|
||||
|
||||
msgid "Unable to create a recovery point."
|
||||
msgstr "復元ポイントを作成できません。"
|
||||
|
||||
msgid "Automatic recovery"
|
||||
msgstr "自動復旧"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "無効"
|
||||
|
||||
msgid "Failed"
|
||||
msgstr "失敗"
|
||||
|
||||
msgid "If HAProxy stops before a listener address exists, retry it after a network interface comes up and retain diagnostics for the seven latest incidents."
|
||||
msgstr "リスナーアドレスが利用可能になる前に HAProxy が停止した場合、ネットワークインターフェースの起動後に再試行し、最新 7 件の診断を保存します。"
|
||||
|
||||
msgid "In progress"
|
||||
msgstr "進行中"
|
||||
|
||||
msgid "Invalid configuration"
|
||||
msgstr "無効な設定"
|
||||
|
||||
msgid "Last recovery"
|
||||
msgstr "最終復旧"
|
||||
|
||||
msgid "No incidents recorded"
|
||||
msgstr "インシデントは記録されていません"
|
||||
|
||||
msgid "Recover HAProxy after networking becomes ready"
|
||||
msgstr "ネットワーク準備完了後に HAProxy を復旧"
|
||||
|
||||
msgid "Recovered"
|
||||
msgstr "復旧済み"
|
||||
|
||||
msgid "Recovery point failed"
|
||||
msgstr "復旧ポイントの作成に失敗"
|
||||
|
||||
msgid "WAN network interface"
|
||||
msgstr "WAN ネットワークインターフェース"
|
||||
|
||||
msgid "Close"
|
||||
msgstr "閉じる"
|
||||
|
||||
msgid "Configuration mode"
|
||||
msgstr "設定モード"
|
||||
|
||||
msgid "Diagnostic report for %s."
|
||||
msgstr "%s の診断レポートです。"
|
||||
|
||||
msgid "Enter an HTTP or HTTPS URL without spaces."
|
||||
msgstr "空白を含まない HTTP または HTTPS URL を入力してください。"
|
||||
|
||||
msgid "Failed, configuration restored"
|
||||
msgstr "失敗したため設定を復元しました"
|
||||
|
||||
msgid "Failed, restore also failed"
|
||||
msgstr "失敗し、復元にも失敗しました"
|
||||
|
||||
msgid "Generated routes"
|
||||
msgstr "生成されたルート"
|
||||
|
||||
msgid "Incident diagnostics"
|
||||
msgstr "インシデント診断"
|
||||
|
||||
msgid "Not managed"
|
||||
msgstr "管理対象外"
|
||||
|
||||
msgid "Raw configuration"
|
||||
msgstr "手動設定"
|
||||
|
||||
msgid "Reconciles generated listeners after WAN address changes, retries a stopped service, and retains diagnostics for the seven latest incidents. Raw configurations are never overwritten."
|
||||
msgstr "WAN アドレスの変更後に生成済みリスナーを同期し、停止したサービスを再試行して、直近 7 件のインシデント診断を保持します。手動設定は上書きされません。"
|
||||
|
||||
msgid "Recovery incidents"
|
||||
msgstr "復旧インシデント"
|
||||
|
||||
msgid "Result"
|
||||
msgstr "結果"
|
||||
|
||||
msgid "Send recovery webhook notifications"
|
||||
msgstr "復旧 Webhook 通知を送信"
|
||||
|
||||
msgid "Sends only the incident identifier, result, action, interface, and reason. Configuration, addresses, and diagnostic logs are never included."
|
||||
msgstr "インシデント ID、結果、アクション、インターフェース、理由のみを送信します。設定、アドレス、診断ログは含まれません。"
|
||||
|
||||
msgid "The selected logical interface is monitored for address changes. In multi-WAN setups, select the interface that receives public connections."
|
||||
msgstr "選択した論理インターフェースのアドレス変更を監視します。マルチ WAN 構成では、外部接続を受信するインターフェースを選択してください。"
|
||||
|
||||
msgid "The seven latest automatic recovery reports are retained on the router."
|
||||
msgstr "直近 7 件の自動復旧レポートがルーターに保持されます。"
|
||||
|
||||
msgid "Trigger"
|
||||
msgstr "トリガー"
|
||||
|
||||
msgid "View"
|
||||
msgstr "表示"
|
||||
|
||||
msgid "WAN address reconciled"
|
||||
msgstr "WAN アドレスを同期しました"
|
||||
|
||||
msgid "WAN address unavailable"
|
||||
msgstr "WAN アドレスを利用できません"
|
||||
|
||||
msgid "Webhook URL"
|
||||
msgstr "Webhook URL"
|
||||
msgid "Generated configuration is invalid"
|
||||
msgstr "生成された設定が無効です"
|
||||
|
||||
msgid "Configuration installation failed"
|
||||
msgstr "設定のインストールに失敗しました"
|
||||
@@ -0,0 +1,499 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Language: ko\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "Command failed with code %d."
|
||||
msgstr "명령이 코드 %d(으)로 종료되었습니다."
|
||||
|
||||
msgid "%d configuration files"
|
||||
msgstr "설정 파일 %d개"
|
||||
|
||||
msgid "%d conflicts"
|
||||
msgstr "충돌 %d개"
|
||||
|
||||
msgid "%d services"
|
||||
msgstr "서비스 %d개"
|
||||
|
||||
msgid "A restorable snapshot is created before migration and every apply."
|
||||
msgstr "마이그레이션 전과 변경 사항을 적용할 때마다 복원 지점을 만듭니다."
|
||||
|
||||
msgid "Active listeners"
|
||||
msgstr "활성 리스너"
|
||||
|
||||
msgid "Add service"
|
||||
msgstr "서비스 추가"
|
||||
|
||||
msgid "Address"
|
||||
msgstr "주소"
|
||||
|
||||
msgid "Applied"
|
||||
msgstr "적용됨"
|
||||
|
||||
msgid "Apply"
|
||||
msgstr "적용"
|
||||
|
||||
msgid "Apply changes"
|
||||
msgstr "변경 사항 적용"
|
||||
|
||||
msgid "Apply raw config"
|
||||
msgstr "설정 적용"
|
||||
|
||||
msgid "Apply raw configuration?"
|
||||
msgstr "수동 설정을 적용하시겠습니까?"
|
||||
|
||||
msgid "Automatic"
|
||||
msgstr "자동"
|
||||
|
||||
msgid "Cancel"
|
||||
msgstr "취소"
|
||||
|
||||
msgid "Check configuration"
|
||||
msgstr "설정 확인"
|
||||
|
||||
msgid "Config is valid"
|
||||
msgstr "설정이 유효합니다"
|
||||
|
||||
msgid "Configuration and firewall are ready."
|
||||
msgstr "설정과 방화벽이 준비되었습니다."
|
||||
|
||||
msgid "Configuration is valid. Firewall conflicts: %d"
|
||||
msgstr "설정이 유효합니다. 방화벽 충돌: %d"
|
||||
|
||||
msgid "Configure shared Web entry ports, router access, and firewall automation."
|
||||
msgstr "공유 Web 포트, 라우터 접근 및 방화벽 자동화를 설정합니다."
|
||||
|
||||
msgid "Conflict"
|
||||
msgstr "충돌"
|
||||
|
||||
msgid "Contents"
|
||||
msgstr "내용"
|
||||
|
||||
msgid "Create recovery point"
|
||||
msgstr "복원 지점 만들기"
|
||||
|
||||
msgid "Created"
|
||||
msgstr "생성 시각"
|
||||
|
||||
msgid "Creates and updates only firewall rules owned by HAProxy Manager."
|
||||
msgstr "HAProxy Manager가 소유한 방화벽 규칙만 생성하고 업데이트합니다."
|
||||
|
||||
msgid "Custom TCP"
|
||||
msgstr "사용자 지정 TCP"
|
||||
|
||||
msgid "Destination"
|
||||
msgstr "대상"
|
||||
|
||||
msgid "Destination host"
|
||||
msgstr "대상 호스트"
|
||||
|
||||
msgid "Destination HTTP port"
|
||||
msgstr "대상 HTTP 포트"
|
||||
|
||||
msgid "Destination HTTPS port"
|
||||
msgstr "대상 HTTPS 포트"
|
||||
|
||||
msgid "Destination RDP port"
|
||||
msgstr "대상 RDP 포트"
|
||||
|
||||
msgid "Destination SSH port"
|
||||
msgstr "대상 SSH 포트"
|
||||
|
||||
msgid "Disable conflicting forwards during apply"
|
||||
msgstr "적용 시 충돌하는 포트 포워딩 비활성화"
|
||||
|
||||
msgid "Domain"
|
||||
msgstr "도메인"
|
||||
|
||||
msgid "Enable managed configuration"
|
||||
msgstr "관리 설정 활성화"
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr "활성화"
|
||||
|
||||
msgid "Every Web service shares these public ports. SSH, Remote Desktop, and Custom TCP services use the ports configured on each service."
|
||||
msgstr "모든 Web 서비스는 이 공개 포트를 공유합니다. SSH, 원격 데스크톱 및 사용자 지정 TCP 서비스는 각 서비스에 설정된 포트를 사용합니다."
|
||||
|
||||
msgid "Existing port-forward conflicts"
|
||||
msgstr "기존 포트 포워딩 충돌"
|
||||
|
||||
msgid "Expert editor for /etc/haproxy.cfg."
|
||||
msgstr "/etc/haproxy.cfg 고급 편집기입니다."
|
||||
|
||||
msgid "Filter services"
|
||||
msgstr "서비스 필터"
|
||||
|
||||
msgid "Firewall"
|
||||
msgstr "방화벽"
|
||||
|
||||
msgid "Firewall automation"
|
||||
msgstr "방화벽 자동화"
|
||||
|
||||
msgid "Firewall conflict detected."
|
||||
msgstr "방화벽 충돌이 감지되었습니다."
|
||||
|
||||
msgid "Generated routes can overwrite manual changes to this file."
|
||||
msgstr "생성된 경로가 이 파일의 수동 변경 내용을 덮어쓸 수 있습니다."
|
||||
|
||||
msgid "Grant access to LuCI HAProxy Manager"
|
||||
msgstr "LuCI HAProxy Manager에 대한 접근 권한 부여"
|
||||
|
||||
msgid "HAProxy"
|
||||
msgstr "HAProxy"
|
||||
|
||||
msgid "HAProxy service"
|
||||
msgstr "HAProxy 서비스"
|
||||
|
||||
msgid "HAProxy Services"
|
||||
msgstr "HAProxy 서비스"
|
||||
|
||||
msgid "HAProxy Settings"
|
||||
msgstr "HAProxy 설정"
|
||||
|
||||
msgid "HAProxy Status"
|
||||
msgstr "HAProxy 상태"
|
||||
|
||||
msgid "HAProxy version"
|
||||
msgstr "HAProxy 버전"
|
||||
|
||||
msgid "HAProxy, firewall, and LuCI settings will be restored from %s."
|
||||
msgstr "HAProxy, 방화벽 및 LuCI 설정을 %s에서 복원합니다."
|
||||
|
||||
msgid "HTTP"
|
||||
msgstr "HTTP"
|
||||
|
||||
msgid "HTTP only"
|
||||
msgstr "HTTP만"
|
||||
|
||||
msgid "HTTPS"
|
||||
msgstr "HTTPS"
|
||||
|
||||
msgid "HTTPS only"
|
||||
msgstr "HTTPS만"
|
||||
|
||||
msgid "Keep LuCI on the LAN address"
|
||||
msgstr "LuCI를 LAN 주소에 유지"
|
||||
|
||||
msgid "Latest"
|
||||
msgstr "최신"
|
||||
|
||||
msgid "Legacy Web"
|
||||
msgstr "기존 Web"
|
||||
|
||||
msgid "LuCI LAN address"
|
||||
msgstr "LuCI LAN 주소"
|
||||
|
||||
msgid "Managed"
|
||||
msgstr "관리됨"
|
||||
|
||||
msgid "Manual"
|
||||
msgstr "수동"
|
||||
|
||||
msgid "My service"
|
||||
msgstr "내 서비스"
|
||||
|
||||
msgid "No HAProxy or LuCI listeners detected."
|
||||
msgstr "HAProxy 또는 LuCI 리스너가 감지되지 않았습니다."
|
||||
|
||||
msgid "No matching services"
|
||||
msgstr "일치하는 서비스가 없습니다"
|
||||
|
||||
msgid "No recovery points yet."
|
||||
msgstr "아직 복원 지점이 없습니다."
|
||||
|
||||
msgid "Not available"
|
||||
msgstr "사용할 수 없음"
|
||||
|
||||
msgid "Not set"
|
||||
msgstr "설정되지 않음"
|
||||
|
||||
msgid "Off"
|
||||
msgstr "꺼짐"
|
||||
|
||||
msgid "On"
|
||||
msgstr "켜짐"
|
||||
|
||||
msgid "Open HAProxy ports on WAN automatically"
|
||||
msgstr "WAN에서 HAProxy 포트를 자동으로 열기"
|
||||
|
||||
msgid "Open recovery"
|
||||
msgstr "복원 열기"
|
||||
|
||||
msgid "Pending"
|
||||
msgstr "적용 대기"
|
||||
|
||||
msgid "Process"
|
||||
msgstr "프로세스"
|
||||
|
||||
msgid "Protocol"
|
||||
msgstr "프로토콜"
|
||||
|
||||
msgid "Public endpoint"
|
||||
msgstr "공개 엔드포인트"
|
||||
|
||||
msgid "Public HTTP port"
|
||||
msgstr "공개 HTTP 포트"
|
||||
|
||||
msgid "Public HTTPS port"
|
||||
msgstr "공개 HTTPS 포트"
|
||||
|
||||
msgid "Public RDP port"
|
||||
msgstr "공개 RDP 포트"
|
||||
|
||||
msgid "Public SSH port"
|
||||
msgstr "공개 SSH 포트"
|
||||
|
||||
msgid "Publish HTTP"
|
||||
msgstr "HTTP 공개"
|
||||
|
||||
msgid "Publish HTTPS"
|
||||
msgstr "HTTPS 공개"
|
||||
|
||||
msgid "Publish Web, SSH, Remote Desktop, and custom TCP services from one place."
|
||||
msgstr "Web, SSH, 원격 데스크톱 및 사용자 지정 TCP 서비스를 한 곳에서 공개합니다."
|
||||
|
||||
msgid "Raw Config"
|
||||
msgstr "원시 설정"
|
||||
|
||||
msgid "Raw HAProxy Config"
|
||||
msgstr "HAProxy 원시 설정"
|
||||
|
||||
msgid "Raw HAProxy configuration"
|
||||
msgstr "HAProxy 원시 설정"
|
||||
|
||||
msgid "Ready"
|
||||
msgstr "준비됨"
|
||||
|
||||
msgid "Recommended when HAProxy uses public ports 80 or 443. LuCI remains available from the local network."
|
||||
msgstr "HAProxy가 공개 포트 80 또는 443을 사용할 때 권장합니다. LuCI는 로컬 네트워크에서 계속 사용할 수 있습니다."
|
||||
|
||||
msgid "Recovery"
|
||||
msgstr "복원"
|
||||
|
||||
msgid "Recovery points"
|
||||
msgstr "복원 지점"
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr "새로 고침"
|
||||
|
||||
msgid "Remote Desktop"
|
||||
msgstr "원격 데스크톱"
|
||||
|
||||
msgid "Restore"
|
||||
msgstr "복원"
|
||||
|
||||
msgid "Restore configuration?"
|
||||
msgstr "설정을 복원하시겠습니까?"
|
||||
|
||||
msgid "Restore HAProxy, firewall, and LuCI settings from a previous snapshot."
|
||||
msgstr "이전 스냅샷에서 HAProxy, 방화벽 및 LuCI 설정을 복원합니다."
|
||||
|
||||
msgid "Restored"
|
||||
msgstr "복원됨"
|
||||
|
||||
msgid "Review firewall settings"
|
||||
msgstr "방화벽 설정 검토"
|
||||
|
||||
msgid "Router access"
|
||||
msgstr "라우터 접근"
|
||||
|
||||
msgid "Running"
|
||||
msgstr "실행 중"
|
||||
|
||||
msgid "Save and apply"
|
||||
msgstr "저장 후 적용"
|
||||
|
||||
msgid "Save settings"
|
||||
msgstr "설정 저장"
|
||||
|
||||
msgid "Service"
|
||||
msgstr "서비스"
|
||||
|
||||
msgid "Service health, active listeners, and recovery points."
|
||||
msgstr "서비스 상태, 활성 리스너 및 복원 지점입니다."
|
||||
|
||||
msgid "Service name"
|
||||
msgstr "서비스 이름"
|
||||
|
||||
msgid "Service type"
|
||||
msgstr "서비스 유형"
|
||||
|
||||
msgid "Services"
|
||||
msgstr "서비스"
|
||||
|
||||
msgid "Settings"
|
||||
msgstr "설정"
|
||||
|
||||
msgid "Settings saved"
|
||||
msgstr "설정이 저장되었습니다"
|
||||
|
||||
msgid "SSH"
|
||||
msgstr "SSH"
|
||||
|
||||
msgid "Status"
|
||||
msgstr "상태"
|
||||
|
||||
msgid "Stop and show conflicts"
|
||||
msgstr "중지하고 충돌 표시"
|
||||
|
||||
msgid "Stopped"
|
||||
msgstr "중지됨"
|
||||
|
||||
msgid "TCP"
|
||||
msgstr "TCP"
|
||||
|
||||
msgid "TCP port mappings"
|
||||
msgstr "TCP 포트 매핑"
|
||||
|
||||
msgid "The file will be validated and backed up before HAProxy restarts."
|
||||
msgstr "HAProxy를 다시 시작하기 전에 파일을 검증하고 백업합니다."
|
||||
|
||||
msgid "Use the public:destination format, for example 8443:443."
|
||||
msgstr "공개:대상 형식을 사용하십시오. 예: 8443:443"
|
||||
|
||||
msgid "Validate"
|
||||
msgstr "검증"
|
||||
|
||||
msgid "WAN address"
|
||||
msgstr "WAN 주소"
|
||||
|
||||
msgid "WAN bind address"
|
||||
msgstr "WAN 바인드 주소"
|
||||
|
||||
msgid "WAN firewall zone"
|
||||
msgstr "WAN 방화벽 영역"
|
||||
|
||||
msgid "Web"
|
||||
msgstr "Web"
|
||||
|
||||
msgid "Web (HTTP + HTTPS)"
|
||||
msgstr "Web (HTTP + HTTPS)"
|
||||
|
||||
msgid "Web entry ports"
|
||||
msgstr "Web 공개 포트"
|
||||
msgid "Changes saved and applied."
|
||||
msgstr "변경 사항이 저장되고 적용되었습니다."
|
||||
|
||||
msgid "Delete and apply"
|
||||
msgstr "삭제 후 적용"
|
||||
|
||||
msgid "Delete service?"
|
||||
msgstr "서비스를 삭제하시겠습니까?"
|
||||
|
||||
msgid "Synchronize now"
|
||||
msgstr "지금 동기화"
|
||||
|
||||
msgid "The service will be removed from HAProxy and the firewall immediately."
|
||||
msgstr "서비스가 HAProxy와 방화벽에서 즉시 제거됩니다."
|
||||
|
||||
msgid "Unable to create a recovery point."
|
||||
msgstr "복구 지점을 만들 수 없습니다."
|
||||
|
||||
msgid "Automatic recovery"
|
||||
msgstr "자동 복구"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "비활성화"
|
||||
|
||||
msgid "Failed"
|
||||
msgstr "실패"
|
||||
|
||||
msgid "If HAProxy stops before a listener address exists, retry it after a network interface comes up and retain diagnostics for the seven latest incidents."
|
||||
msgstr "리스너 주소가 준비되기 전에 HAProxy가 중지되면 네트워크 인터페이스가 올라온 뒤 다시 시작하고 최근 7개 장애의 진단 정보를 보관합니다."
|
||||
|
||||
msgid "In progress"
|
||||
msgstr "진행 중"
|
||||
|
||||
msgid "Invalid configuration"
|
||||
msgstr "잘못된 구성"
|
||||
|
||||
msgid "Last recovery"
|
||||
msgstr "마지막 복구"
|
||||
|
||||
msgid "No incidents recorded"
|
||||
msgstr "기록된 장애 없음"
|
||||
|
||||
msgid "Recover HAProxy after networking becomes ready"
|
||||
msgstr "네트워크 준비 후 HAProxy 복구"
|
||||
|
||||
msgid "Recovered"
|
||||
msgstr "복구됨"
|
||||
|
||||
msgid "Recovery point failed"
|
||||
msgstr "복구 지점 생성 실패"
|
||||
|
||||
msgid "WAN network interface"
|
||||
msgstr "WAN 네트워크 인터페이스"
|
||||
|
||||
msgid "Close"
|
||||
msgstr "닫기"
|
||||
|
||||
msgid "Configuration mode"
|
||||
msgstr "구성 모드"
|
||||
|
||||
msgid "Diagnostic report for %s."
|
||||
msgstr "%s 진단 보고서입니다."
|
||||
|
||||
msgid "Enter an HTTP or HTTPS URL without spaces."
|
||||
msgstr "공백 없이 HTTP 또는 HTTPS URL을 입력하십시오."
|
||||
|
||||
msgid "Failed, configuration restored"
|
||||
msgstr "실패하여 구성을 복원함"
|
||||
|
||||
msgid "Failed, restore also failed"
|
||||
msgstr "실패했으며 복원도 실패함"
|
||||
|
||||
msgid "Generated routes"
|
||||
msgstr "생성된 경로"
|
||||
|
||||
msgid "Incident diagnostics"
|
||||
msgstr "장애 진단"
|
||||
|
||||
msgid "Not managed"
|
||||
msgstr "관리되지 않음"
|
||||
|
||||
msgid "Raw configuration"
|
||||
msgstr "수동 구성"
|
||||
|
||||
msgid "Reconciles generated listeners after WAN address changes, retries a stopped service, and retains diagnostics for the seven latest incidents. Raw configurations are never overwritten."
|
||||
msgstr "WAN 주소 변경 후 생성된 리스너를 동기화하고 중지된 서비스를 다시 시작하며 최근 7개 장애의 진단을 보관합니다. 수동 구성은 절대 덮어쓰지 않습니다."
|
||||
|
||||
msgid "Recovery incidents"
|
||||
msgstr "복구 장애"
|
||||
|
||||
msgid "Result"
|
||||
msgstr "결과"
|
||||
|
||||
msgid "Send recovery webhook notifications"
|
||||
msgstr "복구 웹훅 알림 보내기"
|
||||
|
||||
msgid "Sends only the incident identifier, result, action, interface, and reason. Configuration, addresses, and diagnostic logs are never included."
|
||||
msgstr "장애 식별자, 결과, 작업, 인터페이스 및 원인만 전송합니다. 구성, 주소 및 진단 로그는 포함하지 않습니다."
|
||||
|
||||
msgid "The selected logical interface is monitored for address changes. In multi-WAN setups, select the interface that receives public connections."
|
||||
msgstr "선택한 논리 인터페이스의 주소 변경을 감시합니다. 다중 WAN 구성에서는 외부 연결을 수신하는 인터페이스를 선택하십시오."
|
||||
|
||||
msgid "The seven latest automatic recovery reports are retained on the router."
|
||||
msgstr "최근 7개의 자동 복구 보고서가 라우터에 보관됩니다."
|
||||
|
||||
msgid "Trigger"
|
||||
msgstr "트리거"
|
||||
|
||||
msgid "View"
|
||||
msgstr "보기"
|
||||
|
||||
msgid "WAN address reconciled"
|
||||
msgstr "WAN 주소 동기화됨"
|
||||
|
||||
msgid "WAN address unavailable"
|
||||
msgstr "WAN 주소를 사용할 수 없음"
|
||||
|
||||
msgid "Webhook URL"
|
||||
msgstr "웹훅 URL"
|
||||
msgid "Generated configuration is invalid"
|
||||
msgstr "생성된 구성이 유효하지 않음"
|
||||
|
||||
msgid "Configuration installation failed"
|
||||
msgstr "구성을 설치하지 못함"
|
||||
@@ -0,0 +1,499 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Language: ru\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "Command failed with code %d."
|
||||
msgstr "Команда завершилась с кодом %d."
|
||||
|
||||
msgid "%d configuration files"
|
||||
msgstr "%d файлов конфигурации"
|
||||
|
||||
msgid "%d conflicts"
|
||||
msgstr "%d конфликтов"
|
||||
|
||||
msgid "%d services"
|
||||
msgstr "%d сервисов"
|
||||
|
||||
msgid "A restorable snapshot is created before migration and every apply."
|
||||
msgstr "Перед миграцией и каждым применением создаётся точка восстановления."
|
||||
|
||||
msgid "Active listeners"
|
||||
msgstr "Активные слушатели"
|
||||
|
||||
msgid "Add service"
|
||||
msgstr "Добавить сервис"
|
||||
|
||||
msgid "Address"
|
||||
msgstr "Адрес"
|
||||
|
||||
msgid "Applied"
|
||||
msgstr "Применено"
|
||||
|
||||
msgid "Apply"
|
||||
msgstr "Применить"
|
||||
|
||||
msgid "Apply changes"
|
||||
msgstr "Применить изменения"
|
||||
|
||||
msgid "Apply raw config"
|
||||
msgstr "Применить конфиг"
|
||||
|
||||
msgid "Apply raw configuration?"
|
||||
msgstr "Применить конфигурацию вручную?"
|
||||
|
||||
msgid "Automatic"
|
||||
msgstr "Автоматически"
|
||||
|
||||
msgid "Cancel"
|
||||
msgstr "Отмена"
|
||||
|
||||
msgid "Check configuration"
|
||||
msgstr "Проверить конфигурацию"
|
||||
|
||||
msgid "Config is valid"
|
||||
msgstr "Конфиг корректен"
|
||||
|
||||
msgid "Configuration and firewall are ready."
|
||||
msgstr "Конфигурация и межсетевой экран готовы."
|
||||
|
||||
msgid "Configuration is valid. Firewall conflicts: %d"
|
||||
msgstr "Конфигурация корректна. Конфликтов межсетевого экрана: %d"
|
||||
|
||||
msgid "Configure shared Web entry ports, router access, and firewall automation."
|
||||
msgstr "Настройка общих Web-портов, доступа к роутеру и автоматизации межсетевого экрана."
|
||||
|
||||
msgid "Conflict"
|
||||
msgstr "Конфликт"
|
||||
|
||||
msgid "Contents"
|
||||
msgstr "Содержимое"
|
||||
|
||||
msgid "Create recovery point"
|
||||
msgstr "Создать точку восстановления"
|
||||
|
||||
msgid "Created"
|
||||
msgstr "Создана"
|
||||
|
||||
msgid "Creates and updates only firewall rules owned by HAProxy Manager."
|
||||
msgstr "Создаёт и обновляет только правила межсетевого экрана, принадлежащие HAProxy Manager."
|
||||
|
||||
msgid "Custom TCP"
|
||||
msgstr "Пользовательский TCP"
|
||||
|
||||
msgid "Destination"
|
||||
msgstr "Назначение"
|
||||
|
||||
msgid "Destination host"
|
||||
msgstr "Внутренний хост"
|
||||
|
||||
msgid "Destination HTTP port"
|
||||
msgstr "Внутренний HTTP-порт"
|
||||
|
||||
msgid "Destination HTTPS port"
|
||||
msgstr "Внутренний HTTPS-порт"
|
||||
|
||||
msgid "Destination RDP port"
|
||||
msgstr "Внутренний RDP-порт"
|
||||
|
||||
msgid "Destination SSH port"
|
||||
msgstr "Внутренний SSH-порт"
|
||||
|
||||
msgid "Disable conflicting forwards during apply"
|
||||
msgstr "Отключать конфликтующие пробросы при применении"
|
||||
|
||||
msgid "Domain"
|
||||
msgstr "Домен"
|
||||
|
||||
msgid "Enable managed configuration"
|
||||
msgstr "Включить управляемую конфигурацию"
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr "Включено"
|
||||
|
||||
msgid "Every Web service shares these public ports. SSH, Remote Desktop, and Custom TCP services use the ports configured on each service."
|
||||
msgstr "Все Web-сервисы используют эти общие внешние порты. SSH, удалённый рабочий стол и пользовательские TCP-сервисы используют порты из своих настроек."
|
||||
|
||||
msgid "Existing port-forward conflicts"
|
||||
msgstr "Конфликты с существующими пробросами"
|
||||
|
||||
msgid "Expert editor for /etc/haproxy.cfg."
|
||||
msgstr "Экспертный редактор /etc/haproxy.cfg."
|
||||
|
||||
msgid "Filter services"
|
||||
msgstr "Фильтр сервисов"
|
||||
|
||||
msgid "Firewall"
|
||||
msgstr "Межсетевой экран"
|
||||
|
||||
msgid "Firewall automation"
|
||||
msgstr "Автоматизация межсетевого экрана"
|
||||
|
||||
msgid "Firewall conflict detected."
|
||||
msgstr "Обнаружен конфликт межсетевого экрана."
|
||||
|
||||
msgid "Generated routes can overwrite manual changes to this file."
|
||||
msgstr "Сгенерированные маршруты могут перезаписать ручные изменения этого файла."
|
||||
|
||||
msgid "Grant access to LuCI HAProxy Manager"
|
||||
msgstr "Предоставить доступ к LuCI HAProxy Manager"
|
||||
|
||||
msgid "HAProxy"
|
||||
msgstr "HAProxy"
|
||||
|
||||
msgid "HAProxy service"
|
||||
msgstr "Служба HAProxy"
|
||||
|
||||
msgid "HAProxy Services"
|
||||
msgstr "Сервисы HAProxy"
|
||||
|
||||
msgid "HAProxy Settings"
|
||||
msgstr "Настройки HAProxy"
|
||||
|
||||
msgid "HAProxy Status"
|
||||
msgstr "Статус HAProxy"
|
||||
|
||||
msgid "HAProxy version"
|
||||
msgstr "Версия HAProxy"
|
||||
|
||||
msgid "HAProxy, firewall, and LuCI settings will be restored from %s."
|
||||
msgstr "Настройки HAProxy, межсетевого экрана и LuCI будут восстановлены из точки %s."
|
||||
|
||||
msgid "HTTP"
|
||||
msgstr "HTTP"
|
||||
|
||||
msgid "HTTP only"
|
||||
msgstr "Только HTTP"
|
||||
|
||||
msgid "HTTPS"
|
||||
msgstr "HTTPS"
|
||||
|
||||
msgid "HTTPS only"
|
||||
msgstr "Только HTTPS"
|
||||
|
||||
msgid "Keep LuCI on the LAN address"
|
||||
msgstr "Оставить LuCI на LAN-адресе"
|
||||
|
||||
msgid "Latest"
|
||||
msgstr "Последняя"
|
||||
|
||||
msgid "Legacy Web"
|
||||
msgstr "Устаревший Web"
|
||||
|
||||
msgid "LuCI LAN address"
|
||||
msgstr "LAN-адрес LuCI"
|
||||
|
||||
msgid "Managed"
|
||||
msgstr "Управляется"
|
||||
|
||||
msgid "Manual"
|
||||
msgstr "Вручную"
|
||||
|
||||
msgid "My service"
|
||||
msgstr "Мой сервис"
|
||||
|
||||
msgid "No HAProxy or LuCI listeners detected."
|
||||
msgstr "Слушатели HAProxy или LuCI не обнаружены."
|
||||
|
||||
msgid "No matching services"
|
||||
msgstr "Подходящих сервисов нет"
|
||||
|
||||
msgid "No recovery points yet."
|
||||
msgstr "Точек восстановления пока нет."
|
||||
|
||||
msgid "Not available"
|
||||
msgstr "Недоступно"
|
||||
|
||||
msgid "Not set"
|
||||
msgstr "Не задано"
|
||||
|
||||
msgid "Off"
|
||||
msgstr "Выкл"
|
||||
|
||||
msgid "On"
|
||||
msgstr "Вкл"
|
||||
|
||||
msgid "Open HAProxy ports on WAN automatically"
|
||||
msgstr "Автоматически открывать порты HAProxy на WAN"
|
||||
|
||||
msgid "Open recovery"
|
||||
msgstr "Открыть восстановление"
|
||||
|
||||
msgid "Pending"
|
||||
msgstr "Ожидает применения"
|
||||
|
||||
msgid "Process"
|
||||
msgstr "Процесс"
|
||||
|
||||
msgid "Protocol"
|
||||
msgstr "Протокол"
|
||||
|
||||
msgid "Public endpoint"
|
||||
msgstr "Внешний адрес"
|
||||
|
||||
msgid "Public HTTP port"
|
||||
msgstr "Внешний HTTP-порт"
|
||||
|
||||
msgid "Public HTTPS port"
|
||||
msgstr "Внешний HTTPS-порт"
|
||||
|
||||
msgid "Public RDP port"
|
||||
msgstr "Внешний RDP-порт"
|
||||
|
||||
msgid "Public SSH port"
|
||||
msgstr "Внешний SSH-порт"
|
||||
|
||||
msgid "Publish HTTP"
|
||||
msgstr "Публиковать HTTP"
|
||||
|
||||
msgid "Publish HTTPS"
|
||||
msgstr "Публиковать HTTPS"
|
||||
|
||||
msgid "Publish Web, SSH, Remote Desktop, and custom TCP services from one place."
|
||||
msgstr "Публикация Web, SSH, удалённого рабочего стола и пользовательских TCP-сервисов в одном месте."
|
||||
|
||||
msgid "Raw Config"
|
||||
msgstr "Конфиг"
|
||||
|
||||
msgid "Raw HAProxy Config"
|
||||
msgstr "Конфиг HAProxy"
|
||||
|
||||
msgid "Raw HAProxy configuration"
|
||||
msgstr "Конфигурация HAProxy"
|
||||
|
||||
msgid "Ready"
|
||||
msgstr "Готово"
|
||||
|
||||
msgid "Recommended when HAProxy uses public ports 80 or 443. LuCI remains available from the local network."
|
||||
msgstr "Рекомендуется, когда HAProxy использует внешние порты 80 или 443. LuCI останется доступен из локальной сети."
|
||||
|
||||
msgid "Recovery"
|
||||
msgstr "Восстановление"
|
||||
|
||||
msgid "Recovery points"
|
||||
msgstr "Точки восстановления"
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr "Обновить"
|
||||
|
||||
msgid "Remote Desktop"
|
||||
msgstr "Удалённый рабочий стол"
|
||||
|
||||
msgid "Restore"
|
||||
msgstr "Восстановить"
|
||||
|
||||
msgid "Restore configuration?"
|
||||
msgstr "Восстановить конфигурацию?"
|
||||
|
||||
msgid "Restore HAProxy, firewall, and LuCI settings from a previous snapshot."
|
||||
msgstr "Восстановление настроек HAProxy, межсетевого экрана и LuCI из предыдущего снимка."
|
||||
|
||||
msgid "Restored"
|
||||
msgstr "Восстановлено"
|
||||
|
||||
msgid "Review firewall settings"
|
||||
msgstr "Проверить настройки межсетевого экрана"
|
||||
|
||||
msgid "Router access"
|
||||
msgstr "Доступ к роутеру"
|
||||
|
||||
msgid "Running"
|
||||
msgstr "Работает"
|
||||
|
||||
msgid "Save and apply"
|
||||
msgstr "Сохранить и применить"
|
||||
|
||||
msgid "Save settings"
|
||||
msgstr "Сохранить настройки"
|
||||
|
||||
msgid "Service"
|
||||
msgstr "Сервис"
|
||||
|
||||
msgid "Service health, active listeners, and recovery points."
|
||||
msgstr "Состояние службы, активные слушатели и точки восстановления."
|
||||
|
||||
msgid "Service name"
|
||||
msgstr "Название сервиса"
|
||||
|
||||
msgid "Service type"
|
||||
msgstr "Тип сервиса"
|
||||
|
||||
msgid "Services"
|
||||
msgstr "Сервисы"
|
||||
|
||||
msgid "Settings"
|
||||
msgstr "Настройки"
|
||||
|
||||
msgid "Settings saved"
|
||||
msgstr "Настройки сохранены"
|
||||
|
||||
msgid "SSH"
|
||||
msgstr "SSH"
|
||||
|
||||
msgid "Status"
|
||||
msgstr "Статус"
|
||||
|
||||
msgid "Stop and show conflicts"
|
||||
msgstr "Остановиться и показать конфликты"
|
||||
|
||||
msgid "Stopped"
|
||||
msgstr "Остановлен"
|
||||
|
||||
msgid "TCP"
|
||||
msgstr "TCP"
|
||||
|
||||
msgid "TCP port mappings"
|
||||
msgstr "Соответствия TCP-портов"
|
||||
|
||||
msgid "The file will be validated and backed up before HAProxy restarts."
|
||||
msgstr "Перед перезапуском HAProxy файл будет проверен и сохранён в бэкап."
|
||||
|
||||
msgid "Use the public:destination format, for example 8443:443."
|
||||
msgstr "Используйте формат внешний:внутренний, например 8443:443."
|
||||
|
||||
msgid "Validate"
|
||||
msgstr "Проверить"
|
||||
|
||||
msgid "WAN address"
|
||||
msgstr "WAN-адрес"
|
||||
|
||||
msgid "WAN bind address"
|
||||
msgstr "Адрес привязки WAN"
|
||||
|
||||
msgid "WAN firewall zone"
|
||||
msgstr "WAN-зона межсетевого экрана"
|
||||
|
||||
msgid "Web"
|
||||
msgstr "Web"
|
||||
|
||||
msgid "Web (HTTP + HTTPS)"
|
||||
msgstr "Web (HTTP + HTTPS)"
|
||||
|
||||
msgid "Web entry ports"
|
||||
msgstr "Внешние Web-порты"
|
||||
msgid "Changes saved and applied."
|
||||
msgstr "Изменения сохранены и применены."
|
||||
|
||||
msgid "Delete and apply"
|
||||
msgstr "Удалить и применить"
|
||||
|
||||
msgid "Delete service?"
|
||||
msgstr "Удалить сервис?"
|
||||
|
||||
msgid "Synchronize now"
|
||||
msgstr "Синхронизировать"
|
||||
|
||||
msgid "The service will be removed from HAProxy and the firewall immediately."
|
||||
msgstr "Сервис будет немедленно удалён из HAProxy и межсетевого экрана."
|
||||
|
||||
msgid "Unable to create a recovery point."
|
||||
msgstr "Не удалось создать точку восстановления."
|
||||
|
||||
msgid "Automatic recovery"
|
||||
msgstr "Автоматическое восстановление"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "Отключено"
|
||||
|
||||
msgid "Failed"
|
||||
msgstr "Не удалось"
|
||||
|
||||
msgid "If HAProxy stops before a listener address exists, retry it after a network interface comes up and retain diagnostics for the seven latest incidents."
|
||||
msgstr "Если HAProxy остановился до появления адреса слушателя, повторить запуск после поднятия сетевого интерфейса и сохранить диагностику семи последних инцидентов."
|
||||
|
||||
msgid "In progress"
|
||||
msgstr "Выполняется"
|
||||
|
||||
msgid "Invalid configuration"
|
||||
msgstr "Некорректная конфигурация"
|
||||
|
||||
msgid "Last recovery"
|
||||
msgstr "Последнее восстановление"
|
||||
|
||||
msgid "No incidents recorded"
|
||||
msgstr "Срабатываний не зафиксировано"
|
||||
|
||||
msgid "Recover HAProxy after networking becomes ready"
|
||||
msgstr "Автоматически запускать HAProxy после готовности сети"
|
||||
|
||||
msgid "Recovered"
|
||||
msgstr "Восстановлено"
|
||||
|
||||
msgid "Recovery point failed"
|
||||
msgstr "Не удалось создать точку восстановления"
|
||||
|
||||
msgid "WAN network interface"
|
||||
msgstr "Сетевой интерфейс WAN"
|
||||
|
||||
msgid "Close"
|
||||
msgstr "Закрыть"
|
||||
|
||||
msgid "Configuration mode"
|
||||
msgstr "Режим конфигурации"
|
||||
|
||||
msgid "Diagnostic report for %s."
|
||||
msgstr "Диагностический отчёт для %s."
|
||||
|
||||
msgid "Enter an HTTP or HTTPS URL without spaces."
|
||||
msgstr "Введите HTTP- или HTTPS-адрес без пробелов."
|
||||
|
||||
msgid "Failed, configuration restored"
|
||||
msgstr "Ошибка, конфигурация восстановлена"
|
||||
|
||||
msgid "Failed, restore also failed"
|
||||
msgstr "Ошибка, восстановление также не удалось"
|
||||
|
||||
msgid "Generated routes"
|
||||
msgstr "Сгенерированные маршруты"
|
||||
|
||||
msgid "Incident diagnostics"
|
||||
msgstr "Диагностика инцидента"
|
||||
|
||||
msgid "Not managed"
|
||||
msgstr "Не управляется"
|
||||
|
||||
msgid "Raw configuration"
|
||||
msgstr "Пользовательская конфигурация"
|
||||
|
||||
msgid "Reconciles generated listeners after WAN address changes, retries a stopped service, and retains diagnostics for the seven latest incidents. Raw configurations are never overwritten."
|
||||
msgstr "Синхронизирует сгенерированные слушатели после смены WAN-адреса, повторно запускает остановленную службу и хранит диагностику семи последних инцидентов. Пользовательские конфигурации никогда не перезаписываются."
|
||||
|
||||
msgid "Recovery incidents"
|
||||
msgstr "Инциденты восстановления"
|
||||
|
||||
msgid "Result"
|
||||
msgstr "Результат"
|
||||
|
||||
msgid "Send recovery webhook notifications"
|
||||
msgstr "Отправлять webhook-уведомления о восстановлении"
|
||||
|
||||
msgid "Sends only the incident identifier, result, action, interface, and reason. Configuration, addresses, and diagnostic logs are never included."
|
||||
msgstr "Отправляются только идентификатор инцидента, результат, действие, интерфейс и причина. Конфигурация, адреса и диагностические журналы никогда не передаются."
|
||||
|
||||
msgid "The selected logical interface is monitored for address changes. In multi-WAN setups, select the interface that receives public connections."
|
||||
msgstr "Выбранный логический интерфейс отслеживается на предмет смены адреса. В конфигурациях с несколькими WAN выберите интерфейс, принимающий публичные подключения."
|
||||
|
||||
msgid "The seven latest automatic recovery reports are retained on the router."
|
||||
msgstr "На маршрутизаторе хранятся семь последних отчётов автоматического восстановления."
|
||||
|
||||
msgid "Trigger"
|
||||
msgstr "Событие"
|
||||
|
||||
msgid "View"
|
||||
msgstr "Просмотреть"
|
||||
|
||||
msgid "WAN address reconciled"
|
||||
msgstr "WAN-адрес синхронизирован"
|
||||
|
||||
msgid "WAN address unavailable"
|
||||
msgstr "WAN-адрес недоступен"
|
||||
|
||||
msgid "Webhook URL"
|
||||
msgstr "URL webhook"
|
||||
msgid "Generated configuration is invalid"
|
||||
msgstr "Сгенерированная конфигурация недействительна"
|
||||
|
||||
msgid "Configuration installation failed"
|
||||
msgstr "Не удалось установить конфигурацию"
|
||||
+1
@@ -0,0 +1 @@
|
||||
zh_Hans
|
||||
@@ -0,0 +1,499 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Language: zh_Hans\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "Command failed with code %d."
|
||||
msgstr "命令以代码 %d 退出。"
|
||||
|
||||
msgid "%d configuration files"
|
||||
msgstr "%d 个配置文件"
|
||||
|
||||
msgid "%d conflicts"
|
||||
msgstr "%d 个冲突"
|
||||
|
||||
msgid "%d services"
|
||||
msgstr "%d 个服务"
|
||||
|
||||
msgid "A restorable snapshot is created before migration and every apply."
|
||||
msgstr "迁移前和每次应用更改时都会创建可恢复的快照。"
|
||||
|
||||
msgid "Active listeners"
|
||||
msgstr "活动监听器"
|
||||
|
||||
msgid "Add service"
|
||||
msgstr "添加服务"
|
||||
|
||||
msgid "Address"
|
||||
msgstr "地址"
|
||||
|
||||
msgid "Applied"
|
||||
msgstr "已应用"
|
||||
|
||||
msgid "Apply"
|
||||
msgstr "应用"
|
||||
|
||||
msgid "Apply changes"
|
||||
msgstr "应用更改"
|
||||
|
||||
msgid "Apply raw config"
|
||||
msgstr "应用配置"
|
||||
|
||||
msgid "Apply raw configuration?"
|
||||
msgstr "应用手动配置?"
|
||||
|
||||
msgid "Automatic"
|
||||
msgstr "自动"
|
||||
|
||||
msgid "Cancel"
|
||||
msgstr "取消"
|
||||
|
||||
msgid "Check configuration"
|
||||
msgstr "检查配置"
|
||||
|
||||
msgid "Config is valid"
|
||||
msgstr "配置有效"
|
||||
|
||||
msgid "Configuration and firewall are ready."
|
||||
msgstr "配置和防火墙已就绪。"
|
||||
|
||||
msgid "Configuration is valid. Firewall conflicts: %d"
|
||||
msgstr "配置有效。防火墙冲突:%d"
|
||||
|
||||
msgid "Configure shared Web entry ports, router access, and firewall automation."
|
||||
msgstr "配置共享 Web 入口端口、路由器访问和防火墙自动化。"
|
||||
|
||||
msgid "Conflict"
|
||||
msgstr "冲突"
|
||||
|
||||
msgid "Contents"
|
||||
msgstr "内容"
|
||||
|
||||
msgid "Create recovery point"
|
||||
msgstr "创建恢复点"
|
||||
|
||||
msgid "Created"
|
||||
msgstr "创建时间"
|
||||
|
||||
msgid "Creates and updates only firewall rules owned by HAProxy Manager."
|
||||
msgstr "仅创建和更新由 HAProxy Manager 管理的防火墙规则。"
|
||||
|
||||
msgid "Custom TCP"
|
||||
msgstr "自定义 TCP"
|
||||
|
||||
msgid "Destination"
|
||||
msgstr "目标"
|
||||
|
||||
msgid "Destination host"
|
||||
msgstr "目标主机"
|
||||
|
||||
msgid "Destination HTTP port"
|
||||
msgstr "目标 HTTP 端口"
|
||||
|
||||
msgid "Destination HTTPS port"
|
||||
msgstr "目标 HTTPS 端口"
|
||||
|
||||
msgid "Destination RDP port"
|
||||
msgstr "目标 RDP 端口"
|
||||
|
||||
msgid "Destination SSH port"
|
||||
msgstr "目标 SSH 端口"
|
||||
|
||||
msgid "Disable conflicting forwards during apply"
|
||||
msgstr "应用时禁用冲突的端口转发"
|
||||
|
||||
msgid "Domain"
|
||||
msgstr "域名"
|
||||
|
||||
msgid "Enable managed configuration"
|
||||
msgstr "启用托管配置"
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr "已启用"
|
||||
|
||||
msgid "Every Web service shares these public ports. SSH, Remote Desktop, and Custom TCP services use the ports configured on each service."
|
||||
msgstr "所有 Web 服务共享这些公网端口。SSH、远程桌面和自定义 TCP 服务使用各自配置的端口。"
|
||||
|
||||
msgid "Existing port-forward conflicts"
|
||||
msgstr "现有端口转发冲突"
|
||||
|
||||
msgid "Expert editor for /etc/haproxy.cfg."
|
||||
msgstr "/etc/haproxy.cfg 高级编辑器。"
|
||||
|
||||
msgid "Filter services"
|
||||
msgstr "筛选服务"
|
||||
|
||||
msgid "Firewall"
|
||||
msgstr "防火墙"
|
||||
|
||||
msgid "Firewall automation"
|
||||
msgstr "防火墙自动化"
|
||||
|
||||
msgid "Firewall conflict detected."
|
||||
msgstr "检测到防火墙冲突。"
|
||||
|
||||
msgid "Generated routes can overwrite manual changes to this file."
|
||||
msgstr "生成路由可能会覆盖对此文件的手动修改。"
|
||||
|
||||
msgid "Grant access to LuCI HAProxy Manager"
|
||||
msgstr "授予访问 LuCI HAProxy Manager 的权限"
|
||||
|
||||
msgid "HAProxy"
|
||||
msgstr "HAProxy"
|
||||
|
||||
msgid "HAProxy service"
|
||||
msgstr "HAProxy 服务"
|
||||
|
||||
msgid "HAProxy Services"
|
||||
msgstr "HAProxy 服务"
|
||||
|
||||
msgid "HAProxy Settings"
|
||||
msgstr "HAProxy 设置"
|
||||
|
||||
msgid "HAProxy Status"
|
||||
msgstr "HAProxy 状态"
|
||||
|
||||
msgid "HAProxy version"
|
||||
msgstr "HAProxy 版本"
|
||||
|
||||
msgid "HAProxy, firewall, and LuCI settings will be restored from %s."
|
||||
msgstr "将从 %s 恢复 HAProxy、防火墙和 LuCI 设置。"
|
||||
|
||||
msgid "HTTP"
|
||||
msgstr "HTTP"
|
||||
|
||||
msgid "HTTP only"
|
||||
msgstr "仅 HTTP"
|
||||
|
||||
msgid "HTTPS"
|
||||
msgstr "HTTPS"
|
||||
|
||||
msgid "HTTPS only"
|
||||
msgstr "仅 HTTPS"
|
||||
|
||||
msgid "Keep LuCI on the LAN address"
|
||||
msgstr "让 LuCI 仅使用 LAN 地址"
|
||||
|
||||
msgid "Latest"
|
||||
msgstr "最新"
|
||||
|
||||
msgid "Legacy Web"
|
||||
msgstr "旧版 Web"
|
||||
|
||||
msgid "LuCI LAN address"
|
||||
msgstr "LuCI LAN 地址"
|
||||
|
||||
msgid "Managed"
|
||||
msgstr "已管理"
|
||||
|
||||
msgid "Manual"
|
||||
msgstr "手动"
|
||||
|
||||
msgid "My service"
|
||||
msgstr "我的服务"
|
||||
|
||||
msgid "No HAProxy or LuCI listeners detected."
|
||||
msgstr "未检测到 HAProxy 或 LuCI 监听器。"
|
||||
|
||||
msgid "No matching services"
|
||||
msgstr "没有匹配的服务"
|
||||
|
||||
msgid "No recovery points yet."
|
||||
msgstr "还没有恢复点。"
|
||||
|
||||
msgid "Not available"
|
||||
msgstr "不可用"
|
||||
|
||||
msgid "Not set"
|
||||
msgstr "未设置"
|
||||
|
||||
msgid "Off"
|
||||
msgstr "关闭"
|
||||
|
||||
msgid "On"
|
||||
msgstr "开启"
|
||||
|
||||
msgid "Open HAProxy ports on WAN automatically"
|
||||
msgstr "自动在 WAN 上开放 HAProxy 端口"
|
||||
|
||||
msgid "Open recovery"
|
||||
msgstr "打开恢复"
|
||||
|
||||
msgid "Pending"
|
||||
msgstr "等待应用"
|
||||
|
||||
msgid "Process"
|
||||
msgstr "进程"
|
||||
|
||||
msgid "Protocol"
|
||||
msgstr "协议"
|
||||
|
||||
msgid "Public endpoint"
|
||||
msgstr "公共入口"
|
||||
|
||||
msgid "Public HTTP port"
|
||||
msgstr "公网 HTTP 端口"
|
||||
|
||||
msgid "Public HTTPS port"
|
||||
msgstr "公网 HTTPS 端口"
|
||||
|
||||
msgid "Public RDP port"
|
||||
msgstr "公网 RDP 端口"
|
||||
|
||||
msgid "Public SSH port"
|
||||
msgstr "公网 SSH 端口"
|
||||
|
||||
msgid "Publish HTTP"
|
||||
msgstr "发布 HTTP"
|
||||
|
||||
msgid "Publish HTTPS"
|
||||
msgstr "发布 HTTPS"
|
||||
|
||||
msgid "Publish Web, SSH, Remote Desktop, and custom TCP services from one place."
|
||||
msgstr "在一个界面中发布 Web、SSH、远程桌面和自定义 TCP 服务。"
|
||||
|
||||
msgid "Raw Config"
|
||||
msgstr "原始配置"
|
||||
|
||||
msgid "Raw HAProxy Config"
|
||||
msgstr "HAProxy 原始配置"
|
||||
|
||||
msgid "Raw HAProxy configuration"
|
||||
msgstr "HAProxy 原始配置"
|
||||
|
||||
msgid "Ready"
|
||||
msgstr "就绪"
|
||||
|
||||
msgid "Recommended when HAProxy uses public ports 80 or 443. LuCI remains available from the local network."
|
||||
msgstr "当 HAProxy 使用公网端口 80 或 443 时建议启用。LuCI 仍可从本地网络访问。"
|
||||
|
||||
msgid "Recovery"
|
||||
msgstr "恢复"
|
||||
|
||||
msgid "Recovery points"
|
||||
msgstr "恢复点"
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr "刷新"
|
||||
|
||||
msgid "Remote Desktop"
|
||||
msgstr "远程桌面"
|
||||
|
||||
msgid "Restore"
|
||||
msgstr "恢复"
|
||||
|
||||
msgid "Restore configuration?"
|
||||
msgstr "恢复配置?"
|
||||
|
||||
msgid "Restore HAProxy, firewall, and LuCI settings from a previous snapshot."
|
||||
msgstr "从以前的快照恢复 HAProxy、防火墙和 LuCI 设置。"
|
||||
|
||||
msgid "Restored"
|
||||
msgstr "已恢复"
|
||||
|
||||
msgid "Review firewall settings"
|
||||
msgstr "检查防火墙设置"
|
||||
|
||||
msgid "Router access"
|
||||
msgstr "路由器访问"
|
||||
|
||||
msgid "Running"
|
||||
msgstr "运行中"
|
||||
|
||||
msgid "Save and apply"
|
||||
msgstr "保存并应用"
|
||||
|
||||
msgid "Save settings"
|
||||
msgstr "保存设置"
|
||||
|
||||
msgid "Service"
|
||||
msgstr "服务"
|
||||
|
||||
msgid "Service health, active listeners, and recovery points."
|
||||
msgstr "服务状态、活动监听器和恢复点。"
|
||||
|
||||
msgid "Service name"
|
||||
msgstr "服务名称"
|
||||
|
||||
msgid "Service type"
|
||||
msgstr "服务类型"
|
||||
|
||||
msgid "Services"
|
||||
msgstr "服务"
|
||||
|
||||
msgid "Settings"
|
||||
msgstr "设置"
|
||||
|
||||
msgid "Settings saved"
|
||||
msgstr "设置已保存"
|
||||
|
||||
msgid "SSH"
|
||||
msgstr "SSH"
|
||||
|
||||
msgid "Status"
|
||||
msgstr "状态"
|
||||
|
||||
msgid "Stop and show conflicts"
|
||||
msgstr "停止并显示冲突"
|
||||
|
||||
msgid "Stopped"
|
||||
msgstr "已停止"
|
||||
|
||||
msgid "TCP"
|
||||
msgstr "TCP"
|
||||
|
||||
msgid "TCP port mappings"
|
||||
msgstr "TCP 端口映射"
|
||||
|
||||
msgid "The file will be validated and backed up before HAProxy restarts."
|
||||
msgstr "HAProxy 重启前将验证并备份该文件。"
|
||||
|
||||
msgid "Use the public:destination format, for example 8443:443."
|
||||
msgstr "请使用 公网:目标 格式,例如 8443:443。"
|
||||
|
||||
msgid "Validate"
|
||||
msgstr "验证"
|
||||
|
||||
msgid "WAN address"
|
||||
msgstr "WAN 地址"
|
||||
|
||||
msgid "WAN bind address"
|
||||
msgstr "WAN 绑定地址"
|
||||
|
||||
msgid "WAN firewall zone"
|
||||
msgstr "WAN 防火墙区域"
|
||||
|
||||
msgid "Web"
|
||||
msgstr "Web"
|
||||
|
||||
msgid "Web (HTTP + HTTPS)"
|
||||
msgstr "Web(HTTP + HTTPS)"
|
||||
|
||||
msgid "Web entry ports"
|
||||
msgstr "Web 入口端口"
|
||||
msgid "Changes saved and applied."
|
||||
msgstr "更改已保存并应用。"
|
||||
|
||||
msgid "Delete and apply"
|
||||
msgstr "删除并应用"
|
||||
|
||||
msgid "Delete service?"
|
||||
msgstr "删除服务?"
|
||||
|
||||
msgid "Synchronize now"
|
||||
msgstr "立即同步"
|
||||
|
||||
msgid "The service will be removed from HAProxy and the firewall immediately."
|
||||
msgstr "该服务将立即从 HAProxy 和防火墙中移除。"
|
||||
|
||||
msgid "Unable to create a recovery point."
|
||||
msgstr "无法创建恢复点。"
|
||||
|
||||
msgid "Automatic recovery"
|
||||
msgstr "自动恢复"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "已禁用"
|
||||
|
||||
msgid "Failed"
|
||||
msgstr "失败"
|
||||
|
||||
msgid "If HAProxy stops before a listener address exists, retry it after a network interface comes up and retain diagnostics for the seven latest incidents."
|
||||
msgstr "如果 HAProxy 在监听地址可用前停止,则在网络接口启动后重试,并保留最近七次事件的诊断信息。"
|
||||
|
||||
msgid "In progress"
|
||||
msgstr "进行中"
|
||||
|
||||
msgid "Invalid configuration"
|
||||
msgstr "配置无效"
|
||||
|
||||
msgid "Last recovery"
|
||||
msgstr "最近一次恢复"
|
||||
|
||||
msgid "No incidents recorded"
|
||||
msgstr "尚无事件记录"
|
||||
|
||||
msgid "Recover HAProxy after networking becomes ready"
|
||||
msgstr "网络就绪后恢复 HAProxy"
|
||||
|
||||
msgid "Recovered"
|
||||
msgstr "已恢复"
|
||||
|
||||
msgid "Recovery point failed"
|
||||
msgstr "恢复点创建失败"
|
||||
|
||||
msgid "WAN network interface"
|
||||
msgstr "WAN 网络接口"
|
||||
|
||||
msgid "Close"
|
||||
msgstr "关闭"
|
||||
|
||||
msgid "Configuration mode"
|
||||
msgstr "配置模式"
|
||||
|
||||
msgid "Diagnostic report for %s."
|
||||
msgstr "%s 的诊断报告。"
|
||||
|
||||
msgid "Enter an HTTP or HTTPS URL without spaces."
|
||||
msgstr "请输入不含空格的 HTTP 或 HTTPS URL。"
|
||||
|
||||
msgid "Failed, configuration restored"
|
||||
msgstr "失败,已恢复配置"
|
||||
|
||||
msgid "Failed, restore also failed"
|
||||
msgstr "失败,恢复操作也失败"
|
||||
|
||||
msgid "Generated routes"
|
||||
msgstr "生成的路由"
|
||||
|
||||
msgid "Incident diagnostics"
|
||||
msgstr "事件诊断"
|
||||
|
||||
msgid "Not managed"
|
||||
msgstr "未管理"
|
||||
|
||||
msgid "Raw configuration"
|
||||
msgstr "手动配置"
|
||||
|
||||
msgid "Reconciles generated listeners after WAN address changes, retries a stopped service, and retains diagnostics for the seven latest incidents. Raw configurations are never overwritten."
|
||||
msgstr "WAN 地址变化后同步生成的监听器,重试已停止的服务,并保留最近七次事件的诊断信息。手动配置绝不会被覆盖。"
|
||||
|
||||
msgid "Recovery incidents"
|
||||
msgstr "恢复事件"
|
||||
|
||||
msgid "Result"
|
||||
msgstr "结果"
|
||||
|
||||
msgid "Send recovery webhook notifications"
|
||||
msgstr "发送恢复 Webhook 通知"
|
||||
|
||||
msgid "Sends only the incident identifier, result, action, interface, and reason. Configuration, addresses, and diagnostic logs are never included."
|
||||
msgstr "仅发送事件标识符、结果、操作、接口和原因。绝不会包含配置、地址和诊断日志。"
|
||||
|
||||
msgid "The selected logical interface is monitored for address changes. In multi-WAN setups, select the interface that receives public connections."
|
||||
msgstr "监控所选逻辑接口的地址变化。在多 WAN 配置中,请选择接收公网连接的接口。"
|
||||
|
||||
msgid "The seven latest automatic recovery reports are retained on the router."
|
||||
msgstr "路由器会保留最近七份自动恢复报告。"
|
||||
|
||||
msgid "Trigger"
|
||||
msgstr "触发条件"
|
||||
|
||||
msgid "View"
|
||||
msgstr "查看"
|
||||
|
||||
msgid "WAN address reconciled"
|
||||
msgstr "WAN 地址已同步"
|
||||
|
||||
msgid "WAN address unavailable"
|
||||
msgstr "WAN 地址不可用"
|
||||
|
||||
msgid "Webhook URL"
|
||||
msgstr "Webhook URL"
|
||||
msgid "Generated configuration is invalid"
|
||||
msgstr "生成的配置无效"
|
||||
|
||||
msgid "Configuration installation failed"
|
||||
msgstr "配置安装失败"
|
||||
@@ -0,0 +1,17 @@
|
||||
config settings 'main'
|
||||
option schema_version '3'
|
||||
option enabled '0'
|
||||
option active_mode 'none'
|
||||
option auto_recover '1'
|
||||
option manage_uhttpd_bind '0'
|
||||
option manage_firewall '0'
|
||||
option firewall_zone 'wan'
|
||||
option firewall_conflict_mode 'warn'
|
||||
option lan_bind_ip 'auto'
|
||||
option wan_interface 'wan'
|
||||
option wan_bind_ip 'auto'
|
||||
option http_port '80'
|
||||
option https_port '443'
|
||||
option backup_dir '/root/haproxy-manager-backups'
|
||||
option webhook_enabled '0'
|
||||
option webhook_url ''
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
|
||||
case "${ACTION:-}" in
|
||||
ifup|ifupdate) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
|
||||
[ "${INTERFACE:-}" = "$(uci -q get haproxy_manager.main.wan_interface 2>/dev/null || echo wan)" ] || exit 0
|
||||
[ -x /usr/libexec/haproxy-manager/recover ] || exit 0
|
||||
/usr/libexec/haproxy-manager/recover "$ACTION" "${INTERFACE:-unknown}" >/dev/null 2>&1 &
|
||||
exit 0
|
||||
@@ -0,0 +1,180 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
umask 077
|
||||
|
||||
LOCK_DIR=/var/lock/haproxy-manager.apply
|
||||
LOCK_BUSY_EXIT=75
|
||||
TMP_CFG="/tmp/haproxy-manager-apply.$$"
|
||||
BACKUP_ID=""
|
||||
APPLIED=0
|
||||
UHTTPD_CHANGED=0
|
||||
|
||||
if [ "${1:-}" = --backup ]; then
|
||||
BACKUP_ID="${2:-}"
|
||||
[ "$#" -eq 2 ] || { echo "Usage: apply [--backup YYYYMMDD-HHMMSS]" >&2; exit 1; }
|
||||
is_valid_backup_id "$BACKUP_ID" || { echo "Invalid backup identifier" >&2; exit 1; }
|
||||
elif [ "$#" -ne 0 ]; then
|
||||
echo "Usage: apply [--backup YYYYMMDD-HHMMSS]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
operation_lock_acquire "$LOCK_DIR" || {
|
||||
echo "Another HAProxy Manager operation is already running." >&2
|
||||
exit "$LOCK_BUSY_EXIT"
|
||||
}
|
||||
|
||||
cleanup_temp() {
|
||||
rm -f "$TMP_CFG"
|
||||
}
|
||||
|
||||
release_lock() {
|
||||
operation_lock_release "$LOCK_DIR"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
cleanup_temp
|
||||
release_lock
|
||||
}
|
||||
|
||||
restore_on_error() {
|
||||
status=$?
|
||||
trap - EXIT
|
||||
if [ "$APPLIED" != 1 ] && [ -n "$BACKUP_ID" ]; then
|
||||
if [ "$UHTTPD_CHANGED" = 1 ]; then
|
||||
cleanup_temp
|
||||
(
|
||||
sleep 2
|
||||
if /usr/libexec/haproxy-manager/rollback --sync "$BACKUP_ID" >/dev/null 2>&1; then
|
||||
logger -t haproxy-manager "Apply failed; restored $BACKUP_ID"
|
||||
else
|
||||
logger -t haproxy-manager "Apply failed; could not restore $BACKUP_ID"
|
||||
fi
|
||||
release_lock
|
||||
) >/dev/null 2>&1 &
|
||||
operation_lock_set_pid "$LOCK_DIR" "$!"
|
||||
exit "$status"
|
||||
fi
|
||||
|
||||
if /usr/libexec/haproxy-manager/rollback --sync "$BACKUP_ID" >/dev/null 2>&1; then
|
||||
logger -t haproxy-manager "Apply failed; restored $BACKUP_ID"
|
||||
else
|
||||
logger -t haproxy-manager "Apply failed; could not restore $BACKUP_ID"
|
||||
fi
|
||||
fi
|
||||
cleanup
|
||||
exit "$status"
|
||||
}
|
||||
trap restore_on_error EXIT
|
||||
|
||||
if [ -n "$BACKUP_ID" ]; then
|
||||
[ -d "$(backup_dir)/$BACKUP_ID" ] || { echo "Backup not found: $BACKUP_ID" >&2; exit 1; }
|
||||
else
|
||||
BACKUP_ID="$(basename "$(/usr/libexec/haproxy-manager/backup)")"
|
||||
fi
|
||||
|
||||
/usr/libexec/haproxy-manager/migrate >/dev/null
|
||||
|
||||
defer_disabled_reload() {
|
||||
APPLIED=1
|
||||
trap - EXIT
|
||||
cleanup_temp
|
||||
(
|
||||
sleep 2
|
||||
if /etc/init.d/uhttpd reload >/dev/null 2>&1; then
|
||||
logger -t haproxy-manager "LuCI listener bindings restored"
|
||||
else
|
||||
if /usr/libexec/haproxy-manager/rollback --sync "$BACKUP_ID" >/dev/null 2>&1; then
|
||||
logger -t haproxy-manager "LuCI reload failed; restored $BACKUP_ID"
|
||||
else
|
||||
logger -t haproxy-manager "LuCI reload failed; could not restore $BACKUP_ID"
|
||||
fi
|
||||
fi
|
||||
release_lock
|
||||
) >/dev/null 2>&1 &
|
||||
operation_lock_set_pid "$LOCK_DIR" "$!"
|
||||
}
|
||||
|
||||
if [ "$(uci_get main enabled 0)" != 1 ]; then
|
||||
MODE="$(active_mode)"
|
||||
restore_uhttpd_bindings
|
||||
[ "$UHTTPD_CHANGED" != 1 ] || uci commit uhttpd
|
||||
/usr/libexec/haproxy-manager/firewall-sync --disable >/dev/null
|
||||
case "$MODE" in
|
||||
generated|raw)
|
||||
/etc/init.d/haproxy stop >/dev/null 2>&1 || true
|
||||
/etc/init.d/haproxy disable >/dev/null 2>&1 || true
|
||||
uci set "$CONFIG.main.active_mode=none"
|
||||
uci commit "$CONFIG"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$UHTTPD_CHANGED" = 1 ]; then
|
||||
defer_disabled_reload
|
||||
echo "Managed HAProxy service disabled. LuCI bindings will be restored shortly. Backup: $BACKUP_ID"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
APPLIED=1
|
||||
trap - EXIT
|
||||
cleanup
|
||||
echo "Managed HAProxy service disabled. Backup: $BACKUP_ID"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
/usr/libexec/haproxy-manager/generate "$TMP_CFG" >/dev/null
|
||||
/usr/libexec/haproxy-manager/validate "$TMP_CFG"
|
||||
|
||||
if [ "$(uci_get main manage_firewall 0)" = 1 ]; then
|
||||
/usr/libexec/haproxy-manager/firewall-sync --check >/dev/null
|
||||
fi
|
||||
|
||||
MANAGE_UHTTPD="$(uci_get main manage_uhttpd_bind 0)"
|
||||
LAN_BIND_IP="$(lan_ip || true)"
|
||||
|
||||
if [ "$MANAGE_UHTTPD" = 1 ]; then
|
||||
[ -n "$LAN_BIND_IP" ] || { echo "Cannot detect LAN IPv4 address. Set the LuCI LAN address manually." >&2; exit 1; }
|
||||
is_valid_ip_or_host "$LAN_BIND_IP" || { echo "Invalid LuCI LAN address: $LAN_BIND_IP" >&2; exit 1; }
|
||||
capture_uhttpd_bindings
|
||||
set_uhttpd_list listen_http "$LAN_BIND_IP:80"
|
||||
set_uhttpd_list listen_https "$LAN_BIND_IP:443"
|
||||
else
|
||||
restore_uhttpd_bindings
|
||||
fi
|
||||
|
||||
[ "$UHTTPD_CHANGED" != 1 ] || uci commit uhttpd
|
||||
|
||||
/usr/libexec/haproxy-manager/firewall-sync >/dev/null
|
||||
atomic_install_file "$TMP_CFG" "$HAPROXY_CFG"
|
||||
uci set "$CONFIG.main.active_mode=generated"
|
||||
uci commit "$CONFIG"
|
||||
/etc/init.d/haproxy enable >/dev/null 2>&1 || true
|
||||
|
||||
if [ "$UHTTPD_CHANGED" = 1 ]; then
|
||||
APPLIED=1
|
||||
trap - EXIT
|
||||
cleanup_temp
|
||||
(
|
||||
sleep 2
|
||||
if /etc/init.d/uhttpd reload >/dev/null 2>&1 && /etc/init.d/haproxy restart >/dev/null 2>&1; then
|
||||
logger -t haproxy-manager "LuCI and HAProxy reloaded successfully"
|
||||
else
|
||||
if /usr/libexec/haproxy-manager/rollback --sync "$BACKUP_ID" >/dev/null 2>&1; then
|
||||
logger -t haproxy-manager "Service reload failed; restored $BACKUP_ID"
|
||||
else
|
||||
logger -t haproxy-manager "Service reload failed; could not restore $BACKUP_ID"
|
||||
fi
|
||||
fi
|
||||
release_lock
|
||||
) >/dev/null 2>&1 &
|
||||
operation_lock_set_pid "$LOCK_DIR" "$!"
|
||||
echo "Applied successfully. LuCI and HAProxy will reload shortly. Backup: $BACKUP_ID"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
/etc/init.d/haproxy restart
|
||||
|
||||
APPLIED=1
|
||||
trap - EXIT
|
||||
cleanup
|
||||
echo "Applied successfully. Backup: $BACKUP_ID"
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
umask 077
|
||||
|
||||
SRC="${1:-}"
|
||||
is_safe_temp_path "$SRC" || { echo "Refusing unsafe raw config path: $SRC" >&2; exit 1; }
|
||||
|
||||
[ -f "$SRC" ] || { echo "Raw config file not found: $SRC" >&2; exit 1; }
|
||||
|
||||
LOCK_DIR=/var/lock/haproxy-manager.apply
|
||||
operation_lock_acquire "$LOCK_DIR" || { echo "Another HAProxy Manager operation is already running." >&2; exit 1; }
|
||||
TMP_RAW="/tmp/haproxy-manager-raw-apply.$$"
|
||||
BACKUP_ID=""
|
||||
APPLIED=0
|
||||
|
||||
restore_on_error() {
|
||||
status=$?
|
||||
trap - EXIT
|
||||
if [ "$APPLIED" != 1 ] && [ -n "$BACKUP_ID" ]; then
|
||||
/usr/libexec/haproxy-manager/rollback --sync "$BACKUP_ID" >/dev/null 2>&1 || true
|
||||
logger -t haproxy-manager "Raw apply failed; restored $BACKUP_ID"
|
||||
fi
|
||||
rm -f "$TMP_RAW" "$SRC"
|
||||
operation_lock_release "$LOCK_DIR"
|
||||
exit "$status"
|
||||
}
|
||||
trap restore_on_error EXIT
|
||||
|
||||
cp "$SRC" "$TMP_RAW"
|
||||
rm -f "$SRC"
|
||||
/usr/libexec/haproxy-manager/validate "$TMP_RAW"
|
||||
BACKUP_ID="$(basename "$(/usr/libexec/haproxy-manager/backup)")"
|
||||
|
||||
atomic_install_file "$TMP_RAW" "$HAPROXY_CFG"
|
||||
uci set "$CONFIG.main.active_mode=raw"
|
||||
uci commit "$CONFIG"
|
||||
/etc/init.d/haproxy enable >/dev/null 2>&1 || true
|
||||
/etc/init.d/haproxy restart
|
||||
|
||||
APPLIED=1
|
||||
trap - EXIT
|
||||
rm -f "$TMP_RAW" "$SRC"
|
||||
operation_lock_release "$LOCK_DIR"
|
||||
echo "Raw HAProxy config applied. Backup: $BACKUP_ID"
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
|
||||
REQUESTED_BASE="$(backup_dir)"
|
||||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
|
||||
umask 077
|
||||
mkdir -p "$REQUESTED_BASE"
|
||||
BASE="$(resolve_storage_dir "$REQUESTED_BASE")" || {
|
||||
echo "Recovery point directory resolves outside /root or /mnt" >&2
|
||||
exit 1
|
||||
}
|
||||
chmod 700 "$BASE" 2>/dev/null || true
|
||||
DIR="$BASE/$STAMP"
|
||||
attempt=1
|
||||
while ! mkdir "$DIR" 2>/dev/null; do
|
||||
[ "$attempt" -lt 5 ] || {
|
||||
echo "Cannot create recovery point in $BASE after $attempt attempts" >&2
|
||||
exit 1
|
||||
}
|
||||
sleep 1
|
||||
attempt=$((attempt + 1))
|
||||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
DIR="$BASE/$STAMP"
|
||||
done
|
||||
chmod 700 "$DIR" 2>/dev/null || true
|
||||
[ -f /etc/config/firewall ] && cp /etc/config/firewall "$DIR/firewall"
|
||||
[ -f /etc/config/uhttpd ] && cp /etc/config/uhttpd "$DIR/uhttpd"
|
||||
[ -f /etc/config/haproxy_manager ] && cp /etc/config/haproxy_manager "$DIR/haproxy_manager"
|
||||
[ -f "$HAPROXY_CFG" ] && cp "$HAPROXY_CFG" "$DIR/haproxy.cfg"
|
||||
{
|
||||
if /etc/init.d/haproxy enabled >/dev/null 2>&1; then
|
||||
printf 'enabled=1\n'
|
||||
else
|
||||
printf 'enabled=0\n'
|
||||
fi
|
||||
if /etc/init.d/haproxy status >/dev/null 2>&1; then
|
||||
printf 'running=1\n'
|
||||
else
|
||||
printf 'running=0\n'
|
||||
fi
|
||||
} > "$DIR/haproxy.state"
|
||||
|
||||
printf '%s\n' "$STAMP" > "$BASE/LAST"
|
||||
|
||||
count=0
|
||||
for old in $(find "$BASE" -maxdepth 1 -type d -name '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]' 2>/dev/null | sort -r); do
|
||||
count=$((count + 1))
|
||||
[ "$count" -le "$BACKUP_LIMIT" ] && continue
|
||||
case "$old" in
|
||||
"$BASE"/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9])
|
||||
rm -f "$old"/*
|
||||
rmdir "$old"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
printf '%s\n' "$DIR"
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
|
||||
BASE="$(backup_dir)"
|
||||
[ ! -d "$BASE" ] || BASE="$(resolve_storage_dir "$BASE")" || exit 1
|
||||
LAST=""
|
||||
[ ! -r "$BASE/LAST" ] || LAST="$(basename "$(cat "$BASE/LAST")")"
|
||||
|
||||
count=0
|
||||
for dir in $(find "$BASE" -maxdepth 1 -type d -name '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]' 2>/dev/null | sort -r); do
|
||||
[ -d "$dir" ] || continue
|
||||
id="$(basename "$dir")"
|
||||
current=0
|
||||
[ "$id" != "$LAST" ] || current=1
|
||||
files=0
|
||||
for file in firewall uhttpd haproxy_manager haproxy.cfg; do
|
||||
[ ! -f "$dir/$file" ] || files=$((files + 1))
|
||||
done
|
||||
printf 'backup\t%s\t%s\t%s\n' "$id" "$current" "$files"
|
||||
count=$((count + 1))
|
||||
[ "$count" -lt "$BACKUP_LIMIT" ] || break
|
||||
done
|
||||
@@ -0,0 +1,361 @@
|
||||
#!/bin/sh
|
||||
|
||||
CONFIG=haproxy_manager
|
||||
HAPROXY_CFG=/etc/haproxy.cfg
|
||||
TMP_CFG=/tmp/haproxy-manager-generated.cfg
|
||||
BACKUP_LIMIT=7
|
||||
INCIDENT_LIMIT=7
|
||||
FIREWALL_RULE_NAME='HAProxy Manager: WAN listeners'
|
||||
FIREWALL_DISABLED_PREFIX='HAProxy Manager disabled: '
|
||||
|
||||
uci_get() {
|
||||
local section="$1"
|
||||
local option="$2"
|
||||
local default="${3:-}"
|
||||
uci -q get "$CONFIG.$section.$option" 2>/dev/null || printf '%s\n' "$default"
|
||||
}
|
||||
|
||||
backup_dir() {
|
||||
local dir
|
||||
dir="$(uci_get main backup_dir /root/haproxy-manager-backups)"
|
||||
is_safe_storage_dir "$dir" || {
|
||||
echo "Recovery point directory must be a plain path under /root or /mnt: $dir" >&2
|
||||
return 1
|
||||
}
|
||||
printf '%s\n' "$dir"
|
||||
}
|
||||
|
||||
incident_dir() {
|
||||
local dir
|
||||
dir="$(uci_get main incident_dir /root/haproxy-manager-incidents)"
|
||||
is_safe_storage_dir "$dir" || {
|
||||
echo "Incident directory must be a plain path under /root or /mnt: $dir" >&2
|
||||
return 1
|
||||
}
|
||||
printf '%s\n' "$dir"
|
||||
}
|
||||
|
||||
is_safe_storage_dir() {
|
||||
case "$1" in
|
||||
/root/*|/mnt/*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
case "/${1#/}/" in
|
||||
*/../*|*/./*|*[!A-Za-z0-9_./-]*) return 1 ;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
resolve_storage_dir() {
|
||||
local dir="$1" resolved
|
||||
[ -d "$dir" ] || return 1
|
||||
resolved="$(cd "$dir" 2>/dev/null && pwd -P)" || return 1
|
||||
is_safe_storage_dir "$resolved" || return 1
|
||||
printf '%s\n' "$resolved"
|
||||
}
|
||||
|
||||
is_safe_temp_path() {
|
||||
local path="$1" name
|
||||
name="${path#/tmp/}"
|
||||
[ "$path" = "/tmp/$name" ] || return 1
|
||||
case "$name" in
|
||||
haproxy-manager-[A-Za-z0-9._-]*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
operation_lock_acquire() {
|
||||
local lock_dir="$1" lock_pid
|
||||
if mkdir "$lock_dir" 2>/dev/null; then
|
||||
printf '%s\n' "$$" > "$lock_dir/pid"
|
||||
return 0
|
||||
fi
|
||||
|
||||
lock_pid="$(cat "$lock_dir/pid" 2>/dev/null || true)"
|
||||
case "$lock_pid" in
|
||||
''|*[!0-9]*) lock_pid=0 ;;
|
||||
esac
|
||||
if [ "$lock_pid" -gt 1 ] 2>/dev/null && kill -0 "$lock_pid" 2>/dev/null; then
|
||||
return 1
|
||||
fi
|
||||
rm -f "$lock_dir/pid"
|
||||
rmdir "$lock_dir" 2>/dev/null || return 1
|
||||
mkdir "$lock_dir" 2>/dev/null || return 1
|
||||
printf '%s\n' "$$" > "$lock_dir/pid"
|
||||
}
|
||||
|
||||
operation_lock_set_pid() {
|
||||
printf '%s\n' "$2" > "$1/pid"
|
||||
}
|
||||
|
||||
operation_lock_release() {
|
||||
rm -f "$1/pid"
|
||||
rmdir "$1" 2>/dev/null || true
|
||||
}
|
||||
|
||||
atomic_install_file() {
|
||||
local source="$1" destination="$2" temporary mode
|
||||
[ -f "$source" ] || return 1
|
||||
temporary="$destination.haproxy-manager.$$"
|
||||
rm -f "$temporary"
|
||||
if ! cp "$source" "$temporary"; then
|
||||
rm -f "$temporary"
|
||||
return 1
|
||||
fi
|
||||
mode="$(stat -c '%a' "$destination" 2>/dev/null || stat -c '%a' "$source" 2>/dev/null || echo 600)"
|
||||
chmod "$mode" "$temporary" || { rm -f "$temporary"; return 1; }
|
||||
mv -f "$temporary" "$destination" || { rm -f "$temporary"; return 1; }
|
||||
}
|
||||
|
||||
active_mode() {
|
||||
local mode
|
||||
mode="$(uci_get main active_mode)"
|
||||
case "$mode" in
|
||||
generated|raw|none) printf '%s\n' "$mode" ;;
|
||||
*)
|
||||
if [ "$(uci_get main enabled 0)" = 1 ] &&
|
||||
grep -q '^# Generated by luci-app-haproxy-manager\.' "$HAPROXY_CFG" 2>/dev/null; then
|
||||
printf 'generated\n'
|
||||
else
|
||||
printf 'none\n'
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
wan_interface() {
|
||||
local interface
|
||||
interface="$(uci_get main wan_interface wan)"
|
||||
case "$interface" in
|
||||
''|*[!A-Za-z0-9_.-]*) return 1 ;;
|
||||
esac
|
||||
printf '%s\n' "$interface"
|
||||
}
|
||||
|
||||
wan_ip() {
|
||||
local configured interface
|
||||
configured="$(uci_get main wan_bind_ip auto)"
|
||||
if [ "$configured" != auto ] && [ -n "$configured" ]; then
|
||||
printf '%s\n' "$configured"
|
||||
return 0
|
||||
fi
|
||||
|
||||
interface="$(wan_interface)" || return 1
|
||||
ubus call "network.interface.$interface" status 2>/dev/null \
|
||||
| jsonfilter -e '@["ipv4-address"][0].address' 2>/dev/null \
|
||||
| grep -m1 .
|
||||
}
|
||||
|
||||
lan_ip() {
|
||||
local configured
|
||||
configured="$(uci_get main lan_bind_ip auto)"
|
||||
if [ "$configured" != auto ] && [ -n "$configured" ]; then
|
||||
printf '%s\n' "$configured"
|
||||
return 0
|
||||
fi
|
||||
|
||||
ubus call network.interface.lan status 2>/dev/null \
|
||||
| jsonfilter -e '@["ipv4-address"][0].address' 2>/dev/null \
|
||||
| grep -m1 .
|
||||
}
|
||||
|
||||
route_name() {
|
||||
printf '%s' "$1" | tr '.:-' '___' | tr -cd 'A-Za-z0-9_'
|
||||
}
|
||||
|
||||
is_valid_host() {
|
||||
case "$1" in
|
||||
''|[!A-Za-z0-9]*|*[!A-Za-z0-9]) return 1 ;;
|
||||
*[!A-Za-z0-9.-]*) return 1 ;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
is_valid_ip_or_host() {
|
||||
case "$1" in
|
||||
''|*[!A-Za-z0-9_.:-]*) return 1 ;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
is_valid_backup_id() {
|
||||
case "$1" in
|
||||
[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
backend_address() {
|
||||
local host="$1"
|
||||
local port="$2"
|
||||
|
||||
case "$host" in
|
||||
*:*) printf '[%s]:%s\n' "$host" "$port" ;;
|
||||
*) printf '%s:%s\n' "$host" "$port" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_valid_port() {
|
||||
local port="$1"
|
||||
[ "$port" -ge 1 ] 2>/dev/null && [ "$port" -le 65535 ] 2>/dev/null
|
||||
}
|
||||
|
||||
route_sections() {
|
||||
uci -q show "$CONFIG" \
|
||||
| sed -n "s/^$CONFIG\\.\\(@route\\[[0-9][0-9]*\\]\\)=route/\\1/p"
|
||||
}
|
||||
|
||||
route_kind() {
|
||||
local section="$1"
|
||||
local kind
|
||||
|
||||
kind="$(uci_get "$section" kind)"
|
||||
[ -n "$kind" ] || kind="$(uci_get "$section" protocol http)"
|
||||
printf '%s\n' "$kind"
|
||||
}
|
||||
|
||||
route_port_maps() {
|
||||
local section="$1"
|
||||
local kind="$2"
|
||||
local public_port backend_port
|
||||
|
||||
case "$kind" in
|
||||
ssh)
|
||||
public_port="$(uci_get "$section" ssh_listen_port 22)"
|
||||
backend_port="$(uci_get "$section" ssh_backend_port 22)"
|
||||
printf '%s:%s\n' "$public_port" "$backend_port"
|
||||
;;
|
||||
rdp)
|
||||
public_port="$(uci_get "$section" rdp_listen_port 3389)"
|
||||
backend_port="$(uci_get "$section" rdp_backend_port 3389)"
|
||||
printf '%s:%s\n' "$public_port" "$backend_port"
|
||||
;;
|
||||
custom)
|
||||
for map in $(uci_get "$section" port_map); do
|
||||
printf '%s\n' "$map"
|
||||
done
|
||||
;;
|
||||
tcp)
|
||||
public_port="$(uci_get "$section" listen_port "$(uci_get "$section" backend_port)")"
|
||||
backend_port="$(uci_get "$section" backend_port)"
|
||||
printf '%s:%s\n' "$public_port" "$backend_port"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_valid_port_map() {
|
||||
local map="$1"
|
||||
local public_port backend_port
|
||||
|
||||
case "$map" in
|
||||
*:*:*) return 1 ;;
|
||||
*:*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
|
||||
public_port="${map%%:*}"
|
||||
backend_port="${map#*:}"
|
||||
is_valid_port "$public_port" && is_valid_port "$backend_port"
|
||||
}
|
||||
|
||||
required_ports() {
|
||||
local section enabled kind http_enabled https_enabled map
|
||||
local http_port https_port
|
||||
|
||||
http_port="$(uci_get main http_port 80)"
|
||||
https_port="$(uci_get main https_port 443)"
|
||||
[ "$(uci_get main enabled 0)" = 1 ] || return 0
|
||||
|
||||
for section in $(route_sections); do
|
||||
enabled="$(uci_get "$section" enabled 1)"
|
||||
[ "$enabled" = 1 ] || continue
|
||||
kind="$(route_kind "$section")"
|
||||
|
||||
case "$kind" in
|
||||
web)
|
||||
http_enabled="$(uci_get "$section" web_http 1)"
|
||||
https_enabled="$(uci_get "$section" web_https 1)"
|
||||
[ "$http_enabled" != 1 ] || printf '%s\n' "$http_port"
|
||||
[ "$https_enabled" != 1 ] || printf '%s\n' "$https_port"
|
||||
;;
|
||||
http)
|
||||
printf '%s\n' "$http_port"
|
||||
;;
|
||||
https)
|
||||
printf '%s\n' "$https_port"
|
||||
;;
|
||||
both)
|
||||
printf '%s\n%s\n' "$http_port" "$https_port"
|
||||
;;
|
||||
ssh|rdp|custom|tcp)
|
||||
for map in $(route_port_maps "$section" "$kind"); do
|
||||
printf '%s\n' "${map%%:*}"
|
||||
done
|
||||
;;
|
||||
esac
|
||||
done | sort -nu
|
||||
}
|
||||
|
||||
port_spec_contains() {
|
||||
local spec="$1"
|
||||
local wanted="$2"
|
||||
local token first last
|
||||
|
||||
for token in $(printf '%s' "$spec" | tr ',' ' '); do
|
||||
case "$token" in
|
||||
*-*)
|
||||
first="${token%%-*}"
|
||||
last="${token#*-}"
|
||||
[ "$wanted" -ge "$first" ] 2>/dev/null && [ "$wanted" -le "$last" ] 2>/dev/null && return 0
|
||||
;;
|
||||
*)
|
||||
[ "$token" = "$wanted" ] && return 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
set_uhttpd_list() {
|
||||
local option values current address
|
||||
option="$1"
|
||||
values="$2"
|
||||
current="$(uci -q get "uhttpd.main.$option" 2>/dev/null || true)"
|
||||
[ "$current" = "$values" ] && return 0
|
||||
|
||||
uci -q delete "uhttpd.main.$option" || true
|
||||
for address in $values; do
|
||||
uci add_list "uhttpd.main.$option=$address"
|
||||
done
|
||||
UHTTPD_CHANGED=1
|
||||
}
|
||||
|
||||
capture_uhttpd_bindings() {
|
||||
local saved_http saved_https address
|
||||
[ "$(uci_get main uhttpd_bind_owned 0)" != 1 ] || return 0
|
||||
|
||||
saved_http="$(uci -q get uhttpd.main.listen_http 2>/dev/null || true)"
|
||||
saved_https="$(uci -q get uhttpd.main.listen_https 2>/dev/null || true)"
|
||||
uci -q delete "$CONFIG.main.uhttpd_saved_listen_http" || true
|
||||
uci -q delete "$CONFIG.main.uhttpd_saved_listen_https" || true
|
||||
for address in $saved_http; do
|
||||
uci add_list "$CONFIG.main.uhttpd_saved_listen_http=$address"
|
||||
done
|
||||
for address in $saved_https; do
|
||||
uci add_list "$CONFIG.main.uhttpd_saved_listen_https=$address"
|
||||
done
|
||||
uci set "$CONFIG.main.uhttpd_bind_owned=1"
|
||||
uci commit "$CONFIG"
|
||||
}
|
||||
|
||||
restore_uhttpd_bindings() {
|
||||
[ "$(uci_get main uhttpd_bind_owned 0)" = 1 ] || return 0
|
||||
|
||||
set_uhttpd_list listen_http "$(uci_get main uhttpd_saved_listen_http)"
|
||||
set_uhttpd_list listen_https "$(uci_get main uhttpd_saved_listen_https)"
|
||||
uci -q delete "$CONFIG.main.uhttpd_saved_listen_http" || true
|
||||
uci -q delete "$CONFIG.main.uhttpd_saved_listen_https" || true
|
||||
uci -q delete "$CONFIG.main.uhttpd_bind_owned" || true
|
||||
uci commit "$CONFIG"
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/bin/sh
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
|
||||
ZONE="$(uci_get main firewall_zone wan)"
|
||||
MANAGED="$(uci_get main manage_firewall 0)"
|
||||
MODE="$(uci_get main firewall_conflict_mode warn)"
|
||||
PORTS="$(required_ports)"
|
||||
WAN_POLICY=unknown
|
||||
HAS_MANAGED_RULE=0
|
||||
CONFLICTS=0
|
||||
|
||||
for zone in $(uci -q show firewall | sed -n 's/^firewall\.\(@zone\[[0-9][0-9]*\]\)=zone/\1/p'); do
|
||||
[ "$(uci -q get "firewall.$zone.name")" = "$ZONE" ] || continue
|
||||
WAN_POLICY="$(uci -q get "firewall.$zone.input" 2>/dev/null || echo REJECT)"
|
||||
break
|
||||
done
|
||||
|
||||
for rule in $(uci -q show firewall | sed -n 's/^firewall\.\(@rule\[[0-9][0-9]*\]\)=rule/\1/p'); do
|
||||
name="$(uci -q get "firewall.$rule.name" 2>/dev/null || true)"
|
||||
[ "$name" = "$FIREWALL_RULE_NAME" ] || continue
|
||||
HAS_MANAGED_RULE=1
|
||||
break
|
||||
done
|
||||
|
||||
printf 'enabled\t%s\n' "$MANAGED"
|
||||
printf 'zone\t%s\n' "$ZONE"
|
||||
printf 'policy\t%s\n' "$WAN_POLICY"
|
||||
printf 'mode\t%s\n' "$MODE"
|
||||
printf 'managed_rule\t%s\n' "$HAS_MANAGED_RULE"
|
||||
|
||||
for port in $PORTS; do
|
||||
printf 'port\t%s\n' "$port"
|
||||
done
|
||||
|
||||
for redirect in $(uci -q show firewall | sed -n 's/^firewall\.\(@redirect\[[0-9][0-9]*\]\)=redirect/\1/p'); do
|
||||
redirect_enabled="$(uci -q get "firewall.$redirect.enabled" 2>/dev/null || echo 1)"
|
||||
redirect_name="$(uci -q get "firewall.$redirect.name" 2>/dev/null || true)"
|
||||
if [ "$redirect_enabled" != 1 ]; then
|
||||
case "$redirect_name" in
|
||||
"$FIREWALL_DISABLED_PREFIX"*)
|
||||
[ "$MANAGED" = 1 ] && [ "$MODE" != disable ] || continue
|
||||
;;
|
||||
*) continue ;;
|
||||
esac
|
||||
fi
|
||||
[ "$(uci -q get "firewall.$redirect.src" 2>/dev/null || true)" = "$ZONE" ] || continue
|
||||
|
||||
proto="$(uci -q get "firewall.$redirect.proto" 2>/dev/null || echo 'tcp udp')"
|
||||
case " $proto " in *' tcp '*|*' tcpudp '*) ;; *) continue ;; esac
|
||||
|
||||
spec="$(uci -q get "firewall.$redirect.src_dport" 2>/dev/null || true)"
|
||||
[ -n "$spec" ] || continue
|
||||
|
||||
for port in $PORTS; do
|
||||
port_spec_contains "$spec" "$port" || continue
|
||||
name="${redirect_name:-$redirect}"
|
||||
dest_ip="$(uci -q get "firewall.$redirect.dest_ip" 2>/dev/null || true)"
|
||||
dest_port="$(uci -q get "firewall.$redirect.dest_port" 2>/dev/null || true)"
|
||||
printf 'conflict\t%s\t%s\t%s\t%s\t%s\n' "$redirect" "$name" "$spec" "$dest_ip" "$dest_port"
|
||||
CONFLICTS=$((CONFLICTS + 1))
|
||||
break
|
||||
done
|
||||
done
|
||||
|
||||
printf 'conflicts\t%s\n' "$CONFLICTS"
|
||||
@@ -0,0 +1,106 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
umask 077
|
||||
|
||||
CHECK_ONLY=0
|
||||
FORCE_DISABLED=0
|
||||
case "${1:-}" in
|
||||
'') ;;
|
||||
--check) CHECK_ONLY=1 ;;
|
||||
--disable) FORCE_DISABLED=1 ;;
|
||||
*) echo "Usage: firewall-sync [--check|--disable]" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
ZONE="$(uci_get main firewall_zone wan)"
|
||||
MODE="$(uci_get main firewall_conflict_mode warn)"
|
||||
MANAGED="$(uci_get main manage_firewall 0)"
|
||||
[ "$FORCE_DISABLED" != 1 ] || MANAGED=0
|
||||
PLAN=/tmp/haproxy-manager-firewall-plan.$$
|
||||
/usr/libexec/haproxy-manager/firewall-plan > "$PLAN"
|
||||
trap 'rm -f "$PLAN"' EXIT
|
||||
|
||||
CONFLICTS="$(sed -n 's/^conflicts[[:space:]]*//p' "$PLAN")"
|
||||
if [ "$MANAGED" = 1 ] && [ "${CONFLICTS:-0}" -gt 0 ] && [ "$MODE" != disable ]; then
|
||||
echo "Enabled firewall port forwards conflict with HAProxy listeners:" >&2
|
||||
awk -F '\t' '$1 == "conflict" { printf "- %s (WAN %s -> %s:%s)\n", $3, $4, $5, $6 }' "$PLAN" >&2
|
||||
echo "Choose automatic conflict resolution in Settings or disable the redirects manually." >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
[ "$CHECK_ONLY" != 1 ] || { cat "$PLAN"; exit 0; }
|
||||
|
||||
PORTS=""
|
||||
[ "$MANAGED" != 1 ] || PORTS="$(sed -n 's/^port[[:space:]]*//p' "$PLAN")"
|
||||
CHANGED=0
|
||||
|
||||
for redirect in $(uci -q show firewall | sed -n 's/^firewall\.\(@redirect\[[0-9][0-9]*\]\)=redirect/\1/p'); do
|
||||
name="$(uci -q get "firewall.$redirect.name" 2>/dev/null || true)"
|
||||
case "$name" in
|
||||
"$FIREWALL_DISABLED_PREFIX"*) managed_disabled=1 ;;
|
||||
*) managed_disabled=0 ;;
|
||||
esac
|
||||
[ "$managed_disabled" = 1 ] || continue
|
||||
spec="$(uci -q get "firewall.$redirect.src_dport" 2>/dev/null || true)"
|
||||
still_needed=0
|
||||
for port in $PORTS; do
|
||||
port_spec_contains "$spec" "$port" || continue
|
||||
still_needed=1
|
||||
break
|
||||
done
|
||||
[ "$still_needed" = 1 ] && continue
|
||||
uci set "firewall.$redirect.enabled=1"
|
||||
CHANGED=1
|
||||
name="${name#"$FIREWALL_DISABLED_PREFIX"}"
|
||||
if [ -n "$name" ]; then
|
||||
uci set "firewall.$redirect.name=$name"
|
||||
else
|
||||
uci -q delete "firewall.$redirect.name" || true
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$MANAGED" = 1 ] && [ "$MODE" = disable ]; then
|
||||
for redirect in $(awk -F '\t' '$1 == "conflict" { print $2 }' "$PLAN"); do
|
||||
[ -n "$redirect" ] || continue
|
||||
name="$(uci -q get "firewall.$redirect.name" 2>/dev/null || true)"
|
||||
case "$name" in
|
||||
"$FIREWALL_DISABLED_PREFIX"*) ;;
|
||||
*) uci set "firewall.$redirect.name=$FIREWALL_DISABLED_PREFIX$name" ;;
|
||||
esac
|
||||
uci set "firewall.$redirect.enabled=0"
|
||||
CHANGED=1
|
||||
done
|
||||
fi
|
||||
|
||||
for index in $(uci -q show firewall | sed -n 's/^firewall\.@rule\[\([0-9][0-9]*\)\]=rule/\1/p' | sort -rn); do
|
||||
rule="@rule[$index]"
|
||||
name="$(uci -q get "firewall.$rule.name" 2>/dev/null || true)"
|
||||
if [ "$name" = "$FIREWALL_RULE_NAME" ]; then
|
||||
uci -q delete "firewall.$rule" || true
|
||||
CHANGED=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$PORTS" ]; then
|
||||
rule="$(uci add firewall rule)"
|
||||
uci set "firewall.$rule.name=$FIREWALL_RULE_NAME"
|
||||
uci set "firewall.$rule.src=$ZONE"
|
||||
uci set "firewall.$rule.proto=tcp"
|
||||
uci set "firewall.$rule.target=ACCEPT"
|
||||
uci set "firewall.$rule.family=ipv4"
|
||||
for port in $PORTS; do
|
||||
uci add_list "firewall.$rule.dest_port=$port"
|
||||
done
|
||||
CHANGED=1
|
||||
fi
|
||||
|
||||
if [ "$CHANGED" = 1 ]; then
|
||||
uci commit firewall
|
||||
/etc/init.d/firewall reload
|
||||
fi
|
||||
|
||||
if [ "$MANAGED" = 1 ]; then
|
||||
echo "Firewall rules synchronized"
|
||||
else
|
||||
echo "Firewall automation disabled; managed rules removed"
|
||||
fi
|
||||
@@ -0,0 +1,234 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
umask 077
|
||||
|
||||
OUT="${1:-$TMP_CFG}"
|
||||
is_safe_temp_path "$OUT" || { echo "Refusing unsafe generated config path: $OUT" >&2; exit 1; }
|
||||
[ ! -d "$OUT" ] || { echo "Refusing generated config directory: $OUT" >&2; exit 1; }
|
||||
WORK_OUT="$OUT.new.$$"
|
||||
ENABLED="$(uci_get main enabled 0)"
|
||||
HTTP_PORT="$(uci_get main http_port 80)"
|
||||
HTTPS_PORT="$(uci_get main https_port 443)"
|
||||
WAN_IP="$(wan_ip || true)"
|
||||
|
||||
if [ "$ENABLED" != 1 ]; then
|
||||
echo "Managed HAProxy configuration is disabled." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
is_valid_port "$HTTP_PORT" || { echo "Invalid public HTTP port: $HTTP_PORT" >&2; exit 1; }
|
||||
is_valid_port "$HTTPS_PORT" || { echo "Invalid public HTTPS port: $HTTPS_PORT" >&2; exit 1; }
|
||||
|
||||
if [ -z "$WAN_IP" ]; then
|
||||
echo "Cannot detect WAN IPv4 address. Set the WAN bind address manually." >&2
|
||||
exit 1
|
||||
fi
|
||||
is_valid_ip_or_host "$WAN_IP" || { echo "Invalid WAN bind address: $WAN_IP" >&2; exit 1; }
|
||||
|
||||
TMP_HTTP_RULES=/tmp/haproxy-manager-http-rules.$$
|
||||
TMP_HTTP_BACKENDS=/tmp/haproxy-manager-http-backends.$$
|
||||
TMP_TLS_RULES=/tmp/haproxy-manager-tls-rules.$$
|
||||
TMP_TLS_BACKENDS=/tmp/haproxy-manager-tls-backends.$$
|
||||
TMP_TCP_FRONTENDS=/tmp/haproxy-manager-tcp-frontends.$$
|
||||
TMP_PORTS=/tmp/haproxy-manager-listen-ports.$$
|
||||
: > "$TMP_HTTP_RULES"
|
||||
: > "$TMP_HTTP_BACKENDS"
|
||||
: > "$TMP_TLS_RULES"
|
||||
: > "$TMP_TLS_BACKENDS"
|
||||
: > "$TMP_TCP_FRONTENDS"
|
||||
: > "$TMP_PORTS"
|
||||
trap 'rm -f "$WORK_OUT" "$TMP_HTTP_RULES" "$TMP_HTTP_BACKENDS" "$TMP_TLS_RULES" "$TMP_TLS_BACKENDS" "$TMP_TCP_FRONTENDS" "$TMP_PORTS"' EXIT
|
||||
|
||||
HAS_HTTP=0
|
||||
HAS_HTTPS=0
|
||||
HAS_TCP=0
|
||||
|
||||
claim_port() {
|
||||
local port="$1"
|
||||
local owner="$2"
|
||||
local existing
|
||||
|
||||
if grep -q "^${port}[[:space:]]" "$TMP_PORTS"; then
|
||||
existing="$(sed -n "s/^${port}[[:space:]]*//p" "$TMP_PORTS" | head -1)"
|
||||
echo "Public TCP port $port conflicts with ${existing:-another service}." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s\t%s\n' "$port" "$owner" >> "$TMP_PORTS"
|
||||
}
|
||||
|
||||
add_http_route() {
|
||||
local name="$1" host="$2" backend_host="$3" backend_port="$4"
|
||||
local address
|
||||
address="$(backend_address "$backend_host" "$backend_port")"
|
||||
|
||||
if [ "$HAS_HTTP" != 1 ]; then
|
||||
claim_port "$HTTP_PORT" "the shared Web HTTP listener"
|
||||
HAS_HTTP=1
|
||||
fi
|
||||
|
||||
{
|
||||
printf ' acl host_%s hdr(host),host_only -i %s\n' "$name" "$host"
|
||||
printf ' use_backend be_http_%s if host_%s\n' "$name" "$name"
|
||||
} >> "$TMP_HTTP_RULES"
|
||||
{
|
||||
printf 'backend be_http_%s\n' "$name"
|
||||
printf ' mode http\n'
|
||||
printf ' option httpchk GET /\n'
|
||||
printf ' server srv1 %s check\n\n' "$address"
|
||||
} >> "$TMP_HTTP_BACKENDS"
|
||||
}
|
||||
|
||||
add_https_route() {
|
||||
local name="$1" host="$2" backend_host="$3" backend_port="$4"
|
||||
local address
|
||||
address="$(backend_address "$backend_host" "$backend_port")"
|
||||
|
||||
if [ "$HAS_HTTPS" != 1 ]; then
|
||||
claim_port "$HTTPS_PORT" "the shared Web HTTPS listener"
|
||||
HAS_HTTPS=1
|
||||
fi
|
||||
|
||||
printf ' use_backend be_tls_%s if { req.ssl_sni -i %s }\n' "$name" "$host" >> "$TMP_TLS_RULES"
|
||||
{
|
||||
printf 'backend be_tls_%s\n' "$name"
|
||||
printf ' mode tcp\n'
|
||||
printf ' server srv1 %s check\n\n' "$address"
|
||||
} >> "$TMP_TLS_BACKENDS"
|
||||
}
|
||||
|
||||
add_tcp_route() {
|
||||
local name="$1" label="$2" backend_host="$3" public_port="$4" backend_port="$5"
|
||||
local address bind_address
|
||||
address="$(backend_address "$backend_host" "$backend_port")"
|
||||
bind_address="$(backend_address "$WAN_IP" "$public_port")"
|
||||
|
||||
claim_port "$public_port" "service $label"
|
||||
HAS_TCP=1
|
||||
{
|
||||
printf 'frontend fe_tcp_%s\n' "$name"
|
||||
printf ' bind %s\n' "$bind_address"
|
||||
printf ' mode tcp\n'
|
||||
printf ' default_backend be_tcp_%s\n\n' "$name"
|
||||
printf 'backend be_tcp_%s\n' "$name"
|
||||
printf ' mode tcp\n'
|
||||
printf ' server srv1 %s check\n\n' "$address"
|
||||
} >> "$TMP_TCP_FRONTENDS"
|
||||
}
|
||||
|
||||
for section in $(route_sections); do
|
||||
enabled="$(uci_get "$section" enabled 1)"
|
||||
[ "$enabled" = 1 ] || continue
|
||||
|
||||
kind="$(route_kind "$section")"
|
||||
label="$(uci_get "$section" name)"
|
||||
host="$(uci_get "$section" host)"
|
||||
backend_host="$(uci_get "$section" backend_host)"
|
||||
[ -n "$label" ] || label="$host"
|
||||
[ -n "$label" ] || label="$kind"
|
||||
name="$(route_name "${label}_${section}")"
|
||||
|
||||
is_valid_ip_or_host "$backend_host" || { echo "Invalid backend host for $label: $backend_host" >&2; exit 1; }
|
||||
|
||||
case "$kind" in
|
||||
web)
|
||||
is_valid_host "$host" || { echo "Invalid or missing domain for Web service $label: $host" >&2; exit 1; }
|
||||
http_enabled="$(uci_get "$section" web_http 1)"
|
||||
https_enabled="$(uci_get "$section" web_https 1)"
|
||||
[ "$http_enabled" = 1 ] || [ "$https_enabled" = 1 ] || { echo "Web service $label has no enabled protocol." >&2; exit 1; }
|
||||
|
||||
if [ "$http_enabled" = 1 ]; then
|
||||
backend_port="$(uci_get "$section" backend_http_port 80)"
|
||||
is_valid_port "$backend_port" || { echo "Invalid HTTP backend port for $label: $backend_port" >&2; exit 1; }
|
||||
add_http_route "$name" "$host" "$backend_host" "$backend_port"
|
||||
fi
|
||||
|
||||
if [ "$https_enabled" = 1 ]; then
|
||||
backend_port="$(uci_get "$section" backend_https_port 443)"
|
||||
is_valid_port "$backend_port" || { echo "Invalid HTTPS backend port for $label: $backend_port" >&2; exit 1; }
|
||||
add_https_route "$name" "$host" "$backend_host" "$backend_port"
|
||||
fi
|
||||
;;
|
||||
http|https|both)
|
||||
is_valid_host "$host" || { echo "Invalid or missing domain for legacy service $label: $host" >&2; exit 1; }
|
||||
backend_port="$(uci_get "$section" backend_port)"
|
||||
is_valid_port "$backend_port" || { echo "Invalid backend port for $label: $backend_port" >&2; exit 1; }
|
||||
case "$kind" in http|both) add_http_route "$name" "$host" "$backend_host" "$backend_port" ;; esac
|
||||
case "$kind" in https|both) add_https_route "$name" "$host" "$backend_host" "$backend_port" ;; esac
|
||||
;;
|
||||
ssh|rdp|custom|tcp)
|
||||
map_index=0
|
||||
maps="$(route_port_maps "$section" "$kind")"
|
||||
[ -n "$maps" ] || { echo "No TCP port mappings configured for $label." >&2; exit 1; }
|
||||
for map in $maps; do
|
||||
is_valid_port_map "$map" || { echo "Invalid TCP port mapping for $label: $map" >&2; exit 1; }
|
||||
public_port="${map%%:*}"
|
||||
backend_port="${map#*:}"
|
||||
map_index=$((map_index + 1))
|
||||
add_tcp_route "${name}_${map_index}" "$label" "$backend_host" "$public_port" "$backend_port"
|
||||
done
|
||||
;;
|
||||
*)
|
||||
echo "Invalid service type for $label: $kind" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$HAS_HTTP" != 1 ] && [ "$HAS_HTTPS" != 1 ] && [ "$HAS_TCP" != 1 ]; then
|
||||
echo "No enabled services define a HAProxy listener." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
{
|
||||
printf '# Generated by luci-app-haproxy-manager. Manual changes may be overwritten.\n'
|
||||
printf 'global\n'
|
||||
printf ' log /dev/log local0\n'
|
||||
printf ' maxconn 2048\n'
|
||||
printf ' daemon\n\n'
|
||||
printf 'defaults\n'
|
||||
printf ' log global\n'
|
||||
printf ' timeout connect 5s\n'
|
||||
printf ' timeout client 60s\n'
|
||||
printf ' timeout server 60s\n'
|
||||
printf ' option dontlognull\n\n'
|
||||
|
||||
if [ "$HAS_HTTP" = 1 ]; then
|
||||
http_bind="$(backend_address "$WAN_IP" "$HTTP_PORT")"
|
||||
printf 'frontend fe_web_http\n'
|
||||
printf ' bind %s\n' "$http_bind"
|
||||
printf ' mode http\n'
|
||||
printf ' option httplog\n'
|
||||
cat "$TMP_HTTP_RULES"
|
||||
printf ' default_backend be_not_found\n\n'
|
||||
cat "$TMP_HTTP_BACKENDS"
|
||||
fi
|
||||
|
||||
if [ "$HAS_HTTPS" = 1 ]; then
|
||||
https_bind="$(backend_address "$WAN_IP" "$HTTPS_PORT")"
|
||||
printf 'frontend fe_web_https\n'
|
||||
printf ' bind %s\n' "$https_bind"
|
||||
printf ' mode tcp\n'
|
||||
printf ' tcp-request inspect-delay 5s\n'
|
||||
printf ' tcp-request content accept if { req.ssl_hello_type 1 }\n'
|
||||
cat "$TMP_TLS_RULES"
|
||||
printf ' default_backend be_tls_not_found\n\n'
|
||||
cat "$TMP_TLS_BACKENDS"
|
||||
fi
|
||||
|
||||
cat "$TMP_TCP_FRONTENDS"
|
||||
|
||||
printf 'backend be_not_found\n'
|
||||
printf ' mode http\n'
|
||||
printf ' http-request return status 404 content-type text/plain string "No HAProxy route matched this host.\\n"\n\n'
|
||||
printf 'backend be_tls_not_found\n'
|
||||
printf ' mode tcp\n'
|
||||
printf ' tcp-request content reject\n'
|
||||
} > "$WORK_OUT"
|
||||
|
||||
# Replace the path itself so a stale symlink cannot redirect this root write.
|
||||
rm -f "$OUT"
|
||||
mv "$WORK_OUT" "$OUT"
|
||||
|
||||
printf '%s\n' "$OUT"
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
|
||||
ID="${1:-}"
|
||||
is_valid_backup_id "$ID" || { echo "Invalid incident identifier" >&2; exit 1; }
|
||||
BASE="$(incident_dir)"
|
||||
[ ! -d "$BASE" ] || BASE="$(resolve_storage_dir "$BASE")" || exit 1
|
||||
FILE="$BASE/$ID/diagnostics.log"
|
||||
[ -f "$FILE" ] || { echo "Incident report not found: $ID" >&2; exit 1; }
|
||||
|
||||
# Keep the RPC response bounded even if a platform command produced excessive output.
|
||||
tail -c 65536 "$FILE"
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/bin/sh
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
|
||||
BASE="$(incident_dir)"
|
||||
[ ! -d "$BASE" ] || BASE="$(resolve_storage_dir "$BASE")" || exit 1
|
||||
LAST=""
|
||||
[ ! -r "$BASE/LAST" ] || LAST="$(basename "$(sed -n '1p' "$BASE/LAST")")"
|
||||
|
||||
safe_field() {
|
||||
printf '%s' "$1" | tr '\t\r\n' ' '
|
||||
}
|
||||
|
||||
count=0
|
||||
for dir in $(find "$BASE" -maxdepth 1 -type d -name '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]' 2>/dev/null | sort -r); do
|
||||
[ -d "$dir" ] || continue
|
||||
id="$(basename "$dir")"
|
||||
latest=0
|
||||
[ "$id" != "$LAST" ] || latest=1
|
||||
result="$(sed -n '1p' "$dir/result" 2>/dev/null || echo detected)"
|
||||
action="$(sed -n 's/^action=//p' "$dir/meta" 2>/dev/null | head -1)"
|
||||
interface="$(sed -n 's/^interface=//p' "$dir/meta" 2>/dev/null | head -1)"
|
||||
reason="$(sed -n 's/^reason=//p' "$dir/meta" 2>/dev/null | head -1)"
|
||||
printf 'incident\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
||||
"$id" "$latest" "$(safe_field "$result")" "$(safe_field "$action")" \
|
||||
"$(safe_field "$interface")" "$(safe_field "$reason")"
|
||||
count=$((count + 1))
|
||||
[ "$count" -lt "$INCIDENT_LIMIT" ] || break
|
||||
done
|
||||
@@ -0,0 +1,150 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
|
||||
migrate_firewall_markers() {
|
||||
local section marker name changed
|
||||
changed=0
|
||||
|
||||
for section in $(uci -q show firewall | sed -n 's/^firewall\.\(@redirect\[[0-9][0-9]*\]\)=redirect/\1/p'); do
|
||||
marker="$(uci -q get "firewall.$section.hm_disabled_by_haproxy_manager" 2>/dev/null || true)"
|
||||
[ -n "$marker" ] || continue
|
||||
if [ "$marker" = 1 ]; then
|
||||
name="$(uci -q get "firewall.$section.name" 2>/dev/null || true)"
|
||||
case "$name" in
|
||||
"$FIREWALL_DISABLED_PREFIX"*) ;;
|
||||
*) uci set "firewall.$section.name=$FIREWALL_DISABLED_PREFIX$name" ;;
|
||||
esac
|
||||
fi
|
||||
uci -q delete "firewall.$section.hm_disabled_by_haproxy_manager" || true
|
||||
changed=1
|
||||
done
|
||||
|
||||
for section in $(uci -q show firewall | sed -n 's/^firewall\.\(@rule\[[0-9][0-9]*\]\)=rule/\1/p'); do
|
||||
marker="$(uci -q get "firewall.$section.hm_managed" 2>/dev/null || true)"
|
||||
[ -n "$marker" ] || continue
|
||||
[ "$marker" != 1 ] || uci set "firewall.$section.name=$FIREWALL_RULE_NAME"
|
||||
uci -q delete "firewall.$section.hm_managed" || true
|
||||
changed=1
|
||||
done
|
||||
|
||||
[ "$changed" != 1 ] || uci commit firewall
|
||||
}
|
||||
|
||||
migrate_firewall_markers
|
||||
|
||||
SCHEMA_VERSION="$(uci_get main schema_version 1)"
|
||||
MIGRATED=0
|
||||
if [ "$SCHEMA_VERSION" -lt 2 ] 2>/dev/null; then
|
||||
|
||||
SECTIONS="$(route_sections)"
|
||||
[ -z "$SECTIONS" ] || /usr/libexec/haproxy-manager/backup >/dev/null
|
||||
|
||||
CONSUMED=/tmp/haproxy-manager-migrate-consumed.$$
|
||||
: > "$CONSUMED"
|
||||
trap 'rm -f "$CONSUMED"' EXIT
|
||||
|
||||
is_consumed() {
|
||||
grep -qx "$1" "$CONSUMED"
|
||||
}
|
||||
|
||||
to_web() {
|
||||
local section="$1" http_enabled="$2" https_enabled="$3" http_backend="$4" https_backend="$5"
|
||||
|
||||
uci set "$CONFIG.$section.kind=web"
|
||||
uci set "$CONFIG.$section.web_http=$http_enabled"
|
||||
uci set "$CONFIG.$section.web_https=$https_enabled"
|
||||
[ "$http_enabled" != 1 ] || uci set "$CONFIG.$section.backend_http_port=$http_backend"
|
||||
[ "$https_enabled" != 1 ] || uci set "$CONFIG.$section.backend_https_port=$https_backend"
|
||||
uci -q delete "$CONFIG.$section.protocol" || true
|
||||
uci -q delete "$CONFIG.$section.listen_port" || true
|
||||
uci -q delete "$CONFIG.$section.backend_port" || true
|
||||
}
|
||||
|
||||
clean_web_name() {
|
||||
local section="$1" host="$2" name clean
|
||||
name="$(uci_get "$section" name)"
|
||||
clean="$name"
|
||||
|
||||
case "$name" in
|
||||
"$host 80"|"$host:80"|"$host HTTP"|"$host http") clean="$host" ;;
|
||||
*" HTTP") clean="${name% HTTP}" ;;
|
||||
*" http") clean="${name% http}" ;;
|
||||
esac
|
||||
|
||||
[ -z "$clean" ] || [ "$clean" = "$name" ] || uci set "$CONFIG.$section.name=$clean"
|
||||
}
|
||||
|
||||
for section in $SECTIONS; do
|
||||
protocol="$(uci_get "$section" protocol)"
|
||||
case "$protocol" in
|
||||
http)
|
||||
host="$(uci_get "$section" host)"
|
||||
backend_host="$(uci_get "$section" backend_host)"
|
||||
enabled="$(uci_get "$section" enabled 1)"
|
||||
http_backend="$(uci_get "$section" backend_port 80)"
|
||||
match=""
|
||||
|
||||
for candidate in $SECTIONS; do
|
||||
is_consumed "$candidate" && continue
|
||||
[ "$(uci_get "$candidate" protocol)" = https ] || continue
|
||||
[ "$(uci_get "$candidate" host)" = "$host" ] || continue
|
||||
[ "$(uci_get "$candidate" backend_host)" = "$backend_host" ] || continue
|
||||
[ "$(uci_get "$candidate" enabled 1)" = "$enabled" ] || continue
|
||||
match="$candidate"
|
||||
break
|
||||
done
|
||||
|
||||
if [ -n "$match" ]; then
|
||||
clean_web_name "$section" "$host"
|
||||
to_web "$section" 1 1 "$http_backend" "$(uci_get "$match" backend_port 443)"
|
||||
printf '%s\n' "$match" >> "$CONSUMED"
|
||||
else
|
||||
clean_web_name "$section" "$host"
|
||||
to_web "$section" 1 0 "$http_backend" 443
|
||||
fi
|
||||
;;
|
||||
both)
|
||||
backend="$(uci_get "$section" backend_port 80)"
|
||||
clean_web_name "$section" "$(uci_get "$section" host)"
|
||||
to_web "$section" 1 1 "$backend" "$backend"
|
||||
;;
|
||||
tcp)
|
||||
public_port="$(uci_get "$section" listen_port "$(uci_get "$section" backend_port)")"
|
||||
backend_port="$(uci_get "$section" backend_port)"
|
||||
uci set "$CONFIG.$section.kind=custom"
|
||||
uci -q delete "$CONFIG.$section.port_map" || true
|
||||
uci add_list "$CONFIG.$section.port_map=$public_port:$backend_port"
|
||||
uci -q delete "$CONFIG.$section.protocol" || true
|
||||
uci -q delete "$CONFIG.$section.listen_port" || true
|
||||
uci -q delete "$CONFIG.$section.backend_port" || true
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
for section in $SECTIONS; do
|
||||
is_consumed "$section" && continue
|
||||
[ "$(uci_get "$section" protocol)" = https ] || continue
|
||||
to_web "$section" 0 1 80 "$(uci_get "$section" backend_port 443)"
|
||||
done
|
||||
|
||||
sed -n 's/^@route\[\([0-9][0-9]*\)\]$/\1/p' "$CONSUMED" | sort -rn | while read -r index; do
|
||||
uci -q delete "$CONFIG.@route[$index]" || true
|
||||
done
|
||||
MIGRATED=1
|
||||
|
||||
fi
|
||||
|
||||
if [ "$SCHEMA_VERSION" -lt 3 ] 2>/dev/null; then
|
||||
if [ "$(uci_get main enabled 0)" = 1 ] &&
|
||||
grep -q '^# Generated by luci-app-haproxy-manager\.' "$HAPROXY_CFG" 2>/dev/null; then
|
||||
uci set "$CONFIG.main.active_mode=generated"
|
||||
else
|
||||
uci set "$CONFIG.main.active_mode=none"
|
||||
fi
|
||||
uci set "$CONFIG.main.schema_version=3"
|
||||
uci commit "$CONFIG"
|
||||
MIGRATED=1
|
||||
fi
|
||||
|
||||
[ "$MIGRATED" != 1 ] || echo "Configuration schema upgraded to version 3"
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
|
||||
[ "$(uci_get main webhook_enabled 0)" = 1 ] || exit 0
|
||||
URL="$(uci_get main webhook_url)"
|
||||
case "$URL" in
|
||||
http://*) AUTHORITY="${URL#http://}" ;;
|
||||
https://*) AUTHORITY="${URL#https://}" ;;
|
||||
*) echo "Webhook URL must use HTTP or HTTPS" >&2; exit 1 ;;
|
||||
esac
|
||||
[ -n "${AUTHORITY%%/*}" ] || { echo "Webhook URL must include a host" >&2; exit 1; }
|
||||
case "$URL" in
|
||||
*[![:graph:]]*) echo "Webhook URL contains invalid characters" >&2; exit 1 ;;
|
||||
esac
|
||||
if command -v uclient-fetch >/dev/null 2>&1; then
|
||||
FETCHER=uclient-fetch
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
FETCHER=wget
|
||||
else
|
||||
echo "A compatible wget implementation is required for webhook delivery" >&2
|
||||
exit 1
|
||||
fi
|
||||
[ -r /usr/share/libubox/jshn.sh ] || { echo "jshn.sh is not available" >&2; exit 1; }
|
||||
|
||||
INCIDENT_ID="$(printf '%s' "${1:-unknown}" | tr -cd '0-9-')"
|
||||
RESULT="$(printf '%s' "${2:-unknown}" | tr -cd 'A-Za-z0-9_.:-')"
|
||||
ACTION="$(printf '%s' "${3:-unknown}" | tr -cd 'A-Za-z0-9_.:-')"
|
||||
INTERFACE="$(printf '%s' "${4:-unknown}" | tr -cd 'A-Za-z0-9_.:-')"
|
||||
REASON="$(printf '%s' "${5:-unknown}" | tr -cd 'A-Za-z0-9_.:-')"
|
||||
|
||||
. /usr/share/libubox/jshn.sh
|
||||
json_init
|
||||
json_add_string event haproxy-manager-recovery
|
||||
json_add_string incident "$INCIDENT_ID"
|
||||
json_add_string result "$RESULT"
|
||||
json_add_string action "$ACTION"
|
||||
json_add_string interface "$INTERFACE"
|
||||
json_add_string reason "$REASON"
|
||||
PAYLOAD="$(json_dump)"
|
||||
|
||||
"$FETCHER" -q -T 10 -O /dev/null \
|
||||
--header='Content-Type: application/json' \
|
||||
--post-data="$PAYLOAD" "$URL"
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
|
||||
required_ports
|
||||
@@ -0,0 +1,259 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
umask 077
|
||||
|
||||
ACTION="$(printf '%s' "${1:-network}" | tr -cd 'A-Za-z0-9_.:-')"
|
||||
INTERFACE="$(printf '%s' "${2:-unknown}" | tr -cd 'A-Za-z0-9_.:-')"
|
||||
RECOVER_LOCK=/var/lock/haproxy-manager.recover
|
||||
APPLY_LOCK=/var/lock/haproxy-manager.apply
|
||||
APPLY_LOCK_HELD=0
|
||||
TMP_CFG="/tmp/haproxy-manager-recover.$$"
|
||||
INCIDENT=""
|
||||
BASE=""
|
||||
DIAGNOSTICS=/dev/null
|
||||
|
||||
[ -n "$ACTION" ] || ACTION=network
|
||||
[ -n "$INTERFACE" ] || INTERFACE=unknown
|
||||
[ "$(uci_get main auto_recover 1)" = 1 ] || exit 0
|
||||
[ "$(uci_get main enabled 0)" = 1 ] || exit 0
|
||||
RECONCILE_GENERATED=0
|
||||
case "$ACTION" in
|
||||
ifup|ifupdate)
|
||||
[ "$INTERFACE" = "$(wan_interface)" ] || exit 0
|
||||
RECONCILE_GENERATED=1
|
||||
;;
|
||||
esac
|
||||
[ -x /etc/init.d/haproxy ] || exit 0
|
||||
/etc/init.d/haproxy enabled >/dev/null 2>&1 || exit 0
|
||||
|
||||
MODE="$(active_mode)"
|
||||
case "$MODE" in
|
||||
generated|raw) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
|
||||
cleanup() {
|
||||
rm -f "$TMP_CFG"
|
||||
[ "$APPLY_LOCK_HELD" != 1 ] || operation_lock_release "$APPLY_LOCK"
|
||||
operation_lock_release "$RECOVER_LOCK"
|
||||
}
|
||||
|
||||
trim_incidents() {
|
||||
local count old
|
||||
count=0
|
||||
for old in $(find "$BASE" -maxdepth 1 -type d -name '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]' 2>/dev/null | sort -r); do
|
||||
count=$((count + 1))
|
||||
[ "$count" -le "$INCIDENT_LIMIT" ] && continue
|
||||
case "$old" in
|
||||
"$BASE"/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9])
|
||||
rm -f "$old/meta" "$old/result" "$old/diagnostics.log"
|
||||
rmdir "$old" 2>/dev/null || true
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
create_incident() {
|
||||
local reason="$1" id attempt target_ip requested_base
|
||||
requested_base="$(incident_dir)" || return 1
|
||||
umask 077
|
||||
mkdir -p "$requested_base" || {
|
||||
logger -t haproxy-manager "Cannot create incident report directory: $requested_base"
|
||||
return 1
|
||||
}
|
||||
BASE="$(resolve_storage_dir "$requested_base")" || {
|
||||
logger -t haproxy-manager "Incident directory resolves outside /root or /mnt"
|
||||
return 1
|
||||
}
|
||||
id="$(date +%Y%m%d-%H%M%S)"
|
||||
INCIDENT="$BASE/$id"
|
||||
attempt=1
|
||||
while ! mkdir "$INCIDENT" 2>/dev/null; do
|
||||
[ "$attempt" -lt 5 ] || {
|
||||
logger -t haproxy-manager "Cannot create incident report in $BASE"
|
||||
return 1
|
||||
}
|
||||
sleep 1
|
||||
attempt=$((attempt + 1))
|
||||
id="$(date +%Y%m%d-%H%M%S)"
|
||||
INCIDENT="$BASE/$id"
|
||||
done
|
||||
chmod 700 "$BASE" "$INCIDENT" 2>/dev/null || true
|
||||
target_ip="$(wan_ip 2>/dev/null || echo unknown)"
|
||||
cat > "$INCIDENT/meta" <<EOF || return 1
|
||||
created=$id
|
||||
action=$ACTION
|
||||
interface=$INTERFACE
|
||||
reason=$reason
|
||||
target_ip=$target_ip
|
||||
EOF
|
||||
printf 'detected\n' > "$INCIDENT/result" || return 1
|
||||
printf '%s\n' "$id" > "$BASE/LAST" || return 1
|
||||
trim_incidents
|
||||
DIAGNOSTICS="$INCIDENT/diagnostics.log"
|
||||
if [ "$reason" = wan-address-change ]; then
|
||||
{
|
||||
printf 'HAProxy Manager WAN listener reconciliation\n'
|
||||
printf 'Incident: %s\nAction: %s\nInterface: %s\nReason: %s\nTarget WAN address: %s\n' \
|
||||
"$id" "$ACTION" "$INTERFACE" "$reason" "$target_ip"
|
||||
} > "$DIAGNOSTICS" 2>&1
|
||||
return 0
|
||||
fi
|
||||
{
|
||||
printf 'HAProxy Manager automatic recovery\n'
|
||||
printf 'Incident: %s\nAction: %s\nInterface: %s\nReason: %s\nTarget WAN address: %s\n\n' \
|
||||
"$id" "$ACTION" "$INTERFACE" "$reason" "$target_ip"
|
||||
printf '== Time and uptime ==\n'
|
||||
date || true
|
||||
uptime || true
|
||||
printf '\n== Memory ==\n'
|
||||
free || true
|
||||
printf '\n== IPv4 addresses ==\n'
|
||||
ip -4 addr show || true
|
||||
printf '\n== HAProxy procd state ==\n'
|
||||
ubus call service list '{"name":"haproxy","verbose":true}' || true
|
||||
printf '\n== Network interfaces ==\n'
|
||||
ubus call network.interface dump || true
|
||||
printf '\n== Recent HAProxy, procd, and OOM messages ==\n'
|
||||
logread | grep -iE 'haproxy|procd.*instance|out of memory|oom|killed process' | tail -n 160 || true
|
||||
printf '\n== Kernel OOM messages ==\n'
|
||||
dmesg | grep -Ei 'oom|out of memory|killed process|haproxy' | tail -n 80 || true
|
||||
} > "$DIAGNOSTICS" 2>&1
|
||||
}
|
||||
|
||||
set_result() {
|
||||
local result="$1" id reason
|
||||
if [ -z "$INCIDENT" ] || [ ! -d "$INCIDENT" ]; then
|
||||
logger -t haproxy-manager "Recovery result without incident report: $result"
|
||||
return 0
|
||||
fi
|
||||
if ! printf '%s\n' "$result" > "$INCIDENT/result"; then
|
||||
logger -t haproxy-manager "Could not update incident report with result: $result"
|
||||
INCIDENT=""
|
||||
DIAGNOSTICS=/dev/null
|
||||
return 0
|
||||
fi
|
||||
id="${INCIDENT##*/}"
|
||||
reason="$(sed -n 's/^reason=//p' "$INCIDENT/meta" 2>/dev/null | head -1)"
|
||||
(
|
||||
if ! /usr/libexec/haproxy-manager/notify "$id" "$result" "$ACTION" "$INTERFACE" "$reason"; then
|
||||
logger -t haproxy-manager "Webhook notification failed for incident $id"
|
||||
fi
|
||||
) >/dev/null 2>&1 &
|
||||
}
|
||||
|
||||
operation_lock_acquire "$RECOVER_LOCK" || exit 0
|
||||
trap cleanup EXIT
|
||||
|
||||
# Let netifd finish installing addresses and routes before comparing listeners.
|
||||
sleep 2
|
||||
operation_lock_acquire "$APPLY_LOCK" || exit 0
|
||||
APPLY_LOCK_HELD=1
|
||||
|
||||
CONFIG_CHANGED=0
|
||||
GENERATION_FAILED=0
|
||||
VALIDATION_FAILED=0
|
||||
if [ "$RECONCILE_GENERATED" = 1 ] && [ "$MODE" = generated ] &&
|
||||
[ "$(uci_get main wan_bind_ip auto)" = auto ]; then
|
||||
if /usr/libexec/haproxy-manager/generate "$TMP_CFG" >/dev/null 2>&1; then
|
||||
if /usr/libexec/haproxy-manager/validate "$TMP_CFG" >/dev/null 2>&1; then
|
||||
cmp -s "$TMP_CFG" "$HAPROXY_CFG" || CONFIG_CHANGED=1
|
||||
else
|
||||
VALIDATION_FAILED=1
|
||||
fi
|
||||
else
|
||||
GENERATION_FAILED=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$GENERATION_FAILED" != 1 ] && [ "$VALIDATION_FAILED" != 1 ] &&
|
||||
[ "$CONFIG_CHANGED" != 1 ] && /etc/init.d/haproxy status >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$GENERATION_FAILED" = 1 ]; then
|
||||
REASON=wan-address-unavailable
|
||||
elif [ "$VALIDATION_FAILED" = 1 ]; then
|
||||
REASON=invalid-generated-config
|
||||
elif [ "$CONFIG_CHANGED" = 1 ]; then
|
||||
REASON=wan-address-change
|
||||
else
|
||||
REASON=service-stopped
|
||||
fi
|
||||
if ! create_incident "$REASON"; then
|
||||
logger -t haproxy-manager "Incident report could not be stored; recovery will continue"
|
||||
INCIDENT=""
|
||||
DIAGNOSTICS=/dev/null
|
||||
fi
|
||||
logger -t haproxy-manager "HAProxy recovery started after $ACTION on $INTERFACE ($REASON)"
|
||||
|
||||
if [ "$GENERATION_FAILED" = 1 ]; then
|
||||
set_result address-unavailable
|
||||
logger -t haproxy-manager "Automatic recovery stopped: WAN address is unavailable"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$VALIDATION_FAILED" = 1 ]; then
|
||||
set_result invalid-generated-config
|
||||
logger -t haproxy-manager "Automatic recovery stopped: generated configuration is invalid"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$CONFIG_CHANGED" = 1 ]; then
|
||||
BACKUP_PATH=""
|
||||
if ! BACKUP_PATH="$(/usr/libexec/haproxy-manager/backup 2>> "$DIAGNOSTICS")"; then
|
||||
set_result backup-failed
|
||||
logger -t haproxy-manager "Automatic recovery stopped: snapshot creation failed"
|
||||
exit 1
|
||||
fi
|
||||
printf '\nRecovery point: %s\n' "${BACKUP_PATH##*/}" >> "$DIAGNOSTICS" || true
|
||||
if ! atomic_install_file "$TMP_CFG" "$HAPROXY_CFG"; then
|
||||
set_result install-failed
|
||||
logger -t haproxy-manager "Automatic recovery stopped: HAProxy configuration could not be installed"
|
||||
exit 1
|
||||
fi
|
||||
if /etc/init.d/haproxy restart >> "$DIAGNOSTICS" 2>&1 &&
|
||||
/etc/init.d/haproxy status >/dev/null 2>&1; then
|
||||
set_result reconciled
|
||||
logger -t haproxy-manager "HAProxy listeners reconciled with the current WAN address"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if /usr/libexec/haproxy-manager/rollback --sync "${BACKUP_PATH##*/}" >> "$DIAGNOSTICS" 2>&1; then
|
||||
set_result rolled-back
|
||||
logger -t haproxy-manager "WAN listener reconciliation failed and was rolled back"
|
||||
else
|
||||
set_result rollback-failed
|
||||
logger -t haproxy-manager "WAN listener reconciliation and rollback both failed"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! /etc/init.d/haproxy check >> "$DIAGNOSTICS" 2>&1; then
|
||||
set_result invalid-config
|
||||
logger -t haproxy-manager "Automatic recovery stopped: HAProxy configuration is invalid"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
attempt=1
|
||||
while [ "$attempt" -le 5 ]; do
|
||||
printf '\n== Start attempt %s ==\n' "$attempt" >> "$DIAGNOSTICS" || true
|
||||
/etc/init.d/haproxy restart >> "$DIAGNOSTICS" 2>&1 || true
|
||||
sleep $((attempt + 1))
|
||||
if /etc/init.d/haproxy status >/dev/null 2>&1; then
|
||||
set_result recovered
|
||||
{
|
||||
printf '\n== Recovered listeners ==\n'
|
||||
ps w | grep '[h]aproxy' || true
|
||||
netstat -lntp 2>/dev/null | grep haproxy || true
|
||||
} >> "$DIAGNOSTICS" 2>&1
|
||||
logger -t haproxy-manager "HAProxy recovered after $attempt attempt(s)"
|
||||
exit 0
|
||||
fi
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
set_result failed
|
||||
logger -t haproxy-manager "HAProxy automatic recovery failed after 5 attempts"
|
||||
exit 1
|
||||
@@ -0,0 +1,124 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
|
||||
SYNC=0
|
||||
LOCK_DIR=/var/lock/haproxy-manager.apply
|
||||
LOCK_HELD=0
|
||||
|
||||
if [ "${1:-}" = --sync ]; then
|
||||
[ "$#" -eq 2 ] || { echo "Usage: rollback [--sync] [last|YYYYMMDD-HHMMSS]" >&2; exit 1; }
|
||||
SYNC=1
|
||||
TARGET="$2"
|
||||
else
|
||||
[ "$#" -le 1 ] || { echo "Usage: rollback [--sync] [last|YYYYMMDD-HHMMSS]" >&2; exit 1; }
|
||||
TARGET="${1:-last}"
|
||||
fi
|
||||
|
||||
release_lock() {
|
||||
[ "$LOCK_HELD" != 1 ] || operation_lock_release "$LOCK_DIR"
|
||||
}
|
||||
|
||||
if [ "$SYNC" != 1 ]; then
|
||||
attempt=0
|
||||
while ! operation_lock_acquire "$LOCK_DIR"; do
|
||||
[ "$attempt" -lt 15 ] || { echo "Another HAProxy Manager operation is still running." >&2; exit 1; }
|
||||
attempt=$((attempt + 1))
|
||||
sleep 1
|
||||
done
|
||||
LOCK_HELD=1
|
||||
trap 'status=$?; release_lock; exit "$status"' EXIT
|
||||
fi
|
||||
|
||||
BASE="$(backup_dir)"
|
||||
[ ! -d "$BASE" ] || BASE="$(resolve_storage_dir "$BASE")" || {
|
||||
echo "Recovery point directory resolves outside /root or /mnt" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ "$TARGET" = last ]; then
|
||||
[ -f "$BASE/LAST" ] || { echo "No LAST backup marker found" >&2; exit 1; }
|
||||
TARGET="$(basename "$(cat "$BASE/LAST")")"
|
||||
fi
|
||||
|
||||
is_valid_backup_id "$TARGET" || { echo "Invalid backup identifier" >&2; exit 1; }
|
||||
TARGET_DIR="$BASE/$TARGET"
|
||||
[ -d "$TARGET_DIR" ] || { echo "Backup not found: $TARGET" >&2; exit 1; }
|
||||
|
||||
firewall_changed=0
|
||||
uhttpd_changed=0
|
||||
state_failed=0
|
||||
|
||||
if [ -f "$TARGET_DIR/firewall" ] && ! cmp -s "$TARGET_DIR/firewall" /etc/config/firewall; then
|
||||
atomic_install_file "$TARGET_DIR/firewall" /etc/config/firewall
|
||||
firewall_changed=1
|
||||
fi
|
||||
if [ -f "$TARGET_DIR/uhttpd" ] && ! cmp -s "$TARGET_DIR/uhttpd" /etc/config/uhttpd; then
|
||||
atomic_install_file "$TARGET_DIR/uhttpd" /etc/config/uhttpd
|
||||
uhttpd_changed=1
|
||||
fi
|
||||
[ -f "$TARGET_DIR/haproxy_manager" ] && atomic_install_file "$TARGET_DIR/haproxy_manager" /etc/config/haproxy_manager
|
||||
[ -f "$TARGET_DIR/haproxy.cfg" ] && atomic_install_file "$TARGET_DIR/haproxy.cfg" "$HAPROXY_CFG"
|
||||
|
||||
if [ "$firewall_changed" = 1 ] && ! /etc/init.d/firewall restart >/dev/null 2>&1; then
|
||||
logger -t haproxy-manager "Firewall restart failed while restoring $TARGET"
|
||||
state_failed=1
|
||||
fi
|
||||
|
||||
haproxy_enabled=1
|
||||
haproxy_running=1
|
||||
if [ -f "$TARGET_DIR/haproxy.state" ]; then
|
||||
haproxy_enabled="$(sed -n 's/^enabled=//p' "$TARGET_DIR/haproxy.state" | head -1)"
|
||||
haproxy_running="$(sed -n 's/^running=//p' "$TARGET_DIR/haproxy.state" | head -1)"
|
||||
fi
|
||||
|
||||
if [ "$haproxy_enabled" = 1 ]; then
|
||||
if ! /etc/init.d/haproxy enable >/dev/null 2>&1; then
|
||||
logger -t haproxy-manager "Could not enable HAProxy while restoring $TARGET"
|
||||
state_failed=1
|
||||
fi
|
||||
else
|
||||
if ! /etc/init.d/haproxy disable >/dev/null 2>&1; then
|
||||
logger -t haproxy-manager "Could not disable HAProxy while restoring $TARGET"
|
||||
state_failed=1
|
||||
fi
|
||||
fi
|
||||
|
||||
restore_services() {
|
||||
local failed="$state_failed"
|
||||
if [ "$uhttpd_changed" = 1 ] && ! /etc/init.d/uhttpd restart >/dev/null 2>&1; then
|
||||
logger -t haproxy-manager "LuCI restart failed while restoring $TARGET"
|
||||
failed=1
|
||||
fi
|
||||
if [ "$haproxy_running" = 1 ]; then
|
||||
if ! /etc/init.d/haproxy restart >/dev/null 2>&1 ||
|
||||
! /etc/init.d/haproxy status >/dev/null 2>&1; then
|
||||
logger -t haproxy-manager "HAProxy restart failed while restoring $TARGET"
|
||||
failed=1
|
||||
fi
|
||||
else
|
||||
if ! /etc/init.d/haproxy stop >/dev/null 2>&1 ||
|
||||
/etc/init.d/haproxy status >/dev/null 2>&1; then
|
||||
logger -t haproxy-manager "HAProxy stop failed while restoring $TARGET"
|
||||
failed=1
|
||||
fi
|
||||
fi
|
||||
return "$failed"
|
||||
}
|
||||
|
||||
if [ "$SYNC" = 1 ]; then
|
||||
restore_services || exit 1
|
||||
else
|
||||
LOCK_HELD=0
|
||||
trap - EXIT
|
||||
(
|
||||
sleep 2
|
||||
if ! restore_services; then
|
||||
logger -t haproxy-manager "Service restoration failed for recovery point $TARGET"
|
||||
fi
|
||||
operation_lock_release "$LOCK_DIR"
|
||||
) >/dev/null 2>&1 &
|
||||
operation_lock_set_pid "$LOCK_DIR" "$!"
|
||||
fi
|
||||
|
||||
echo "Restored backup $TARGET. Affected services will reload shortly."
|
||||
@@ -0,0 +1,56 @@
|
||||
#!/bin/sh
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
|
||||
enabled="$(uci_get main enabled 0)"
|
||||
wan="$(wan_ip 2>/dev/null || echo unknown)"
|
||||
version="not-available"
|
||||
service="stopped"
|
||||
backup_count=0
|
||||
auto_recovery="$(uci_get main auto_recover 1)"
|
||||
mode="$(active_mode)"
|
||||
last_incident=""
|
||||
last_incident_result="none"
|
||||
last_incident_interface=""
|
||||
|
||||
if command -v haproxy >/dev/null 2>&1; then
|
||||
version="$(haproxy -v 2>/dev/null | sed -n '1s/^HAProxy version \([^ ]*\).*/\1/p')"
|
||||
[ -n "$version" ] || version="unknown"
|
||||
fi
|
||||
|
||||
if [ -x /etc/init.d/haproxy ] && /etc/init.d/haproxy status >/dev/null 2>&1; then
|
||||
service="running"
|
||||
fi
|
||||
|
||||
backup_count="$(/usr/libexec/haproxy-manager/backups 2>/dev/null | wc -l | tr -d ' ')"
|
||||
|
||||
INCIDENT_BASE="$(incident_dir)"
|
||||
if [ -d "$INCIDENT_BASE" ]; then
|
||||
INCIDENT_BASE="$(resolve_storage_dir "$INCIDENT_BASE")" || INCIDENT_BASE=""
|
||||
fi
|
||||
if [ -f "$INCIDENT_BASE/LAST" ]; then
|
||||
last_incident="$(sed -n '1p' "$INCIDENT_BASE/LAST")"
|
||||
if is_valid_backup_id "$last_incident" && [ -d "$INCIDENT_BASE/$last_incident" ]; then
|
||||
last_incident_result="$(sed -n '1p' "$INCIDENT_BASE/$last_incident/result" 2>/dev/null || echo detected)"
|
||||
last_incident_interface="$(sed -n 's/^interface=//p' "$INCIDENT_BASE/$last_incident/meta" 2>/dev/null | head -1)"
|
||||
else
|
||||
last_incident=""
|
||||
last_incident_result="none"
|
||||
fi
|
||||
fi
|
||||
|
||||
printf 'enabled\t%s\n' "$enabled"
|
||||
printf 'wan_ip\t%s\n' "$wan"
|
||||
printf 'version\t%s\n' "$version"
|
||||
printf 'service\t%s\n' "$service"
|
||||
printf 'backup_count\t%s\n' "$backup_count"
|
||||
printf 'auto_recovery\t%s\n' "$auto_recovery"
|
||||
printf 'active_mode\t%s\n' "$mode"
|
||||
printf 'last_incident\t%s\n' "$last_incident"
|
||||
printf 'last_incident_result\t%s\n' "$last_incident_result"
|
||||
printf 'last_incident_interface\t%s\n' "$last_incident_interface"
|
||||
|
||||
netstat -lntp 2>/dev/null | awk '
|
||||
NR > 2 && $6 == "LISTEN" && ($7 ~ /haproxy/ || $7 ~ /uhttpd/) {
|
||||
printf "listener\t%s\t%s\t%s\n", $1, $4, $7
|
||||
}
|
||||
'
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/bin/sh
|
||||
set -u
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
|
||||
LOCK_DIR=/var/lock/haproxy-manager.apply
|
||||
LOCK_HELD=0
|
||||
UHTTPD_CHANGED=0
|
||||
MODE="$(active_mode)"
|
||||
FAILED=0
|
||||
|
||||
attempt=0
|
||||
while ! operation_lock_acquire "$LOCK_DIR"; do
|
||||
[ "$attempt" -lt 15 ] || {
|
||||
logger -t haproxy-manager "Package removal could not acquire the operation lock"
|
||||
exit 1
|
||||
}
|
||||
attempt=$((attempt + 1))
|
||||
sleep 1
|
||||
done
|
||||
LOCK_HELD=1
|
||||
trap '[ "$LOCK_HELD" != 1 ] || operation_lock_release "$LOCK_DIR"' EXIT
|
||||
|
||||
/usr/libexec/haproxy-manager/backup >/dev/null 2>&1 ||
|
||||
logger -t haproxy-manager "Could not create a final recovery point before package removal"
|
||||
|
||||
if ! restore_uhttpd_bindings; then
|
||||
logger -t haproxy-manager "Could not restore the original LuCI listener bindings"
|
||||
FAILED=1
|
||||
fi
|
||||
if [ "$UHTTPD_CHANGED" = 1 ] && ! uci commit uhttpd; then
|
||||
logger -t haproxy-manager "Could not commit the restored LuCI listener bindings"
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
if ! /usr/libexec/haproxy-manager/firewall-sync --disable >/dev/null 2>&1; then
|
||||
logger -t haproxy-manager "Could not remove owned firewall state"
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
case "$MODE" in
|
||||
generated|raw)
|
||||
if ! /etc/init.d/haproxy stop >/dev/null 2>&1 ||
|
||||
/etc/init.d/haproxy status >/dev/null 2>&1; then
|
||||
logger -t haproxy-manager "Could not stop HAProxy during package removal"
|
||||
FAILED=1
|
||||
fi
|
||||
if ! /etc/init.d/haproxy disable >/dev/null 2>&1; then
|
||||
logger -t haproxy-manager "Could not disable HAProxy during package removal"
|
||||
FAILED=1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$UHTTPD_CHANGED" = 1 ]; then
|
||||
/etc/init.d/uhttpd restart >/dev/null 2>&1 ||
|
||||
{ logger -t haproxy-manager "Could not restart LuCI after restoring its listeners"; FAILED=1; }
|
||||
fi
|
||||
|
||||
if [ "$FAILED" != 0 ]; then
|
||||
logger -t haproxy-manager "Package runtime cleanup is incomplete; removal was stopped"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
uci set "$CONFIG.main.active_mode=none"
|
||||
uci commit "$CONFIG"
|
||||
|
||||
LOCK_HELD=0
|
||||
operation_lock_release "$LOCK_DIR"
|
||||
logger -t haproxy-manager "Removed package-owned HAProxy, firewall, and LuCI runtime state"
|
||||
exit 0
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
|
||||
CFG="${1:-/etc/haproxy.cfg}"
|
||||
if [ "$CFG" != "$HAPROXY_CFG" ]; then
|
||||
is_safe_temp_path "$CFG" || { echo "Refusing unsafe HAProxy config path: $CFG" >&2; exit 1; }
|
||||
fi
|
||||
if ! command -v haproxy >/dev/null 2>&1; then
|
||||
echo "haproxy binary is not installed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
haproxy -c -f "$CFG"
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"admin/services/haproxy-manager": {
|
||||
"title": "HAProxy",
|
||||
"i18n": "haproxy-manager",
|
||||
"order": 60,
|
||||
"action": {
|
||||
"type": "firstchild"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-haproxy-manager" ]
|
||||
}
|
||||
},
|
||||
"admin/services/haproxy-manager/status": {
|
||||
"title": "Status",
|
||||
"i18n": "haproxy-manager",
|
||||
"order": 20,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "haproxy-manager/status"
|
||||
}
|
||||
},
|
||||
"admin/services/haproxy-manager/routes": {
|
||||
"title": "Services",
|
||||
"i18n": "haproxy-manager",
|
||||
"order": 10,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "haproxy-manager/routes"
|
||||
}
|
||||
},
|
||||
"admin/services/haproxy-manager/settings": {
|
||||
"title": "Settings",
|
||||
"i18n": "haproxy-manager",
|
||||
"order": 30,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "haproxy-manager/settings"
|
||||
}
|
||||
},
|
||||
"admin/services/haproxy-manager/raw": {
|
||||
"title": "Raw Config",
|
||||
"i18n": "haproxy-manager",
|
||||
"order": 40,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "haproxy-manager/raw"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"luci-app-haproxy-manager": {
|
||||
"description": "Grant access to LuCI HAProxy Manager",
|
||||
"read": {
|
||||
"uci": [ "haproxy_manager" ],
|
||||
"file": {
|
||||
"/etc/haproxy.cfg": [ "read" ],
|
||||
"/tmp/haproxy-manager-*": [ "read" ]
|
||||
},
|
||||
"ubus": {
|
||||
"file": [ "read", "stat" ]
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
"uci": [ "haproxy_manager" ],
|
||||
"file": {
|
||||
"/usr/libexec/haproxy-manager/status": [ "exec" ],
|
||||
"/usr/libexec/haproxy-manager/generate": [ "exec" ],
|
||||
"/usr/libexec/haproxy-manager/apply": [ "exec" ],
|
||||
"/usr/libexec/haproxy-manager/backup": [ "exec" ],
|
||||
"/usr/libexec/haproxy-manager/backups": [ "exec" ],
|
||||
"/usr/libexec/haproxy-manager/incidents": [ "exec" ],
|
||||
"/usr/libexec/haproxy-manager/incident": [ "exec" ],
|
||||
"/usr/libexec/haproxy-manager/firewall-plan": [ "exec" ],
|
||||
"/usr/libexec/haproxy-manager/rollback": [ "exec" ],
|
||||
"/usr/libexec/haproxy-manager/validate": [ "exec" ],
|
||||
"/usr/libexec/haproxy-manager/apply-raw-file": [ "exec" ],
|
||||
"/tmp/haproxy-manager-*": [ "write" ]
|
||||
},
|
||||
"ubus": {
|
||||
"file": [ "exec", "write" ],
|
||||
"uci": [ "commit" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user