mirror of
https://github.com/openwrt/luci.git
synced 2025-12-22 01:44:35 +04:00
luci-app-wol: Enables persistent configuration of hosts to wake up
Signed-off-by: Martin Devolder <martin.devolder2@gmail.com>
This commit is contained in:
@@ -1,8 +1,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
'require view';
|
'require view';
|
||||||
'require dom';
|
|
||||||
'require uci';
|
'require uci';
|
||||||
'require fs';
|
|
||||||
'require ui';
|
'require ui';
|
||||||
'require rpc';
|
'require rpc';
|
||||||
'require form';
|
'require form';
|
||||||
@@ -11,8 +9,10 @@
|
|||||||
const ETHERWAKE_BIN = '/usr/bin/etherwake';
|
const ETHERWAKE_BIN = '/usr/bin/etherwake';
|
||||||
const WAKEONLAN_BIN = '/usr/bin/wakeonlan';
|
const WAKEONLAN_BIN = '/usr/bin/wakeonlan';
|
||||||
|
|
||||||
|
const PACKAGES_URL = 'admin/system/package-manager';
|
||||||
|
|
||||||
return view.extend({
|
return view.extend({
|
||||||
formdata: { wol: {} },
|
outputText: '',
|
||||||
|
|
||||||
callStat: rpc.declare({
|
callStat: rpc.declare({
|
||||||
object: 'luci.wol',
|
object: 'luci.wol',
|
||||||
@@ -31,138 +31,261 @@ return view.extend({
|
|||||||
callHostHints: rpc.declare({
|
callHostHints: rpc.declare({
|
||||||
object: 'luci-rpc',
|
object: 'luci-rpc',
|
||||||
method: 'getHostHints',
|
method: 'getHostHints',
|
||||||
expect: { '': {} }
|
expect: {
|
||||||
|
'': {}
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
load: function() {
|
option_install_etherwake() {
|
||||||
|
window.open(L.url(PACKAGES_URL) +
|
||||||
|
'?query=etherwake', '_blank', 'noopener');
|
||||||
|
},
|
||||||
|
|
||||||
|
option_install_wakeonlan() {
|
||||||
|
window.open(L.url(PACKAGES_URL) +
|
||||||
|
'?query=wakeonlan', '_blank', 'noopener');
|
||||||
|
},
|
||||||
|
|
||||||
|
load() {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
L.resolveDefault(this.callStat()),
|
L.resolveDefault(this.callStat()),
|
||||||
this.callHostHints(),
|
this.callHostHints(),
|
||||||
uci.load('etherwake')
|
uci.load('luci-wol')
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|
||||||
render([stat, hosts]) {
|
render([stat, hosts]) {
|
||||||
var has_ewk = stat && stat.etherwake,
|
const has_ewk = stat && stat.etherwake,
|
||||||
has_wol = stat && stat.wakeonlan,
|
has_wol = stat && stat.wakeonlan;
|
||||||
m, s, o;
|
let m, s, o;
|
||||||
|
|
||||||
this.formdata.has_ewk = has_ewk;
|
// Check if at least one Wake on LAN utility is available, else show install buttons
|
||||||
this.formdata.has_wol = has_wol;
|
if (!has_ewk && !has_wol) {
|
||||||
|
m = new form.Map('luci-wol', _('Wake on LAN'),
|
||||||
m = new form.JSONMap(this.formdata, _('Wake on LAN'),
|
|
||||||
_('Wake on LAN is a mechanism to boot computers remotely in the local network.'));
|
_('Wake on LAN is a mechanism to boot computers remotely in the local network.'));
|
||||||
|
|
||||||
s = m.section(form.NamedSection, 'wol');
|
s = m.section(form.NamedSection, 'packages', 'packages',
|
||||||
|
_('Required Packages'),
|
||||||
|
_('At least one Wake on LAN utility is needed. Please install one of the following packages (some extra permissions may be required):'));
|
||||||
|
|
||||||
|
s.render = L.bind(function(view) {
|
||||||
|
return form.NamedSection.prototype.render.apply(this, arguments)
|
||||||
|
.then(L.bind(function(node) {
|
||||||
|
node.appendChild(E('div', {
|
||||||
|
'class': 'control-group'
|
||||||
|
}, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button-action',
|
||||||
|
'click': ui.createHandlerFn(view, 'option_install_etherwake', this.map),
|
||||||
|
'title': _('Install etherwake package')
|
||||||
|
}, [_('Install etherwake')]),
|
||||||
|
' ',
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button-action',
|
||||||
|
'click': ui.createHandlerFn(view, 'option_install_wakeonlan', this.map),
|
||||||
|
'title': _('Install wakeonlan package')
|
||||||
|
}, [_('Install wakeonlan')])
|
||||||
|
]));
|
||||||
|
return node;
|
||||||
|
}, this));
|
||||||
|
}, s, this);
|
||||||
|
|
||||||
|
return m.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
m = new form.Map('luci-wol', _('Wake on LAN'),
|
||||||
|
_('Wake on LAN is a mechanism to boot computers remotely in the local network.'));
|
||||||
|
|
||||||
|
// Default settings section (used executable)
|
||||||
|
s = m.section(form.NamedSection, 'defaults', 'wol', _('Default Settings'));
|
||||||
|
|
||||||
if (has_ewk && has_wol) {
|
if (has_ewk && has_wol) {
|
||||||
o = s.option(form.ListValue, 'executable', _('WoL program'),
|
o = s.option(form.ListValue, 'executable', _('Default WoL program'),
|
||||||
_('Sometimes only one of the two tools works. If one fails, try the other one'));
|
_('Choose the default Wake on LAN utility'));
|
||||||
|
|
||||||
o.value(ETHERWAKE_BIN, 'Etherwake');
|
o.value(ETHERWAKE_BIN, 'Etherwake');
|
||||||
o.value(WAKEONLAN_BIN, 'Wakeonlan');
|
o.value(WAKEONLAN_BIN, 'Wakeonlan');
|
||||||
|
o.default = ETHERWAKE_BIN;
|
||||||
|
o.onchange = function(ev, section_id, value) {
|
||||||
|
return m.save(null, true);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// If only one binary is available, show info message with install button for the other
|
||||||
|
o = s.option(form.DummyValue, '_info');
|
||||||
|
o.rawhtml = true;
|
||||||
|
o.default = E('div', {}, [
|
||||||
|
E('p', {}, [
|
||||||
|
_('Binary used') + ': ',
|
||||||
|
E('strong', {}, has_ewk ? 'Etherwake' : 'Wakeonlan')
|
||||||
|
]),
|
||||||
|
E('p', {
|
||||||
|
'style': 'margin-top: 10px'
|
||||||
|
},
|
||||||
|
_('You can also install the alternative Wake on LAN utility (some extra permissions may be required):')),
|
||||||
|
E('div', {
|
||||||
|
'class': 'control-group'
|
||||||
|
}, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button-action',
|
||||||
|
'click': ui.createHandlerFn(this, has_ewk ? 'option_install_wakeonlan' : 'option_install_etherwake'),
|
||||||
|
'title': _('Install the alternative Wake on LAN package')
|
||||||
|
}, [_('Install %s').format(has_ewk ? 'wakeonlan' : 'etherwake')])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (has_ewk) {
|
// Targets section with GridSection
|
||||||
o = s.option(widgets.DeviceSelect, 'iface', _('Network interface to use'),
|
s = m.section(form.GridSection, 'target', _('Wake on LAN Targets'), _('Configure hosts that can be woken up. Click the Wake button to send a magic packet.') + '<br>' + _('Note: wakeonlan binary does not support interface, broadcast, and password options (etherwake only).') + ' ' + _('These options will be ignored if wakeonlan is used.'));
|
||||||
_('Specifies the interface the WoL packet is sent on'));
|
|
||||||
|
|
||||||
o.default = uci.get('etherwake', 'setup', 'interface');
|
s.addremove = true;
|
||||||
|
s.anonymous = true;
|
||||||
|
s.sortable = true;
|
||||||
|
s.nodescriptions = true;
|
||||||
|
|
||||||
|
// Name column
|
||||||
|
o = s.option(form.Value, 'name', _('Name'), _('Mandatory'));
|
||||||
o.rmempty = false;
|
o.rmempty = false;
|
||||||
|
o.datatype = 'string';
|
||||||
|
|
||||||
|
// MAC address column
|
||||||
|
o = s.option(form.Value, 'mac', _('MAC Address'), _('Mandatory'));
|
||||||
|
o.rmempty = false;
|
||||||
|
o.datatype = 'macaddr';
|
||||||
|
L.sortedKeys(hosts).forEach(function(mac) { // Add host hints, need 'getHostHints' acl (luci-rpc)
|
||||||
|
const hint = hosts[mac].name ||
|
||||||
|
L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4)[0] ||
|
||||||
|
L.toArray(hosts[mac].ip6addrs || hosts[mac].ipv6)[0];
|
||||||
|
o.value(mac, hint ? '%s (%s)'.format(mac, hint) : mac);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Interface column (only for etherwake)
|
||||||
|
if (has_ewk) {
|
||||||
|
o = s.option(widgets.DeviceSelect, 'iface', _('Interface'), _('Etherwake only')); // Network device selector widget, needs 'getNetworkDevices' acl (luci-rpc)
|
||||||
o.noaliases = true;
|
o.noaliases = true;
|
||||||
o.noinactive = true;
|
o.noinactive = true;
|
||||||
|
|
||||||
uci.sections('etherwake', 'target', function(section) {
|
|
||||||
if (section.mac && section.name) {
|
|
||||||
// Create a host entry if it doesn't exist
|
|
||||||
if (!hosts[section.mac]) {
|
|
||||||
hosts[section.mac] = { name: section.name };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (has_wol)
|
|
||||||
o.depends('executable', ETHERWAKE_BIN);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
o = s.option(form.Value, 'mac', _('Host to wake up'),
|
// Broadcast flag (only for etherwake)
|
||||||
_('Choose the host to wake up or enter a custom MAC address to use'));
|
|
||||||
|
|
||||||
o.rmempty = false;
|
|
||||||
|
|
||||||
L.sortedKeys(hosts).forEach(function(mac) {
|
|
||||||
o.value(mac, E([], [ mac, ' (', E('strong', [
|
|
||||||
hosts[mac].name ||
|
|
||||||
L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4)[0] ||
|
|
||||||
L.toArray(hosts[mac].ip6addrs || hosts[mac].ipv6)[0] ||
|
|
||||||
'?'
|
|
||||||
]), ')' ]));
|
|
||||||
});
|
|
||||||
|
|
||||||
if (has_ewk) {
|
if (has_ewk) {
|
||||||
o = s.option(form.Flag, 'broadcast', _('Send to broadcast address'));
|
o = s.option(form.Flag, 'broadcast', _('Broadcast'), _('Etherwake only'));
|
||||||
|
o.default = o.disabled;
|
||||||
if (has_wol)
|
|
||||||
o.depends('executable', ETHERWAKE_BIN);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Password field (only for etherwake)
|
||||||
|
if (has_ewk) {
|
||||||
|
o = s.option(form.Value, 'password', _('Password'), _('Etherwake only'));
|
||||||
|
o.datatype = 'string';
|
||||||
|
o.placeholder = '00:22:44:66:88:aa or 192.168.1.1';
|
||||||
|
o.datatype = 'or(macaddr,ip4addr("nomask"))'; // Accept MAC or IPv4 address format
|
||||||
|
}
|
||||||
|
|
||||||
|
// When editing, set modal title to include target name
|
||||||
|
s.modaltitle = L.bind(function(section_id) {
|
||||||
|
var name = uci.get('luci-wol', section_id, 'name');
|
||||||
|
return _('Edit target') + (name ? ': ' + name : '');
|
||||||
|
}, this);
|
||||||
|
|
||||||
|
// Keep reference to GridSection for button handlers
|
||||||
|
const gridSection = s;
|
||||||
|
|
||||||
|
// Take default row actions and add "Wake" button
|
||||||
|
s.renderRowActions = L.bind(function(section_id) {
|
||||||
|
const defaultButtons = form.GridSection.prototype.renderRowActions.call(gridSection, section_id, _('Edit'));
|
||||||
|
|
||||||
|
const wakeButton = E('button', {
|
||||||
|
'class': 'cbi-button cbi-button-action',
|
||||||
|
'click': ui.createHandlerFn(this, function() {
|
||||||
|
return this.handleWakeup(section_id, has_ewk, has_wol);
|
||||||
|
})
|
||||||
|
}, _('Wake'));
|
||||||
|
|
||||||
|
const buttonContainer = defaultButtons.querySelector('div');
|
||||||
|
if (buttonContainer) {
|
||||||
|
buttonContainer.insertBefore(wakeButton, buttonContainer.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultButtons;
|
||||||
|
}, this);
|
||||||
|
|
||||||
|
// Output section, for wake results
|
||||||
|
s = m.section(form.NamedSection, 'output', 'wol', _('Output'));
|
||||||
|
s.anonymous = true;
|
||||||
|
s.render = L.bind(function() {
|
||||||
|
return E('div', {
|
||||||
|
'class': 'cbi-section'
|
||||||
|
}, [
|
||||||
|
E('h3', {}, _('Output')),
|
||||||
|
E('textarea', {
|
||||||
|
'readonly': true,
|
||||||
|
'rows': 10,
|
||||||
|
'style': 'width: 100%; font-family: monospace;',
|
||||||
|
'id': 'wol-output-text'
|
||||||
|
}, this.outputText)
|
||||||
|
]);
|
||||||
|
}, this);
|
||||||
|
|
||||||
return m.render();
|
return m.render();
|
||||||
},
|
},
|
||||||
|
|
||||||
handleWakeup: function(ev) {
|
handleWakeup(section_id, has_ewk, has_wol) {
|
||||||
var map = document.querySelector('#maincontent .cbi-map'),
|
const self = this;
|
||||||
data = this.formdata,
|
const name = uci.get('luci-wol', section_id, 'name');
|
||||||
self = this;
|
const mac = uci.get('luci-wol', section_id, 'mac');
|
||||||
|
|
||||||
return dom.callClassMethod(map, 'save').then(function() {
|
// Determine which binary to use and verify availability
|
||||||
if (!data.wol.mac)
|
const defaultBin = uci.get('luci-wol', 'defaults', 'executable');
|
||||||
return alert(_('No target host specified!'));
|
let bin = defaultBin || (has_ewk ? ETHERWAKE_BIN : WAKEONLAN_BIN);
|
||||||
|
|
||||||
var bin = data.wol.executable || (data.has_ewk ? ETHERWAKE_BIN : WAKEONLAN_BIN),
|
if (bin == ETHERWAKE_BIN && !has_ewk)
|
||||||
args = [];
|
bin = WAKEONLAN_BIN;
|
||||||
|
else if (bin == WAKEONLAN_BIN && !has_wol)
|
||||||
|
bin = ETHERWAKE_BIN;
|
||||||
|
|
||||||
|
// Build argument list based on selected binary
|
||||||
|
const args = [];
|
||||||
|
|
||||||
if (bin == ETHERWAKE_BIN) {
|
if (bin == ETHERWAKE_BIN) {
|
||||||
args.push('-D', '-i', data.wol.iface);
|
args.push('-D');
|
||||||
|
const iface = uci.get('luci-wol', section_id, 'iface');
|
||||||
|
if (iface)
|
||||||
|
args.push('-i', iface);
|
||||||
|
|
||||||
if (data.wol.broadcast == '1')
|
const broadcast = uci.get('luci-wol', section_id, 'broadcast');
|
||||||
|
if (broadcast == '1')
|
||||||
args.push('-b');
|
args.push('-b');
|
||||||
|
|
||||||
args.push(data.wol.mac);
|
const password = uci.get('luci-wol', section_id, 'password');
|
||||||
}
|
if (password)
|
||||||
else {
|
args.push('-p', password);
|
||||||
args.push(data.wol.mac);
|
|
||||||
|
args.push(mac);
|
||||||
|
} else {
|
||||||
|
args.push(mac);
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.showModal(_('Waking host'), [
|
// Execute the wake command and handle output
|
||||||
E('p', { 'class': 'spinning' }, [ _('Starting WoL utility…') ])
|
this.appendOutput(`Sending wakeup to ${name} (${mac})...\n`);
|
||||||
]);
|
|
||||||
|
|
||||||
return self.callExec(bin, args).then(function(res) {
|
return this.callExec(bin, args).then(function(res) {
|
||||||
ui.showModal(_('Waking host'), [
|
if (res.stdout)
|
||||||
res.stdout ? E('p', [ res.stdout ]) : '',
|
self.appendOutput(res.stdout + '\n');
|
||||||
res.stderr ? E('pre', [ res.stderr ]) : '',
|
if (res.stderr)
|
||||||
E('div', { 'class': 'right' }, [
|
self.appendOutput('Error: ' + res.stderr + '\n');
|
||||||
E('button', {
|
if (!res.stdout && !res.stderr)
|
||||||
'class': 'cbi-button cbi-button-primary',
|
self.appendOutput('Command completed with code ' + (res.code || 0) + '\n');
|
||||||
'click': ui.hideModal
|
self.appendOutput('\n');
|
||||||
}, [ _('Dismiss') ])
|
|
||||||
])
|
|
||||||
]);
|
|
||||||
}).catch(function(err) {
|
}).catch(function(err) {
|
||||||
ui.hideModal();
|
self.appendOutput('Error: ' + err + '\n\n');
|
||||||
ui.addNotification(null, [
|
|
||||||
E('p', [ _('Waking host failed: '), err ])
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
addFooter: function() {
|
appendOutput(text) {
|
||||||
return E('div', { 'class': 'cbi-page-actions' }, [
|
// Append text to the output textarea and scroll to bottom
|
||||||
E('button', {
|
this.outputText += text;
|
||||||
'class': 'cbi-button cbi-button-save',
|
const textarea = document.getElementById('wol-output-text');
|
||||||
'click': L.ui.createHandlerFn(this, 'handleWakeup')
|
if (textarea) {
|
||||||
}, [ _('Wake up host') ])
|
textarea.value = this.outputText;
|
||||||
]);
|
textarea.scrollTop = textarea.scrollHeight;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
1
applications/luci-app-wol/root/etc/config/luci-wol
Normal file
1
applications/luci-app-wol/root/etc/config/luci-wol
Normal file
@@ -0,0 +1 @@
|
|||||||
|
config wol 'defaults'
|
||||||
@@ -6,12 +6,14 @@
|
|||||||
"luci.wol": [ "stat" ],
|
"luci.wol": [ "stat" ],
|
||||||
"luci-rpc": [ "getHostHints", "getNetworkDevices" ]
|
"luci-rpc": [ "getHostHints", "getNetworkDevices" ]
|
||||||
},
|
},
|
||||||
"uci": [ "etherwake" ]
|
"uci": [ "luci-wol" ]
|
||||||
},
|
},
|
||||||
"write": {
|
"write": {
|
||||||
"ubus": {
|
"ubus": {
|
||||||
"luci.wol": [ "exec" ]
|
"luci.wol": [ "exec" ],
|
||||||
}
|
"uci": [ "add", "set", "delete", "order" ]
|
||||||
|
},
|
||||||
|
"uci": [ "luci-wol" ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const methods = {
|
|||||||
|
|
||||||
result.stdout = fd.read('all');
|
result.stdout = fd.read('all');
|
||||||
result.stderr = '';
|
result.stderr = '';
|
||||||
result.code = 0;
|
result.code = fd.close();
|
||||||
} else {
|
} else {
|
||||||
result.stdout = '';
|
result.stdout = '';
|
||||||
result.stderr = 'disallowed';
|
result.stderr = 'disallowed';
|
||||||
|
|||||||
Reference in New Issue
Block a user