summaryrefslogtreecommitdiffstats
path: root/modules-available/rebootcontrol
diff options
context:
space:
mode:
Diffstat (limited to 'modules-available/rebootcontrol')
-rw-r--r--modules-available/rebootcontrol/api.inc.php36
-rw-r--r--modules-available/rebootcontrol/clientscript.js12
-rw-r--r--modules-available/rebootcontrol/config.json3
-rw-r--r--modules-available/rebootcontrol/hooks/client-update.inc.php25
-rw-r--r--modules-available/rebootcontrol/hooks/config-tgz.inc.php6
-rw-r--r--modules-available/rebootcontrol/hooks/cron.inc.php37
-rw-r--r--modules-available/rebootcontrol/inc/rebootcontrol.inc.php487
-rw-r--r--modules-available/rebootcontrol/inc/rebootutils.inc.php15
-rw-r--r--modules-available/rebootcontrol/inc/scheduler.inc.php330
-rw-r--r--modules-available/rebootcontrol/inc/sshkey.inc.php27
-rw-r--r--modules-available/rebootcontrol/install.inc.php28
-rw-r--r--modules-available/rebootcontrol/lang/de/messages.json4
-rw-r--r--modules-available/rebootcontrol/lang/de/module.json2
-rw-r--r--modules-available/rebootcontrol/lang/de/permissions.json5
-rw-r--r--modules-available/rebootcontrol/lang/de/template-tags.json29
-rw-r--r--modules-available/rebootcontrol/lang/en/messages.json18
-rw-r--r--modules-available/rebootcontrol/lang/en/module.json6
-rw-r--r--modules-available/rebootcontrol/lang/en/permissions.json16
-rw-r--r--modules-available/rebootcontrol/lang/en/template-tags.json86
-rw-r--r--modules-available/rebootcontrol/page.inc.php27
-rw-r--r--modules-available/rebootcontrol/pages/exec.inc.php1
-rw-r--r--modules-available/rebootcontrol/pages/jumphost.inc.php18
-rw-r--r--modules-available/rebootcontrol/pages/subnet.inc.php43
-rw-r--r--modules-available/rebootcontrol/pages/task.inc.php23
-rw-r--r--modules-available/rebootcontrol/permissions/permissions.json3
-rw-r--r--modules-available/rebootcontrol/templates/header.html49
-rw-r--r--modules-available/rebootcontrol/templates/jumphost-list.html6
-rw-r--r--modules-available/rebootcontrol/templates/status-checkconnection.html2
-rw-r--r--modules-available/rebootcontrol/templates/status-exec.html15
-rw-r--r--modules-available/rebootcontrol/templates/status-reboot.html7
-rw-r--r--modules-available/rebootcontrol/templates/status-wol.html52
-rw-r--r--modules-available/rebootcontrol/templates/subnet-edit.html8
-rw-r--r--modules-available/rebootcontrol/templates/subnet-list.html35
-rw-r--r--modules-available/rebootcontrol/templates/task-list.html4
34 files changed, 1057 insertions, 408 deletions
diff --git a/modules-available/rebootcontrol/api.inc.php b/modules-available/rebootcontrol/api.inc.php
index 6ebc8399..b3e9e976 100644
--- a/modules-available/rebootcontrol/api.inc.php
+++ b/modules-available/rebootcontrol/api.inc.php
@@ -10,39 +10,3 @@ if (Request::any('action') === 'rebuild' && isLocalExecution()) {
}
exit(0);
}
-/*
- Needed POST-Parameters:
- 'token' -- for authentification
- 'action' -- which action should be performed (shutdown or reboot)
- 'clients' -- which are to reboot/shutdown (json encoded array!)
- 'timer' -- (optional) when to perform action in minutes (default value is 0)
-*/
-
-$ips = json_decode(Request::post('clients'));
-$minutes = Request::post('timer', 0, 'int');
-
-$clients = array();
-foreach ($ips as $client) {
- $clients[] = array("ip" => $client);
-}
-
-$apikey = Property::get("rebootcontrol_APIPOSTKEY", 'not-set');
-if (!empty($apikey) && Request::post('token') === $apikey) {
- if (Request::isPost()) {
- if (Request::post('action') == 'shutdown') {
- $shutdown = true;
- $task = Taskmanager::submit("RemoteReboot", array("clients" => $clients, "shutdown" => $shutdown, "minutes" => $minutes));
- echo $task["id"];
- } else if (Request::post('action') == 'reboot') {
- $shutdown = false;
- $task = Taskmanager::submit("RemoteReboot", array("clients" => $clients, "shutdown" => $shutdown, "minutes" => $minutes));
- echo $task["id"];
- } else {
- echo "Only action=shutdown and action=reboot available.";
- }
- } else {
- echo "Only POST Method available.";
- }
-} else {
- echo "Not authorized";
-} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/clientscript.js b/modules-available/rebootcontrol/clientscript.js
index 80be2533..447072a0 100644
--- a/modules-available/rebootcontrol/clientscript.js
+++ b/modules-available/rebootcontrol/clientscript.js
@@ -1,13 +1,13 @@
-var stillActive = true;
+var stillActive = 10;
document.addEventListener('DOMContentLoaded', function() {
var clients = [];
$('.machineuuid').each(function() { clients.push($(this).data('uuid')); });
if (clients.length === 0)
return;
function updateClientStatus() {
- if (!stillActive) return;
- stillActive = false;
- setTimeout(updateClientStatus, 5000);
+ if (stillActive <= 0) return;
+ stillActive--;
+ setTimeout(updateClientStatus, Math.max(1, 30 - stillActive) * 1000);
$.ajax({
url: "?do=rebootcontrol",
method: "POST",
@@ -19,9 +19,9 @@ document.addEventListener('DOMContentLoaded', function() {
return;
for (var e in data) {
$('#status-' + e).prop('class', 'glyphicon ' + data[e]);
- if (!stillActive) $('#spinner-' + e).remove();
+ if (stillActive <= 0) $('#spinner-' + e).remove();
}
});
}
- setTimeout(updateClientStatus, 1000);
+ setTimeout(updateClientStatus, 10);
}); \ No newline at end of file
diff --git a/modules-available/rebootcontrol/config.json b/modules-available/rebootcontrol/config.json
index f823e2eb..4608665b 100644
--- a/modules-available/rebootcontrol/config.json
+++ b/modules-available/rebootcontrol/config.json
@@ -3,6 +3,7 @@
"collapse": true,
"dependencies": [
"locations",
- "js_stupidtable"
+ "js_stupidtable",
+ "statistics"
]
} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/hooks/client-update.inc.php b/modules-available/rebootcontrol/hooks/client-update.inc.php
new file mode 100644
index 00000000..e934988d
--- /dev/null
+++ b/modules-available/rebootcontrol/hooks/client-update.inc.php
@@ -0,0 +1,25 @@
+<?php
+
+if ($type === '~poweron') {
+ $NOW = time();
+ $subnet = Request::post('subnet', false, 'string');
+ if ($subnet !== false && ($subnet = explode('/', $subnet)) !== false && count($subnet) === 2
+ && $subnet[0] === $ip && $subnet[1] >= 8 && $subnet[1] < 32) {
+ $start = ip2long($ip);
+ if ($start !== false) {
+ $maskHost = (int)(2 ** (32 - $subnet[1]) - 1);
+ $maskNet = ~$maskHost & 0xffffffff;
+ $end = $start | $maskHost;
+ $start &= $maskNet;
+ $netparams = ['start' => sprintf('%u', $start), 'end' => sprintf('%u', $end), 'now' => $NOW];
+ $affected = Database::exec('UPDATE reboot_subnet
+ SET lastseen = :now, seencount = seencount + 1
+ WHERE start = :start AND end = :end', $netparams);
+ if ($affected === 0) {
+ // New entry
+ Database::exec('INSERT INTO reboot_subnet (start, end, fixed, isdirect, lastseen, seencount)
+ VALUES (:start, :end, 0, 0, :now, 1)', $netparams);
+ }
+ }
+ }
+}
diff --git a/modules-available/rebootcontrol/hooks/config-tgz.inc.php b/modules-available/rebootcontrol/hooks/config-tgz.inc.php
index 90e32e8a..c9ce1255 100644
--- a/modules-available/rebootcontrol/hooks/config-tgz.inc.php
+++ b/modules-available/rebootcontrol/hooks/config-tgz.inc.php
@@ -8,8 +8,10 @@ if (!is_file($tmpfile) || !is_readable($tmpfile) || filemtime($tmpfile) + 86400
}
try {
$a = new PharData($tmpfile);
- $a["/etc/ssh/mgmt/authorized_keys"] = $pubkey;
- $a["/etc/ssh/mgmt/authorized_keys"]->chmod(0600);
+ $a->addFromString("/etc/ssh/mgmt/authorized_keys", $pubkey);
+ $fi = $a->offsetGet("/etc/ssh/mgmt/authorized_keys");
+ /** @var PharFileInfo $fi */
+ $fi->chmod(0600);
$file = $tmpfile;
} catch (Exception $e) {
EventLog::failure('Could not include ssh key for reboot-control in config.tgz', (string)$e);
diff --git a/modules-available/rebootcontrol/hooks/cron.inc.php b/modules-available/rebootcontrol/hooks/cron.inc.php
index e387e055..289426c7 100644
--- a/modules-available/rebootcontrol/hooks/cron.inc.php
+++ b/modules-available/rebootcontrol/hooks/cron.inc.php
@@ -5,15 +5,18 @@
*/
if (in_array((int)date('G'), [6, 7, 9, 12, 15]) && in_array(date('i'), ['00', '01', '02', '03'])) {
$res = Database::simpleQuery('SELECT hostid, host, port, username, sshkey, script FROM reboot_jumphost');
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $row) {
RebootControl::wakeViaJumpHost($row, '255.255.255.255', [['macaddr' => '00:11:22:33:44:55']]);
}
}
+// CRON for Scheduler
+Scheduler::cron();
+
/*
* Client reachability test -- can be disabled
*/
-if (mt_rand(1, 2) !== 1 || Property::get(RebootControl::KEY_AUTOSCAN_DISABLED))
+if (Property::get(RebootControl::KEY_AUTOSCAN_DISABLED))
return;
class Stuff
@@ -21,7 +24,7 @@ class Stuff
public static $subnets;
}
-function destSawPw($destTask, $destMachine, $passwd)
+function destSawPw(array $destTask, array $destMachine, string $passwd): bool
{
return strpos($destTask['data']['result'][$destMachine['machineuuid']]['stdout'], "passwd=$passwd") !== false;
}
@@ -36,7 +39,7 @@ function spawnDestinationListener($dstid, &$destMachine, &$destTask, &$destDeadl
$destDeadline = 0;
foreach ($destMachines as $machine) {
cron_log("Trying to use {$machine['clientip']} as listener for " . long2ip($machine['bcast']));
- $destTask = RebootControl::runScript([$machine], "echo 'Running-MARK'\nbusybox timeout -t 8 jawol -v -l", 10);
+ $destTask = RebootControl::runScript([$machine], "echo 'Running-MARK'\nbusybox timeout 8 jawol -v -l -l", 10);
Taskmanager::release($destTask);
$destDeadline = time() + 10;
if (!Taskmanager::isRunning($destTask))
@@ -82,6 +85,11 @@ function testClientToClient($srcid, $dstid)
usleep(250000);
$destTask = Taskmanager::status($destTask);
}
+ // Wait for destination listener task to finish; we might want to reuse that client,
+ // and trying to spawn a new listener while the old one is running will fail
+ for ($to = 0; $to < 30 && Taskmanager::isRunning($destTask); ++$to) {
+ usleep(250000);
+ }
cron_log($destTask['data']['result'][$destMachine['machineuuid']]['stdout']);
// Final moment: did dest see the packets from src? Determine this by looking for the generated password
if (destSawPw($destTask, $destMachine, $passwd))
@@ -108,6 +116,11 @@ function testServerToClient($dstid)
$task = Taskmanager::waitComplete($task, 2000);
$destTask = Taskmanager::status($destTask);
}
+ // Wait for destination listener task to finish; we might want to reuse that client,
+ // and trying to spawn a new listener while the old one is running will fail
+ for ($to = 0; $to < 30 && Taskmanager::isRunning($destTask); ++$to) {
+ usleep(250000);
+ }
cron_log($destTask['data']['result'][$destMachine['machineuuid']]['stdout']);
if (destSawPw($destTask, $destMachine, $passwd))
return 1;
@@ -127,7 +140,7 @@ function resultToTime($result)
$next = 86400 * 7; // a week
} else {
// Test finished, reachable
- $next = 86400 * 30; // a month
+ $next = 86400 * 14; // two weeks
}
return time() + round($next * mt_rand(90, 133) / 100);
}
@@ -137,12 +150,12 @@ function resultToTime($result)
*/
// First, cleanup: delete orphaned subnets that don't exist anymore, or don't have any clients using our server
-$cutoff = strtotime('-180 days');
+$cutoff = strtotime('-720 days');
Database::exec('DELETE FROM reboot_subnet WHERE fixed = 0 AND lastseen < :cutoff', ['cutoff' => $cutoff]);
// Get machines running, group by subnet
$cutoff = time() - 301; // Really only the ones that didn't miss the most recent update
-$res = Database::simpleQuery("SELECT s.subnetid, s.end AS bcast, m.machineuuid, m.clientip, m.macaddr
+$res = Database::simpleQuery("SELECT s.subnetid, s.end AS bcast, m.machineuuid, m.clientip, m.macaddr, m.locationid
FROM reboot_subnet s
INNER JOIN machine m ON (
(m.state = 'IDLE' OR m.state = 'OCCUPIED')
@@ -158,12 +171,13 @@ if ($res->rowCount() === 0)
return;
Stuff::$subnets = [];
-while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+foreach ($res as $row) {
if (!isset(Stuff::$subnets[$row['subnetid']])) {
Stuff::$subnets[$row['subnetid']] = [];
}
Stuff::$subnets[$row['subnetid']][] = $row;
}
+unset($res);
$task = Taskmanager::submit('DummyTask', []);
$task = Taskmanager::waitComplete($task, 4000);
@@ -181,7 +195,7 @@ $res = Database::simpleQuery("SELECT subnetid FROM reboot_subnet
WHERE subnetid IN (:active) AND nextdirectcheck < UNIX_TIMESTAMP() AND fixed = 0
ORDER BY nextdirectcheck ASC LIMIT 10", ['active' => array_keys(Stuff::$subnets)]);
cron_log('Direct checks: ' . $res->rowCount() . ' (' . implode(', ', array_keys(Stuff::$subnets)) . ')');
-while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+foreach ($res as $row) {
$dst = (int)$row['subnetid'];
cron_log('Direct check for subnetid ' . $dst);
$result = testServerToClient($dst);
@@ -201,6 +215,9 @@ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
* Try client to client
*/
+if (!Property::get(RebootControl::KEY_SCAN_CLIENT_TO_CLIENT))
+ return;
+
// Query all possible combos
$combos = [];
foreach (Stuff::$subnets as $src => $_) {
@@ -223,7 +240,7 @@ if (count($combos) > 0) {
ORDER BY sxs.nextcheck ASC
LIMIT 10", ['combos' => $combos]);
cron_log('C2C checks: ' . $res->rowCount());
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $row) {
$src = (int)$row['srcid'];
$dst = (int)$row['dstid'];
$result = testClientToClient($src, $dst);
diff --git a/modules-available/rebootcontrol/inc/rebootcontrol.inc.php b/modules-available/rebootcontrol/inc/rebootcontrol.inc.php
index f287e728..107c2a50 100644
--- a/modules-available/rebootcontrol/inc/rebootcontrol.inc.php
+++ b/modules-available/rebootcontrol/inc/rebootcontrol.inc.php
@@ -7,6 +7,12 @@ class RebootControl
const KEY_AUTOSCAN_DISABLED = 'rebootcontrol.disable.scan';
+ const KEY_SCAN_CLIENT_TO_CLIENT = 'rebootcontrol.scan.c2c';
+
+ const KEY_UDP_PORT = 'rebootcontrol.port';
+
+ const KEY_BROADCAST_ADDRESS = 'rebootcontrol.broadcast-addr';
+
const REBOOT = 'REBOOT';
const KEXEC_REBOOT = 'KEXEC_REBOOT';
const SHUTDOWN = 'SHUTDOWN';
@@ -19,7 +25,7 @@ class RebootControl
* @param bool $kexec whether to trigger kexec-reboot instead of full BIOS cycle
* @return false|array task struct for the reboot job
*/
- public static function reboot($uuids, $kexec = false)
+ public static function reboot(array $uuids, bool $kexec = false)
{
$list = RebootUtils::getMachinesByUuid($uuids);
if (empty($list))
@@ -31,10 +37,9 @@ class RebootControl
* @param array $list list of clients containing each keys 'machineuuid', 'clientip' and 'locationid'
* @param string $mode reboot mode: RebootControl::REBOOT ::KEXEC_REBOOT or ::SHUTDOWN
* @param int $minutes delay in minutes for action
- * @param int $locationId meta data only: locationId of clients
* @return array|false the task, or false if it could not be started
*/
- public static function execute($list, $mode, $minutes)
+ public static function execute(array $list, string $mode, int $minutes)
{
$task = Taskmanager::submit("RemoteReboot", array(
"clients" => $list,
@@ -44,45 +49,20 @@ class RebootControl
"port" => 9922, // Hard-coded, must match mgmt-sshd module
));
if (!Taskmanager::isFailed($task)) {
- self::addTask($task['id'], self::TASK_REBOOTCTL, $list, $task['id'], ['action' => $mode]);
- }
- return $task;
- }
-
- private static function extractLocationIds($from, &$to)
- {
- if (is_numeric($from)) {
- $to[$from] = true;
- return;
- }
- if (!is_array($from))
- return;
- $allnum = true;
- foreach ($from as $k => $v) {
- if (is_numeric($k) && is_numeric($v))
- continue;
- $allnum = false;
- if (is_numeric($k) && is_array($v)) {
- self::extractLocationIds($v, $to);
- } else {
- $k = strtolower($k);
- if ($k === 'locationid' || $k === 'locationids' || $k === 'location' || $k === 'locations' || $k === 'lid' || $k === 'lids') {
- self::extractLocationIds($v, $to);
- } elseif ($k === 'client' || $k === 'clients' || $k === 'machine' || $k === 'machines') {
- if (is_array($v)) {
- self::extractLocationIds($v, $to);
- }
- }
- }
- }
- if ($allnum) {
- foreach ($from as $v) {
- $to[$v] = true;
+ self::addTask($task['id'], self::TASK_REBOOTCTL, $list, ['action' => $mode]);
+ foreach ($list as $client) {
+ $client['mode'] = $mode;
+ $client['minutes'] = $minutes;
+ EventLog::applyFilterRules('#action-power', $client);
}
}
+ return $task;
}
- private static function addTask($id, $type, $clients, $taskIds, $other = false)
+ /**
+ * Add wake task metadata to database, so we can display job details on the summary page.
+ */
+ private static function addTask(string $taskId, string $type, array $clients, array $other = null): void
{
$lids = ArrayUtil::flattenByKey($clients, 'locationid');
$lids = array_unique($lids);
@@ -94,15 +74,13 @@ class RebootControl
}
$newClients[] = $d;
}
- if (!is_array($taskIds)) {
- $taskIds = [$taskIds];
- }
$data = [
- 'id' => $id,
+ 'id' => $taskId,
'type' => $type,
'locations' => $lids,
'clients' => $newClients,
- 'tasks' => $taskIds,
+ 'tasks' => [$taskId], // This did hold multiple tasks in the past; keep it in case we need this again
+ 'timestamp' => time(),
];
if (is_array($other)) {
$data += $other;
@@ -112,19 +90,20 @@ class RebootControl
/**
* @param int[]|null $locations filter by these locations
+ * @param ?string $id only with this TaskID
* @return array|false list of active tasks for reboots/shutdowns.
*/
- public static function getActiveTasks($locations = null, $id = null)
+ public static function getActiveTasks(array $locations = null, string $id = null)
{
if (is_array($locations) && in_array(0, $locations)) {
$locations = null;
}
$list = Property::getList(RebootControl::KEY_TASKLIST);
$return = [];
- foreach ($list as $entry) {
+ foreach ($list as $subkey => $entry) {
$p = json_decode($entry, true);
if (!is_array($p) || !isset($p['id'])) {
- Property::removeFromList(RebootControl::KEY_TASKLIST, $entry);
+ Property::removeFromListByKey(RebootControl::KEY_TASKLIST, $subkey);
continue;
}
if (is_array($locations) && is_array($p['locations']) && array_diff($p['locations'], $locations) !== [])
@@ -147,7 +126,7 @@ class RebootControl
}
}
if (!$valid) {
- Property::removeFromList(RebootControl::KEY_TASKLIST, $entry);
+ Property::removeFromListByKey(RebootControl::KEY_TASKLIST, $subkey);
continue;
}
$return[] = $p;
@@ -167,18 +146,18 @@ class RebootControl
* @param string $command Command or script to execute on client
* @param int $timeout in seconds
* @param string|false $privkey SSH private key to use to connect
- * @return array|false
+ * @return array|false task struct, false on error
*/
- public static function runScript($clients, $command, $timeout = 5, $privkey = false)
+ public static function runScript(array $clients, string $command, int $timeout = 5, $privkey = false)
{
$task = self::runScriptInternal($clients, $command, $timeout, $privkey);
if (!Taskmanager::isFailed($task)) {
- self::addTask($task['id'], self::TASK_EXEC, $clients, $task['id']);
+ self::addTask($task['id'], self::TASK_EXEC, $clients);
}
return $task;
}
- private static function runScriptInternal(&$clients, $command, $timeout = 5, $privkey = false)
+ private static function runScriptInternal(array &$clients, string $command, int $timeout = 5, $privkey = false)
{
$valid = [];
$invalid = [];
@@ -196,7 +175,7 @@ class RebootControl
if (!empty($invalid)) {
$res = Database::simpleQuery('SELECT machineuuid, clientip, locationid FROM machine WHERE machineuuid IN (:uuids)',
['uuids' => array_keys($invalid)]);
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $row) {
if (isset($invalid[$row['machineuuid']])) {
$valid[] = $row + $invalid[$row['machineuuid']];
} else {
@@ -236,49 +215,74 @@ class RebootControl
}
/**
- * @param array $sourceMachines list of source machines. array of [clientip, machineuuid] entries
- * @param string $bcast directed broadcast address to send to
- * @param string|string[] $macaddr destination mac address(es)
- * @param string $passwd optional WOL password, mac address or ipv4 notation
- * @return array|false task struct, false on error
+ * Wake clients given by MAC address(es) via jawol util.
+ * Multiple MAC addresses can be passed as a space separated list.
*/
- public static function wakeViaClient($sourceMachines, $macaddr, $bcast = false, $passwd = false)
+ private static function buildClientWakeCommand(string $macs, string $bcast = null, string $passwd = null): string
{
$command = 'jawol';
if (!empty($bcast)) {
$command .= " -d '$bcast'";
+ } else {
+ $command .= ' -i br0';
}
if (!empty($passwd)) {
$command .= " -p '$passwd'";
}
- if (is_array($macaddr)) {
- $macaddr = implode("' '", $macaddr);
- }
- $command .= " '$macaddr'";
- // Yes there is one zero missing from the usleep -- that's the whole point: we prefer 100ms sleeps
+ $command .= " $macs";
+ return $command;
+ }
+
+ /**
+ * @param array $sourceMachines list of source machines. array of [clientip, machineuuid] entries
+ * @param string $macaddr destination mac address(es)
+ * @param string $bcast directed broadcast address to send to
+ * @param string $passwd optional WOL password, mac address or ipv4 notation
+ * @return array|false task struct, false on error
+ */
+ public static function wakeViaClient(array $sourceMachines, string $macaddr, string $bcast = null, string $passwd = null)
+ {
+ $command = self::buildClientWakeCommand($macaddr, $bcast, $passwd);
+ // Yes there is one zero "missing" from the usleep -- that's the whole point: we prefer 100ms sleeps
return self::runScriptInternal($sourceMachines,
"for i in 1 1 0; do $command; usleep \${i}00000 2> /dev/null || sleep \$i; done");
}
/**
* @param string|string[] $macaddr destination mac address(es)
- * @param string $bcast directed broadcast address to send to
- * @param string $passwd optional WOL password, mac address or ipv4 notation
+ * @param ?string $bcast directed broadcast address to send to
+ * @param ?string $passwd optional WOL password; mac address or ipv4 notation
* @return array|false task struct, false on error
*/
- public static function wakeDirectly($macaddr, $bcast = false, $passwd = false)
+ public static function wakeDirectly($macaddr, string $bcast = null, string $passwd = null)
{
if (!is_array($macaddr)) {
$macaddr = [$macaddr];
}
- return Taskmanager::submit('WakeOnLan', [
- 'ip' => $bcast,
- 'password' => $passwd,
- 'macs' => $macaddr,
- ]);
+ $port = (int)Property::get(RebootControl::KEY_UDP_PORT);
+ if ($port < 1 || $port > 65535) {
+ $port = 9;
+ }
+ $arg = [];
+ foreach ($macaddr as $mac) {
+ $arg[] = [
+ 'ip' => $bcast,
+ 'mac' => $mac,
+ 'methods' => ['DIRECT'],
+ 'password' => $passwd,
+ ];
+ }
+ return Taskmanager::submit('WakeOnLan', ['clients' => $arg]);
}
- public static function wakeViaJumpHost($jumphost, $bcast, $clients)
+ /**
+ * Explicitly wake given clients via jumphost
+ * @param array $jumphost the according row from the database, representing the desired jumphost
+ * @param string $bcast (directed) broadcast address for WOL packet, %IP% in command template
+ * @param array $clients list of clients, must contain at least key 'macaddr' for every client
+ * @return array|false task struct on successful submission to TM, false on error
+ */
+ public static function wakeViaJumpHost(array $jumphost, string $bcast, array $clients)
{
$hostid = $jumphost['hostid'];
$macs = ArrayUtil::flattenByKey($clients, 'macaddr');
@@ -289,11 +293,12 @@ class RebootControl
$macs = "'" . implode("' '", $macs) . "'";
$macs = str_replace('-', ':', $macs);
$script = str_replace(['%IP%', '%MACS%'], [$bcast, $macs], $jumphost['script']);
- $task = RebootControl::runScriptInternal($_ = [[
+ $arg = [[
'clientip' => $jumphost['host'],
'port' => $jumphost['port'],
'username' => $jumphost['username'],
- ]], $script, 6, $jumphost['sshkey']);
+ ]];
+ $task = RebootControl::runScriptInternal($arg, $script, 6, $jumphost['sshkey']);
if ($task !== false && isset($task['id'])) {
TaskmanagerCallback::addCallback($task, 'rbcConnCheck', $hostid);
}
@@ -301,59 +306,42 @@ class RebootControl
}
/**
- * @param array $list list of clients containing each keys 'macaddr' and 'clientip'
- * @return string id of this job
+ * @param array $clientList list of clients containing each keys 'macaddr' and 'clientip', optionally 'locationid'
+ * @param ?array $failed list of failed clients from $clientList
+ * @return ?string taskid of this job
*/
- public static function wakeMachines($list)
+ public static function wakeMachines(array $clientList, array &$failed = null): ?string
{
- /* TODO: Refactor mom's spaghetti
- * Now that I figured out what I want, do something like this:
- * 1) Group clients by subnet
- * 2) Only after step 1, start to collect possible ways to wake up clients for each subnet that's not empty
- * 3) Habe some priority list for the methods, extend Taskmanager to have "negative dependency"
- * i.e. submit task B with task A as parent task, but only launch task B if task A failed.
- * If task A succeeded, mark task B as FINISHED immediately without actually running it.
- * (or introduce new statusCode for this?)
- */
$errors = '';
- $tasks = [];
- $bad = $unknown = [];
+ $sent = $unknown = $unreachable = $failed = [];
+ // For event filtering by rule
// Need all subnets...
+ /* subnetid => [
+ * subnetid => 1234,
+ * start => 1234, (ip2long)
+ * end => 5678, (ip2long)
+ * jumphosts => [id1, id2, ...],
+ */
$subnets = [];
$res = Database::simpleQuery('SELECT subnetid, start, end, isdirect FROM reboot_subnet');
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $row) {
$row += [
- 'jumphosts' => [],
- 'direct' => [],
- 'indirect' => [],
+ 'djumphosts' => [],
+ 'ijumphosts' => [],
];
$subnets[$row['subnetid']] = $row;
}
// Get all jump hosts
- $jumphosts = [];
- $res = Database::simpleQuery('SELECT jh.hostid, host, port, username, sshkey, script, jh.reachable,
- Group_Concat(jxs.subnetid) AS subnets1, Group_Concat(sxs.dstid) AS subnets2
- FROM reboot_jumphost jh
- LEFT JOIN reboot_jumphost_x_subnet jxs ON (jh.hostid = jxs.hostid)
- LEFT JOIN reboot_subnet s ON (INET_ATON(jh.host) BETWEEN s.start AND s.end)
- LEFT JOIN reboot_subnet_x_subnet sxs ON (sxs.srcid = s.subnetid AND sxs.reachable <> 0)
- GROUP BY jh.hostid');
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
- if ($row['subnets1'] === null && $row['subnets2'] === null)
- continue;
- $nets = explode(',', $row['subnets1'] . ',' . $row['subnets2']);
- foreach ($nets as $net) {
- if (empty($net) || !isset($subnets[$net]))
- continue;
- $subnets[$net]['jumphosts'][$row['hostid']] = $row['hostid'];
- }
- $row['jobs'] = [];
- $jumphosts[] = $row;
- }
- // Group by subnet
- foreach ($list as $client) {
- $ip = sprintf('%u', ip2long($client['clientip']));
- //$client['numip'] = $ip;
+ self::addJumphostsToSubnets($subnets);
+ // Determine method for all clients
+ $taskClients = []; // array of arrays with keys [ip, mac, methods]
+ $taskSsh = []; // SSH configs for task, array of arrays with keys [username, sshkey, ip, port, command]
+ $overrideBroadcast = Property::get(self::KEY_BROADCAST_ADDRESS);
+ if (empty($overrideBroadcast)) {
+ $overrideBroadcast = false;
+ }
+ foreach ($clientList as $dbClient) {
+ $ip = sprintf('%u', ip2long($dbClient['clientip'])); // 32Bit snprintf
unset($subnet);
$subnet = false;
foreach ($subnets as &$sn) {
@@ -362,139 +350,96 @@ class RebootControl
break;
}
}
- $ok = false;
- if (!$ok && $subnet === false) {
- $unknown[] = $client;
- $ok = true;
- }
- if (!$ok && $subnet['isdirect']) {
- // Directly reachable
- $subnet['direct'][] = $client;
- $ok = true;
- }
- if (!$ok && !empty($subnet['jumphosts'])) {
- foreach ($subnet['jumphosts'] as $hostid) {
- if ($jumphosts[$hostid]['reachable'] != 0) {
- $jumphosts[$hostid]['jobs'][$subnet['end']][] = $client;
- $ok = true;
- break;
- }
- }
+ if ($subnet === false) {
+ $unknown[] = $dbClient;
+ continue;
}
- if (!$ok) {
- // find clients in same subnet, or reachable ones
- self::findMachinesForSubnet($subnet);
- if (empty($subnet['dclients']) && empty($subnet['iclients'])) {
- // Nothing found -- cannot wake this host
- $bad[] = $client;
- } else {
- // Found suitable indirect host
- $subnet['indirect'][] = $client;
- }
+ $taskClient = [
+ 'ip' => long2ip($subnet['end']),
+ 'mac' => $dbClient['macaddr'],
+ 'methods' => [],
+ ];
+ // If we have an override broadcast address, unconditionally add this as the
+ // first method
+ if ($overrideBroadcast !== false) {
+ $taskClient['ip'] = $overrideBroadcast;
+ $taskClient['methods'][] = 'DIRECT';
}
- }
- unset($subnet);
- // Batch process
- // First, via jump host
- foreach ($jumphosts as $jh) {
- foreach ($jh['jobs'] as $bcast => $clients) {
- $errors .= 'Via jumphost ' . $jh['host'] . ': ' . implode(', ', ArrayUtil::flattenByKey($clients, 'clientip')) . "\n";
- $task = self::wakeViaJumpHost($jh, $bcast, $clients);
- if (Taskmanager::isFailed($task)) {
- // TODO: Figure out $subnet from $bcast and queue as indirect
- // (rather, overhaul this whole spaghetti code)
- $errors .= ".... FAILED TO LAUNCH TASK ON JUMPHOST!\n";
- }
+ self::findMachinesForSubnet($subnet);
+ // Highest priority - clients in same subnet, no directed broadcast
+ // required, should be most reliable
+ self::addSshMethodUsingClient($subnet['dclients'], $taskClient['methods'], $taskSsh);
+ // Jumphost - usually in same subnet
+ self::addSshMethodUsingJumphost($subnet['djumphosts'], true, $taskClient['methods'], $taskSsh);
+ // Jumphosts in other subnets, determined to be able to reach destination subnet
+ self::addSshMethodUsingJumphost($subnet['ijumphosts'], true, $taskClient['methods'], $taskSsh);
+ // If directly reachable from server, prefer this now over the questionable approaches below,
+ // but only if we didn't already add this above because of override
+ if ($overrideBroadcast === false && $subnet['isdirect']) {
+ $taskClient['methods'][] = 'DIRECT';
}
- }
- // Server or client
- foreach ($subnets as $subnet) {
- if (!empty($subnet['direct'])) {
- // Can wake directly
- if (!self::wakeGroup('From server', $tasks, $errors, null, $subnet['direct'], $subnet['end'])) {
- if (!empty($subnet['dclients']) || !empty($subnet['iclients'])) {
- $errors .= "Re-queueing clients for indirect wakeup\n";
- $subnet['indirect'] = array_merge($subnet['indirect'], $subnet['direct']);
- }
- }
+ // Use clients in other subnets, known to be able to reach the destination net
+ self::addSshMethodUsingClient($subnet['iclients'], $taskClient['methods'], $taskSsh);
+ // Add warning if nothing works
+ if (empty($taskClient['methods'])) {
+ $unreachable[] = $dbClient;
+ } else {
+ // TODO: Remember WOL was attempted
}
- if (!empty($subnet['indirect'])) {
- // Can wake indirectly
- $ok = false;
- if (!empty($subnet['dclients'])) {
- $ok = true;
- if (!self::wakeGroup('in same subnet', $tasks, $errors, $subnet['dclients'], $subnet['indirect'])) {
- if (!empty($subnet['iclients'])) {
- $errors .= "Re-re-queueing clients for indirect wakeup\n";
- $ok = false;
- }
- }
- }
- if (!$ok && !empty($subnet['iclients'])) {
- $ok = self::wakeGroup('across subnets', $tasks, $errors, $subnet['dclients'], $subnet['indirect'], $subnet['end']);
- }
- if (!$ok) {
- $errors .= "I'm all out of ideas.\n";
- }
+ // "Questionable approaches":
+ // Last fallback is jumphosts that were not reachable when last checked, this is really a last resort
+ self::addSshMethodUsingJumphost($subnet['djumphosts'], false, $taskClient['methods'], $taskSsh);
+ self::addSshMethodUsingJumphost($subnet['ijumphosts'], false, $taskClient['methods'], $taskSsh);
+
+ if (!empty($taskClient['methods'])) {
+ $taskClients[] = $taskClient;
+ $sent[] = $dbClient;
}
}
- if (!empty($bad)) {
- $ips = ArrayUtil::flattenByKey($bad, 'clientip');
- $errors .= "**** WARNING ****\nNo way to send WOL packets to the following machines:\n" . implode("\n", $ips) . "\n";
- }
+ unset($subnet);
+
if (!empty($unknown)) {
$ips = ArrayUtil::flattenByKey($unknown, 'clientip');
$errors .= "**** WARNING ****\nThe following clients do not belong to a known subnet (bug?)\n" . implode("\n", $ips) . "\n";
+ foreach ($unknown as $val) {
+ $failed[$val['clientip']] = $val;
+ }
}
- $id = Util::randomUuid();
- self::addTask($id, self::TASK_WOL, $list, $tasks, ['log' => $errors]);
- return $id;
- }
-
- private static function wakeGroup($type, &$tasks, &$errors, $via, $clients, $bcast = false)
- {
- $macs = ArrayUtil::flattenByKey($clients, 'macaddr');
- $ips = ArrayUtil::flattenByKey($clients, 'clientip');
- if ($via !== null) {
- $srcips = ArrayUtil::flattenByKey($via, 'clientip');
- $errors .= 'Via ' . implode(', ', $srcips) . ' ';
- }
- $errors .= $type . ': ' . implode(', ', $ips);
- if ($bcast !== false) {
- $errors .= ' (UDP to ' . long2ip($bcast) . ')';
- }
- $errors .= "\n";
- if ($via === null) {
- $task = self::wakeDirectly($macs, $bcast);
- } else {
- $task = self::wakeViaClient($via, $macs, $bcast);
- }
- if ($task !== false && isset($task['id'])) {
- $tasks[] = $task['id'];
+ if (!empty($unreachable)) {
+ $ips = ArrayUtil::flattenByKey($unreachable, 'clientip');
+ $errors .= "**** WARNING ****\nThe following clients are not reachable with any method\n" . implode("\n", $ips) . "\n";
+ foreach ($unreachable as $val) {
+ $failed[$val['clientip']] = $val;
+ }
}
- if (Taskmanager::isFailed($task)) {
- $errors .= ".... FAILED TO START ACCORDING TASK!\n";
- return false;
+ $task = Taskmanager::submit('WakeOnLan', [
+ 'clients' => $taskClients,
+ 'ssh' => $taskSsh,
+ ]);
+ if (isset($task['id'])) {
+ $id = $task['id'];
+ self::addTask($id, self::TASK_WOL, $clientList, ['log' => $errors]);
+ foreach ($sent as $dbClient) {
+ EventLog::applyFilterRules('#action-wol', $dbClient);
+ }
+ return $id;
}
- return true;
+ return null;
}
private static function findMachinesForSubnet(&$subnet)
{
if (isset($subnet['dclients']))
return;
- $cutoff = time() - 302;
+ $cutoff = time() - 320;
// Get clients from same subnet first
- $subnet['dclients'] = Database::queryAll("SELECT machineuuid, clientip FROM machine
+ $subnet['dclients'] = Database::queryColumnArray("SELECT clientip FROM machine
WHERE state IN ('IDLE', 'OCCUPIED') AND INET_ATON(clientip) BETWEEN :start AND :end AND lastseen > :cutoff
LIMIT 3",
['start' => $subnet['start'], 'end' => $subnet['end'], 'cutoff' => $cutoff]);
- $subnet['iclients'] = [];
- if (!empty($subnet['dclients']))
- return;
// If none, get clients from other subnets known to be able to reach this one
- $subnet['iclients'] = Database::queryAll("SELECT m.machineuuid, m.clientip FROM reboot_subnet_x_subnet sxs
- INNER JOIN reboot_subnet s ON (s.subnetid = sxs.srcid AND sxs.dstid = :subnetid)
+ $subnet['iclients'] = Database::queryColumnArray("SELECT m.clientip FROM reboot_subnet_x_subnet sxs
+ INNER JOIN reboot_subnet s ON (s.subnetid = sxs.srcid AND sxs.dstid = :subnetid AND sxs.reachable = 1)
INNER JOIN machine m ON (INET_ATON(m.clientip) BETWEEN s.start AND s.end AND state IN ('IDLE', 'OCCUPIED') AND m.lastseen > :cutoff)
LIMIT 20", ['subnetid' => $subnet['subnetid'], 'cutoff' => $cutoff]);
shuffle($subnet['iclients']);
@@ -503,15 +448,99 @@ class RebootControl
public static function prepareExec()
{
- User::assertPermission('action.exec');
+ User::assertPermission('.rebootcontrol.action.exec');
$uuids = array_values(Request::post('uuid', Request::REQUIRED, 'array'));
- $machines = RebootUtils::getFilteredMachineList($uuids, 'action.exec');
+ $machines = RebootUtils::getFilteredMachineList($uuids, '.rebootcontrol.action.exec');
if ($machines === false)
return;
$id = mt_rand();
Session::set('exec-' . $id, $machines, 60);
- Session::save();
Util::redirect('?do=rebootcontrol&show=exec&what=prepare&id=' . $id);
}
+ /**
+ * Append a "wake via client" WOL method for the given client. Append at least one, but stop
+ * if there are at least two methods already.
+ *
+ * @param array $sshClients [in] list of online clients to use for waking
+ * @param array $c [out] The client's methods array
+ * @param array $taskSsh [out] add according task struct to this array, if not already exists
+ * @return void
+ */
+ private static function addSshMethodUsingClient(array $sshClients, array &$methods, array &$taskSsh)
+ {
+ foreach ($sshClients as $host) {
+ if (!isset($taskSsh[$host])) {
+ $taskSsh[$host] = [
+ 'username' => 'root',
+ 'sshkey' => SSHKey::getPrivateKey(),
+ 'ip' => $host,
+ 'port' => 9922,
+ 'command' => self::buildClientWakeCommand('%MACS%', '%IP%'),
+ ];
+ }
+ $methods[] = $host;
+ if (count($methods) >= 2)
+ break;
+ }
+ }
+
+ private static function addSshMethodUsingJumphost(array $jumpHosts, bool $reachable, array &$methods, array &$taskSsh)
+ {
+ // If it's the fallback to apparently unreachable jump-hosts, ignore if we already have two methods
+ if (!$reachable && count($methods) >= 2)
+ return;
+ // host, port, username, sshkey, script, jh.reachable
+ foreach ($jumpHosts as $jh) {
+ if ($reachable !== (bool)$jh['reachable'])
+ continue;
+ $key = substr(md5($jh['host'] . ':' . $jh['port'] . ':' . $jh['username']), 0, 10);
+ if (!isset($taskSsh[$key])) {
+ $taskSsh[$key] = [
+ 'username' => $jh['username'],
+ 'sshkey' => $jh['sshkey'],
+ 'ip' => $jh['host'],
+ 'port' => $jh['port'],
+ 'command' => $jh['script'],
+ ];
+ }
+ $methods[] = $key;
+ if (count($methods) >= 2)
+ break;
+ }
+ }
+
+ /**
+ * Load all jumphosts from DB, sort into passed $subnets. Also split up
+ * by directly assigned subnets, and indirectly dtermined, reachable subnets.
+ * @param array $subnets [in]
+ * @return void
+ */
+ private static function addJumphostsToSubnets(array &$subnets)
+ {
+ $res = Database::simpleQuery('SELECT host, port, username, sshkey, script, jh.reachable,
+ Group_Concat(jxs.subnetid) AS dsubnets, Group_Concat(sxs.dstid) AS isubnets
+ FROM reboot_jumphost jh
+ LEFT JOIN reboot_jumphost_x_subnet jxs ON (jh.hostid = jxs.hostid)
+ LEFT JOIN reboot_subnet s ON (INET_ATON(jh.host) BETWEEN s.start AND s.end)
+ LEFT JOIN reboot_subnet_x_subnet sxs ON (sxs.srcid = s.subnetid AND sxs.reachable <> 0)
+ GROUP BY jh.hostid');
+ foreach ($res as $row) {
+ $dnets = empty($row['dsubnets']) ? [] : explode(',', $row['dsubnets']);
+ $inets = empty($row['isubnets']) ? [] : explode(',', $row['isubnets']);
+ $inets = array_diff($inets, $dnets); // There might be duplicates if both joins match
+ foreach ($dnets as $net) {
+ if (empty($net) || !isset($subnets[$net]))
+ continue;
+ $subnets[$net]['djumphosts'][] =& $row;
+ }
+ foreach ($inets as $net) {
+ if (empty($net) || !isset($subnets[$net]))
+ continue;
+ $subnets[$net]['ijumphosts'][] =& $row;
+ }
+ unset($row);
+ }
+ }
+
}
diff --git a/modules-available/rebootcontrol/inc/rebootutils.inc.php b/modules-available/rebootcontrol/inc/rebootutils.inc.php
index 99235e8a..e05d90dc 100644
--- a/modules-available/rebootcontrol/inc/rebootutils.inc.php
+++ b/modules-available/rebootcontrol/inc/rebootutils.inc.php
@@ -8,19 +8,18 @@ class RebootUtils
* @param string[] $list list of system UUIDs
* @return array list of machines with machineuuid, hostname, clientip, state and locationid
*/
- public static function getMachinesByUuid($list, $assoc = false, $columns = ['machineuuid', 'hostname', 'clientip', 'state', 'locationid'])
+ public static function getMachinesByUuid(array $list, bool $assoc = false,
+ array $columns = ['machineuuid', 'hostname', 'clientip', 'state', 'locationid']): array
{
if (empty($list))
return array();
- if (is_array($columns)) {
- $columns = implode(',', $columns);
- }
+ $columns = implode(',', $columns);
$res = Database::simpleQuery("SELECT $columns FROM machine
WHERE machineuuid IN (:list)", compact('list'));
if (!$assoc)
- return $res->fetchAll(PDO::FETCH_ASSOC);
+ return $res->fetchAll();
$ret = [];
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $row) {
$ret[$row['machineuuid']] = $row;
}
return $ret;
@@ -31,7 +30,7 @@ class RebootUtils
* Requires the array elements to have key "state" from machine table.
* @param array $clients list of clients
*/
- public static function sortRunningFirst(&$clients)
+ public static function sortRunningFirst(array &$clients): void
{
usort($clients, function($a, $b) {
$a = ($a['state'] === 'IDLE' || $a['state'] === 'OCCUPIED');
@@ -49,7 +48,7 @@ class RebootUtils
* @param string $permission name of location-aware permission to check
* @return array|false List of clients the user has access to.
*/
- public static function getFilteredMachineList($requestedClients, $permission)
+ public static function getFilteredMachineList(array $requestedClients, string $permission)
{
$actualClients = RebootUtils::getMachinesByUuid($requestedClients);
if (count($actualClients) !== count($requestedClients)) {
diff --git a/modules-available/rebootcontrol/inc/scheduler.inc.php b/modules-available/rebootcontrol/inc/scheduler.inc.php
new file mode 100644
index 00000000..19a01beb
--- /dev/null
+++ b/modules-available/rebootcontrol/inc/scheduler.inc.php
@@ -0,0 +1,330 @@
+<?php
+
+class Scheduler
+{
+
+ const ACTION_SHUTDOWN = 'SHUTDOWN';
+ const ACTION_REBOOT = 'REBOOT';
+ const ACTION_WOL = 'WOL';
+ const RA_NEVER = 'NEVER';
+ const RA_SELECTIVE = 'SELECTIVE';
+ const RA_ALWAYS = 'ALWAYS';
+ const SCHEDULE_OPTIONS_DEFAULT = ['wol' => false, 'sd' => false, 'wol-offset' => 0, 'sd-offset' => 0, 'ra-mode' => self::RA_ALWAYS];
+
+ /**
+ * @param int $locationid ID of location to delete WOL/shutdown settings for
+ */
+ public static function deleteSchedule(int $locationid)
+ {
+ Database::exec("DELETE FROM `reboot_scheduler`
+ WHERE locationid = :lid", ['lid' => $locationid]);
+ }
+
+ /**
+ * Calculate next time the given time description is reached.
+ * @param int $now unix timestamp representing now
+ * @param string $day Name of weekday
+ * @param string $time Time, fi. 13:45
+ * @return false|int unix timestamp in the future when we reach the given time
+ */
+ private static function calculateTimestamp(int $now, string $day, string $time)
+ {
+ $ts = strtotime("$day $time");
+ if ($ts < $now) {
+ $ts = strtotime("next $day $time");
+ }
+ if ($ts < $now) {
+ EventLog::warning("Invalid params to calculateTimestamp(): 'next $day $time'");
+ $ts = $now + 864000;
+ }
+ return $ts;
+ }
+
+ /**
+ * Take WOL/SD options and opening times schedule, return next event.
+ * @return array|false array with keys 'time' and 'action' false if no next event
+ */
+ private static function calculateNext(array $options, array $openingTimes)
+ {
+ // If ra-mode is selective, still execute even if wol and shutdown is disabled,
+ // because we still want to shutdown any sessions in the wrong runmode then
+ $selectiveRa = ($options['ra-mode'] === self::RA_SELECTIVE);
+ if ((!$options['wol'] && !$options['sd'] && !$selectiveRa) || empty($openingTimes))
+ return false;
+ $now = time();
+ $events = [];
+ $findWol = $options['wol'] || $options['ra-mode'] === self::RA_SELECTIVE;
+ $findSd = $options['sd'] || $options['ra-mode'] === self::RA_SELECTIVE;
+ foreach ($openingTimes as $row) {
+ foreach ($row['days'] as $day) {
+ if ($findWol) {
+ $events[] = ['action' => self::ACTION_WOL,
+ 'time' => self::calculateTimestamp($now, $day, $row['openingtime'])];
+ }
+ if ($findSd) {
+ $events[] = ['action' => self::ACTION_SHUTDOWN,
+ 'time' => self::calculateTimestamp($now, $day, $row['closingtime'])];
+ }
+ }
+ }
+ $tmp = ArrayUtil::flattenByKey($events, 'time');
+ array_multisort($tmp, SORT_NUMERIC | SORT_ASC, $events);
+
+ // Only apply offsets now, so we can detect nonsensical overlap
+ $wolOffset = $options['wol-offset'] * 60;
+ $sdOffset = $options['sd-offset'] * 60;
+ $prev = PHP_INT_MAX;
+ for ($i = count($events) - 1; $i >= 0; --$i) {
+ $event =& $events[$i];
+ if ($event['action'] === self::ACTION_WOL) {
+ $event['time'] -= $wolOffset;
+ } elseif ($event['action'] === self::ACTION_SHUTDOWN) {
+ $event['time'] += $sdOffset;
+ } else {
+ error_log('BUG Unhandled event type ' . $event['action']);
+ }
+ if ($event['time'] >= $prev || $event['time'] < $now) {
+ // Overlap, delete this event
+ unset($events[$i]);
+ } else {
+ $prev = $event['time'];
+ }
+ }
+ unset($event);
+ // Reset array keys
+ $events = array_values($events);
+
+ // See which is the next suitable event to act upon
+ $lastEvent = count($events) - 1;
+ for ($i = 0; $i <= $lastEvent; $i++) {
+ $event =& $events[$i];
+ $diff = ($i === $lastEvent ? PHP_INT_MAX : $events[$i + 1]['time'] - $event['time']);
+ if ($diff < 300 && $event['action'] !== $events[$i + 1]['action']) {
+ // If difference to next event is < 5 min, ignore.
+ continue;
+ }
+ if ($diff < 900 && $event['action'] === self::ACTION_SHUTDOWN && $events[$i + 1]['action'] === self::ACTION_WOL) {
+ // If difference to next WOL is < 15 min and this is a shutdown, reboot instead.
+ $res['action'] = self::ACTION_REBOOT;
+ $res['time'] = $event['time'];
+ } else {
+ // Use first event.
+ $res = $event;
+ }
+ return $res;
+ }
+ unset($event);
+ return false;
+ }
+
+ /**
+ * Check if any actions have to be taken. To be called periodically by cron.
+ */
+ public static function cron()
+ {
+ $now = time();
+ $res = Database::simpleQuery("SELECT s.locationid, s.action, s.nextexecution, s.options
+ FROM reboot_scheduler s
+ WHERE s.nextexecution < :now AND s.nextexecution > 0", ['now' => $now]);
+ foreach ($res as $row) {
+ // Calculate next_execution for the event and update DB.
+ $options = json_decode($row['options'], true) + self::SCHEDULE_OPTIONS_DEFAULT;
+ // Determine proper opening times by waling up tree
+ $openingTimes = OpeningTimes::forLocation($row['locationid']);
+ if ($openingTimes !== null) {
+ self::updateScheduleSingle($row['locationid'], $options, $openingTimes);
+ }
+ // Weird clock drift? Server offline for a while? Do nothing.
+ if ($row['nextexecution'] + 900 < $now)
+ continue;
+ $selectiveRa = ($options['ra-mode'] === self::RA_SELECTIVE);
+ // Now, if selective remote access is active, we might modify the actual event:
+ if ($selectiveRa) {
+ // If this is WOL, and WOL is actually enabled, then reboot any running machines
+ // in remoteaccess mode, in addition to waking the others, so they exit remote access mode.
+ if ($row['action'] === Scheduler::ACTION_WOL && $options['wol']) {
+ self::executeCronForLocation($row['locationid'], Scheduler::ACTION_REBOOT, 'remoteaccess');
+ self::executeCronForLocation($row['locationid'], Scheduler::ACTION_WOL);
+ }
+ // If this is WOL, and WOL is disabled, shut down any running machines, this is so
+ // anybody walking into this room will not mess with a user's session by yanking the
+ // power cord etc.
+ if ($row['action'] === Scheduler::ACTION_WOL && !$options['wol']) {
+ self::executeCronForLocation($row['locationid'], Scheduler::ACTION_SHUTDOWN, 'remoteaccess');
+ }
+ // If this is SHUTDOWN, and SHUTDOWN is enabled, leave it at that.
+ if ($row['action'] === Scheduler::ACTION_SHUTDOWN && $options['sd']) {
+ self::executeCronForLocation($row['locationid'], Scheduler::ACTION_SHUTDOWN);
+ }
+ // If this is SHUTDOWN, and SHUTDOWN is disabled, do a reboot, so the machine ends up
+ // in the proper runmode.
+ if ($row['action'] === Scheduler::ACTION_SHUTDOWN && !$options['sd']) {
+ self::executeCronForLocation($row['locationid'], Scheduler::ACTION_REBOOT, '');
+ }
+ } else {
+ // Regular case, no selective remoteaccess – just do what the cron entry says
+ self::executeCronForLocation($row['locationid'], $row['action']);
+ }
+ }
+ }
+
+ /**
+ * Execute the given action for the given location.
+ * @param int $locationId location
+ * @param string $action action to perform, Scheduler::*
+ * @param string|null $onlyRunmode if not null, only process running clients in given runmode
+ * @return void
+ */
+ private static function executeCronForLocation(int $locationId, string $action, string $onlyRunmode = null)
+ {
+ if ($onlyRunmode === null) {
+ $machines = Database::queryAll("SELECT machineuuid, clientip, macaddr, locationid FROM machine
+ WHERE locationid = :locid", ['locid' => $locationId]);
+ } else {
+ $machines = Database::queryAll("SELECT machineuuid, clientip, macaddr, locationid FROM machine
+ WHERE locationid = :locid AND currentrunmode = :runmode AND state <> 'OFFLINE'",
+ ['locid' => $locationId, 'runmode' => $onlyRunmode]);
+ }
+ if (empty($machines))
+ return;
+ if ($action === Scheduler::ACTION_SHUTDOWN) {
+ RebootControl::execute($machines, RebootControl::SHUTDOWN, 0);
+ } elseif ($action === Scheduler::ACTION_WOL) {
+ RebootControl::wakeMachines($machines);
+ } elseif ($action === Scheduler::ACTION_REBOOT) {
+ RebootControl::execute($machines, RebootControl::REBOOT, 0);
+ } else {
+ EventLog::warning("Invalid action '$action' in schedule for location " . $locationId);
+ }
+ }
+
+ /**
+ * Get current settings for given location.
+ */
+ public static function getLocationOptions(int $id): array
+ {
+ static $optionList = false;
+ if ($optionList === false) {
+ $optionList = Database::queryKeyValueList("SELECT locationid, `options` FROM `reboot_scheduler`");
+ }
+ if (isset($optionList[$id])) {
+ return (json_decode($optionList[$id], true) ?? []) + self::SCHEDULE_OPTIONS_DEFAULT;
+ }
+ return self::SCHEDULE_OPTIONS_DEFAULT;
+ }
+
+ /**
+ * Write new WOL/Shutdown options for given location.
+ * @param array $options 'wol' 'sd' 'wol-offset' 'sd-offset' 'ra-mode'
+ */
+ public static function setLocationOptions(int $locationId, array $options)
+ {
+ $options += self::SCHEDULE_OPTIONS_DEFAULT;
+ $openingTimes = OpeningTimes::forLocation($locationId);
+ if (!$options['wol'] && !$options['sd'] && $options['ra-mode'] === self::RA_ALWAYS) {
+ self::deleteSchedule($locationId);
+ } else {
+ // Sanitize
+ Util::clamp($options['wol-offset'], 0, 60);
+ Util::clamp($options['sd-offset'], 0, 60);
+ $json_options = json_encode($options);
+ // Write settings, reset schedule
+ Database::exec("INSERT INTO `reboot_scheduler` (locationid, action, nextexecution, options)
+ VALUES (:lid, :act, :next, :opt)
+ ON DUPLICATE KEY UPDATE
+ action = VALUES(action), nextexecution = VALUES(nextexecution), options = VALUES(options)", [
+ 'lid' => $locationId,
+ 'act' => 'WOL',
+ 'next' => 0,
+ 'opt' => $json_options,
+ ]);
+ // Write new timestamps for this location
+ if ($openingTimes !== null) {
+ self::updateScheduleSingle($locationId, $options, $openingTimes);
+ }
+ }
+ // In either case, refresh data for children as well
+ if ($openingTimes !== null) {
+ self::updateScheduleRecursive($locationId, $openingTimes);
+ }
+ }
+
+ /**
+ * Write next WOL/shutdown action to DB, using given options and opening times.
+ * @param int $locationid Location to store settings for
+ * @param array $options Options for calculation (WOL/Shutdown enabled, offsets)
+ * @param array $openingTimes Opening times to use
+ */
+ private static function updateScheduleSingle(int $locationid, array $options, array $openingTimes)
+ {
+ if (!$options['wol'] && !$options['sd'] && $options['ra-mode'] === self::RA_ALWAYS) {
+ self::deleteSchedule($locationid);
+ return;
+ }
+ $nextexec = self::calculateNext($options, $openingTimes);
+ if ($nextexec === false) {
+ // Empty opening times, or all intervals seem to be < 5 minutes, disable.
+ $nextexec = [
+ 'action' => 'WOL',
+ 'time' => 0,
+ ];
+ }
+ Database::exec("UPDATE reboot_scheduler
+ SET action = :act, nextexecution = :next
+ WHERE locationid = :lid", [
+ 'lid' => $locationid,
+ 'act' => $nextexec['action'],
+ 'next' => $nextexec['time'],
+ ]);
+ }
+
+ /**
+ * Recurse into all child locations of the given location-id and re-calculate the next
+ * WOL or shutdown event, based on the given opening times. Recursion stops at locations
+ * that come with their own opening times.
+ * @param int $parentId parent location to start recursion from. Not actually processed.
+ * @param array $openingTimes Opening times to use for calculations
+ */
+ private static function updateScheduleRecursive(int $parentId, array $openingTimes)
+ {
+ $list = Location::getLocationsAssoc();
+ if (!isset($list[$parentId]))
+ return;
+ $childIdList = $list[$parentId]['directchildren'];
+ if (empty($childIdList))
+ return;
+ $res = Database::simpleQuery("SELECT l.locationid, l.openingtime, rs.options
+ FROM location l
+ LEFT JOIN reboot_scheduler rs USING (locationid)
+ WHERE l.locationid IN (:list)", ['list' => $childIdList]);
+ $locationData = [];
+ foreach ($res as $row) {
+ $locationData[$row['locationid']] = $row;
+ }
+ // Handle all child locations
+ foreach ($childIdList as $locationId) {
+ if (!isset($locationData[$locationId]) || $locationData[$locationId]['openingtime'] !== null) {
+ continue; // Ignore entire sub-tree where new opening times are assigned
+ }
+ // This location doesn't have a new openingtimes schedule
+ // If any options are set for this location, update its schedule
+ if ($locationData[$locationId]['options'] !== null) {
+ $options = json_decode($locationData[$locationId]['options'], true);
+ if (!is_array($options)) {
+ trigger_error("Invalid options for lid:$locationId", E_USER_WARNING);
+ } else {
+ $options += self::SCHEDULE_OPTIONS_DEFAULT;
+ self::updateScheduleSingle($locationId, $options, $openingTimes);
+ }
+ }
+ // Either way, further walk down the tree
+ self::updateScheduleRecursive($locationId, $openingTimes);
+ }
+ }
+
+ public static function isValidRaMode(string $raMode): bool
+ {
+ return $raMode === self::RA_ALWAYS || $raMode === self::RA_NEVER || $raMode === self::RA_SELECTIVE;
+ }
+
+} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/inc/sshkey.inc.php b/modules-available/rebootcontrol/inc/sshkey.inc.php
index cce9b3dc..e0954415 100644
--- a/modules-available/rebootcontrol/inc/sshkey.inc.php
+++ b/modules-available/rebootcontrol/inc/sshkey.inc.php
@@ -3,13 +3,17 @@
class SSHKey
{
- public static function getPrivateKey(&$regen = false) {
+ public static function getPrivateKey(?bool &$regen = false): ?string
+ {
$privKey = Property::get("rebootcontrol-private-key");
if (!$privKey) {
- $rsaKey = openssl_pkey_new(array(
+ $rsaKey = openssl_pkey_new([
'private_key_bits' => 2048,
- 'private_key_type' => OPENSSL_KEYTYPE_RSA));
- openssl_pkey_export( openssl_pkey_get_private($rsaKey), $privKey);
+ 'private_key_type' => OPENSSL_KEYTYPE_RSA]);
+ if (!openssl_pkey_export( openssl_pkey_get_private($rsaKey), $privKey)) {
+ $regen = false;
+ return null;
+ }
Property::set("rebootcontrol-private-key", $privKey);
if (Module::isAvailable('sysconfig')) {
ConfigTgz::rebuildAllConfigs();
@@ -19,21 +23,30 @@ class SSHKey
return $privKey;
}
- public static function getPublicKey() {
+ public static function getPublicKey(): ?string
+ {
$pkImport = openssl_pkey_get_private(self::getPrivateKey());
+ if ($pkImport === false)
+ return null;
return self::sshEncodePublicKey($pkImport);
}
- private static function sshEncodePublicKey($privKey) {
+ private static function sshEncodePublicKey($privKey): ?string
+ {
$keyInfo = openssl_pkey_get_details($privKey);
+ if ($keyInfo === false)
+ return null;
$buffer = pack("N", 7) . "ssh-rsa" .
self::sshEncodeBuffer($keyInfo['rsa']['e']) .
self::sshEncodeBuffer($keyInfo['rsa']['n']);
return "ssh-rsa " . base64_encode($buffer);
}
- private static function sshEncodeBuffer($buffer) {
+ private static function sshEncodeBuffer(string $buffer): string
+ {
$len = strlen($buffer);
+ // Prefix with extra null byte if the MSB is set, to ensure
+ // nobody will ever interpret this as a negative number
if (ord($buffer[0]) & 0x80) {
$len++;
$buffer = "\x00" . $buffer;
diff --git a/modules-available/rebootcontrol/install.inc.php b/modules-available/rebootcontrol/install.inc.php
index 0aedfa20..d45a2443 100644
--- a/modules-available/rebootcontrol/install.inc.php
+++ b/modules-available/rebootcontrol/install.inc.php
@@ -37,6 +37,13 @@ $output[] = tableCreate('reboot_subnet_x_subnet', "
PRIMARY KEY (`srcid`, `dstid`),
KEY `nextcheck` (`nextcheck`)");
+$output[] = tableCreate('reboot_scheduler', "
+ `locationid` INT(11) NOT NULL,
+ `action` ENUM('WOL', 'SHUTDOWN', 'REBOOT'),
+ `nextexecution` INT(10) UNSIGNED NOT NULL DEFAULT 0,
+ `options` BLOB,
+ PRIMARY KEY (`locationid`)");
+
$output[] = tableAddConstraint('reboot_jumphost_x_subnet', 'hostid', 'reboot_jumphost', 'hostid',
'ON UPDATE CASCADE ON DELETE CASCADE');
$output[] = tableAddConstraint('reboot_jumphost_x_subnet', 'subnetid', 'reboot_subnet', 'subnetid',
@@ -45,5 +52,26 @@ $output[] = tableAddConstraint('reboot_subnet_x_subnet', 'srcid', 'reboot_subnet
'ON UPDATE CASCADE ON DELETE CASCADE');
$output[] = tableAddConstraint('reboot_subnet_x_subnet', 'dstid', 'reboot_subnet', 'subnetid',
'ON UPDATE CASCADE ON DELETE CASCADE');
+$output[] = tableAddConstraint('reboot_scheduler', 'locationid', 'location', 'locationid',
+ 'ON UPDATE CASCADE ON DELETE CASCADE');
+
+if (tableColumnKeyType('reboot_scheduler', 'action') === 'PRI') {
+ Database::exec("DELETE FROM reboot_scheduler WHERE action <> 'wol'");
+ $res = Database::exec("ALTER TABLE `reboot_scheduler` DROP PRIMARY KEY, ADD PRIMARY KEY (`locationid`)");
+ $output[] = $res !== false ? UPDATE_DONE : UPDATE_FAILED;
+}
+if (strpos(tableColumnType('reboot_scheduler', 'action'), 'REBOOT') === false) {
+ // Fiddle with column to rename ENUM values
+ $res = Database::exec("ALTER TABLE `reboot_scheduler` MODIFY COLUMN `action` ENUM('sd', 'rb', 'WOL', 'SHUTDOWN', 'REBOOT')");
+ handleUpdateResult($res);
+ $res = Database::exec("UPDATE reboot_scheduler SET action =
+ CASE WHEN action = 'sd' THEN 'SHUTDOWN' WHEN action = 'rb' THEN 'REBOOT' ELSE 'WOL' END");
+ handleUpdateResult($res);
+ $res = Database::exec("ALTER TABLE `reboot_scheduler` MODIFY COLUMN `action` ENUM('WOL', 'SHUTDOWN', 'REBOOT')");
+ handleUpdateResult($res);
+ $output[] = UPDATE_DONE;
+}
+
+
responseFromArray($output); \ No newline at end of file
diff --git a/modules-available/rebootcontrol/lang/de/messages.json b/modules-available/rebootcontrol/lang/de/messages.json
index 32da10b1..b481d64a 100644
--- a/modules-available/rebootcontrol/lang/de/messages.json
+++ b/modules-available/rebootcontrol/lang/de/messages.json
@@ -2,6 +2,7 @@
"invalid-cidr": "Ung\u00fcltige CIDR-Angabe: {{0}}",
"invalid-port": "Ung\u00fcltiger Port: {{0}}",
"invalid-subnet": "Ung\u00fcltiges Subnetz: {{0}}",
+ "jumphost-deleted": "Sprung-Host {{0}} gel\u00f6scht",
"jumphost-saved": "Sprung-Host {{0}} gespeichert",
"no-clients-selected": "Keine Clients ausgew\u00e4hlt",
"no-current-tasks": "Keine aktuellen oder k\u00fcrzlich abgeschlossenen Aufgaben",
@@ -10,8 +11,9 @@
"some-machine-not-found": "Einige Clients aus dem POST request wurden nicht gefunden",
"subnet-already-exists": "Subnetz existiert bereits",
"subnet-created": "Subnetz angelegt",
+ "subnet-deleted": "Subnetz gel\u00f6scht",
"subnet-updated": "Subnetz aktualisiert",
- "unknown-exec-job": "Unbekannte Job-ID",
+ "unknown-exec-job": "Unbekannte Job-ID: {{0}}",
"unknown-task-type": "Unbekannter Task-Typ",
"woldiscover-disabled": "Automatische WOL-Ermittlung deaktiviert",
"woldiscover-enabled": "Automatische WOL-Ermittlung aktiviert"
diff --git a/modules-available/rebootcontrol/lang/de/module.json b/modules-available/rebootcontrol/lang/de/module.json
index 24dae3f1..2488fc81 100644
--- a/modules-available/rebootcontrol/lang/de/module.json
+++ b/modules-available/rebootcontrol/lang/de/module.json
@@ -1,6 +1,6 @@
{
"jumphosts": "Sprung-Hosts",
- "module_name": "WakeOnLAN",
+ "module_name": "Fernsteuerung \/ WakeOnLAN",
"page_title": "WakeOnLAN",
"subnets": "Subnetze"
} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/lang/de/permissions.json b/modules-available/rebootcontrol/lang/de/permissions.json
index a5ae8c9c..589db5b9 100644
--- a/modules-available/rebootcontrol/lang/de/permissions.json
+++ b/modules-available/rebootcontrol/lang/de/permissions.json
@@ -2,13 +2,14 @@
"action.exec": "Befehle als root auf laufenden Clients ausf\u00fchren.",
"action.reboot": "Client neustarten.",
"action.shutdown": "Client herunterfahren.",
+ "action.view": "Laufende WOL\/Reboot\/Exec-Tasks sehen.",
"action.wol": "Client per WOL starten.",
"jumphost.assign-subnet": "Einem Sprung-Host ein Subnetz zuweisen.",
"jumphost.edit": "Einen Sprung-Host bearbeiten.",
"jumphost.view": "Liste der Sprung-Hosts sehen.",
"newkeypair": "Neues Schl\u00fcsselpaar generieren.",
"subnet.edit": "Subnetze hinzuf\u00fcgen\/entfernen.",
- "subnet.flag": "Eigenschaften eines Subnetzs bearbeiten.",
- "subnet.view": "Liste der Subnetzs sehen.",
+ "subnet.flag": "Eigenschaften eines Subnetzes bearbeiten.",
+ "subnet.view": "Liste der Subnetze sehen.",
"woldiscover": "Automatische Ermittlung von subnetz\u00fcbergreifender WOL-F\u00e4higkeit."
} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/lang/de/template-tags.json b/modules-available/rebootcontrol/lang/de/template-tags.json
index 3dd5a3e6..b54adbcd 100644
--- a/modules-available/rebootcontrol/lang/de/template-tags.json
+++ b/modules-available/rebootcontrol/lang/de/template-tags.json
@@ -11,7 +11,11 @@
"lang_checkingJumpHost": "Teste Sprung-Host",
"lang_client": "Client",
"lang_clientCount": "# Clients",
+ "lang_confirmDeleteSubnet": "Dieses Subnetz wirklich l\u00f6schen?",
"lang_connecting": "Verbinde...",
+ "lang_directedBroadcastAddress": "Ziel-Adresse",
+ "lang_directedBroadcastDescription": "Diese Adresse wird als Ziel-Adresse zum Wecken s\u00e4mtlicher Clients benutzt. Dies ist bei Verwendung von WOL-Proxies sinnvoll. Wenn das Feld leer ist, wird die Directed Broadcast Adresse des Zielnetzes verwendet.",
+ "lang_directedBroadcastOverrideHeading": "Directed Broadcast Adresse \u00fcberschreiben",
"lang_editJumpHost": "Sprung-Host bearbeiten",
"lang_editSubnet": "Subnetz bearbeiten",
"lang_enterCommand": "Auszuf\u00fchrende Befehle",
@@ -19,23 +23,26 @@
"lang_execRemoteCommand": "Befehl auf Rechner(n) ausf\u00fchren",
"lang_executingRemotely": "F\u00fchre auf gew\u00e4hlten Clients aus...",
"lang_exitCode": "Exit Code",
- "lang_fixSubnetDesc": "Wenn aktiviert, wird die Erreichbarkeit f\u00fcr ideses Subnetz nicht mehr automatisch ermittelt. Sie k\u00f6nnen in diesem Fall selber festlegen, ob das Subnetz WOL-Pakete vom Satelliten-Server empfangen kann. Au\u00dferdem wird das Subnetz bei Setzen dieser Option nicht mehr automatisch aus der Datenbank gel\u00f6scht, wenn 6 Monate lang kein Client in diesem Subnetz gesehen wurde.",
+ "lang_fixSubnetDesc": "Wenn aktiviert, wird die Erreichbarkeit f\u00fcr dieses Subnetz nicht mehr automatisch ermittelt. Sie k\u00f6nnen in diesem Fall selbst festlegen, ob das Subnetz WOL-Pakete vom Satellitenserver empfangen kann. Au\u00dferdem wird das Subnetz bei Setzen dieser Option nicht mehr automatisch aus der Datenbank gel\u00f6scht, wenn 6 Monate lang kein Client in diesem Subnetz gesehen wurde.",
"lang_fixSubnetSettings": "Subnetz-Einstellungen manuell festlegen",
"lang_genNew": "Neues Schl\u00fcsselpaar generieren",
+ "lang_help": "Hilfe",
"lang_host": "Host",
"lang_hostDeleteConfirm": "Diesen Sprung-Host l\u00f6schen?",
"lang_hostNonZeroExit": "Das hinterlegte Script hat einen Exit-Code ungleich 0 zur\u00fcckgeliefert",
"lang_hostNotReachable": "Host nicht erreichbar",
"lang_hostReachable": "Host erreichbar",
"lang_ip": "IP",
- "lang_isDirect": "Vom Satellit erreichbar",
+ "lang_isDirect": "Direkt erreichbar",
+ "lang_isDirectHelp": "Dieses Subnetz kann WOL-Pakete direkt vom Satellitenserver empfangen. Keine Sprung-Hosts oder laufende Clients im Zielnetz notwendig.",
"lang_isFixed": "Manuell konfiguriert",
+ "lang_isFixedHelp": "Die direkte Erreichbarkeit vom Satelliten aus wird nicht periodisch automatisch ermittelt, sondern manuell \u00fcber die Weboberfl\u00e4che festgelegt.",
"lang_jumpHost": "Sprung-Host",
"lang_jumpHosts": "Sprung-Hosts",
"lang_keypairConfirmCheck": "Ich bin sicher",
"lang_lastseen": "Zuletzt gesehen",
"lang_location": "Standort",
- "lang_moduleHeading": "WakeOnLAN",
+ "lang_moduleHeading": "Fernsteuerung \/ WakeOnLAN",
"lang_new": "Neu",
"lang_newKeypairExplanation": "Sie k\u00f6nnen ein neues Schl\u00fcsselpaar erzeugen lassen. In diesem Fall wird das alte Schl\u00fcsselpaar verworfen, sodass alle zum jetzigen Zeitpunkt bereits gestarteten Rechner nicht mehr aus der Ferne bedient werden k\u00f6nnen, bis diese manuell neugestartet wurden.",
"lang_noTasksForJob": "Keine Tasks f\u00fcr diesen Job",
@@ -46,12 +53,11 @@
"lang_pubKey": "SSH Public Key:",
"lang_reachable": "Erreichbar",
"lang_reachableFrom": "Erreichbar von",
- "lang_reachableFromServer": "Erreichbar vom Satelliten-Server",
+ "lang_reachableFromServer": "Erreichbar vom Satellitenserver",
"lang_reachableFromServerDesc": "Wenn dieser Haken gesetzt ist wird angenommen, dass WOL-Pakete, die vom Server aus gesendet werden, dieses Subnetz erreichen k\u00f6nnen. Dazu muss der Router des Ziel-Netzes sog. \"Directed Broadcasts\" unterst\u00fctzen bzw. nicht filtern.",
"lang_rebootAt": "Neustart um:",
"lang_rebooting": "Neustart...",
"lang_remoteExec": "Ausf\u00fchren",
- "lang_saveWolAutoDiscover": "Auto-Erkennung ein\/ausschalten",
"lang_scriptOrCommand": "Befehl \/ Script",
"lang_settings": "Einstellungen",
"lang_shutdown": "Herunterfahren",
@@ -61,14 +67,19 @@
"lang_stdout": "Standard-Output Ausgabe",
"lang_subnet": "Subnetz",
"lang_subnets": "Subnetze",
- "lang_subnetsDescription": "Dies sind dem Satelliten-Server bekannte Subnetze. Damit WOL \u00fcber Subnetz-Grenzen hinaus funktioniert, muss bekannt sein, in welche Netze \"Directed Broadcasts\" gesendet werden k\u00f6nnen, bzw. f\u00fcr welche Netze ein \"Sprung-Host\" existiert. Diese Liste wird sich automatisch f\u00fcllen, wenn Clients gestartet werden. Au\u00dferdem wird automatisch ermittelt, welche Netze mittels \"Directed Broadcasts\" erreichbar sind, sofern diese Funktion nicht oben unter \"Einstellungen\" deaktiviert wird.",
+ "lang_subnetsDescription": "Dies sind dem Satellitenserver bekannte Subnetze. Damit WOL \u00fcber Subnetz-Grenzen hinaus funktioniert, muss bekannt sein, in welche Netze \"Directed Broadcasts\" gesendet werden k\u00f6nnen, bzw. f\u00fcr welche Netze ein \"Sprung-Host\" existiert. Diese Liste wird sich automatisch f\u00fcllen, wenn Clients gestartet werden. Au\u00dferdem wird automatisch ermittelt, welche Netze mittels \"Directed Broadcasts\" erreichbar sind, sofern diese Funktion nicht oben unter \"Einstellungen\" deaktiviert wird.",
"lang_task": "Task",
"lang_taskListIntro": "Hier sehen Sie eine Liste k\u00fcrzlich gestarteter Aufgaben, wie z.B. WOL-Aktionen, das Neustarten oder Herunterfahren von Clients, etc.",
"lang_wakeScriptHelp": "Dieses Script wird auf dem Sprung-Host ausgef\u00fchrt, um den\/die gew\u00fcnschten Maschinen aufzuwecken. Es wird unter der Standard-Shell des oben angegebenen Benutzers ausgef\u00fchrt. Das Script kann zwei spezielle Platzhalter enthalten, die vor dem Ausf\u00fchren des Scripts vom Satellitenserver ersetzt werden: %MACS% ist eine durch Leerzeichen getrennte Liste von MAC-Adressen, die aufzuwecken sind. Das Tool \"wakeonlan\" unterst\u00fctzt direkt mehrere MAC-Adressen, sodass der Platzhalter %MACS% direkt als Kommandozeilenargument verwendet werden kann. Das Tool \"etherwake\" hingegen kann pro Aufruf immer nur einen Host aufwecken, weshalb eine for-Schleife notwendig ist. Au\u00dferdem wird der Platzhalter %IP% ersetzt, welcher je nach Ziel entweder \"255.255.255.255\" ist, oder bei einem netz\u00fcbergreifenden WOL-Paket die \"directed broadcast address\" des Zielnetzes. Netz\u00fcbergreifende WOL-Pakete werden vom \"etherwake\" nicht unterst\u00fctzt.",
"lang_wakeupScript": "Aufweck-Script",
- "lang_wolAutoDiscoverCheck": "WOL-Erreichbarkeit von Subnetzs automatisch ermitteln",
- "lang_wolDiscoverDescription": "Ist diese Option aktiv, ermitteln Server und Clients automatisch, welche Netze von wo mittels WOL erreichbar sind.",
+ "lang_when": "Wann",
+ "lang_wol": "WakeOnLAN",
+ "lang_wolAutoDiscoverCheck": "WOL-Erreichbarkeit von Subnetzen automatisch ermitteln",
+ "lang_wolDestPort": "UDP-Ziel-Port f\u00fcr Pakete, die vom Satelliten gesendet werden",
+ "lang_wolDiscoverClientToClient": "Auch Erreichbarkeit zwischen Client-Subnetzen pr\u00fcfen",
+ "lang_wolDiscoverDescription": "Ist die erste Option aktiv, ermittelt der Satellitenserver automatisch, welche Client-Subnetze direkt per \"Directed Broadcast\" erreichbar sind, unter Verwendung des oben angegebenen Ports. Ist die zweite Option aktiviert, wird zus\u00e4tzlich noch ermittelt, welche Client-Subnetze sich untereinander UDP-WOL-Pakete schicken k\u00f6nnen. Dies ist i.d.R. nicht notwendig, au\u00dfer in Setups mit ungew\u00f6hnlichen Firewall-Regelungen.",
"lang_wolDiscoverHeading": "Automatische WOL-Ermittlung",
"lang_wolMachineSupportText": "Sind die technischen Voraussetzungen erf\u00fcllt, dass ein WOL-Paket den gew\u00fcnschten Rechner erreichen kann, ist es weiterhin erforderlich, dass der Rechner mittels BIOS und evtl. vorhandenem Betriebssystem so konfiguriert wird, dass er auch auf WOL-Pakete reagiert. Zum einen muss die Funktion im BIOS aktiviert sein. Hier ist auch darauf zu achten, ob es eine zus\u00e4tzliche Einstellung gibt, die die normale Bootreihenfolge \u00fcberschreibt, und dass diese wie gew\u00fcnscht konfiguriert wird. Ist WOL im BIOS aktiviert, kann das Betriebssystem die Funktionalit\u00e4t noch per Software ein- und ausschalten. Unter Windows erfolgt dies im Ger\u00e4temanager in den Eigenschaften der Netzwerkkarte. Dies ist relevant, wenn parallel zu bwLehrpool noch ein Windows von der lokaler Platte betrieben wird. Unter Linux kann die WOL-Funktion mit dem ethtool beeinflusst werden. bwLehrpool aktiviert WOL automatisch bei jedem Boot.",
- "lang_wolReachability": "WOL-Erreichbarkeit"
+ "lang_wolReachability": "Erreichbarkeit",
+ "lang_wolReachabilityHelp": "Die erste Zahl ist die Anzahl von Sprung-Hosts, die dieses Subnetz erreichen k\u00f6nnen. Die zweite Zahl ist die Anzahl anderer Subnetze, von denen aus WOL-Pakete dieses Subnetz erreichen k\u00f6nnen."
} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/lang/en/messages.json b/modules-available/rebootcontrol/lang/en/messages.json
index 50bdd7fe..f125e944 100644
--- a/modules-available/rebootcontrol/lang/en/messages.json
+++ b/modules-available/rebootcontrol/lang/en/messages.json
@@ -1,4 +1,20 @@
{
+ "invalid-cidr": "Invalid CIDR notion: {{0}}",
+ "invalid-port": "Invalid port: {{0}}",
+ "invalid-subnet": "Invalid subnet: {{0}}",
+ "jumphost-deleted": "Delete jump host {{0}}",
+ "jumphost-saved": "Saved jump host {{0}}",
"no-clients-selected": "No clients selected",
- "some-machine-not-found": "Some machines from your POST request don't exist"
+ "no-current-tasks": "No recent tasks",
+ "no-such-jumphost": "No such jump host {{0}}",
+ "no-such-task": "No such task: {{0}}",
+ "some-machine-not-found": "Some machines from your POST request don't exist",
+ "subnet-already-exists": "Subnet already exists",
+ "subnet-created": "Created subnet",
+ "subnet-deleted": "Deleted subnet",
+ "subnet-updated": "Updated subnet",
+ "unknown-exec-job": "Invalid job ID: {{0}}",
+ "unknown-task-type": "Invalid task type",
+ "woldiscover-disabled": "Automatic WOL-detection disabled",
+ "woldiscover-enabled": "Automatic WOL-detection enabled"
} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/lang/en/module.json b/modules-available/rebootcontrol/lang/en/module.json
index 47d91e98..1308976b 100644
--- a/modules-available/rebootcontrol/lang/en/module.json
+++ b/modules-available/rebootcontrol/lang/en/module.json
@@ -1,4 +1,6 @@
{
- "module_name": "WakeOnLAN",
- "page_title": "WakeOnLAN"
+ "jumphosts": "Jump hosts",
+ "module_name": "Remote \/ WakeOnLAN",
+ "page_title": "WakeOnLAN",
+ "subnets": "Subnets"
} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/lang/en/permissions.json b/modules-available/rebootcontrol/lang/en/permissions.json
index 34badbaf..b925c2b2 100644
--- a/modules-available/rebootcontrol/lang/en/permissions.json
+++ b/modules-available/rebootcontrol/lang/en/permissions.json
@@ -1,5 +1,15 @@
{
- "action.shutdown": "Shutdown Client.",
- "action.reboot": "Reboot Client.",
- "newkeypair": "Generate new Keypair."
+ "action.exec": "Execute commands on clients (as root).",
+ "action.reboot": "Reboot Client.",
+ "action.shutdown": "Shutdown Client.",
+ "action.view": "See running WOL\/reboot\/exec jobs.",
+ "action.wol": "Send WOL packet to client.",
+ "jumphost.assign-subnet": "Assign subnet to jump host.",
+ "jumphost.edit": "Edit jump host.",
+ "jumphost.view": "See list of jump hosts.",
+ "newkeypair": "Generate new Key pair.",
+ "subnet.edit": "Add\/remove subnets.",
+ "subnet.flag": "Edit subnet properties.",
+ "subnet.view": "See list of subnets.",
+ "woldiscover": "Toggle automatic determination of WOL-reachability."
} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/lang/en/template-tags.json b/modules-available/rebootcontrol/lang/en/template-tags.json
index 9c7830aa..5740b208 100644
--- a/modules-available/rebootcontrol/lang/en/template-tags.json
+++ b/modules-available/rebootcontrol/lang/en/template-tags.json
@@ -1,41 +1,85 @@
{
+ "lang_aWolJob": "WakeOnLAN job",
"lang_activeTasks": "Active tasks",
+ "lang_add": "Add",
+ "lang_addNewSubnet": "Manually add new subnet",
+ "lang_assignedJumpHosts": "Assigned jump hosts",
+ "lang_assignedSubnets": "Assigned subnets",
"lang_authFail": "Authentication failed",
+ "lang_check": "Check",
+ "lang_checkOutputLabel": "Output",
+ "lang_checkingJumpHost": "Check jump host",
"lang_client": "Client",
"lang_clientCount": "# clients",
- "lang_confirmNewKeypair": "Really create new key pair?",
+ "lang_confirmDeleteSubnet": "Delete this subnet?",
"lang_connecting": "Connecting...",
+ "lang_directedBroadcastAddress": "Destination address",
+ "lang_directedBroadcastDescription": "This address will be used as the destination address for wake on LAN. This is useful if you want to use a WOL-proxy that does the actual waking. If you leave this field empty, the broadcast address of the destination network will be used.",
+ "lang_directedBroadcastOverrideHeading": "Override directed broadcast address",
+ "lang_editJumpHost": "Edit jump host",
+ "lang_editSubnet": "Edit subnet",
+ "lang_enterCommand": "Command(s) to execute",
"lang_error": "Not available",
- "lang_genNew": "Generate new keypair",
+ "lang_execRemoteCommand": "Execute command on clients",
+ "lang_executingRemotely": "Executing on selected clients...",
+ "lang_exitCode": "Exit code",
+ "lang_fixSubnetDesc": "If enabled, reachability of this subnet will not be determined automatically any more. You can then set manually whether the subnet is reachable from this server. Additionally, the subnet will not be purged from the database anymore if there was no client activity for six months.",
+ "lang_fixSubnetSettings": "Manually configure this subnet",
+ "lang_genNew": "Generate new key pair",
+ "lang_help": "Help",
+ "lang_host": "Host",
+ "lang_hostDeleteConfirm": "Delete this jump host",
+ "lang_hostNonZeroExit": "The assigned script exited with a return code other than 0",
+ "lang_hostNotReachable": "Host not reachable",
+ "lang_hostReachable": "Host reachable",
"lang_ip": "IP",
- "lang_kexecRebootCheck": "Quick reboot straight to bwLehrpool (kexec)",
+ "lang_isDirect": "Directly reachable",
+ "lang_isDirectHelp": "This subnet can directly receive WOL packets from the server. No jump host etc. required.",
+ "lang_isFixed": "Manually configured",
+ "lang_isFixedHelp": "Reachability from the satellite server is not determined automatically, but set in the web UI.",
+ "lang_jumpHost": "Jump host",
+ "lang_jumpHosts": "Jump hosts",
"lang_keypairConfirmCheck": "I'm sure",
+ "lang_lastseen": "Last seen",
"lang_location": "Location",
- "lang_minutes": " Minutes",
- "lang_mode": "Mode",
"lang_moduleHeading": "WakeOnLAN",
- "lang_newKeypairExplanation": "You can create a new keypair, which will replace the old one. Please note that after doing so, you cannot poweroff or reboot clients that are already running, since they still use the old key. They have to be rebooted manually first.",
- "lang_off": "Off",
- "lang_on": "On",
+ "lang_new": "New",
+ "lang_newKeypairExplanation": "You can create a new key pair, which will replace the old one. Please note that after doing so, you cannot poweroff or reboot clients that are already running, since they still use the old key. They have to be rebooted manually first.",
+ "lang_noTasksForJob": "No tasks for this job",
+ "lang_numAssignedSubnets": "# Subnets",
"lang_online": "Online",
+ "lang_port": "Port",
+ "lang_privkey": "Private key",
"lang_pubKey": "SSH Public Key:",
- "lang_reboot": "Reboot",
+ "lang_reachable": "Reachable",
+ "lang_reachableFrom": "Reachable via",
+ "lang_reachableFromServer": "Reachable from server",
+ "lang_reachableFromServerDesc": "If checked it will be assumed that the server can send WOL packets to clients in this subnet. This requires the router of the destination subnet to forward directed broadcasts.",
"lang_rebootAt": "Reboot at:",
- "lang_rebootButton": "Reboot",
- "lang_rebootCheck": "Do you really want to reboot the selected clients?",
- "lang_rebootControl": "Reboot Control",
- "lang_rebootIn": "Reboot in:",
"lang_rebooting": "Rebooting...",
- "lang_selectall": "Select all",
- "lang_selected": "Selected",
- "lang_session": "Session",
+ "lang_remoteExec": "Execute",
+ "lang_scriptOrCommand": "Command \/ Script",
"lang_settings": "Settings",
"lang_shutdown": "Shut Down",
"lang_shutdownAt": "Shutdown at: ",
- "lang_shutdownButton": "Shutdown",
- "lang_shutdownCheck": "Do you really want to shut down the selected clients?",
- "lang_shutdownIn": "Shutdown in: ",
"lang_status": "Status",
- "lang_time": "Time",
- "lang_unselectall": "Unselect all"
+ "lang_stderr": "Standard error",
+ "lang_stdout": "Standard output",
+ "lang_subnet": "Subnet",
+ "lang_subnets": "Subnets",
+ "lang_subnetsDescription": "These are subnets known to the server. For WOL to work across subnets the server needs to know which subnets are reachable via directed broadcasts. This list will be populated automatically as new clients boot up. For subnets that are not reachable via directed broadcasts, you can set up \"jump hosts\", which is any kind of host that can reach the desired destination subnet. Also, the satellite server will automatically use other bwLehrpool-Clients to reach selected machines, if applicable. If you don't want this to be detected automatically, you can disable this feature in the Settings above.",
+ "lang_task": "Task",
+ "lang_taskListIntro": "This is a list of running or recently finished tasks (WOL, Shutdown, Reboot, ...).",
+ "lang_wakeScriptHelp": "This script will be executed on the jump host to wake up the selected machines. If will be executed using the default shell of the host. There are two special placeholders which will be replaced before executing the script on the host: %MACS% will be a space separated list of mac addresses to wake up. %IP% is either the directed broadcast address of the destination subnet, or simply 255.255.255.255, if the destination subnet is the same as the jump host's address.",
+ "lang_wakeupScript": "Wake script",
+ "lang_when": "When",
+ "lang_wol": "WakeOnLAN",
+ "lang_wolAutoDiscoverCheck": "Automatically determine WOL-reachability of subnets",
+ "lang_wolDestPort": "UDP destination port for packets sent from satellite",
+ "lang_wolDiscoverClientToClient": "Check reachability between different client subnets too",
+ "lang_wolDiscoverDescription": "If the first option is active, the satellite server automatically determines which client subnets are directly reachable via \"Directed Broadcasts\", using the port specified above. If the second option is enabled, the satellite server also determines which client subnets can send UDP WOL packets to each other. This is usually not necessary, except in setups with unusual firewall rules. ",
+ "lang_wolDiscoverHeading": "Automatic WOL detection",
+ "lang_wolMachineSupportText": "If the technical requirements for reaching a destination host with WOL packets are met, it is still required to enable wake on LAN on the client computer. This usually happens in the BIOS, where sometimes it is also possible to set a different boot order for when the machine was powered on via WOL. After enabling WOL in the BIOS, it is still possible to disable WOL in the driver after booting up an Operating System. In Windows, this can be configured in the device manager, which would be relevant if bwLehrpool is being used in a dual-boot environment. On Linux you can change this setting with the ethtool utility. bwLehrpool (re-)enables WOL on every boot.",
+ "lang_wolReachability": "Reachability",
+ "lang_wolReachabilityHelp": "The first figure is the number of jump hosts that are configured for this subnet. The second figure is the number of client subnets that can reach this subnet."
} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/page.inc.php b/modules-available/rebootcontrol/page.inc.php
index cf87a3b3..80eff842 100644
--- a/modules-available/rebootcontrol/page.inc.php
+++ b/modules-available/rebootcontrol/page.inc.php
@@ -21,10 +21,10 @@ class Page_RebootControl extends Page
}
if (User::hasPermission('jumphost.*')) {
- Dashboard::addSubmenu('?do=rebootcontrol&show=jumphost', Dictionary::translate('jumphosts', true));
+ Dashboard::addSubmenu('?do=rebootcontrol&show=jumphost', Dictionary::translate('jumphosts'));
}
if (User::hasPermission('subnet.*')) {
- Dashboard::addSubmenu('?do=rebootcontrol&show=subnet', Dictionary::translate('subnets', true));
+ Dashboard::addSubmenu('?do=rebootcontrol&show=subnet', Dictionary::translate('subnets'));
}
$section = Request::any('show', false, 'string');
@@ -46,7 +46,13 @@ class Page_RebootControl extends Page
} elseif ($action === 'toggle-wol') {
User::assertPermission('woldiscover');
$enabled = Request::post('enabled', false);
+ $c2c = Request::post('enabled-c2c', false);
+ $port = Request::post('port', 9, 'int');
+ $dbcast = Request::post('dbcast', '', 'string');
Property::set(RebootControl::KEY_AUTOSCAN_DISABLED, !$enabled);
+ Property::set(RebootControl::KEY_SCAN_CLIENT_TO_CLIENT, $c2c);
+ Property::set(RebootControl::KEY_UDP_PORT, $port);
+ Property::set(RebootControl::KEY_BROADCAST_ADDRESS, $dbcast);
if ($enabled) {
Message::addInfo('woldiscover-enabled');
} else {
@@ -59,7 +65,13 @@ class Page_RebootControl extends Page
if (Request::isPost()) {
Util::redirect('?do=rebootcontrol' . ($section ? '&show=' . $section : ''));
} elseif ($section === false) {
- Util::redirect('?do=rebootcontrol&show=task');
+ if (User::hasPermission('action.*')) {
+ Util::redirect('?do=rebootcontrol&show=task');
+ } elseif (User::hasPermission('jumphost.*')) {
+ Util::redirect('?do=rebootcontrol&show=jumphost');
+ } else {
+ Util::redirect('?do=rebootcontrol&show=subnet');
+ }
}
}
@@ -89,7 +101,6 @@ class Page_RebootControl extends Page
if (Taskmanager::isTask($task)) {
Util::redirect("?do=rebootcontrol&show=task&what=task&taskid=" . $task["id"]);
}
- return;
}
/**
@@ -98,17 +109,23 @@ class Page_RebootControl extends Page
protected function doRender()
{
+ $port = (int)Property::get(RebootControl::KEY_UDP_PORT);
+ if ($port < 1 || $port > 65535) {
+ $port = 9;
+ }
// Always show public key (it's public, isn't it?)
$data = [
'pubkey' => SSHKey::getPublicKey(),
'wol_auto_checked' => Property::get(RebootControl::KEY_AUTOSCAN_DISABLED) ? '' : 'checked',
+ 'wol_c2c_checked' => Property::get(RebootControl::KEY_SCAN_CLIENT_TO_CLIENT) ? 'checked' : '',
+ 'port' => $port,
+ 'dbcast' => Property::get(RebootControl::KEY_BROADCAST_ADDRESS),
];
Permission::addGlobalTags($data['perms'], null, ['newkeypair', 'woldiscover']);
Render::addTemplate('header', $data);
if ($this->haveSubpage) {
SubPage::doRender();
- return;
}
}
diff --git a/modules-available/rebootcontrol/pages/exec.inc.php b/modules-available/rebootcontrol/pages/exec.inc.php
index e5fe3cd8..6b5ea407 100644
--- a/modules-available/rebootcontrol/pages/exec.inc.php
+++ b/modules-available/rebootcontrol/pages/exec.inc.php
@@ -46,7 +46,6 @@ class SubPage
return;
}
Session::set('exec-' . $id, false);
- Session::save();
Render::addTemplate('exec-enter-command', ['clients' => $machines, 'id' => $id]);
}
diff --git a/modules-available/rebootcontrol/pages/jumphost.inc.php b/modules-available/rebootcontrol/pages/jumphost.inc.php
index 7d1877d2..d9aae234 100644
--- a/modules-available/rebootcontrol/pages/jumphost.inc.php
+++ b/modules-available/rebootcontrol/pages/jumphost.inc.php
@@ -28,10 +28,16 @@ class SubPage
self::execCheckConnection($id);
return;
}
+ $id = Request::post('deleteid', false, 'int');
+ if ($id !== false) {
+ User::assertPermission('jumphost.edit');
+ self::deleteJumphost($id);
+ }
}
private static function execCheckConnection($hostid)
{
+ // Permcheck in caller
$host = self::getJumpHost($hostid);
$task = RebootControl::wakeViaJumpHost($host, '255.255.255.255', [['macaddr' => '00:11:22:33:44:55']]);
if (!Taskmanager::isTask($task))
@@ -39,6 +45,14 @@ class SubPage
Util::redirect('?do=rebootcontrol&show=task&type=checkhost&what=task&taskid=' . $task['id']);
}
+ private static function deleteJumphost($hostid)
+ {
+ // Permcheck in caller
+ $host = self::getJumpHost($hostid);
+ Database::exec('DELETE FROM reboot_jumphost WHERE hostid = :hostid LIMIT 1', ['hostid' => $hostid]);
+ Message::addSuccess('jumphost-deleted', $host['host']);
+ }
+
private static function saveJumpHost()
{
User::assertPermission('jumphost.edit');
@@ -120,7 +134,7 @@ class SubPage
LEFT JOIN reboot_jumphost_x_subnet jxs USING (hostid)
GROUP BY hostid
ORDER BY hostid');
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $row) {
$hosts[] = $row;
}
$data = [
@@ -173,7 +187,7 @@ class SubPage
ORDER BY start ASC',
['id' => $id]);
$list = [];
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $row) {
$row['cidr'] = IpUtil::rangeToCidr($row['start'], $row['end']);
if ($row['hostid'] !== null) {
$row['checked'] = 'checked';
diff --git a/modules-available/rebootcontrol/pages/subnet.inc.php b/modules-available/rebootcontrol/pages/subnet.inc.php
index 45151954..a6d8d837 100644
--- a/modules-available/rebootcontrol/pages/subnet.inc.php
+++ b/modules-available/rebootcontrol/pages/subnet.inc.php
@@ -10,6 +10,8 @@ class SubPage
self::addSubnet();
} elseif ($action === 'edit') {
self::editSubnet();
+ } elseif ($action === 'delete') {
+ self::deleteSubnet();
}
}
@@ -22,7 +24,7 @@ class SubPage
User::assertPermission('subnet.edit');
$cidr = Request::post('cidr', Request::REQUIRED, 'string');
$range = IpUtil::parseCidr($cidr);
- if ($range === false) {
+ if ($range === null) {
Message::addError('invalid-cidr', $cidr);
return;
}
@@ -59,7 +61,7 @@ class SubPage
if (User::hasPermission('jumphost.assign-subnet')) {
$hosts = Request::post('jumphost', [], 'array');
if (empty($hosts)) {
- Database::exec('DELETE FROM reboot_jumphost_x_subnet WHERE subnetid = :id AND', ['id' => $id]);
+ Database::exec('DELETE FROM reboot_jumphost_x_subnet WHERE subnetid = :id', ['id' => $id]);
} else {
$hosts = array_keys($hosts);
Database::exec('DELETE FROM reboot_jumphost_x_subnet WHERE subnetid = :id AND hostid NOT IN (:hosts)',
@@ -73,6 +75,19 @@ class SubPage
Message::addSuccess('subnet-updated');
}
+ private static function deleteSubnet()
+ {
+ User::assertPermission('subnet.edit');
+ User::assertPermission('subnet.flag');
+ $id = Request::post('id', Request::REQUIRED, 'int');
+ $num = Database::exec('DELETE FROM reboot_subnet WHERE subnetid = :id', ['id' => $id]);
+ if ($num < 1) {
+ Message::addError('invalid-subnet', $id);
+ return;
+ }
+ Message::addSuccess('subnet-deleted');
+ }
+
/*
* Render
*/
@@ -91,6 +106,7 @@ class SubPage
{
User::assertPermission('subnet.*');
$nets = [];
+ $c2c = Property::get(RebootControl::KEY_SCAN_CLIENT_TO_CLIENT);
$res = Database::simpleQuery('SELECT subnetid, start, end, fixed, isdirect,
nextdirectcheck, lastseen, seencount, Count(hxs.hostid) AS jumphostcount, Count(sxs.srcid) AS sourcecount
FROM reboot_subnet s
@@ -99,12 +115,15 @@ class SubPage
GROUP BY subnetid, start, end
ORDER BY start ASC, end DESC');
$deadline = strtotime('-60 days');
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $row) {
$row['cidr'] = IpUtil::rangeToCidr($row['start'], $row['end']);
$row['lastseen_s'] = Util::prettyTime($row['lastseen']);
if ($row['lastseen'] && $row['lastseen'] < $deadline) {
$row['lastseen_class'] = 'text-danger';
}
+ if (!$c2c) {
+ $row['sourcecount'] = '-';
+ }
$nets[] = $row;
}
$data = ['subnets' => $nets];
@@ -130,20 +149,24 @@ class SubPage
ORDER BY h.host ASC', ['id' => $id]);
// Mark those assigned to the current subnet
$jh = [];
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $row) {
$row['checked'] = $row['subnetid'] === null ? '' : 'checked';
$jh[] = $row;
}
$subnet['jumpHosts'] = $jh;
- // Get list of all subnets that can broadcast into this one
- $res = Database::simpleQuery('SELECT s.start, s.end FROM reboot_subnet s
+ $c2c = Property::get(RebootControl::KEY_SCAN_CLIENT_TO_CLIENT);
+ if ($c2c) {
+ // Get list of all subnets that can broadcast into this one
+ $res = Database::simpleQuery('SELECT s.start, s.end FROM reboot_subnet s
INNER JOIN reboot_subnet_x_subnet sxs ON (s.subnetid = sxs.srcid AND sxs.dstid = :id AND sxs.reachable = 1)
ORDER BY s.start ASC', ['id' => $id]);
- $sn = [];
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
- $sn[] = ['cidr' => IpUtil::rangeToCidr($row['start'], $row['end'])];
+ $sn = [];
+ foreach ($res as $row) {
+ $sn[] = ['cidr' => IpUtil::rangeToCidr($row['start'], $row['end'])];
+ }
+ $subnet['sourceNets'] = $sn;
+ $subnet['showC2C'] = true;
}
- $subnet['sourceNets'] = $sn;
Permission::addGlobalTags($subnet['perms'], null, ['subnet.flag', 'jumphost.view', 'jumphost.assign-subnet']);
Render::addTemplate('subnet-edit', $subnet);
}
diff --git a/modules-available/rebootcontrol/pages/task.inc.php b/modules-available/rebootcontrol/pages/task.inc.php
index 691fd9e2..7db2a90b 100644
--- a/modules-available/rebootcontrol/pages/task.inc.php
+++ b/modules-available/rebootcontrol/pages/task.inc.php
@@ -26,6 +26,8 @@ class SubPage
private static function showTask()
{
+ // No permission check here - user had to guess the UUID, not very likely,
+ // but this way we can still link to some implicitly triggered job
$taskid = Request::get("taskid", Request::REQUIRED, 'string');
$type = Request::get('type', false, 'string');
if ($type === 'checkhost') {
@@ -93,8 +95,9 @@ class SubPage
} elseif ($type === RebootControl::TASK_WOL) {
// Nothing (yet)
} else {
- Util::traceError('oopsie');
+ ErrorHandler::traceError('oopsie');
}
+ $job['timestamp_s'] = Util::prettyTime($job['timestamp']);
Render::addTemplate('status-' . $template, $job);
}
@@ -103,6 +106,9 @@ class SubPage
Render::addTemplate('task-header');
// Append list of active reboot/shutdown tasks
$allowedLocs = User::getAllowedLocations("action.*");
+ if (empty($allowedLocs)) {
+ User::assertPermission('action.*');
+ }
$active = RebootControl::getActiveTasks($allowedLocs);
if (empty($active)) {
Message::addInfo('no-current-tasks');
@@ -112,8 +118,10 @@ class SubPage
if (isset($entry['clients'])) {
$entry['clients'] = count($entry['clients']);
}
+ $entry['timestamp_s'] = Util::prettyTime($entry['timestamp']);
}
unset($entry);
+ ArrayUtil::sortByColumn($active, 'timestamp', SORT_ASC, SORT_NUMERIC);
Render::addTemplate('task-list', ['list' => $active]);
}
}
@@ -121,7 +129,7 @@ class SubPage
private static function expandLocationIds(&$lids)
{
foreach ($lids as &$locid) {
- if ($locid === 0) {
+ if ($locid === null || $locid === 0) {
$name = '-';
} else {
$name = Location::getName($locid);
@@ -137,14 +145,3 @@ class SubPage
}
}
-
-
-// Remove when we require >= 7.3.0
-if (!function_exists('array_key_first')) {
- function array_key_first(array $arr) {
- foreach($arr as $key => $unused) {
- return $key;
- }
- return NULL;
- }
-}
diff --git a/modules-available/rebootcontrol/permissions/permissions.json b/modules-available/rebootcontrol/permissions/permissions.json
index 5416a482..a508f8b6 100644
--- a/modules-available/rebootcontrol/permissions/permissions.json
+++ b/modules-available/rebootcontrol/permissions/permissions.json
@@ -34,5 +34,8 @@
},
"action.exec": {
"location-aware": true
+ },
+ "action.view": {
+ "location-aware": true
}
} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/templates/header.html b/modules-available/rebootcontrol/templates/header.html
index 6d38b939..47d97714 100644
--- a/modules-available/rebootcontrol/templates/header.html
+++ b/modules-available/rebootcontrol/templates/header.html
@@ -27,29 +27,52 @@
<span class="glyphicon glyphicon-refresh"></span>
{{lang_genNew}}
</button>
- <div class="clearfix"></div>
+ <div class="clearfix slx-space"></div>
</div>
- <div class="modal-body">
- <label>{{lang_wolDiscoverHeading}}</label>
- <form method="post" action="?do=rebootcontrol">
- <input type="hidden" name="token" value="{{token}}">
- <input type="hidden" name="action" value="toggle-wol">
+ <form method="post" action="?do=rebootcontrol">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="action" value="toggle-wol">
+ <div class="modal-body">
+ <label for="port-input">{{lang_wol}}</label>
+ <div class="input-group">
+ <span class="input-group-addon">{{lang_wolDestPort}}</span>
+ <input {{perms.woldiscover.disabled}} type="number" min="1" max="65535"
+ class="form-control" name="port" value="{{port}}" id="port-input">
+ </div>
+ </div>
+ <div class="modal-body">
+ <label>{{lang_wolDiscoverHeading}}</label>
<div class="checkbox">
- <input {{perms.woldiscover.disabled}} id="wol-auto-discover" type="checkbox" name="enabled" {{wol_auto_checked}}>
+ <input {{perms.woldiscover.disabled}} id="wol-auto-discover"
+ type="checkbox" name="enabled" {{wol_auto_checked}}>
<label for="wol-auto-discover">{{lang_wolAutoDiscoverCheck}}</label>
</div>
+ <div class="checkbox">
+ <input {{perms.woldiscover.disabled}} id="wol-c2c"
+ type="checkbox" name="enabled-c2c" {{wol_c2c_checked}}>
+ <label for="wol-c2c">{{lang_wolDiscoverClientToClient}}</label>
+ </div>
<div class="slx-space"></div>
<p>{{lang_wolDiscoverDescription}}</p>
+ </div>
+ <div class="modal-body">
+ <label for="bcast-input">{{lang_directedBroadcastOverrideHeading}}</label>
+ <div class="input-group">
+ <span class="input-group-addon">{{lang_directedBroadcastAddress}}</span>
+ <input {{perms.woldiscover.disabled}} type="text" pattern="[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+"
+ minlength="7" maxlength="15" class="form-control" name="dbcast" value="{{dbcast}}" id="bcast-input">
+ </div>
+ <p>{{lang_directedBroadcastDescription}}</p>
+ </div>
+ <div class="modal-body">
<button {{perms.woldiscover.disabled}} class="btn btn-primary pull-right"
onclick="generateNewKeypair()" type="submit">
<span class="glyphicon glyphicon-floppy-disk"></span>
- {{lang_saveWolAutoDiscover}}
+ {{lang_save}}
</button>
<div class="clearfix"></div>
- </form>
- </div>
- <div class="modal-body">
- </div>
+ </div>
+ </form>
</div>
</div>
</div>
@@ -84,4 +107,4 @@ document.addEventListener('DOMContentLoaded', function() {
});
-</script> \ No newline at end of file
+</script>
diff --git a/modules-available/rebootcontrol/templates/jumphost-list.html b/modules-available/rebootcontrol/templates/jumphost-list.html
index 6023c872..083480b8 100644
--- a/modules-available/rebootcontrol/templates/jumphost-list.html
+++ b/modules-available/rebootcontrol/templates/jumphost-list.html
@@ -1,8 +1,6 @@
-<h2>{{lang_settings}}</h2>
+<h2>{{lang_wolReachability}}</h2>
-<h3>{{lang_wolReachability}}</h3>
-
-<h4>{{lang_jumpHosts}}</h4>
+<h3>{{lang_jumpHosts}}</h3>
<form method="post" action="?do=rebootcontrol">
<input type="hidden" name="token" value="{{token}}">
diff --git a/modules-available/rebootcontrol/templates/status-checkconnection.html b/modules-available/rebootcontrol/templates/status-checkconnection.html
index e31d95ea..da1177e7 100644
--- a/modules-available/rebootcontrol/templates/status-checkconnection.html
+++ b/modules-available/rebootcontrol/templates/status-checkconnection.html
@@ -1,4 +1,4 @@
-<h3>{{lang_checkingJumpHost}}: {{host}}</h3>
+<h3>{{lang_checkingJumpHost}}: {{host}} – {{timestamp_s}}</h3>
<div class="clearfix"></div>
<div class="collapse alert alert-success" id="result-ok">
diff --git a/modules-available/rebootcontrol/templates/status-exec.html b/modules-available/rebootcontrol/templates/status-exec.html
index 403b7fca..a3efef5f 100644
--- a/modules-available/rebootcontrol/templates/status-exec.html
+++ b/modules-available/rebootcontrol/templates/status-exec.html
@@ -1,3 +1,5 @@
+<h3>{{timestamp_s}}</h3>
+
<div data-tm-id="{{id}}" data-tm-log="error" data-tm-callback="updateStatus">{{lang_executingRemotely}}</div>
<div class="slx-space"></div>
@@ -14,7 +16,11 @@
{{#clients}}
<div class="list-group-item" id="client-{{machineuuid}}">
<div class="row">
- <div class="col-md-6 col-sm-8 col-xs-12 slx-bold">{{hostname}}{{^hostname}}{{clientip}}{{/hostname}}</div>
+ <div class="col-md-6 col-sm-8 col-xs-12 slx-bold">
+ <a href="?do=statistics&amp;uuid={{machineuuid}}">
+ {{hostname}}{{^hostname}}{{clientip}}{{/hostname}}
+ </a>
+ </div>
<div class="col-md-4 col-sm-2 col-xs-6 state"></div>
<div class="col-md-2 col-sm-2 col-xs-6 text-right exitCode"></div>
</div>
@@ -57,6 +63,13 @@ function updateStatusClient(id, status) {
$p.find('.exitCode').text(status.exitCode).addClass((status.exitCode === 0 ? 'text-success' : 'text-danger'));
}
ignoreHosts[id] = true;
+ var txt = status.stdout.trim();
+ if (txt.startsWith('<') && txt.endsWith('</svg>')) {
+ var $i = $('<img class="img-responsive">');
+ $i[0].src = 'data:image/svg+xml,' + encodeURIComponent(txt);
+ $p.find('.stdout').hide();
+ $p.append($i);
+ }
}
}
diff --git a/modules-available/rebootcontrol/templates/status-reboot.html b/modules-available/rebootcontrol/templates/status-reboot.html
index 240c4387..34971845 100644
--- a/modules-available/rebootcontrol/templates/status-reboot.html
+++ b/modules-available/rebootcontrol/templates/status-reboot.html
@@ -1,4 +1,5 @@
-<h3>{{action}}</h3>
+<h3>{{action}} – {{timestamp_s}}</h3>
+
{{#locations}}
<div class="loc">{{name}}</div>
{{/locations}}
@@ -18,7 +19,7 @@
<tbody>
{{#clients}}
<tr>
- <td>{{hostname}}{{^hostname}}{{machineuuid}}{{/hostname}}</td>
+ <td><a href="?do=statistics&amp;uuid={{machineuuid}}">{{hostname}}{{^hostname}}{{machineuuid}}{{/hostname}}</a></td>
<td>{{clientip}}</td>
<td>
<span id="status-{{machineuuid}}" class="machineuuid" data-uuid="{{machineuuid}}"></span>
@@ -46,7 +47,7 @@
function updateStatus(task) {
if (!task || !task.data || !task.data.clientStatus)
return;
- stillActive = true;
+ stillActive = 25;
var clientStatus = task.data.clientStatus;
for (var uuid in clientStatus) {
if (!clientStatus.hasOwnProperty(uuid))
diff --git a/modules-available/rebootcontrol/templates/status-wol.html b/modules-available/rebootcontrol/templates/status-wol.html
index 3e83126c..70517f84 100644
--- a/modules-available/rebootcontrol/templates/status-wol.html
+++ b/modules-available/rebootcontrol/templates/status-wol.html
@@ -1,10 +1,12 @@
+<h3>{{timestamp_s}}</h3>
+
{{#locations}}
<div class="loc">{{name}}</div>
{{/locations}}
<div class="clearfix slx-space"></div>
{{#tasks}}
-<div data-tm-id="{{.}}" data-tm-callback="wolCallback">{{lang_aWolJob}}</div>
+<div data-tm-id="{{.}}" data-tm-callback="wolCallback" data-tm-log="messages">{{lang_aWolJob}}</div>
{{/tasks}}
{{^tasks}}
<div class="alert alert-warning">
@@ -29,24 +31,52 @@
<tbody>
{{#clients}}
<tr>
- <td>{{hostname}}{{^hostname}}{{machineuuid}}{{^machineuuid}}{{clientip}}{{/machineuuid}}{{/hostname}}</td>
- <td>{{clientip}}</td>
- {{#machineuuid}}
<td>
- <span id="status-{{machineuuid}}" class="machineuuid" data-uuid="{{machineuuid}}"></span>
- <span id="spinner-{{machineuuid}}" class="glyphicon glyphicon-refresh slx-rotation">
- </td>
+ {{#machineuuid}}
+ <a href="?do=statistics&amp;uuid={{machineuuid}}">
+ {{hostname}}{{^hostname}}{{machineuuid}}{{/hostname}}
+ </a>
{{/machineuuid}}
{{^machineuuid}}
- <td></td>
+ {{hostname}}{{^hostname}}{{clientip}}{{/hostname}}
+ {{/machineuuid}}
+ </td>
+ <td>{{clientip}}</td>
+ <td>
+ {{#machineuuid}}
+ <span id="status-{{machineuuid}}" class="machineuuid" data-uuid="{{machineuuid}}"></span>
+ <span id="spinner-{{machineuuid}}" class="glyphicon glyphicon-refresh slx-rotation"></span>
{{/machineuuid}}
+ </td>
</tr>
{{/clients}}
</tbody>
</table>
-<script><!--
+<a class="text-muted" href="#debug-out" data-toggle="collapse">Debug</a>
+<pre id="debug-out" class="collapse"></pre>
+
+<script>
function wolCallback(task) {
- stillActive = true;
+ if (task.statusCode === 'TASK_WAITING' || task.statusCode === 'TASK_PROCESSING') {
+ stillActive = 25;
+ } else if (task.data && task.data.result) {
+ var $do = $('#debug-out');
+ var txt = $do.text();
+ var res = task.data.result;
+ for (var k in res) {
+ if (res.hasOwnProperty(k)) {
+ txt += k + ":\n";
+ if (res[k].stdout && res[k].stdout.trimEnd && res[k].stdout.trimEnd()) {
+ txt += res[k].stdout.trimEnd() + "\n";
+ }
+ if (res[k].stderr && res[k].stderr.trimEnd && res[k].stderr.trimEnd()) {
+ txt += res[k].stderr.trimEnd() + "\n";
+ }
+ txt += "Exit " + res[k].exitCode + "\n\n";
+ }
+ }
+ $do.text(txt);
+ }
}
-//--></script> \ No newline at end of file
+</script>
diff --git a/modules-available/rebootcontrol/templates/subnet-edit.html b/modules-available/rebootcontrol/templates/subnet-edit.html
index d8173863..570865c7 100644
--- a/modules-available/rebootcontrol/templates/subnet-edit.html
+++ b/modules-available/rebootcontrol/templates/subnet-edit.html
@@ -39,14 +39,22 @@
</div>
{{/jumpHosts}}
</div>
+ {{#showC2C}}
<div class="list-group-item">
<label>{{lang_reachableFrom}}</label>
{{#sourceNets}}
<div>{{cidr}}</div>
{{/sourceNets}}
</div>
+ {{/showC2C}}
</div>
<div class="panel-footer text-right">
+ <button type="submit" class="btn btn-danger" name="action" value="delete"
+ data-confirm="{{lang_confirmDeleteSubnet}}"
+ {{perms.subnet.edit.disabled}} {{perms.subnet.flag.disabled}}>
+ <span class="glyphicon glyphicon-trash"></span>
+ {{lang_delete}}
+ </button>
<button type="submit" class="btn btn-primary" name="action" value="edit" {{perms.subnet.flag.disabled}}>
<span class="glyphicon glyphicon-floppy-disk"></span>
{{lang_save}}
diff --git a/modules-available/rebootcontrol/templates/subnet-list.html b/modules-available/rebootcontrol/templates/subnet-list.html
index bf6cee1c..2bc9208f 100644
--- a/modules-available/rebootcontrol/templates/subnet-list.html
+++ b/modules-available/rebootcontrol/templates/subnet-list.html
@@ -1,6 +1,6 @@
-<!-- subnetid, start, end, fixed, isdirect, lastdirectcheck, lastseen, seencount -->
+<h2>{{lang_wolReachability}}</h2>
-<h2>{{lang_subnets}}</h2>
+<h3>{{lang_subnets}}</h3>
<p>{{lang_subnetsDescription}}</p>
@@ -20,17 +20,42 @@
{{#subnets}}
<tr>
<td>
- <a href="?do=rebootcontrol&show=subnet&what=subnet&id={{subnetid}}">{{cidr}}</a>
+ <div style="display: inline-block; min-width: 10em">
+ <a href="?do=rebootcontrol&amp;show=subnet&amp;what=subnet&amp;id={{subnetid}}">{{cidr}}</a>
+ </div>
+ <a href="?do=statistics&amp;show=list&amp;filters=clientip={{cidr}}" class="btn btn-default btn-xs">
+ <span class="glyphicon glyphicon-eye-open"></span>
+ </a>
</td>
<td class="text-center">{{#fixed}}<span class="glyphicon glyphicon-lock"></span>{{/fixed}}</td>
<td class="text-center">{{#isdirect}}<span class="glyphicon glyphicon-ok"></span>{{/isdirect}}</td>
- <td class="text-right">{{jumphostcount}} / {{sourcecount}}</td>
- <td class="{{lastseen_class}}">{{lastseen_s}}</td>
+ <td class="text-right"><span class="badge">{{jumphostcount}}</span> / <span class="badge">{{sourcecount}}</span></td>
+ <td class="{{lastseen_class}} text-nowrap">{{lastseen_s}}</td>
</tr>
{{/subnets}}
</tbody>
</table>
+<div class="panel panel-default">
+ <div class="panel-heading">
+ {{lang_help}}
+ </div>
+ <div class="panel-body">
+ <p>
+ <b>{{lang_isFixed}}</b>:
+ {{lang_isFixedHelp}}
+ </p>
+ <p>
+ <b>{{lang_isDirect}}</b>:
+ {{lang_isDirectHelp}}
+ </p>
+ <p>
+ <b>{{lang_wolReachability}}</b>:
+ {{lang_wolReachabilityHelp}}
+ </p>
+ </div>
+</div>
+
<form method="post" action="?do=rebootcontrol">
<input type="hidden" name="token" value="{{token}}">
<input type="hidden" name="show" value="subnet">
diff --git a/modules-available/rebootcontrol/templates/task-list.html b/modules-available/rebootcontrol/templates/task-list.html
index 5ab75675..dcb04450 100644
--- a/modules-available/rebootcontrol/templates/task-list.html
+++ b/modules-available/rebootcontrol/templates/task-list.html
@@ -2,6 +2,7 @@
<table class="table">
<thead>
<tr>
+ <th>{{lang_when}}</th>
<th>{{lang_task}}</th>
<th>{{lang_location}}</th>
<th>{{lang_clientCount}}</th>
@@ -12,6 +13,9 @@
{{#list}}
<tr>
<td class="text-nowrap">
+ {{timestamp_s}}
+ </td>
+ <td class="text-nowrap">
<a href="?do=rebootcontrol&amp;show=task&amp;what=task&amp;taskid={{id}}">{{type}}</a>
<div class="small">{{action}}</div>
</td>