summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimon Rettberg2019-12-07 13:52:54 +0100
committerSimon Rettberg2019-12-07 13:52:54 +0100
commit3e45ec44d22f03ca6642e08f695c6d7274cecfaf (patch)
treea71b2ec81895b198757d8ec7272548b42ee074b8
parent[apis/cron] Simple logging function for debugging (diff)
downloadslx-admin-3e45ec44d22f03ca6642e08f695c6d7274cecfaf.tar.gz
slx-admin-3e45ec44d22f03ca6642e08f695c6d7274cecfaf.tar.xz
slx-admin-3e45ec44d22f03ca6642e08f695c6d7274cecfaf.zip
[statistics/rebootcontrol] Add WOL button to statistics module
* Overhauled task display in rebootcontrol module * Can only add subnets by CIDR now instead of start and end
-rw-r--r--inc/arrayutil.inc.php24
-rw-r--r--modules-available/rebootcontrol/clientscript.js27
-rw-r--r--modules-available/rebootcontrol/hooks/cron.inc.php223
-rw-r--r--modules-available/rebootcontrol/inc/rebootcontrol.inc.php411
-rw-r--r--modules-available/rebootcontrol/inc/rebootqueries.inc.php53
-rw-r--r--modules-available/rebootcontrol/install.inc.php6
-rw-r--r--modules-available/rebootcontrol/lang/de/messages.json9
-rw-r--r--modules-available/rebootcontrol/lang/de/permissions.json3
-rw-r--r--modules-available/rebootcontrol/lang/de/template-tags.json20
-rw-r--r--modules-available/rebootcontrol/page.inc.php42
-rw-r--r--modules-available/rebootcontrol/pages/jumphost.inc.php8
-rw-r--r--modules-available/rebootcontrol/pages/subnet.inc.php42
-rw-r--r--modules-available/rebootcontrol/pages/task.inc.php119
-rw-r--r--modules-available/rebootcontrol/permissions/permissions.json6
-rw-r--r--modules-available/rebootcontrol/style.css30
-rw-r--r--modules-available/rebootcontrol/templates/header.html26
-rw-r--r--modules-available/rebootcontrol/templates/status-reboot.html56
-rw-r--r--modules-available/rebootcontrol/templates/status-wol.html52
-rw-r--r--modules-available/rebootcontrol/templates/subnet-edit.html10
-rw-r--r--modules-available/rebootcontrol/templates/subnet-list.html33
-rw-r--r--modules-available/rebootcontrol/templates/task-header.html4
-rw-r--r--modules-available/rebootcontrol/templates/task-list.html22
-rw-r--r--modules-available/statistics/lang/de/template-tags.json1
-rw-r--r--modules-available/statistics/lang/en/template-tags.json1
-rw-r--r--modules-available/statistics/page.inc.php60
-rw-r--r--modules-available/statistics/pages/list.inc.php4
-rw-r--r--modules-available/statistics/templates/clientlist.html6
27 files changed, 1050 insertions, 248 deletions
diff --git a/inc/arrayutil.inc.php b/inc/arrayutil.inc.php
new file mode 100644
index 00000000..ec6e2a5f
--- /dev/null
+++ b/inc/arrayutil.inc.php
@@ -0,0 +1,24 @@
+<?php
+
+class ArrayUtil
+{
+
+ /**
+ * Take an array of arrays, take given key from each sub-array and return
+ * new array with just those corresponding values.
+ * @param array $list
+ * @param string $key
+ * @return array
+ */
+ public static function flattenByKey($list, $key)
+ {
+ $ret = [];
+ foreach ($list as $item) {
+ if (array_key_exists($key, $item)) {
+ $ret[] = $item[$key];
+ }
+ }
+ return $ret;
+ }
+
+} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/clientscript.js b/modules-available/rebootcontrol/clientscript.js
new file mode 100644
index 00000000..80be2533
--- /dev/null
+++ b/modules-available/rebootcontrol/clientscript.js
@@ -0,0 +1,27 @@
+var stillActive = true;
+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);
+ $.ajax({
+ url: "?do=rebootcontrol",
+ method: "POST",
+ dataType: 'json',
+ data: { token: TOKEN, action: "clientstatus", clients: clients }
+ }).done(function(data) {
+ console.log(data);
+ if (!data)
+ return;
+ for (var e in data) {
+ $('#status-' + e).prop('class', 'glyphicon ' + data[e]);
+ if (!stillActive) $('#spinner-' + e).remove();
+ }
+ });
+ }
+ setTimeout(updateClientStatus, 1000);
+}); \ No newline at end of file
diff --git a/modules-available/rebootcontrol/hooks/cron.inc.php b/modules-available/rebootcontrol/hooks/cron.inc.php
new file mode 100644
index 00000000..79a7ec2a
--- /dev/null
+++ b/modules-available/rebootcontrol/hooks/cron.inc.php
@@ -0,0 +1,223 @@
+<?php
+
+if (/* mt_rand(1, 2) !== 1 || */ Property::get(RebootControl::KEY_AUTOSCAN_DISABLED))
+ return;
+
+class Stuff
+{
+ public static $subnets;
+}
+
+function destSawPw($destTask, $destMachine, $passwd)
+{
+ return strpos($destTask['data']['result'][$destMachine['machineuuid']]['stdout'], "passwd=$passwd") !== false;
+}
+
+function spawnDestinationListener($dstid, &$destMachine, &$destTask, &$destDeadline)
+{
+ $destMachines = Stuff::$subnets[$dstid];
+ cron_log(count($destMachines) . ' potential destination machines for subnet ' . $dstid);
+ shuffle($destMachines);
+ $destMachines = array_slice($destMachines, 0, 3);
+ $destTask = $destMachine = false;
+ $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);
+ Taskmanager::release($destTask);
+ $destDeadline = time() + 10;
+ if (!Taskmanager::isRunning($destTask))
+ continue;
+ sleep(2); // Wait a bit and re-check job is running; only then proceed with this host
+ $destTask = Taskmanager::status($destTask);
+ cron_log("....is {$destTask['statusCode']} {$machine['machineuuid']}");
+ if (Taskmanager::isRunning($destTask)
+ && strpos($destTask['data']['result'][$machine['machineuuid']]['stdout'], 'Running-MARK') !== false) {
+ $destMachine = $machine;
+ break; // GOOD TO GO
+ }
+ cron_log(print_r($destTask, true));
+ cron_log("Dest isn't running or didn't have MARK in output, trying another one...");
+ }
+}
+
+function testClientToClient($srcid, $dstid)
+{
+ $sourceMachines = Stuff::$subnets[$srcid];
+ // Start listener on destination
+ spawnDestinationListener($dstid, $destMachine, $destTask, $destDeadline);
+ if ($destMachine === false || !Taskmanager::isRunning($destTask))
+ return false; // No suitable dest-host found
+ // Find a source host
+ $passwd = sprintf('%02x:%02x:%02x:%02x:%02x:%02x', mt_rand(0, 255), mt_rand(0, 255),
+ mt_rand(0, 255), mt_rand(0, 255), mt_rand(0, 255), mt_rand(0, 255));
+ shuffle($sourceMachines);
+ $sourceMachines = array_slice($sourceMachines, 0, 3);
+ cron_log("Running sending task on "
+ . implode(', ', array_map(function($item) { return $item['clientip']; }, $sourceMachines)));
+ $sourceTask = RebootControl::wakeViaClient($sourceMachines, $destMachine['macaddr'], $destMachine['bcast'], $passwd);
+ Taskmanager::release($sourceTask);
+ if (!Taskmanager::isRunning($sourceTask)) {
+ cron_log('Failed to launch task for source hosts...');
+ return false;
+ }
+ cron_log('Waiting for testing tasks to finish...');
+ // Loop as long as destination task and source task is running and we didn't see the pw at destination yet
+ while (Taskmanager::isRunning($destTask) && Taskmanager::isRunning($sourceTask)
+ && !destSawPw($destTask, $destMachine, $passwd) && $destDeadline > time()) {
+ $sourceTask = Taskmanager::status($sourceTask);
+ usleep(250000);
+ $destTask = Taskmanager::status($destTask);
+ }
+ 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))
+ return 1; // Found pw
+ return 0; // Nothing :-(
+}
+
+function testServerToClient($dstid)
+{
+ spawnDestinationListener($dstid, $destMachine, $destTask, $destDeadline);
+ if ($destMachine === false || !Taskmanager::isRunning($destTask))
+ return false; // No suitable dest-host found
+ $passwd = sprintf('%02x:%02x:%02x:%02x:%02x:%02x', mt_rand(0, 255), mt_rand(0, 255),
+ mt_rand(0, 255), mt_rand(0, 255), mt_rand(0, 255), mt_rand(0, 255));
+ cron_log('Sending WOL packets from Sat Server...');
+ $task = RebootControl::wakeDirectly($destMachine['macaddr'], $destMachine['bcast'], $passwd);
+ usleep(200000);
+ $destTask = Taskmanager::status($destTask);
+ if (!destSawPw($destTask, $destMachine, $passwd) && !Taskmanager::isTask($task))
+ return false;
+ cron_log('Waiting for receive on destination...');
+ $task = Taskmanager::status($task);
+ if (!destSawPw($destTask, $destMachine, $passwd)) {
+ $task = Taskmanager::waitComplete($task, 2000);
+ $destTask = Taskmanager::status($destTask);
+ }
+ cron_log($destTask['data']['result'][$destMachine['machineuuid']]['stdout']);
+ if (destSawPw($destTask, $destMachine, $passwd))
+ return 1;
+ return 0;
+}
+
+/**
+ * Take test result, turn into "next check" timestamp
+ */
+function resultToTime($result)
+{
+ if ($result === false) {
+ // Temporary failure -- couldn't run at least one destination and one source task
+ $next = 7200; // 2 hours
+ } elseif ($result === 0) {
+ // Test finished, subnet not reachable
+ $next = 86400 * 7; // a week
+ } else {
+ // Test finished, reachable
+ $next = 86400 * 30; // a month
+ }
+ return time() + round($next * mt_rand(90, 133) / 100);
+}
+
+/*
+ *
+ */
+
+// First, cleanup: delete orphaned subnets that don't exist anymore, or don't have any clients using our server
+$cutoff = strtotime('-180 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
+ FROM reboot_subnet s
+ INNER JOIN machine m ON (
+ (m.state = 'IDLE' OR m.state = 'OCCUPIED')
+ AND
+ (m.lastseen >= $cutoff)
+ AND
+ (INET_ATON(m.clientip) BETWEEN s.start AND s.end)
+ )");
+
+//cron_log('Machine: ' . $res->rowCount());
+
+if ($res->rowCount() === 0)
+ return;
+
+Stuff::$subnets = [];
+while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ if (!isset(Stuff::$subnets[$row['subnetid']])) {
+ Stuff::$subnets[$row['subnetid']] = [];
+ }
+ Stuff::$subnets[$row['subnetid']][] = $row;
+}
+
+$task = Taskmanager::submit('DummyTask', []);
+$task = Taskmanager::waitComplete($task, 4000);
+if (!Taskmanager::isFinished($task)) {
+ cron_log('Task manager down. Doing nothing.');
+ return; // No :-(
+}
+unset($task);
+
+/*
+ * Try server to client
+ */
+
+$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)) {
+ $dst = (int)$row['subnetid'];
+ cron_log('Direct check for subnetid ' . $dst);
+ $result = testServerToClient($dst);
+ $next = resultToTime($result);
+ if ($result === false) {
+ Database::exec('UPDATE reboot_subnet
+ SET nextdirectcheck = :nextcheck
+ WHERE subnetid = :dst', ['nextcheck' => $next, 'dst' => $dst]);
+ } else {
+ Database::exec('UPDATE reboot_subnet
+ SET nextdirectcheck = :nextcheck, isdirect = :isdirect
+ WHERE subnetid = :dst', ['nextcheck' => $next, 'isdirect' => $result, 'dst' => $dst]);
+ }
+}
+
+/*
+ * Try client to client
+ */
+
+// Query all possible combos
+$combos = [];
+foreach (Stuff::$subnets as $src => $_) {
+ $src = (int)$src;
+ foreach (Stuff::$subnets as $dst => $_) {
+ $dst = (int)$dst;
+ if ($src !== $dst) {
+ $combos[] = [$src, $dst];
+ }
+ }
+}
+
+// Check subnet to subnet
+if (count($combos) > 0) {
+ $res = Database::simpleQuery("SELECT ss.subnetid AS srcid, sd.subnetid AS dstid
+ FROM reboot_subnet ss
+ INNER JOIN reboot_subnet sd ON ((ss.subnetid, sd.subnetid) IN (:combos) AND sd.fixed = 0)
+ LEFT JOIN reboot_subnet_x_subnet sxs ON (ss.subnetid = sxs.srcid AND sd.subnetid = sxs.dstid)
+ WHERE sxs.nextcheck < UNIX_TIMESTAMP() OR sxs.nextcheck IS NULL
+ ORDER BY sxs.nextcheck ASC
+ LIMIT 10", ['combos' => $combos]);
+ cron_log('C2C checks: ' . $res->rowCount());
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $src = (int)$row['srcid'];
+ $dst = (int)$row['dstid'];
+ $result = testClientToClient($src, $dst);
+ $next = resultToTime($result);
+ Database::exec('INSERT INTO reboot_subnet_x_subnet (srcid, dstid, reachable, nextcheck)
+ VALUES (:srcid, :dstid, :reachable, :nextcheck)
+ ON DUPLICATE KEY UPDATE ' . ($result === false ? '' : 'reachable = VALUES(reachable),') . ' nextcheck = VALUES(nextcheck)',
+ ['srcid' => $src, 'dstid' => $dst, 'reachable' => (int)$result, 'nextcheck' => $next]);
+ }
+}
diff --git a/modules-available/rebootcontrol/inc/rebootcontrol.inc.php b/modules-available/rebootcontrol/inc/rebootcontrol.inc.php
index 8a85e3ff..489b0252 100644
--- a/modules-available/rebootcontrol/inc/rebootcontrol.inc.php
+++ b/modules-available/rebootcontrol/inc/rebootcontrol.inc.php
@@ -5,9 +5,14 @@ class RebootControl
const KEY_TASKLIST = 'rebootcontrol.tasklist';
+ const KEY_AUTOSCAN_DISABLED = 'rebootcontrol.disable.scan';
+
const REBOOT = 'REBOOT';
const KEXEC_REBOOT = 'KEXEC_REBOOT';
const SHUTDOWN = 'SHUTDOWN';
+ const TASK_REBOOTCTL = 'TASK_REBOOTCTL';
+ const TASK_WOL = 'WAKE_ON_LAN';
+ const TASK_EXEC = 'REMOTE_EXEC';
/**
* @param string[] $uuids List of machineuuids to reboot
@@ -19,66 +24,136 @@ class RebootControl
$list = RebootQueries::getMachinesByUuid($uuids);
if (empty($list))
return false;
- return self::execute($list, $kexec ? RebootControl::KEXEC_REBOOT : RebootControl::REBOOT, 0, 0);
+ return self::execute($list, $kexec ? RebootControl::KEXEC_REBOOT : RebootControl::REBOOT, 0);
}
/**
- * @param array $list list of clients containing each keys 'machineuuid' and 'clientip'
+ * @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, $locationId)
+ public static function execute($list, $mode, $minutes)
{
$task = Taskmanager::submit("RemoteReboot", array(
"clients" => $list,
"mode" => $mode,
"minutes" => $minutes,
- "locationId" => $locationId,
"sshkey" => SSHKey::getPrivateKey(),
"port" => 9922, // Hard-coded, must match mgmt-sshd module
));
if (!Taskmanager::isFailed($task)) {
- Property::addToList(RebootControl::KEY_TASKLIST, $locationId . '/' . $task["id"], 60 * 24);
+ 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;
+ }
+ }
+ }
+
+ private static function addTask($id, $type, $clients, $taskIds, $other = false)
+ {
+ $lids = ArrayUtil::flattenByKey($clients, 'locationid');
+ $lids = array_unique($lids);
+ $newClients = [];
+ foreach ($clients as $c) {
+ $d = ['clientip' => $c['clientip']];
+ if (isset($c['machineuuid'])) {
+ $d['machineuuid'] = $c['machineuuid'];
+ }
+ $newClients[] = $d;
+ }
+ if (!is_array($taskIds)) {
+ $taskIds = [$taskIds];
+ }
+ $data = [
+ 'id' => $id,
+ 'type' => $type,
+ 'locations' => $lids,
+ 'clients' => $newClients,
+ 'tasks' => $taskIds,
+ ];
+ if (is_array($other)) {
+ $data += $other;
+ }
+ Property::addToList(RebootControl::KEY_TASKLIST, json_encode($data), 20);
+ }
+
/**
* @param int[]|null $locations filter by these locations
- * @return array list of active tasks for reboots/shutdowns.
+ * @return array|false list of active tasks for reboots/shutdowns.
*/
- public static function getActiveTasks($locations = null)
+ public static function getActiveTasks($locations = null, $id = null)
{
- if (is_array($locations) && in_array(0,$locations)) {
+ if (is_array($locations) && in_array(0, $locations)) {
$locations = null;
}
$list = Property::getList(RebootControl::KEY_TASKLIST);
$return = [];
foreach ($list as $entry) {
- $p = explode('/', $entry, 2);
- if (count($p) !== 2) {
+ $p = json_decode($entry, true);
+ if (!is_array($p) || !isset($p['id'])) {
Property::removeFromList(RebootControl::KEY_TASKLIST, $entry);
continue;
}
- if (is_array($locations) && !in_array($p[0], $locations)) // Ignore
+ if (is_array($locations) && is_array($p['locations']) && array_diff($p['locations'], $locations) !== [])
+ continue; // Not allowed
+ if ($id !== null) {
+ if ($p['id'] === $id)
+ return $p;
continue;
- $id = $p[1];
- $task = Taskmanager::status($id);
- if (!Taskmanager::isTask($task)) {
+ }
+ $valid = empty($p['tasks']);
+ if (!$valid) {
+ // Validate at least one task is still valid
+ foreach ($p['tasks'] as $task) {
+ $task = Taskmanager::status($task);
+ if (Taskmanager::isTask($task)) {
+ $p['status'] = $task['statusCode'];
+ $valid = true;
+ break;
+ }
+ }
+ }
+ if (!$valid) {
Property::removeFromList(RebootControl::KEY_TASKLIST, $entry);
continue;
}
- $return[] = [
- 'taskId' => $task['id'],
- 'locationId' => $task['data']['locationId'],
- 'time' => $task['data']['time'],
- 'mode' => $task['data']['mode'],
- 'clientCount' => count($task['data']['clients']),
- 'status' => $task['statusCode'],
- ];
+ $return[] = $p;
}
+ if ($id !== null)
+ return false;
return $return;
}
@@ -96,6 +171,15 @@ class RebootControl
*/
public static function runScript($clients, $command, $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']);
+ }
+ return $task;
+ }
+
+ private static function runScriptInternal(&$clients, $command, $timeout = 5, $privkey = false)
+ {
$valid = [];
$invalid = [];
foreach ($clients as $client) {
@@ -110,7 +194,7 @@ class RebootControl
}
}
if (!empty($invalid)) {
- $res = Database::simpleQuery('SELECT machineuuid, clientip FROM machine WHERE machineuuid IN (:uuids)',
+ $res = Database::simpleQuery('SELECT machineuuid, clientip, locationid FROM machine WHERE machineuuid IN (:uuids)',
['uuids' => array_keys($invalid)]);
while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
if (isset($invalid[$row['machineuuid']])) {
@@ -120,20 +204,21 @@ class RebootControl
}
}
}
+ $clients = $valid;
+ if (empty($clients)) {
+ error_log('RebootControl::runScript called without any clients');
+ return false;
+ }
if ($privkey === false) {
$privkey = SSHKey::getPrivateKey();
}
- $task = Taskmanager::submit('RemoteExec', [
- 'clients' => $valid,
+ return Taskmanager::submit('RemoteExec', [
+ 'clients' => $clients,
'command' => $command,
'timeoutSeconds' => $timeout,
'sshkey' => $privkey,
'port' => 9922, // Fallback if no port given in client struct
]);
- if (!Taskmanager::isFailed($task)) {
- Property::addToList(RebootControl::KEY_TASKLIST, '0/' . $task["id"], 60 * 24);
- }
- return $task;
}
public static function connectionCheckCallback($task, $hostId)
@@ -150,4 +235,270 @@ class RebootControl
['id' => $hostId, 'reachable' => $reachable]);
}
-} \ No newline at end of file
+ /**
+ * @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
+ */
+ public static function wakeViaClient($sourceMachines, $macaddr, $bcast = false, $passwd = false)
+ {
+ $command = 'jawol';
+ if (!empty($bcast)) {
+ $command .= " -d '$bcast'";
+ }
+ 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
+ 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
+ * @return array|false task struct, false on error
+ */
+ public static function wakeDirectly($macaddr, $bcast = false, $passwd = false)
+ {
+ if (!is_array($macaddr)) {
+ $macaddr = [$macaddr];
+ }
+ return Taskmanager::submit('WakeOnLan', [
+ 'ip' => $bcast,
+ 'password' => $passwd,
+ 'macs' => $macaddr,
+ ]);
+ }
+
+ public static function wakeViaJumpHost($jumphost, $bcast, $clients)
+ {
+ $hostid = $jumphost['hostid'];
+ $macs = ArrayUtil::flattenByKey($clients, 'macaddr');
+ if (empty($macs)) {
+ error_log('Called wakeViaJumpHost without clients');
+ return false;
+ }
+ $macs = "'" . implode("' '", $macs) . "'";
+ $macs = str_replace('-', ':', $macs);
+ $script = str_replace(['%IP%', '%MACS%'], [$bcast, $macs], $jumphost['script']);
+ $task = RebootControl::runScriptInternal($_ = [[
+ 'clientip' => $jumphost['host'],
+ 'port' => $jumphost['port'],
+ 'username' => $jumphost['username'],
+ ]], $script, 6, $jumphost['sshkey']);
+ if ($task !== false && isset($task['id'])) {
+ TaskmanagerCallback::addCallback($task, 'rbcConnCheck', $hostid);
+ }
+ return $task;
+ }
+
+ /**
+ * @param array $list list of clients containing each keys 'macaddr' and 'clientip'
+ * @return string id of this job
+ */
+ public static function wakeMachines($list)
+ {
+ /* 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 = [];
+ // Need all subnets...
+ $subnets = [];
+ $res = Database::simpleQuery('SELECT subnetid, start, end, isdirect FROM reboot_subnet');
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $row += [
+ 'jumphosts' => [],
+ 'direct' => [],
+ 'indirect' => [],
+ ];
+ $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;
+ unset($subnet);
+ $subnet = false;
+ foreach ($subnets as &$sn) {
+ if ($sn['start'] <= $ip && $sn['end'] >= $ip) {
+ $subnet =& $sn;
+ 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 (!$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;
+ }
+ }
+ }
+ 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";
+ }
+ }
+ }
+ // 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']);
+ }
+ }
+ }
+ 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";
+ }
+ }
+ }
+ 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";
+ }
+ 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";
+ }
+ $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 (Taskmanager::isFailed($task)) {
+ $errors .= ".... FAILED TO START ACCORDING TASK!\n";
+ return false;
+ }
+ return true;
+ }
+
+ private static function findMachinesForSubnet(&$subnet)
+ {
+ if (isset($subnet['dclients']))
+ return;
+ $cutoff = time() - 302;
+ // Get clients from same subnet first
+ $subnet['dclients'] = Database::queryAll("SELECT machineuuid, 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)
+ 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']);
+ $subnet['iclients'] = array_slice($subnet['iclients'], 0, 3);
+ }
+
+}
diff --git a/modules-available/rebootcontrol/inc/rebootqueries.inc.php b/modules-available/rebootcontrol/inc/rebootqueries.inc.php
index 063b36e4..c0c479bd 100644
--- a/modules-available/rebootcontrol/inc/rebootqueries.inc.php
+++ b/modules-available/rebootcontrol/inc/rebootqueries.inc.php
@@ -3,56 +3,27 @@
class RebootQueries
{
- // Get Client+IP+CurrentVM+CurrentUser+Location to fill the table
- public static function getMachineTable($locationId) {
- $queryArgs = array('cutoff' => strtotime('-30 days'));
- if ($locationId === 0) {
- $where = 'machine.locationid IS NULL';
- } else {
- $where = 'machine.locationid = :locationid';
- $queryArgs['locationid'] = $locationId;
- }
- $leftJoin = '';
- $sessionField = 'machine.currentsession';
- if (Module::get('dozmod') !== false) {
- // SELECT lectureid, displayname FROM sat.lecture WHERE lectureid = :lectureid
- $leftJoin = 'LEFT JOIN sat.lecture ON (lecture.lectureid = machine.currentsession)';
- $sessionField = 'IFNULL(lecture.displayname, machine.currentsession) AS currentsession';
- }
- $res = Database::simpleQuery("
- SELECT machine.machineuuid, machine.hostname, machine.clientip,
- machine.lastboot, machine.lastseen, machine.logintime, machine.state,
- $sessionField, machine.currentuser, machine.locationid
- FROM machine
- $leftJoin
- WHERE $where AND machine.lastseen > :cutoff", $queryArgs);
- $ret = $res->fetchAll(PDO::FETCH_ASSOC);
- foreach ($ret as &$row) {
- if ($row['state'] === 'IDLE' || $row['state'] === 'OCCUPIED') {
- $row['status'] = 1;
- } else {
- $row['status'] = 0;
- }
- if ($row['state'] !== 'OCCUPIED') {
- $row['currentuser'] = '';
- $row['currentsession'] = '';
- }
- }
- return $ret;
- }
-
/**
* Get machines by list of UUIDs
* @param string[] $list list of system UUIDs
* @return array list of machines with machineuuid, hostname, clientip, state and locationid
*/
- public static function getMachinesByUuid($list)
+ public static function getMachinesByUuid($list, $assoc = false, $columns = ['machineuuid', 'hostname', 'clientip', 'state', 'locationid'])
{
if (empty($list))
return array();
- $res = Database::simpleQuery("SELECT machineuuid, hostname, clientip, state, locationid FROM machine
+ if (is_array($columns)) {
+ $columns = implode(',', $columns);
+ }
+ $res = Database::simpleQuery("SELECT $columns FROM machine
WHERE machineuuid IN (:list)", compact('list'));
- return $res->fetchAll(PDO::FETCH_ASSOC);
+ if (!$assoc)
+ return $res->fetchAll(PDO::FETCH_ASSOC);
+ $ret = [];
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $ret[$row['machineuuid']] = $row;
+ }
+ return $ret;
}
} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/install.inc.php b/modules-available/rebootcontrol/install.inc.php
index eb484d3e..0aedfa20 100644
--- a/modules-available/rebootcontrol/install.inc.php
+++ b/modules-available/rebootcontrol/install.inc.php
@@ -8,7 +8,7 @@ $output[] = tableCreate('reboot_subnet', "
`end` INT(10) UNSIGNED NOT NULL,
`fixed` BOOL NOT NULL,
`isdirect` BOOL NOT NULL,
- `lastdirectcheck` INT(10) UNSIGNED NOT NULL DEFAULT '0',
+ `nextdirectcheck` INT(10) UNSIGNED NOT NULL DEFAULT '0',
`lastseen` INT(10) UNSIGNED NOT NULL DEFAULT '0',
`seencount` INT(10) UNSIGNED NOT NULL DEFAULT '0',
PRIMARY KEY (`subnetid`),
@@ -33,9 +33,9 @@ $output[] = tableCreate('reboot_subnet_x_subnet', "
`srcid` INT(10) UNSIGNED NOT NULL,
`dstid` INT(10) UNSIGNED NOT NULL,
`reachable` BOOL NOT NULL,
- `lastcheck` INT(10) UNSIGNED NOT NULL DEFAULT '0',
+ `nextcheck` INT(10) UNSIGNED NOT NULL DEFAULT '0',
PRIMARY KEY (`srcid`, `dstid`),
- KEY `lastcheck` (`lastcheck`)");
+ KEY `nextcheck` (`nextcheck`)");
$output[] = tableAddConstraint('reboot_jumphost_x_subnet', 'hostid', 'reboot_jumphost', 'hostid',
'ON UPDATE CASCADE ON DELETE CASCADE');
diff --git a/modules-available/rebootcontrol/lang/de/messages.json b/modules-available/rebootcontrol/lang/de/messages.json
index 788e57a2..81ddcb67 100644
--- a/modules-available/rebootcontrol/lang/de/messages.json
+++ b/modules-available/rebootcontrol/lang/de/messages.json
@@ -1,10 +1,17 @@
{
+ "invalid-ip-address": "Ung\u00fcltige IP-Adresse: {{0}}",
"invalid-port": "Ung\u00fcltiger Port: {{0}}",
+ "invalid-subnet": "Ung\u00fcltiges Subnet: {{0}}",
"jumphost-saved": "Sprung-Host {{0}} gespeichert",
"no-clients-selected": "Keine Clients ausgew\u00e4hlt",
"no-current-tasks": "Keine aktuellen oder k\u00fcrzlich abgeschlossenen Aufgaben",
"no-such-jumphost": "Sprung-Host {{0}} existiert nicht",
"no-such-task": "Task {{0}} existiert nicht",
"some-machine-not-found": "Einige Clients aus dem POST request wurden nicht gefunden",
- "unknown-task-type": "Unbekannter Task-Typ"
+ "subnet-already-exists": "Subnet existiert bereits",
+ "subnet-created": "Subnet angelegt",
+ "subnet-updated": "Subnet aktualisiert",
+ "unknown-task-type": "Unbekannter Task-Typ",
+ "woldiscover-disabled": "Automatische WOL-Ermittlung deaktiviert",
+ "woldiscover-enabled": "Automatische WOL-Ermittlung aktiviert"
} \ 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 fdc0b35f..405fd9f5 100644
--- a/modules-available/rebootcontrol/lang/de/permissions.json
+++ b/modules-available/rebootcontrol/lang/de/permissions.json
@@ -8,5 +8,6 @@
"newkeypair": "Neues Schl\u00fcsselpaar generieren.",
"subnet.edit": "Subnets hinzuf\u00fcgen\/entfernen.",
"subnet.flag": "Eigenschaften eines Subnets bearbeiten.",
- "subnet.view": "Liste der Subnets sehen."
+ "subnet.view": "Liste der Subnets sehen.",
+ "woldiscover": "Automatische Ermittlung von subnet\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 365a739b..37b01e35 100644
--- a/modules-available/rebootcontrol/lang/de/template-tags.json
+++ b/modules-available/rebootcontrol/lang/de/template-tags.json
@@ -1,5 +1,8 @@
{
"lang_activeTasks": "Laufende Jobs",
+ "lang_add": "Hinzuf\u00fcgen",
+ "lang_addNewSubnet": "Ein Subnet manuell hinzuf\u00fcgen",
+ "lang_assignedJumpHosts": "Zugewiesene Sprung-Hosts",
"lang_assignedSubnets": "Subnets",
"lang_authFail": "Authentifizierung fehlgeschlagen",
"lang_check": "Testen",
@@ -9,7 +12,10 @@
"lang_clientCount": "# Clients",
"lang_connecting": "Verbinde...",
"lang_editJumpHost": "Sprung-Host bearbeiten",
+ "lang_editSubnet": "Subnet bearbeiten",
"lang_error": "Nicht erreichbar",
+ "lang_fixSubnetDesc": "Wenn aktiviert, wird die Erreichbarkeit f\u00fcr ideses Subnet nicht mehr automatisch ermittelt. Sie k\u00f6nnen in diesem Fall selber festlegen, ob das Subnet WOL-Pakete vom Satelliten-Server empfangen kann. Au\u00dferdem wird das Subnet bei Setzen dieser Option nicht mehr automatisch aus der Datenbank gel\u00f6scht, wenn 6 Monate lang kein Client in diesem Subnet gesehen wurde.",
+ "lang_fixSubnetSettings": "Subnet-Einstellungen manuell festlegen",
"lang_genNew": "Neues Schl\u00fcsselpaar generieren",
"lang_host": "Host",
"lang_hostDeleteConfirm": "Diesen Sprung-Host l\u00f6schen?",
@@ -17,8 +23,11 @@
"lang_hostNotReachable": "Host nicht erreichbar",
"lang_hostReachable": "Host erreichbar",
"lang_ip": "IP",
+ "lang_isDirect": "Vom Satellit erreichbar",
+ "lang_isFixed": "Manuell konfiguriert",
"lang_jumpHosts": "Sprung-Hosts",
"lang_keypairConfirmCheck": "Ich bin sicher",
+ "lang_lastseen": "Zuletzt gesehen",
"lang_location": "Standort",
"lang_mode": "Modus",
"lang_moduleHeading": "WakeOnLAN",
@@ -29,14 +38,25 @@
"lang_privkey": "Geheimer Schl\u00fcssel",
"lang_pubKey": "SSH Public Key:",
"lang_reachable": "Erreichbar",
+ "lang_reachableFrom": "Erreichbar von",
+ "lang_reachableFromServer": "Erreichbar vom Satelliten-Server",
+ "lang_reachableFromServerDesc": "Wenn dieser Haken gesetzt ist wird angenommen, dass WOL-Pakete, die vom Server aus gesendet werden, dieses Subnet 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_saveWolAutoDiscover": "Auto-Erkennung ein\/ausschalten",
"lang_settings": "Einstellungen",
"lang_shutdown": "Herunterfahren",
"lang_shutdownAt": "Herunterfahren um: ",
"lang_status": "Status",
+ "lang_subnet": "Subnet",
+ "lang_subnets": "Subnets",
+ "lang_subnetsDescription": "Dies sind dem Satelliten-Server bekannte Subnetze. Damit WOL \u00fcber Subnet-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_taskListIntro": "Hier sehen Sie eine Liste k\u00fcrzlich gestarteter Aufgaben, wie z.B. WOL-Aktionen, das Neustarten oder Herunterfahren von Clients, etc.",
"lang_time": "Zeit",
"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 Subnets automatisch ermitteln",
+ "lang_wolDiscoverDescription": "Ist diese Option aktiv, ermitteln Server und Clients automatisch, welche Netze von wo mittels WOL erreichbar sind.",
+ "lang_wolDiscoverHeading": "Automatische WOL-Ermittlung",
"lang_wolReachability": "WOL-Erreichbarkeit"
} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/page.inc.php b/modules-available/rebootcontrol/page.inc.php
index eaa3c2e6..764a3d7b 100644
--- a/modules-available/rebootcontrol/page.inc.php
+++ b/modules-available/rebootcontrol/page.inc.php
@@ -43,11 +43,23 @@ class Page_RebootControl extends Page
if ($action === 'reboot' || $action === 'shutdown') {
$this->execRebootShutdown($action);
+ } elseif ($action === 'toggle-wol') {
+ User::assertPermission('woldiscover');
+ $enabled = Request::post('enabled', false);
+ Property::set(RebootControl::KEY_AUTOSCAN_DISABLED, !$enabled);
+ if ($enabled) {
+ Message::addInfo('woldiscover-enabled');
+ } else {
+ Message::addInfo('woldiscover-disabled');
+ }
+ $section = 'subnet'; // For redirect below
}
}
if (Request::isPost()) {
Util::redirect('?do=rebootcontrol' . ($section ? '&show=' . $section : ''));
+ } elseif ($section === false) {
+ Util::redirect('?do=rebootcontrol&show=task');
}
}
@@ -70,8 +82,6 @@ class Page_RebootControl extends Page
if (!User::hasPermission('action.' . $action, $actualClients[$idx]['locationid'])) {
Message::addWarning('locations.no-permission-location', $actualClients[$idx]['locationid']);
unset($actualClients[$idx]);
- } else {
- $locationId = $actualClients[$idx]['locationid'];
}
}
// See if anything is left
@@ -96,7 +106,7 @@ class Page_RebootControl extends Page
$mode = 'REBOOT';
$minutes = Request::post('r-minutes', 0, 'int');
}
- $task = RebootControl::execute($actualClients, $mode, $minutes, $locationId);
+ $task = RebootControl::execute($actualClients, $mode, $minutes);
if (Taskmanager::isTask($task)) {
Util::redirect("?do=rebootcontrol&show=task&what=task&taskid=" . $task["id"]);
}
@@ -110,8 +120,11 @@ class Page_RebootControl extends Page
protected function doRender()
{
// Always show public key (it's public, isn't it?)
- $data = ['pubkey' => SSHKey::getPublicKey()];
- Permission::addGlobalTags($data['perms'], null, ['newkeypair']);
+ $data = [
+ 'pubkey' => SSHKey::getPublicKey(),
+ 'wol_auto_checked' => Property::get(RebootControl::KEY_AUTOSCAN_DISABLED) ? '' : 'checked',
+ ];
+ Permission::addGlobalTags($data['perms'], null, ['newkeypair', 'woldiscover']);
Render::addTemplate('header', $data);
if ($this->haveSubpage) {
@@ -127,6 +140,25 @@ class Page_RebootControl extends Page
User::assertPermission("newkeypair");
Property::set("rebootcontrol-private-key", false);
echo SSHKey::getPublicKey();
+ } elseif ($action === 'clientstatus') {
+ $clients = Request::post('clients');
+ if (is_array($clients)) {
+ // XXX No permission check here, should we consider this as leaking sensitive information?
+ $machines = RebootQueries::getMachinesByUuid(array_values($clients), false, ['machineuuid', 'state']);
+ $ret = [];
+ foreach ($machines as $machine) {
+ switch ($machine['state']) {
+ case 'OFFLINE': $val = 'glyphicon-off'; break;
+ case 'IDLE': $val = 'glyphicon-ok green'; break;
+ case 'OCCUPIED': $val = 'glyphicon-user red'; break;
+ case 'STANDBY': $val = 'glyphicon-off green'; break;
+ default: $val = 'glyphicon-question-sign'; break;
+ }
+ $ret[$machine['machineuuid']] = $val;
+ }
+ Header('Content-Type: application/json; charset=utf-8');
+ echo json_encode($ret);
+ }
} else {
echo 'Invalid action.';
}
diff --git a/modules-available/rebootcontrol/pages/jumphost.inc.php b/modules-available/rebootcontrol/pages/jumphost.inc.php
index 111560ef..7dcdd52c 100644
--- a/modules-available/rebootcontrol/pages/jumphost.inc.php
+++ b/modules-available/rebootcontrol/pages/jumphost.inc.php
@@ -30,15 +30,9 @@ class SubPage
private static function execCheckConnection($hostid)
{
$host = self::getJumpHost($hostid);
- $script = str_replace(['%IP%', '%MACS%'], ['255.255.255.255', '00:11:22:33:44:55'], $host['script']);
- $task = RebootControl::runScript([[
- 'clientip' => $host['host'],
- 'port' => $host['port'],
- 'username' => $host['username'],
- ]], $script, 5, $host['sshkey']);
+ $task = RebootControl::wakeViaJumpHost($host, '255.255.255.255', [['macaddr' => '00:11:22:33:44:55']]);
if (!Taskmanager::isTask($task))
return;
- TaskmanagerCallback::addCallback($task, 'rbcConnCheck', $hostid);
Util::redirect('?do=rebootcontrol&show=task&type=checkhost&what=task&taskid=' . $task['id']);
}
diff --git a/modules-available/rebootcontrol/pages/subnet.inc.php b/modules-available/rebootcontrol/pages/subnet.inc.php
index 946d2d64..c38c7595 100644
--- a/modules-available/rebootcontrol/pages/subnet.inc.php
+++ b/modules-available/rebootcontrol/pages/subnet.inc.php
@@ -20,23 +20,16 @@ class SubPage
private static function addSubnet()
{
User::assertPermission('subnet.edit');
- $range = [];
- foreach (['start', 'end'] as $key) {
- $range[$key] = Request::post($key, Request::REQUIRED, 'string');
- $range[$key . '_l'] = ip2long($range[$key]);
- if ($range[$key . '_l'] === false) {
- Message::addError('invalid-ip-address', $range[$key]);
- return;
- }
- }
- if ($range['start_l'] > $range['end_l']) {
- Message::addError('invalid-range', $range['start'], $range['end']);
+ $cidr = Request::post('cidr', Request::REQUIRED, 'string');
+ $range = IpUtil::parseCidr($cidr);
+ if ($range === false) {
+ Message::addError('invalid-cidr', $cidr);
return;
}
$ret = Database::exec('INSERT INTO reboot_subnet (start, end, fixed, isdirect)
VALUES (:start, :end, 1, 0)', [
- 'start' => sprintf('%u', $range['start_l']),
- 'end' => sprintf('%u', $range['end_l']),
+ 'start' => $range['start'],
+ 'end' => $range['end'],
], true);
if ($ret === false) {
Message::addError('subnet-already-exists');
@@ -97,15 +90,15 @@ class SubPage
User::assertPermission('subnet.*');
$nets = [];
$res = Database::simpleQuery('SELECT subnetid, start, end, fixed, isdirect,
- lastdirectcheck, lastseen, seencount, Count(hxs.hostid) AS jumphostcount
- FROM reboot_subnet
+ nextdirectcheck, lastseen, seencount, Count(hxs.hostid) AS jumphostcount, Count(sxs.srcid) AS sourcecount
+ FROM reboot_subnet s
LEFT JOIN reboot_jumphost_x_subnet hxs USING (subnetid)
+ LEFT JOIN reboot_subnet_x_subnet sxs ON (s.subnetid = sxs.dstid AND sxs.reachable <> 0)
GROUP BY subnetid, start, end
ORDER BY start ASC, end DESC');
$deadline = strtotime('-60 days');
while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
- $row['start_s'] = long2ip($row['start']);
- $row['end_s'] = long2ip($row['end']);
+ $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';
@@ -114,7 +107,6 @@ class SubPage
}
$data = ['subnets' => $nets];
Render::addTemplate('subnet-list', $data);
- Module::isAvailable('js_ip');
}
private static function showSubnet()
@@ -127,17 +119,29 @@ class SubPage
Message::addError('invalid-subnet', $id);
return;
}
+ $subnet['cidr'] = IpUtil::rangeToCidr($subnet['start'], $subnet['end']);
$subnet['start_s'] = long2ip($subnet['start']);
$subnet['end_s'] = long2ip($subnet['end']);
+ // Get list of jump hosts
$res = Database::simpleQuery('SELECT h.hostid, h.host, h.port, hxs.subnetid FROM reboot_jumphost h
LEFT JOIN reboot_jumphost_x_subnet hxs ON (h.hostid = hxs.hostid AND hxs.subnetid = :id)
ORDER BY h.host ASC', ['id' => $id]);
+ // Mark those assigned to the current subnet
$jh = [];
while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
$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
+ 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'])];
+ }
+ $subnet['sourceNets'] = $sn;
Permission::addGlobalTags($subnet['perms'], null, ['subnet.flag', 'jumphost.view', 'jumphost.assign-subnet']);
Render::addTemplate('subnet-edit', $subnet);
}
@@ -147,4 +151,4 @@ class SubPage
}
-} \ No newline at end of file
+}
diff --git a/modules-available/rebootcontrol/pages/task.inc.php b/modules-available/rebootcontrol/pages/task.inc.php
index 15449aaf..e52eb981 100644
--- a/modules-available/rebootcontrol/pages/task.inc.php
+++ b/modules-available/rebootcontrol/pages/task.inc.php
@@ -10,6 +10,12 @@ class SubPage
public static function doRender()
{
+ $xxx = Request::get('tasks');
+ if (is_array($xxx)) {
+ $data = array_map(function($item) { return ['id' => $item]; }, $xxx);
+ Render::addTemplate('status-wol', ['tasks' => $data]);
+ return;
+ }
$show = Request::get('what', 'tasklist', 'string');
if ($show === 'tasklist') {
self::showTaskList();
@@ -21,67 +27,110 @@ class SubPage
private static function showTask()
{
$taskid = Request::get("taskid", Request::REQUIRED, 'string');
- $task = Taskmanager::status($taskid);
-
- if (!Taskmanager::isTask($task) || !isset($task['data'])) {
- Message::addError('no-such-task', $taskid);
- return;
- }
-
- $td =& $task['data'];
$type = Request::get('type', false, 'string');
- if ($type === false) {
- // Try to guess
- if (isset($td['locationId']) || isset($td['clients'])) {
- $type = 'reboot';
- } elseif (isset($td['result'])) {
- $type = 'exec';
+ if ($type === 'checkhost') {
+ // Override
+ $task = Taskmanager::status($taskid);
+ if (!Taskmanager::isTask($task) || !isset($task['data'])) {
+ Message::addError('no-such-task', $taskid);
+ return;
}
- }
- if ($type === 'reboot') {
- $data = [
- 'taskId' => $task['id'],
- 'locationId' => $td['locationId'],
- 'locationName' => Location::getName($td['locationId']),
- ];
- $uuids = array_map(function ($entry) {
- return $entry['machineuuid'];
- }, $td['clients']);
- $data['clients'] = RebootQueries::getMachinesByUuid($uuids);
- Render::addTemplate('status-reboot', $data);
- } elseif ($type === 'exec') {
- $data = [
- 'taskId' => $task['id'],
- ];
- Render::addTemplate('status-exec', $data);
- } elseif ($type === 'checkhost') {
+ $td =& $task['data'];
$ip = array_key_first($td['result']);
$data = [
'taskId' => $task['id'],
'host' => $ip,
];
Render::addTemplate('status-checkconnection', $data);
- } else {
+ return;
+ }
+ if ($type !== false) {
Message::addError('unknown-task-type');
}
+
+ $job = RebootControl::getActiveTasks(null, $taskid);
+ if ($job === false) {
+ Message::addError('no-such-task', $taskid);
+ return;
+ }
+ if (isset($job['type'])) {
+ $type = $job['type'];
+ }
+ if ($type === RebootControl::TASK_EXEC) {
+ $template = $perm = 'exec';
+ } elseif ($type === RebootControl::TASK_REBOOTCTL) {
+ $template = 'reboot';
+ if ($job['action'] === RebootControl::SHUTDOWN) {
+ $perm = 'shutdown';
+ } else {
+ $perm = 'reboot';
+ }
+ } elseif ($type == RebootControl::TASK_WOL) {
+ $template = $perm = 'wol';
+ } else {
+ Message::addError('unknown-task-type', $type);
+ return;
+ }
+ if (!empty($job['locations'])) {
+ $allowedLocs = User::getAllowedLocations("action.$perm");
+ if (!in_array(0, $allowedLocs) && array_diff($job['locations'], $allowedLocs) !== []) {
+ Message::addError('main.no-permission');
+ return;
+ }
+ self::expandLocationIds($job['locations']);
+ }
+
+ // Output
+ if ($type === RebootControl::TASK_REBOOTCTL) {
+ $job['clients'] = RebootQueries::getMachinesByUuid(ArrayUtil::flattenByKey($job['clients'], 'machineuuid'));
+ } elseif ($type === RebootControl::TASK_EXEC) {
+ $details = RebootQueries::getMachinesByUuid(ArrayUtil::flattenByKey($job['clients'], 'machineuuid'), true);
+ foreach ($job['clients'] as &$client) {
+ if (isset($client['machineuuid']) && isset($details[$client['machineuuid']])) {
+ $client += $details[$client['machineuuid']];
+ }
+ }
+ } elseif ($type === RebootControl::TASK_WOL) {
+ // Nothing (yet)
+ } else {
+ Util::traceError('oopsie');
+ }
+ Render::addTemplate('status-' . $template, $job);
}
private static function showTaskList()
{
+ Render::addTemplate('task-header');
// Append list of active reboot/shutdown tasks
- $allowedLocs = User::getAllowedLocations("*");
+ $allowedLocs = User::getAllowedLocations("action.*");
$active = RebootControl::getActiveTasks($allowedLocs);
if (empty($active)) {
Message::addInfo('no-current-tasks');
} else {
foreach ($active as &$entry) {
- $entry['locationName'] = Location::getName($entry['locationId']);
+ self::expandLocationIds($entry['locations']);
+ if (isset($entry['clients'])) {
+ $entry['clients'] = count($entry['clients']);
+ }
}
unset($entry);
Render::addTemplate('task-list', ['list' => $active]);
}
}
+ private static function expandLocationIds(&$lids)
+ {
+ foreach ($lids as &$locid) {
+ if ($locid === 0) {
+ $name = '-';
+ } else {
+ $name = Location::getName($locid);
+ }
+ $locid = ['id' => $locid, 'name' => $name];
+ }
+ $lids = array_values($lids);
+ }
+
public static function doAjax()
{
diff --git a/modules-available/rebootcontrol/permissions/permissions.json b/modules-available/rebootcontrol/permissions/permissions.json
index 5983447e..5416a482 100644
--- a/modules-available/rebootcontrol/permissions/permissions.json
+++ b/modules-available/rebootcontrol/permissions/permissions.json
@@ -2,6 +2,9 @@
"newkeypair": {
"location-aware": false
},
+ "woldiscover": {
+ "location-aware": false
+ },
"subnet.view": {
"location-aware": false
},
@@ -28,5 +31,8 @@
},
"action.wol": {
"location-aware": true
+ },
+ "action.exec": {
+ "location-aware": true
}
} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/style.css b/modules-available/rebootcontrol/style.css
index e35bce29..0f96c09f 100644
--- a/modules-available/rebootcontrol/style.css
+++ b/modules-available/rebootcontrol/style.css
@@ -1,25 +1,3 @@
-.rebootTimerForm {
- margin-top: 20px;
-}
-
-.statusColumn {
- text-align: center;
-}
-
-.table > tbody > tr > td {
- vertical-align: middle;
- height: 50px;
-}
-
-.checkbox {
- margin-top: 0;
- margin-bottom: 0;
-}
-
-.select-button {
- min-width: 150px;
-}
-
#dataTable {
margin-top: 20px;
}
@@ -33,6 +11,10 @@ pre {
white-space: pre-wrap;
}
-th[data-sort] {
- cursor: pointer;
+div.loc {
+ margin: 1px 2px;
+ border-radius: 2px;
+ padding: 1px 3px;
+ background: #eee;
+ float: left;
} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/templates/header.html b/modules-available/rebootcontrol/templates/header.html
index 4f240b81..6d38b939 100644
--- a/modules-available/rebootcontrol/templates/header.html
+++ b/modules-available/rebootcontrol/templates/header.html
@@ -15,20 +15,40 @@
<h4 class="modal-title"><b>{{lang_settings}}</b></h4>
</div>
<div class="modal-body">
- <p>{{lang_pubKey}}</p>
+ <label for="pubkey">{{lang_pubKey}}</label>
<pre id="pubkey">{{pubkey}}</pre>
<p>{{lang_newKeypairExplanation}}</p>
<div class="checkbox">
<input {{perms.newkeypair.disabled}} type="checkbox" id="keypair-confirm">
<label for="keypair-confirm">{{lang_keypairConfirmCheck}}</label>
</div>
- </div>
- <div class="modal-footer">
<button {{perms.newkeypair.disabled}} class="btn btn-danger pull-right" id="keypair-button"
onclick="generateNewKeypair()" type="button">
<span class="glyphicon glyphicon-refresh"></span>
{{lang_genNew}}
</button>
+ <div class="clearfix"></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">
+ <div class="checkbox">
+ <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="slx-space"></div>
+ <p>{{lang_wolDiscoverDescription}}</p>
+ <button {{perms.woldiscover.disabled}} class="btn btn-primary pull-right"
+ onclick="generateNewKeypair()" type="submit">
+ <span class="glyphicon glyphicon-floppy-disk"></span>
+ {{lang_saveWolAutoDiscover}}
+ </button>
+ <div class="clearfix"></div>
+ </form>
+ </div>
+ <div class="modal-body">
</div>
</div>
</div>
diff --git a/modules-available/rebootcontrol/templates/status-reboot.html b/modules-available/rebootcontrol/templates/status-reboot.html
index 4be95e81..240c4387 100644
--- a/modules-available/rebootcontrol/templates/status-reboot.html
+++ b/modules-available/rebootcontrol/templates/status-reboot.html
@@ -1,30 +1,35 @@
-<h3>{{lang_location}}: {{locationName}}</h3>
+<h3>{{action}}</h3>
+{{#locations}}
+<div class="loc">{{name}}</div>
+{{/locations}}
+<div class="clearfix slx-space"></div>
-<div>
- <table class="table table-hover stupidtable" id="dataTable">
- <thead>
- <tr>
- <th data-sort="string">{{lang_client}}</th>
- <th data-sort="ipv4">{{lang_ip}}</th>
- <th data-sort="string">
- {{lang_status}}
- </th>
- </tr>
- </thead>
+<table class="table table-hover stupidtable" id="dataTable">
+ <thead>
+ <tr>
+ <th data-sort="string">{{lang_client}}</th>
+ <th data-sort="ipv4">{{lang_ip}}</th>
+ <th data-sort="string">
+ {{lang_status}}
+ </th>
+ </tr>
+ </thead>
- <tbody>
- {{#clients}}
- <tr>
- <td>{{hostname}}{{^hostname}}{{machineuuid}}{{/hostname}}</td>
- <td>{{clientip}}</td>
- <td id="status-{{machineuuid}}"></td>
- </tr>
- {{/clients}}
- </tbody>
- </table>
-</div>
+ <tbody>
+ {{#clients}}
+ <tr>
+ <td>{{hostname}}{{^hostname}}{{machineuuid}}{{/hostname}}</td>
+ <td>{{clientip}}</td>
+ <td>
+ <span id="status-{{machineuuid}}" class="machineuuid" data-uuid="{{machineuuid}}"></span>
+ <span id="text-{{machineuuid}}"></span>
+ </td>
+ </tr>
+ {{/clients}}
+ </tbody>
+</table>
-<div data-tm-id="{{taskId}}" data-tm-log="error" data-tm-callback="updateStatus"></div>
+<div data-tm-id="{{id}}" data-tm-log="error" data-tm-callback="updateStatus"></div>
<script type="application/javascript">
statusStrings = {
@@ -41,11 +46,12 @@
function updateStatus(task) {
if (!task || !task.data || !task.data.clientStatus)
return;
+ stillActive = true;
var clientStatus = task.data.clientStatus;
for (var uuid in clientStatus) {
if (!clientStatus.hasOwnProperty(uuid))
continue;
- var $s = $("#status-" + uuid);
+ var $s = $("#text-" + uuid);
var status = clientStatus[uuid];
if ($s.data('state') === status)
continue;
diff --git a/modules-available/rebootcontrol/templates/status-wol.html b/modules-available/rebootcontrol/templates/status-wol.html
new file mode 100644
index 00000000..da19b57d
--- /dev/null
+++ b/modules-available/rebootcontrol/templates/status-wol.html
@@ -0,0 +1,52 @@
+{{#locations}}
+<div class="loc">{{name}}</div>
+{{/locations}}
+<div class="clearfix slx-space"></div>
+
+{{#tasks}}
+<div data-tm-id="{{id}}" data-tm-callback="wolCallback">{{lang_aWolJob}}</div>
+{{/tasks}}
+{{^tasks}}
+<div class="alert alert-warning">
+ <span class="glyphicon glyphicon-exclamation-sign"></span>
+ {{lang_noTasksForJob}}
+</div>
+{{/tasks}}
+
+<pre>{{log}}</pre>
+
+<table class="table table-hover stupidtable" id="dataTable">
+ <thead>
+ <tr>
+ <th data-sort="string">{{lang_client}}</th>
+ <th data-sort="ipv4">{{lang_ip}}</th>
+ <th data-sort="string">
+ {{lang_status}}
+ </th>
+ </tr>
+ </thead>
+
+ <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}}
+ {{^machineuuid}}
+ <td></td>
+ {{/machineuuid}}
+ </tr>
+ {{/clients}}
+ </tbody>
+</table>
+
+<script><!--
+function wolCallback(task) {
+ stillActive = true;
+}
+//--></script> \ No newline at end of file
diff --git a/modules-available/rebootcontrol/templates/subnet-edit.html b/modules-available/rebootcontrol/templates/subnet-edit.html
index 4c3702ba..d8173863 100644
--- a/modules-available/rebootcontrol/templates/subnet-edit.html
+++ b/modules-available/rebootcontrol/templates/subnet-edit.html
@@ -6,7 +6,7 @@
<input type="hidden" name="id" value="{{subnetid}}">
<div class="panel panel-default">
<div class="panel-heading">
- {{lang_editSubnet}}: <b>{{start_s}} - {{end_s}}</b>
+ {{lang_editSubnet}}: <b>{{cidr}}</b> ({{start_s}}&thinsp;-&thinsp;{{end_s}})
</div>
<div class="list-group">
<div class="list-group-item">
@@ -39,6 +39,12 @@
</div>
{{/jumpHosts}}
</div>
+ <div class="list-group-item">
+ <label>{{lang_reachableFrom}}</label>
+ {{#sourceNets}}
+ <div>{{cidr}}</div>
+ {{/sourceNets}}
+ </div>
</div>
<div class="panel-footer text-right">
<button type="submit" class="btn btn-primary" name="action" value="edit" {{perms.subnet.flag.disabled}}>
@@ -61,4 +67,4 @@ document.addEventListener('DOMContentLoaded', function() {
}).change();
});
-//--></script> \ No newline at end of file
+//--></script>
diff --git a/modules-available/rebootcontrol/templates/subnet-list.html b/modules-available/rebootcontrol/templates/subnet-list.html
index e2747316..c5faae2b 100644
--- a/modules-available/rebootcontrol/templates/subnet-list.html
+++ b/modules-available/rebootcontrol/templates/subnet-list.html
@@ -7,26 +7,22 @@
<table class="table">
<thead>
<tr>
- <th>{{lang_start}}</th>
- <th>{{lang_end}}</th>
+ <th>{{lang_subnet}}</th>
<th class="slx-smallcol">{{lang_isFixed}}</th>
<th class="slx-smallcol">{{lang_isDirect}}</th>
- <th class="slx-smallcol">{{lang_jumphostCount}}</th>
- <th>{{lang_lastseen}}</th>
+ <th class="slx-smallcol">{{lang_wolReachability}}</th>
+ <th class="slx-smallcol">{{lang_lastseen}}</th>
</tr>
</thead>
<tbody>
{{#subnets}}
<tr>
<td>
- <a href="?do=rebootcontrol&show=subnet&what=subnet&id={{subnetid}}">{{start_s}}</a>
+ <a href="?do=rebootcontrol&show=subnet&what=subnet&id={{subnetid}}">{{cidr}}</a>
</td>
- <td>
- <a href="?do=rebootcontrol&show=subnet&what=subnet&id={{subnetid}}">{{end_s}}</a>
- </td>
- <td>{{#fixed}}<span class="glyphicon glyphicon-lock"></span>{{/fixed}}</td>
- <td>{{#isdirect}}<span class="glyphicon glyphicon-ok"></span>{{/isdirect}}</td>
- <td>{{jumphostcount}}</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>
</tr>
{{/subnets}}
@@ -37,22 +33,19 @@
<input type="hidden" name="token" value="{{token}}">
<input type="hidden" name="show" value="subnet">
<div class="list-group">
- <div class="list-group-item cidrmagic">
+ <div class="list-group-item">
<label>{{lang_addNewSubnet}}</label>
<div class="row">
- <div class="col-md-4">
- <input class="form-control cidrstart" type="text" name="start" placeholder="1.2.3.4/24">
- </div>
- <div class="col-md-4">
- <input class="form-control cidrend" type="text" name="end">
+ <div class="col-md-4 col-sm-6">
+ <input class="form-control" type="text" name="cidr" placeholder="1.2.3.0/24">
</div>
- <div class="col-md-4 text-right">
+ <div class="col-md-4 col-sm-6">
<button class="btn btn-primary" name="action" value="add">
<span class="glyphicon glyphicon-floppy-disk"></span>
- {{lang_create}}
+ {{lang_add}}
</button>
</div>
</div>
</div>
</div>
-</form> \ No newline at end of file
+</form>
diff --git a/modules-available/rebootcontrol/templates/task-header.html b/modules-available/rebootcontrol/templates/task-header.html
new file mode 100644
index 00000000..211c16e5
--- /dev/null
+++ b/modules-available/rebootcontrol/templates/task-header.html
@@ -0,0 +1,4 @@
+<p>
+ {{lang_taskListIntro}}
+</p>
+<div class="slx-space"></div> \ No newline at end of file
diff --git a/modules-available/rebootcontrol/templates/task-list.html b/modules-available/rebootcontrol/templates/task-list.html
index 8ae4975f..5ab75675 100644
--- a/modules-available/rebootcontrol/templates/task-list.html
+++ b/modules-available/rebootcontrol/templates/task-list.html
@@ -2,9 +2,8 @@
<table class="table">
<thead>
<tr>
- <th>{{lang_mode}}</th>
+ <th>{{lang_task}}</th>
<th>{{lang_location}}</th>
- <th>{{lang_time}}</th>
<th>{{lang_clientCount}}</th>
<th>{{lang_status}}</th>
</tr>
@@ -12,19 +11,20 @@
<tbody>
{{#list}}
<tr>
- <td>
- <a href="?do=rebootcontrol&amp;show=task&amp;what=task&amp;taskid={{taskId}}">{{mode}}</a>
+ <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>
<td>
- {{locationName}}
+ {{#locations}}
+ <div class="loc">{{name}}</div>
+ {{/locations}}
+ <div class="clearfix"></div>
</td>
- <td>
- {{time}}
+ <td class="text-nowrap">
+ {{clients}}
</td>
- <td>
- {{clientCount}}
- </td>
- <td>
+ <td class="text-nowrap">
{{status}}
</td>
</tr>
diff --git a/modules-available/statistics/lang/de/template-tags.json b/modules-available/statistics/lang/de/template-tags.json
index 277ac651..1a2a6513 100644
--- a/modules-available/statistics/lang/de/template-tags.json
+++ b/modules-available/statistics/lang/de/template-tags.json
@@ -114,6 +114,7 @@
"lang_usageState": "Zustand",
"lang_uuid": "UUID",
"lang_virtualCores": "Virtuelle Kerne",
+ "lang_wakeOnLan": "WakeOnLan",
"lang_when": "Wann",
"lang_withBadSectors": "Clients mit potentiell defekten Festplatten (mehr als 10 defekte Sektoren)"
} \ No newline at end of file
diff --git a/modules-available/statistics/lang/en/template-tags.json b/modules-available/statistics/lang/en/template-tags.json
index 781bceb1..c38f3350 100644
--- a/modules-available/statistics/lang/en/template-tags.json
+++ b/modules-available/statistics/lang/en/template-tags.json
@@ -114,6 +114,7 @@
"lang_usageState": "State",
"lang_uuid": "UUID",
"lang_virtualCores": "Virtual cores",
+ "lang_wakeOnLan": "WakeOnLan",
"lang_when": "When",
"lang_withBadSectors": "Clients with potentially bad HDDs (more than 10 reallocated sectors)"
} \ No newline at end of file
diff --git a/modules-available/statistics/page.inc.php b/modules-available/statistics/page.inc.php
index be838fc0..ff5a59cd 100644
--- a/modules-available/statistics/page.inc.php
+++ b/modules-available/statistics/page.inc.php
@@ -72,6 +72,8 @@ class Page_Statistics extends Page
$this->rebootControl(true);
} elseif ($action === 'shutdownmachines') {
$this->rebootControl(false);
+ } elseif ($action === 'wol') {
+ $this->wol();
}
// Make sure we don't render any content for POST requests - should be handled above and then
@@ -79,6 +81,23 @@ class Page_Statistics extends Page
Util::redirect('?do=statistics');
}
+ private function wol()
+ {
+ if (!Module::isAvailable('rebootcontrol'))
+ return;
+ $ids = Request::post('uuid', [], 'array');
+ $ids = array_values($ids);
+ if (empty($ids)) {
+ Message::addError('main.parameter-empty', 'uuid');
+ return;
+ }
+ $this->getAllowedMachines(".rebootcontrol.action.wol", $ids, $allowedMachines);
+ if (empty($allowedMachines))
+ return;
+ $taskid = RebootControl::wakeMachines($allowedMachines);
+ Util::redirect('?do=rebootcontrol&show=task&what=task&taskid=' . $taskid);
+ }
+
/**
* @param bool $reboot true = reboot, false = shutdown
*/
@@ -92,12 +111,31 @@ class Page_Statistics extends Page
Message::addError('main.parameter-empty', 'uuid');
return;
}
- $allowedLocations = User::getAllowedLocations(".rebootcontrol.action." . ($reboot ? 'reboot' : 'shutdown'));
+ $this->getAllowedMachines(".rebootcontrol.action." . ($reboot ? 'reboot' : 'shutdown'), $ids, $allowedMachines);
+ if (empty($allowedMachines))
+ return;
+ if ($reboot && Request::post('kexec', false)) {
+ $action = RebootControl::KEXEC_REBOOT;
+ } elseif ($reboot) {
+ $action = RebootControl::REBOOT;
+ } else {
+ $action = RebootControl::SHUTDOWN;
+ }
+ $task = RebootControl::execute($allowedMachines, $action, 0);
+ if (Taskmanager::isTask($task)) {
+ Util::redirect("?do=rebootcontrol&show=task&what=task&taskid=" . $task["id"]);
+ }
+ }
+
+ private function getAllowedMachines($permission, $ids, &$allowedMachines)
+ {
+ $allowedLocations = User::getAllowedLocations($permission);
if (empty($allowedLocations)) {
Message::addError('main.no-permission');
Util::redirect('?do=statistics');
}
- $res = Database::simpleQuery('SELECT machineuuid, clientip, locationid FROM machine WHERE machineuuid IN (:ids)', compact('ids'));
+ $res = Database::simpleQuery('SELECT machineuuid, clientip, macaddr, locationid FROM machine
+ WHERE machineuuid IN (:ids)', compact('ids'));
$ids = array_flip($ids);
$allowedMachines = [];
$seenLocations = [];
@@ -114,24 +152,6 @@ class Page_Statistics extends Page
if (!empty($ids)) {
Message::addWarning('unknown-machine', implode(', ', array_keys($ids)));
}
- if (!empty($allowedMachines)) {
- if (count($seenLocations) === 1) {
- $locactionId = (int)array_keys($seenLocations)[0];
- } else {
- $locactionId = 0;
- }
- if ($reboot && Request::post('kexec', false)) {
- $action = RebootControl::KEXEC_REBOOT;
- } elseif ($reboot) {
- $action = RebootControl::REBOOT;
- } else {
- $action = RebootControl::SHUTDOWN;
- }
- $task = RebootControl::execute($allowedMachines, $action, 0, $locactionId);
- if (Taskmanager::isTask($task)) {
- Util::redirect("?do=rebootcontrol&what=task&taskid=" . $task["id"]);
- }
- }
}
private function deleteMachines()
diff --git a/modules-available/statistics/pages/list.inc.php b/modules-available/statistics/pages/list.inc.php
index d1c9f2e9..f223dfb2 100644
--- a/modules-available/statistics/pages/list.inc.php
+++ b/modules-available/statistics/pages/list.inc.php
@@ -59,6 +59,7 @@ class SubPage
$deleteAllowedLocations = User::getAllowedLocations("machine.delete");
$rebootAllowedLocations = User::getAllowedLocations('.rebootcontrol.action.reboot');
$shutdownAllowedLocations = User::getAllowedLocations('.rebootcontrol.action.reboot');
+ $wolAllowedLocations = User::getAllowedLocations('.rebootcontrol.action.wol');
// Only make client clickable if user is allowed to view details page
$detailsAllowedLocations = User::getAllowedLocations("machine.view-details");
while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
@@ -123,8 +124,9 @@ class SubPage
'canReboot' => !empty($rebootAllowedLocations),
'canShutdown' => !empty($shutdownAllowedLocations),
'canDelete' => !empty($deleteAllowedLocations),
+ 'canWol' => !empty($wolAllowedLocations),
);
Render::addTemplate('clientlist', $data);
}
-} \ No newline at end of file
+}
diff --git a/modules-available/statistics/templates/clientlist.html b/modules-available/statistics/templates/clientlist.html
index 5420d65c..6ff9bac7 100644
--- a/modules-available/statistics/templates/clientlist.html
+++ b/modules-available/statistics/templates/clientlist.html
@@ -172,6 +172,12 @@
{{lang_reboot}}
</button>
{{/canReboot}}
+ {{#canWol}}
+ <button type="submit" name="action" value="wol" class="btn btn-primary btn-machine-action">
+ <span class="glyphicon glyphicon-bell"></span>
+ {{lang_wakeOnLan}}
+ </button>
+ {{/canWol}}
{{/rebootcontrol}}
{{#canDelete}}
<button type="submit" name="action" value="delmachines" class="btn btn-danger btn-machine-action"