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.js27
-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.php253
-rw-r--r--modules-available/rebootcontrol/inc/rebootcontrol.inc.php519
-rw-r--r--modules-available/rebootcontrol/inc/rebootqueries.inc.php58
-rw-r--r--modules-available/rebootcontrol/inc/rebootutils.inc.php74
-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.php77
-rw-r--r--modules-available/rebootcontrol/lang/de/messages.json18
-rw-r--r--modules-available/rebootcontrol/lang/de/module.json6
-rw-r--r--modules-available/rebootcontrol/lang/de/permissions.json16
-rw-r--r--modules-available/rebootcontrol/lang/de/template-tags.json84
-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.json88
-rw-r--r--modules-available/rebootcontrol/page.inc.php241
-rw-r--r--modules-available/rebootcontrol/pages/exec.inc.php57
-rw-r--r--modules-available/rebootcontrol/pages/jumphost.inc.php222
-rw-r--r--modules-available/rebootcontrol/pages/subnet.inc.php179
-rw-r--r--modules-available/rebootcontrol/pages/task.inc.php147
-rw-r--r--modules-available/rebootcontrol/permissions/permissions.json30
-rw-r--r--modules-available/rebootcontrol/style.css30
-rw-r--r--modules-available/rebootcontrol/templates/_page.html184
-rw-r--r--modules-available/rebootcontrol/templates/exec-enter-command.html43
-rw-r--r--modules-available/rebootcontrol/templates/header.html125
-rw-r--r--modules-available/rebootcontrol/templates/jumphost-edit.html42
-rw-r--r--modules-available/rebootcontrol/templates/jumphost-list.html63
-rw-r--r--modules-available/rebootcontrol/templates/jumphost-subnets.html28
-rw-r--r--modules-available/rebootcontrol/templates/status-checkconnection.html47
-rw-r--r--modules-available/rebootcontrol/templates/status-exec.html76
-rw-r--r--modules-available/rebootcontrol/templates/status-reboot.html (renamed from modules-available/rebootcontrol/templates/status.html)64
-rw-r--r--modules-available/rebootcontrol/templates/status-wol.html82
-rw-r--r--modules-available/rebootcontrol/templates/subnet-edit.html78
-rw-r--r--modules-available/rebootcontrol/templates/subnet-list.html78
-rw-r--r--modules-available/rebootcontrol/templates/task-header.html4
-rw-r--r--modules-available/rebootcontrol/templates/task-list.html24
41 files changed, 2923 insertions, 608 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
new file mode 100644
index 00000000..447072a0
--- /dev/null
+++ b/modules-available/rebootcontrol/clientscript.js
@@ -0,0 +1,27 @@
+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 <= 0) return;
+ stillActive--;
+ setTimeout(updateClientStatus, Math.max(1, 30 - stillActive) * 1000);
+ $.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 <= 0) $('#spinner-' + e).remove();
+ }
+ });
+ }
+ 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
new file mode 100644
index 00000000..289426c7
--- /dev/null
+++ b/modules-available/rebootcontrol/hooks/cron.inc.php
@@ -0,0 +1,253 @@
+<?php
+
+/*
+ * JumpHost availability test, 5 times a day...
+ */
+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');
+ 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 (Property::get(RebootControl::KEY_AUTOSCAN_DISABLED))
+ return;
+
+class Stuff
+{
+ public static $subnets;
+}
+
+function destSawPw(array $destTask, array $destMachine, string $passwd): bool
+{
+ 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 8 jawol -v -l -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);
+ }
+ // 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))
+ 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);
+ }
+ // 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;
+ 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 * 14; // two weeks
+ }
+ 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('-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, m.locationid
+ 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 = [];
+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);
+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)) . ')');
+foreach ($res as $row) {
+ $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
+ */
+
+if (!Property::get(RebootControl::KEY_SCAN_CLIENT_TO_CLIENT))
+ return;
+
+// 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());
+ foreach ($res as $row) {
+ $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 ec4b84ed..107c2a50 100644
--- a/modules-available/rebootcontrol/inc/rebootcontrol.inc.php
+++ b/modules-available/rebootcontrol/inc/rebootcontrol.inc.php
@@ -5,81 +5,542 @@ class RebootControl
const KEY_TASKLIST = 'rebootcontrol.tasklist';
+ 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';
+ const TASK_REBOOTCTL = 'TASK_REBOOTCTL';
+ const TASK_WOL = 'WAKE_ON_LAN';
+ const TASK_EXEC = 'REMOTE_EXEC';
/**
* @param string[] $uuids List of machineuuids to reboot
* @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 = RebootQueries::getMachinesByUuid($uuids);
+ $list = RebootUtils::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(array $list, string $mode, int $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, ['action' => $mode]);
+ foreach ($list as $client) {
+ $client['mode'] = $mode;
+ $client['minutes'] = $minutes;
+ EventLog::applyFilterRules('#action-power', $client);
+ }
}
return $task;
}
/**
+ * 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);
+ $newClients = [];
+ foreach ($clients as $c) {
+ $d = ['clientip' => $c['clientip']];
+ if (isset($c['machineuuid'])) {
+ $d['machineuuid'] = $c['machineuuid'];
+ }
+ $newClients[] = $d;
+ }
+ $data = [
+ 'id' => $taskId,
+ 'type' => $type,
+ 'locations' => $lids,
+ 'clients' => $newClients,
+ '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;
+ }
+ 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.
+ * @param ?string $id only with this TaskID
+ * @return array|false list of active tasks for reboots/shutdowns.
*/
- public static function getActiveTasks($locations = null)
+ public static function getActiveTasks(array $locations = null, string $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) {
- Property::removeFromList(RebootControl::KEY_TASKLIST, $entry);
+ foreach ($list as $subkey => $entry) {
+ $p = json_decode($entry, true);
+ if (!is_array($p) || !isset($p['id'])) {
+ Property::removeFromListByKey(RebootControl::KEY_TASKLIST, $subkey);
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)) {
- Property::removeFromList(RebootControl::KEY_TASKLIST, $entry);
+ }
+ $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::removeFromListByKey(RebootControl::KEY_TASKLIST, $subkey);
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;
}
-} \ No newline at end of file
+ /**
+ * Execute given command or script on a list of hosts. The list of hosts is an array of structs containing
+ * each a known machine-uuid and/or hostname, and optionally a port to use, which would otherwise default to 9922,
+ * and optionally a username to use, which would default to root.
+ * The command should be compatible with the remote user's default shell (most likely bash).
+ *
+ * @param array $clients [ { clientip: <host>, machineuuid: <uuid>, port: <port>, username: <username> }, ... ]
+ * @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 task struct, false on error
+ */
+ 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);
+ }
+ return $task;
+ }
+
+ private static function runScriptInternal(array &$clients, string $command, int $timeout = 5, $privkey = false)
+ {
+ $valid = [];
+ $invalid = [];
+ foreach ($clients as $client) {
+ if (is_string($client)) {
+ $invalid[strtoupper($client)] = []; // Assume machineuuid
+ } elseif (!isset($client['clientip']) && !isset($client['machineuuid'])) {
+ error_log('RebootControl::runScript called with list entry that has neither IP nor UUID');
+ } elseif (!isset($client['clientip'])) {
+ $invalid[$client['machineuuid']] = $client;
+ } else {
+ $valid[] = $client;
+ }
+ }
+ if (!empty($invalid)) {
+ $res = Database::simpleQuery('SELECT machineuuid, clientip, locationid FROM machine WHERE machineuuid IN (:uuids)',
+ ['uuids' => array_keys($invalid)]);
+ foreach ($res as $row) {
+ if (isset($invalid[$row['machineuuid']])) {
+ $valid[] = $row + $invalid[$row['machineuuid']];
+ } else {
+ $valid[] = $row;
+ }
+ }
+ }
+ $clients = $valid;
+ if (empty($clients)) {
+ error_log('RebootControl::runScript called without any clients');
+ return false;
+ }
+ if ($privkey === false) {
+ $privkey = SSHKey::getPrivateKey();
+ }
+ return Taskmanager::submit('RemoteExec', [
+ 'clients' => $clients,
+ 'command' => $command,
+ 'timeoutSeconds' => $timeout,
+ 'sshkey' => $privkey,
+ 'port' => 9922, // Fallback if no port given in client struct
+ ]);
+ }
+
+ public static function connectionCheckCallback($task, $hostId)
+ {
+ $reachable = 0;
+ if (isset($task['data']['result'])) {
+ foreach ($task['data']['result'] as $res) {
+ if ($res['exitCode'] == 0) {
+ $reachable = 1;
+ }
+ }
+ }
+ Database::exec('UPDATE reboot_jumphost SET reachable = :reachable WHERE hostid = :id',
+ ['id' => $hostId, 'reachable' => $reachable]);
+ }
+
+ /**
+ * Wake clients given by MAC address(es) via jawol util.
+ * Multiple MAC addresses can be passed as a space separated list.
+ */
+ 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'";
+ }
+ $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
+ * @return array|false task struct, false on error
+ */
+ public static function wakeDirectly($macaddr, string $bcast = null, string $passwd = null)
+ {
+ if (!is_array($macaddr)) {
+ $macaddr = [$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]);
+ }
+
+ /**
+ * 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');
+ 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']);
+ $arg = [[
+ 'clientip' => $jumphost['host'],
+ 'port' => $jumphost['port'],
+ 'username' => $jumphost['username'],
+ ]];
+ $task = RebootControl::runScriptInternal($arg, $script, 6, $jumphost['sshkey']);
+ if ($task !== false && isset($task['id'])) {
+ TaskmanagerCallback::addCallback($task, 'rbcConnCheck', $hostid);
+ }
+ return $task;
+ }
+
+ /**
+ * @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(array $clientList, array &$failed = null): ?string
+ {
+ $errors = '';
+ $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');
+ foreach ($res as $row) {
+ $row += [
+ 'djumphosts' => [],
+ 'ijumphosts' => [],
+ ];
+ $subnets[$row['subnetid']] = $row;
+ }
+ // Get all jump hosts
+ 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) {
+ if ($sn['start'] <= $ip && $sn['end'] >= $ip) {
+ $subnet =& $sn;
+ break;
+ }
+ }
+ if ($subnet === false) {
+ $unknown[] = $dbClient;
+ continue;
+ }
+ $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';
+ }
+ 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';
+ }
+ // 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
+ }
+ // "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;
+ }
+ }
+ 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;
+ }
+ }
+ 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;
+ }
+ }
+ $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 null;
+ }
+
+ private static function findMachinesForSubnet(&$subnet)
+ {
+ if (isset($subnet['dclients']))
+ return;
+ $cutoff = time() - 320;
+ // Get clients from same subnet first
+ $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]);
+ // If none, get clients from other subnets known to be able to reach this one
+ $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']);
+ $subnet['iclients'] = array_slice($subnet['iclients'], 0, 3);
+ }
+
+ public static function prepareExec()
+ {
+ User::assertPermission('.rebootcontrol.action.exec');
+ $uuids = array_values(Request::post('uuid', Request::REQUIRED, 'array'));
+ $machines = RebootUtils::getFilteredMachineList($uuids, '.rebootcontrol.action.exec');
+ if ($machines === false)
+ return;
+ $id = mt_rand();
+ Session::set('exec-' . $id, $machines, 60);
+ 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/rebootqueries.inc.php b/modules-available/rebootcontrol/inc/rebootqueries.inc.php
deleted file mode 100644
index 063b36e4..00000000
--- a/modules-available/rebootcontrol/inc/rebootqueries.inc.php
+++ /dev/null
@@ -1,58 +0,0 @@
-<?php
-
-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)
- {
- if (empty($list))
- return array();
- $res = Database::simpleQuery("SELECT machineuuid, hostname, clientip, state, locationid FROM machine
- WHERE machineuuid IN (:list)", compact('list'));
- return $res->fetchAll(PDO::FETCH_ASSOC);
- }
-
-} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/inc/rebootutils.inc.php b/modules-available/rebootcontrol/inc/rebootutils.inc.php
new file mode 100644
index 00000000..e05d90dc
--- /dev/null
+++ b/modules-available/rebootcontrol/inc/rebootutils.inc.php
@@ -0,0 +1,74 @@
+<?php
+
+class RebootUtils
+{
+
+ /**
+ * 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(array $list, bool $assoc = false,
+ array $columns = ['machineuuid', 'hostname', 'clientip', 'state', 'locationid']): array
+ {
+ if (empty($list))
+ return array();
+ $columns = implode(',', $columns);
+ $res = Database::simpleQuery("SELECT $columns FROM machine
+ WHERE machineuuid IN (:list)", compact('list'));
+ if (!$assoc)
+ return $res->fetchAll();
+ $ret = [];
+ foreach ($res as $row) {
+ $ret[$row['machineuuid']] = $row;
+ }
+ return $ret;
+ }
+
+ /**
+ * Sort list of clients so that machines that are up and running come first.
+ * Requires the array elements to have key "state" from machine table.
+ * @param array $clients list of clients
+ */
+ public static function sortRunningFirst(array &$clients): void
+ {
+ usort($clients, function($a, $b) {
+ $a = ($a['state'] === 'IDLE' || $a['state'] === 'OCCUPIED');
+ $b = ($b['state'] === 'IDLE' || $b['state'] === 'OCCUPIED');
+ if ($a === $b)
+ return 0;
+ return $a ? -1 : 1;
+ });
+ }
+
+ /**
+ * Query list of clients (by uuid), taking user context into account, by filtering
+ * by given $permission.
+ * @param array $requestedClients list of uuids
+ * @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(array $requestedClients, string $permission)
+ {
+ $actualClients = RebootUtils::getMachinesByUuid($requestedClients);
+ if (count($actualClients) !== count($requestedClients)) {
+ // We could go ahead an see which ones were not found in DB but this should not happen anyways unless the
+ // user manipulated the request
+ Message::addWarning('some-machine-not-found');
+ }
+ // Filter ones with no permission
+ foreach (array_keys($actualClients) as $idx) {
+ if (!User::hasPermission($permission, $actualClients[$idx]['locationid'])) {
+ Message::addWarning('locations.no-permission-location', $actualClients[$idx]['locationid']);
+ unset($actualClients[$idx]);
+ }
+ }
+ // See if anything is left
+ if (!is_array($actualClients) || empty($actualClients)) {
+ Message::addError('no-clients-selected');
+ return false;
+ }
+ return $actualClients;
+ }
+
+} \ No newline at end of file
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
new file mode 100644
index 00000000..d45a2443
--- /dev/null
+++ b/modules-available/rebootcontrol/install.inc.php
@@ -0,0 +1,77 @@
+<?php
+
+$output = array();
+
+$output[] = tableCreate('reboot_subnet', "
+ `subnetid` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `start` INT(10) UNSIGNED NOT NULL,
+ `end` INT(10) UNSIGNED NOT NULL,
+ `fixed` BOOL NOT NULL,
+ `isdirect` BOOL NOT NULL,
+ `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`),
+ UNIQUE KEY `range` (`start`, `end`)");
+
+$output[] = tableCreate('reboot_jumphost', "
+ `hostid` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `host` VARCHAR(100) NOT NULL,
+ `port` SMALLINT(10) UNSIGNED NOT NULL,
+ `username` VARCHAR(30) NOT NULL,
+ `reachable` BOOL NOT NULL,
+ `sshkey` BLOB NOT NULL,
+ `script` BLOB NOT NULL,
+ PRIMARY KEY (`hostid`)");
+
+$output[] = tableCreate('reboot_jumphost_x_subnet', "
+ `hostid` INT(10) UNSIGNED NOT NULL,
+ `subnetid` INT(10) UNSIGNED NOT NULL,
+ PRIMARY KEY (`hostid`, `subnetid`)");
+
+$output[] = tableCreate('reboot_subnet_x_subnet', "
+ `srcid` INT(10) UNSIGNED NOT NULL,
+ `dstid` INT(10) UNSIGNED NOT NULL,
+ `reachable` BOOL NOT NULL,
+ `nextcheck` INT(10) UNSIGNED NOT NULL DEFAULT '0',
+ 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',
+ 'ON UPDATE CASCADE ON DELETE CASCADE');
+$output[] = tableAddConstraint('reboot_subnet_x_subnet', 'srcid', 'reboot_subnet', 'subnetid',
+ '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 2a7e1299..b481d64a 100644
--- a/modules-available/rebootcontrol/lang/de/messages.json
+++ b/modules-available/rebootcontrol/lang/de/messages.json
@@ -1,4 +1,20 @@
{
+ "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",
- "some-machine-not-found": "Einige Clients aus dem POST request wurden nicht gefunden"
+ "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",
+ "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: {{0}}",
+ "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/module.json b/modules-available/rebootcontrol/lang/de/module.json
index 1f325354..2488fc81 100644
--- a/modules-available/rebootcontrol/lang/de/module.json
+++ b/modules-available/rebootcontrol/lang/de/module.json
@@ -1,4 +1,6 @@
{
- "module_name": "Reboot Control",
- "page_title": "Reboot Control"
+ "jumphosts": "Sprung-Hosts",
+ "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 12ec4c83..589db5b9 100644
--- a/modules-available/rebootcontrol/lang/de/permissions.json
+++ b/modules-available/rebootcontrol/lang/de/permissions.json
@@ -1,5 +1,15 @@
{
- "action.shutdown": "Client herunterfahren.",
- "action.reboot": "Client neustarten.",
- "newkeypair": "Neues Schlüsselpaar generieren."
+ "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 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 c678ef88..b54adbcd 100644
--- a/modules-available/rebootcontrol/lang/de/template-tags.json
+++ b/modules-available/rebootcontrol/lang/de/template-tags.json
@@ -1,39 +1,85 @@
{
+ "lang_aWolJob": "WakeOnLAN-Job",
"lang_activeTasks": "Laufende Jobs",
+ "lang_add": "Hinzuf\u00fcgen",
+ "lang_addNewSubnet": "Ein Subnetz manuell hinzuf\u00fcgen",
+ "lang_assignedJumpHosts": "Zugewiesene Sprung-Hosts",
+ "lang_assignedSubnets": "Zugewiesene Subnetze",
"lang_authFail": "Authentifizierung fehlgeschlagen",
+ "lang_check": "Testen",
+ "lang_checkOutputLabel": "Ausgabe",
+ "lang_checkingJumpHost": "Teste Sprung-Host",
"lang_client": "Client",
"lang_clientCount": "# Clients",
- "lang_confirmNewKeypair": "Wirklich neues Schl\u00fcsselpaar erzeugen?",
+ "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",
"lang_error": "Nicht erreichbar",
+ "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 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_kexecRebootCheck": "Schneller Reboot direkt in bwLehrpool",
+ "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_minutes": " Minuten",
- "lang_mode": "Modus",
+ "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_off": "Aus",
- "lang_on": "An",
+ "lang_noTasksForJob": "Keine Tasks f\u00fcr diesen Job",
+ "lang_numAssignedSubnets": "# Netze",
"lang_online": "Online",
+ "lang_port": "Port",
+ "lang_privkey": "Geheimer Schl\u00fcssel",
"lang_pubKey": "SSH Public Key:",
- "lang_reboot": "Neustarten",
+ "lang_reachable": "Erreichbar",
+ "lang_reachableFrom": "Erreichbar von",
+ "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_rebootButton": "Neustarten",
- "lang_rebootCheck": "Wollen Sie die ausgew\u00e4hlten Rechner wirklich neustarten?",
- "lang_rebootControl": "Reboot Control",
- "lang_rebootIn": "Neustart in:",
"lang_rebooting": "Neustart...",
- "lang_selectall": "Alle ausw\u00e4hlen",
- "lang_selected": "Ausgew\u00e4hlt",
- "lang_session": "Sitzung",
+ "lang_remoteExec": "Ausf\u00fchren",
+ "lang_scriptOrCommand": "Befehl \/ Script",
"lang_settings": "Einstellungen",
"lang_shutdown": "Herunterfahren",
"lang_shutdownAt": "Herunterfahren um: ",
- "lang_shutdownButton": "Herunterfahren",
- "lang_shutdownCheck": "Wollen Sie die ausgew\u00e4hlten Rechner wirklich herunterfahren?",
- "lang_shutdownIn": "Herunterfahren in: ",
"lang_status": "Status",
- "lang_time": "Zeit",
- "lang_unselectall": "Alle abw\u00e4hlen"
+ "lang_stderr": "Standard-Error Ausgabe",
+ "lang_stdout": "Standard-Output Ausgabe",
+ "lang_subnet": "Subnetz",
+ "lang_subnets": "Subnetze",
+ "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_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": "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 1f325354..1308976b 100644
--- a/modules-available/rebootcontrol/lang/en/module.json
+++ b/modules-available/rebootcontrol/lang/en/module.json
@@ -1,4 +1,6 @@
{
- "module_name": "Reboot Control",
- "page_title": "Reboot Control"
+ "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 c64014ff..5740b208 100644
--- a/modules-available/rebootcontrol/lang/en/template-tags.json
+++ b/modules-available/rebootcontrol/lang/en/template-tags.json
@@ -1,39 +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_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_moduleHeading": "WakeOnLAN",
+ "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 3a438504..80eff842 100644
--- a/modules-available/rebootcontrol/page.inc.php
+++ b/modules-available/rebootcontrol/page.inc.php
@@ -3,7 +3,10 @@
class Page_RebootControl extends Page
{
- private $action = false;
+ /**
+ * @var bool whether we have a SubPage from the pages/ subdir
+ */
+ private $haveSubpage = false;
/**
* Called before any page rendering happens - early hook to check parameters etc.
@@ -17,62 +20,87 @@ class Page_RebootControl extends Page
Util::redirect('?do=Main'); // does not return
}
- $this->action = Request::any('action', 'show', 'string');
-
-
- if ($this->action === 'reboot' || $this->action === 'shutdown') {
-
- $requestedClients = Request::post('clients', false, 'array');
- if (!is_array($requestedClients) || empty($requestedClients)) {
- Message::addError('no-clients-selected');
- Util::redirect();
- }
+ if (User::hasPermission('jumphost.*')) {
+ Dashboard::addSubmenu('?do=rebootcontrol&show=jumphost', Dictionary::translate('jumphosts'));
+ }
+ if (User::hasPermission('subnet.*')) {
+ Dashboard::addSubmenu('?do=rebootcontrol&show=subnet', Dictionary::translate('subnets'));
+ }
- $actualClients = RebootQueries::getMachinesByUuid($requestedClients);
- if (count($actualClients) !== count($requestedClients)) {
- // We could go ahead an see which ones were not found in DB but this should not happen anyways unless the
- // user manipulated the request
- Message::addWarning('some-machine-not-found');
+ $section = Request::any('show', false, 'string');
+ if ($section !== false) {
+ $section = preg_replace('/[^a-z]/', '', $section);
+ if (file_exists('modules/rebootcontrol/pages/' . $section . '.inc.php')) {
+ require_once 'modules/rebootcontrol/pages/' . $section . '.inc.php';
+ $this->haveSubpage = true;
+ SubPage::doPreprocess();
+ } else {
+ Message::addError('main.invalid-action', $section);
+ return;
}
- // Filter ones with no permission
- foreach (array_keys($actualClients) as $idx) {
- if (!User::hasPermission('action.' . $this->action, $actualClients[$idx]['locationid'])) {
- Message::addWarning('locations.no-permission-location', $actualClients[$idx]['locationid']);
- unset($actualClients[$idx]);
+ } else {
+ $action = Request::post('action', 'show', 'string');
+
+ if ($action === 'reboot' || $action === 'shutdown') {
+ $this->execRebootShutdown($action);
+ } 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 {
- $locationId = $actualClients[$idx]['locationid'];
+ Message::addInfo('woldiscover-disabled');
}
+ $section = 'subnet'; // For redirect below
}
- // See if anything is left
- if (!is_array($actualClients) || empty($actualClients)) {
- Message::addError('no-clients-selected');
- Util::redirect();
- }
- usort($actualClients, function($a, $b) {
- $a = ($a['state'] === 'IDLE' || $a['state'] === 'OCCUPIED');
- $b = ($b['state'] === 'IDLE' || $b['state'] === 'OCCUPIED');
- if ($a === $b)
- return 0;
- return $a ? -1 : 1;
- });
- if ($this->action === 'shutdown') {
- $mode = 'SHUTDOWN';
- $minutes = Request::post('s-minutes', 0, 'int');
- } elseif (Request::any('quick', false, 'string') === 'on') {
- $mode = 'KEXEC_REBOOT';
- $minutes = Request::post('r-minutes', 0, 'int');
- } else {
- $mode = 'REBOOT';
- $minutes = Request::post('r-minutes', 0, 'int');
- }
- $task = RebootControl::execute($actualClients, $mode, $minutes, $locationId);
- if (Taskmanager::isTask($task)) {
- Util::redirect("?do=rebootcontrol&taskid=" . $task["id"]);
+ }
+
+ if (Request::isPost()) {
+ Util::redirect('?do=rebootcontrol' . ($section ? '&show=' . $section : ''));
+ } elseif ($section === false) {
+ 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");
+ Util::redirect('?do=rebootcontrol&show=subnet');
}
}
+ }
+ private function execRebootShutdown($action)
+ {
+ $requestedClients = Request::post('clients', false, 'array');
+ if (!is_array($requestedClients) || empty($requestedClients)) {
+ Message::addError('no-clients-selected');
+ return;
+ }
+
+ $actualClients = RebootUtils::getFilteredMachineList($requestedClients, 'action.' . $action);
+ if ($actualClients === false)
+ return;
+ RebootUtils::sortRunningFirst($actualClients);
+ if ($action === 'shutdown') {
+ $mode = 'SHUTDOWN';
+ $minutes = Request::post('s-minutes', 0, 'int');
+ } elseif (Request::any('quick', false, 'string') === 'on') {
+ $mode = 'KEXEC_REBOOT';
+ $minutes = Request::post('r-minutes', 0, 'int');
+ } else {
+ $mode = 'REBOOT';
+ $minutes = Request::post('r-minutes', 0, 'int');
+ }
+ $task = RebootControl::execute($actualClients, $mode, $minutes);
+ if (Taskmanager::isTask($task)) {
+ Util::redirect("?do=rebootcontrol&show=task&what=task&taskid=" . $task["id"]);
+ }
}
/**
@@ -81,94 +109,55 @@ class Page_RebootControl extends Page
protected function doRender()
{
- if ($this->action === 'show') {
-
- $data = [];
- $task = Request::get("taskid", false, 'string');
- if ($task !== false) {
- $task = Taskmanager::status($task);
- }
-
- if (Taskmanager::isTask($task)) {
-
- $data['taskId'] = $task['id'];
- $data['locationId'] = $task['data']['locationId'];
- $data['locationName'] = Location::getName($task['data']['locationId']);
- $uuids = array_map(function($entry) {
- return $entry['machineuuid'];
- }, $task['data']['clients']);
- $data['clients'] = RebootQueries::getMachinesByUuid($uuids);
- Render::addTemplate('status', $data);
-
- } else {
-
- //location you want to see, default are "not assigned" clients
- $requestedLocation = Request::get('location', false, 'int');
- $allowedLocs = User::getAllowedLocations("action.*");
- if (empty($allowedLocs)) {
- User::assertPermission('action.*');
- }
-
- if ($requestedLocation === false) {
- if (in_array(0, $allowedLocs)) {
- $requestedLocation = 0;
- } else {
- $requestedLocation = reset($allowedLocs);
- }
- }
-
- $data['locations'] = Location::getLocations($requestedLocation, 0, true);
-
- // disable each location user has no permission for
- foreach ($data['locations'] as &$loc) {
- if (!in_array($loc["locationid"], $allowedLocs)) {
- $loc["disabled"] = "disabled";
- } elseif ($loc["locationid"] == $requestedLocation) {
- $data['location'] = $loc['locationname'];
- }
- }
- // Always show public key (it's public, isn't it?)
- $data['pubKey'] = SSHKey::getPublicKey();
-
- // Only enable shutdown/reboot-button if user has permission for the location
- Permission::addGlobalTags($data['perms'], $requestedLocation, ['newkeypair', 'action.shutdown', 'action.reboot']);
-
- Render::addTemplate('header', $data);
-
- // only fill table if user has at least one permission for the location
- if (!in_array($requestedLocation, $allowedLocs)) {
- Message::addError('locations.no-permission-location', $requestedLocation);
- } else {
- $data['data'] = RebootQueries::getMachineTable($requestedLocation);
- Render::addTemplate('_page', $data);
- }
-
- // Append list of active reboot/shutdown tasks
- $active = RebootControl::getActiveTasks($allowedLocs);
- if (!empty($active)) {
- foreach ($active as &$entry) {
- $entry['locationName'] = Location::getName($entry['locationId']);
- }
- unset($entry);
- Render::addTemplate('task-list', ['list' => $active]);
- }
-
- }
+ $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();
}
}
- function doAjax()
+ protected function doAjax()
{
- $this->action = Request::post('action', false, 'string');
- if ($this->action === 'generateNewKeypair') {
+ $action = Request::post('action', false, 'string');
+ if ($action === 'generateNewKeypair') {
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 = RebootUtils::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/exec.inc.php b/modules-available/rebootcontrol/pages/exec.inc.php
new file mode 100644
index 00000000..6b5ea407
--- /dev/null
+++ b/modules-available/rebootcontrol/pages/exec.inc.php
@@ -0,0 +1,57 @@
+<?php
+
+class SubPage
+{
+
+ public static function doPreprocess()
+ {
+ $action = Request::post('action', false, 'string');
+ if ($action === 'exec') {
+ self::execExec();
+ }
+ }
+
+ private static function execExec()
+ {
+ $uuids = array_values(Request::post('uuid', Request::REQUIRED, 'array'));
+ $machines = RebootUtils::getFilteredMachineList($uuids, 'action.exec');
+ if (empty($machines))
+ return;
+ RebootUtils::sortRunningFirst($machines);
+ $script = preg_replace('/\r\n?/', "\n", Request::post('script', Request::REQUIRED, 'string'));
+ $task = RebootControl::runScript($machines, $script);
+ if (Taskmanager::isTask($task)) {
+ Util::redirect("?do=rebootcontrol&show=task&what=task&taskid=" . $task["id"]);
+ }
+ }
+
+ /*
+ * Render
+ */
+
+ public static function doRender()
+ {
+ $what = Request::get('what', 'list', 'string');
+ if ($what === 'prepare') {
+ self::showPrepare();
+ }
+ }
+
+ private static function showPrepare()
+ {
+ $id = Request::get('id', Request::REQUIRED, 'int');
+ $machines = Session::get('exec-' . $id);
+ if (!is_array($machines)) {
+ Message::addError('unknown-exec-job', $id);
+ return;
+ }
+ Session::set('exec-' . $id, false);
+ Render::addTemplate('exec-enter-command', ['clients' => $machines, 'id' => $id]);
+ }
+
+ public static function doAjax()
+ {
+
+ }
+
+} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/pages/jumphost.inc.php b/modules-available/rebootcontrol/pages/jumphost.inc.php
new file mode 100644
index 00000000..d9aae234
--- /dev/null
+++ b/modules-available/rebootcontrol/pages/jumphost.inc.php
@@ -0,0 +1,222 @@
+<?php
+
+class SubPage
+{
+
+ public static function doPreprocess()
+ {
+ $action = Request::post('action', false, 'string');
+ if ($action === 'save') {
+ self::saveJumpHost();
+ } elseif ($action === 'assign') {
+ self::saveSubnetAssignment();
+ } elseif ($action === 'list') {
+ self::listAction();
+ }
+ }
+
+ /*
+ * POST
+ */
+
+ private static function listAction()
+ {
+ $id = Request::post('checkid', false, 'int');
+ if ($id !== false) {
+ // Check connectivity
+ User::assertPermission('jumphost.edit');
+ 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))
+ return;
+ 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');
+ $id = Request::post('hostid', Request::REQUIRED, 'string');
+ $host = Request::post('host', Request::REQUIRED, 'string');
+ $port = Request::post('port', Request::REQUIRED, 'int');
+ if ($port < 1 || $port > 65535) {
+ Message::addError('invalid-port', $port);
+ return;
+ }
+ $username = Request::post('username', Request::REQUIRED, 'string');
+ $sshkey = Request::post('sshkey', Request::REQUIRED, 'string');
+ $script = preg_replace('/\r\n?/', "\n", Request::post('script', Request::REQUIRED, 'string'));
+ if ($id === 'new') {
+ $ret = Database::exec('INSERT INTO reboot_jumphost (host, port, username, sshkey, script, reachable)
+ VALUE (:host, :port, :username, :sshkey, :script, 0)', compact('host', 'port', 'username', 'sshkey', 'script'));
+ $id = Database::lastInsertId();
+ } else {
+ $ret = Database::exec('UPDATE reboot_jumphost SET
+ host = :host, port = :port, username = :username, sshkey = :sshkey, script = :script, reachable = 0
+ WHERE hostid = :id', compact('host', 'port', 'username', 'sshkey', 'script', 'id'));
+ if ($ret === 0) {
+ $ret = Database::queryFirst('SELECT hostid FROM reboot_jumphost WHERE hostid = :id', ['id' => $id]);
+ if ($ret !== false) {
+ $ret = 1;
+ }
+ }
+ }
+ if ($ret > 0) {
+ Message::addSuccess('jumphost-saved', $host);
+ self::execCheckConnection($id);
+ } else {
+ Message::addError('no-such-jumphost', $id);
+ }
+ }
+
+ private static function saveSubnetAssignment()
+ {
+ User::assertPermission('jumphost.edit');
+ $id = Request::post('hostid', Request::REQUIRED, 'string');
+ $host = self::getJumpHost($id);
+ $nets = Request::post('subnet', [], 'array');
+ if (empty($nets)) {
+ Database::exec('DELETE FROM reboot_jumphost_x_subnet WHERE hostid = :id', ['id' => $id]);
+ } else {
+ $nets = array_keys($nets);
+ Database::exec('DELETE FROM reboot_jumphost_x_subnet WHERE hostid = :id AND subnetid NOT IN (:nets)',
+ ['id' => $id, 'nets' => $nets]);
+ $nets = array_map(function($item) use ($id) {
+ return [$id, $item];
+ }, $nets);
+ Database::exec('INSERT IGNORE INTO reboot_jumphost_x_subnet (hostid, subnetid) VALUES :nets', ['nets' => $nets]);
+ }
+ Message::addSuccess('jumphost-saved', $host['host']);
+ }
+
+ /*
+ * Render
+ */
+
+ public static function doRender()
+ {
+ $what = Request::get('what', 'list', 'string');
+ if ($what === 'edit') {
+ self::showJumpHost();
+ } elseif ($what === 'assign') {
+ self::showAssignSubnets();
+ } else {
+ self::showJumpHosts();
+ }
+ }
+
+ private static function showJumpHosts()
+ {
+ User::assertPermission('jumphost.*');
+ $hosts = [];
+ $res = Database::simpleQuery('SELECT hostid, host, port, Count(jxs.subnetid) AS subnetCount, reachable
+ FROM reboot_jumphost jh
+ LEFT JOIN reboot_jumphost_x_subnet jxs USING (hostid)
+ GROUP BY hostid
+ ORDER BY hostid');
+ foreach ($res as $row) {
+ $hosts[] = $row;
+ }
+ $data = [
+ 'jumpHosts' => $hosts
+ ];
+ Permission::addGlobalTags($data['perms'], null, ['jumphost.edit', 'jumphost.assign-subnet']);
+ Render::addTemplate('jumphost-list', $data);
+ }
+
+ private static function showJumpHost()
+ {
+ User::assertPermission('jumphost.edit');
+ $id = Request::get('id', Request::REQUIRED, 'string');
+ if ($id === 'new') {
+ $host = ['hostid' => 'new', 'port' => 22, 'script' => "# Assume bash\n"
+ . "MACS='%MACS%'\n"
+ . "IP='%IP%'\n"
+ . "EW=false\n"
+ . "WOL=false\n"
+ . "command -v etherwake > /dev/null && ( [ \"\$(id -u)\" = 0 ] || [ -u \"\$(which etherwake)\" ] ) && EW=true\n"
+ . "command -v wakeonlan > /dev/null && WOL=true\n"
+ . "if \$EW && ( ! \$WOL || [ \"\$IP\" = '255.255.255.255' ] ); then\n"
+ . "\tifaces=\"\$(ls -1 /sys/class/net/)\"\n"
+ . "\t[ -z \"\$ifaces\" ] && ifaces=eth0\n"
+ . "\tfor ifc in \$ifaces; do\n"
+ . "\t\t[ \"\$ifc\" = 'lo' ] && continue\n"
+ . "\t\tfor mac in \$MACS; do\n"
+ . "\t\t\tetherwake -i \"\$ifc\" \"\$mac\"\n"
+ . "\t\tdone\n"
+ . "\tdone\n"
+ . "elif \$WOL; then\n"
+ . "\twakeonlan -i \"\$IP\" \$MACS\n"
+ . "else\n"
+ . "\techo 'No suitable WOL tool found' >&2\n"
+ . "\texit 1\n"
+ . "fi\n"];
+ } else {
+ $host = self::getJumpHost($id);
+ }
+ Render::addTemplate('jumphost-edit', $host);
+ }
+
+ private static function showAssignSubnets()
+ {
+ User::assertPermission('jumphost.assign-subnet');
+ $id = Request::get('id', Request::REQUIRED, 'int');
+ $host = self::getJumpHost($id);
+ $res = Database::simpleQuery('SELECT s.subnetid, s.start, s.end, jxs.hostid FROM reboot_subnet s
+ LEFT JOIN reboot_jumphost_x_subnet jxs ON (s.subnetid = jxs.subnetid AND jxs.hostid = :id)
+ ORDER BY start ASC',
+ ['id' => $id]);
+ $list = [];
+ foreach ($res as $row) {
+ $row['cidr'] = IpUtil::rangeToCidr($row['start'], $row['end']);
+ if ($row['hostid'] !== null) {
+ $row['checked'] = 'checked';
+ }
+ $list[] = $row;
+ }
+ $host['list'] = $list;
+ Render::addTemplate('jumphost-subnets', $host);
+ }
+
+ public static function doAjax()
+ {
+
+ }
+
+ /*
+ * MISC
+ */
+
+ private static function getJumpHost($hostid)
+ {
+ $host = Database::queryFirst('SELECT hostid, host, port, username, sshkey, script
+ FROM reboot_jumphost
+ WHERE hostid = :id', ['id' => $hostid]);
+ if ($host === false) {
+ Message::addError('no-such-jumphost', $hostid);
+ Util::redirect('?do=rebootcontrol');
+ }
+ return $host;
+ }
+
+} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/pages/subnet.inc.php b/modules-available/rebootcontrol/pages/subnet.inc.php
new file mode 100644
index 00000000..a6d8d837
--- /dev/null
+++ b/modules-available/rebootcontrol/pages/subnet.inc.php
@@ -0,0 +1,179 @@
+<?php
+
+class SubPage
+{
+
+ public static function doPreprocess()
+ {
+ $action = Request::post('action', false, 'string');
+ if ($action === 'add') {
+ self::addSubnet();
+ } elseif ($action === 'edit') {
+ self::editSubnet();
+ } elseif ($action === 'delete') {
+ self::deleteSubnet();
+ }
+ }
+
+ /*
+ * POST
+ */
+
+ private static function addSubnet()
+ {
+ User::assertPermission('subnet.edit');
+ $cidr = Request::post('cidr', Request::REQUIRED, 'string');
+ $range = IpUtil::parseCidr($cidr);
+ if ($range === null) {
+ Message::addError('invalid-cidr', $cidr);
+ return;
+ }
+ $ret = Database::exec('INSERT INTO reboot_subnet (start, end, fixed, isdirect)
+ VALUES (:start, :end, 1, 0)', [
+ 'start' => $range['start'],
+ 'end' => $range['end'],
+ ], true);
+ if ($ret === false) {
+ Message::addError('subnet-already-exists');
+ } else {
+ Message::addSuccess('subnet-created');
+ Util::redirect('?do=rebootcontrol&show=subnet&what=subnet&id=' . Database::lastInsertId());
+ }
+ }
+
+ private static function editSubnet()
+ {
+ User::assertPermission('subnet.flag');
+ $id = Request::post('id', Request::REQUIRED, 'int');
+ $subnet = Database::queryFirst('SELECT subnetid
+ FROM reboot_subnet WHERE subnetid = :id', ['id' => $id]);
+ if ($subnet === false) {
+ Message::addError('invalid-subnet', $id);
+ return;
+ }
+ $params = [
+ 'id' => $id,
+ 'fixed' => !empty(Request::post('fixed', false, 'string')),
+ 'isdirect' => !empty(Request::post('isdirect', false, 'string')),
+ ];
+ Database::exec('UPDATE reboot_subnet SET fixed = :fixed, isdirect = If(:fixed, :isdirect, isdirect)
+ WHERE subnetid = :id', $params);
+ 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', ['id' => $id]);
+ } else {
+ $hosts = array_keys($hosts);
+ Database::exec('DELETE FROM reboot_jumphost_x_subnet WHERE subnetid = :id AND hostid NOT IN (:hosts)',
+ ['id' => $id, 'hosts' => $hosts]);
+ $hosts = array_map(function($item) use ($id) {
+ return [$item, $id];
+ }, $hosts);
+ Database::exec('INSERT IGNORE INTO reboot_jumphost_x_subnet (hostid, subnetid) VALUES :hosts', ['hosts' => $hosts]);
+ }
+ }
+ 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
+ */
+
+ public static function doRender()
+ {
+ $what = Request::get('what', 'list', 'string');
+ if ($what === 'list') {
+ self::showSubnets();
+ } elseif ($what === 'subnet') {
+ self::showSubnet();
+ }
+ }
+
+ private static function showSubnets()
+ {
+ 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
+ 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');
+ 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];
+ Render::addTemplate('subnet-list', $data);
+ }
+
+ private static function showSubnet()
+ {
+ User::assertPermission('subnet.*');
+ $id = Request::get('id', Request::REQUIRED, 'int');
+ $subnet = Database::queryFirst('SELECT subnetid, start, end, fixed, isdirect
+ FROM reboot_subnet WHERE subnetid = :id', ['id' => $id]);
+ if ($subnet === false) {
+ 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 = [];
+ foreach ($res as $row) {
+ $row['checked'] = $row['subnetid'] === null ? '' : 'checked';
+ $jh[] = $row;
+ }
+ $subnet['jumpHosts'] = $jh;
+ $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 = [];
+ foreach ($res as $row) {
+ $sn[] = ['cidr' => IpUtil::rangeToCidr($row['start'], $row['end'])];
+ }
+ $subnet['sourceNets'] = $sn;
+ $subnet['showC2C'] = true;
+ }
+ Permission::addGlobalTags($subnet['perms'], null, ['subnet.flag', 'jumphost.view', 'jumphost.assign-subnet']);
+ Render::addTemplate('subnet-edit', $subnet);
+ }
+
+ public static function doAjax()
+ {
+
+ }
+
+}
diff --git a/modules-available/rebootcontrol/pages/task.inc.php b/modules-available/rebootcontrol/pages/task.inc.php
new file mode 100644
index 00000000..7db2a90b
--- /dev/null
+++ b/modules-available/rebootcontrol/pages/task.inc.php
@@ -0,0 +1,147 @@
+<?php
+
+class SubPage
+{
+
+ public static function doPreprocess()
+ {
+
+ }
+
+ 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();
+ } elseif ($show === 'task') {
+ self::showTask();
+ }
+ }
+
+ 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') {
+ // Override
+ $task = Taskmanager::status($taskid);
+ if (!Taskmanager::isTask($task) || !isset($task['data'])) {
+ Message::addError('no-such-task', $taskid);
+ return;
+ }
+ $td =& $task['data'];
+ $ip = array_key_first($td['result']);
+ $data = [
+ 'taskId' => $task['id'],
+ 'host' => $ip,
+ ];
+ Render::addTemplate('status-checkconnection', $data);
+ 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'] = RebootUtils::getMachinesByUuid(ArrayUtil::flattenByKey($job['clients'], 'machineuuid'));
+ } elseif ($type === RebootControl::TASK_EXEC) {
+ $details = RebootUtils::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 {
+ ErrorHandler::traceError('oopsie');
+ }
+ $job['timestamp_s'] = Util::prettyTime($job['timestamp']);
+ Render::addTemplate('status-' . $template, $job);
+ }
+
+ private static function showTaskList()
+ {
+ 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');
+ } else {
+ foreach ($active as &$entry) {
+ self::expandLocationIds($entry['locations']);
+ 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]);
+ }
+ }
+
+ private static function expandLocationIds(&$lids)
+ {
+ foreach ($lids as &$locid) {
+ if ($locid === null || $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 a058ffbf..a508f8b6 100644
--- a/modules-available/rebootcontrol/permissions/permissions.json
+++ b/modules-available/rebootcontrol/permissions/permissions.json
@@ -2,10 +2,40 @@
"newkeypair": {
"location-aware": false
},
+ "woldiscover": {
+ "location-aware": false
+ },
+ "subnet.view": {
+ "location-aware": false
+ },
+ "subnet.edit": {
+ "location-aware": false
+ },
+ "subnet.flag": {
+ "location-aware": false
+ },
+ "jumphost.view": {
+ "location-aware": false
+ },
+ "jumphost.edit": {
+ "location-aware": false
+ },
+ "jumphost.assign-subnet": {
+ "location-aware": false
+ },
"action.reboot": {
"location-aware": true
},
"action.shutdown": {
"location-aware": true
+ },
+ "action.wol": {
+ "location-aware": true
+ },
+ "action.exec": {
+ "location-aware": true
+ },
+ "action.view": {
+ "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/_page.html b/modules-available/rebootcontrol/templates/_page.html
deleted file mode 100644
index a124e165..00000000
--- a/modules-available/rebootcontrol/templates/_page.html
+++ /dev/null
@@ -1,184 +0,0 @@
-<h3>{{location}}</h3>
-
-<form method="post" action="?do=rebootcontrol" class="form-inline">
- <input type="hidden" name="token" value="{{token}}">
- <div class="row">
- <div class="col-md-12">
- <table class="table table-condensed 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>
- <th data-sort="string">{{lang_session}}</th>
- <th data-sort="string">{{lang_user}}</th>
- <th data-sort="int" data-sort-default="desc">{{lang_selected}}</th>
- </tr>
- </thead>
-
- <tbody>
- {{#data}}
- <tr>
- <td>
- {{hostname}}
- {{^hostname}}{{clientip}}{{/hostname}}
- </td>
- <td>{{clientip}}</td>
- <td class="statusColumn">
- {{#status}}
- <span class="text-success">{{lang_on}}</span>
- {{/status}}
- {{^status}}
- <span class="text-danger">{{lang_off}}</span>
- {{/status}}
- </td>
- <td>{{#status}}{{currentsession}}{{/status}}</td>
- <td>{{#status}}{{currentuser}}{{/status}}</td>
- <td data-sort-value="0" class="checkboxColumn slx-smallcol">
- <div class="checkbox">
- <input id="m-{{machineuuid}}" type="checkbox" name="clients[]" value='{{machineuuid}}'>
- <label for="m-{{machineuuid}}"></label>
- </div>
- </td>
- </tr>
- {{/data}}
- </tbody>
- </table>
- </div>
- </div>
-
- <!-- Modals -->
- <div class ="modal fade" id="rebootModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
- <div class="modal-dialog" role="document">
- <div class="modal-content">
- <div class="modal-header">
- <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
- <h4 class="modal-title" id="myModalLabel">{{lang_rebootButton}}</h4>
- </div>
- <div class="modal-body">
- <div>{{lang_rebootCheck}}</div>
- <div>{{lang_rebootIn}} <input name="r-minutes" title="{{lang_shutdownIn}}" type="number" value="0" min="0" pattern="\d+"> {{lang_minutes}}</div>
- <div>
- <div class="checkbox checkbox-inline">
- <input name="quick" type="checkbox" value="on" id="rb-quick">
- <label for="rb-quick">{{lang_kexecRebootCheck}}</label>
- </div>
- </div>
- </div>
- <div class="modal-footer">
- <button type="button" class="btn btn-default" data-dismiss="modal">{{lang_cancel}}</button>
- <button type="submit" {{perms.action.reboot.disabled}} name="action" value="reboot" class="btn btn-warning">
- <span class="glyphicon glyphicon-repeat"></span>
- {{lang_reboot}}
- </button>
- </div>
- </div>
- </div>
- </div>
-
- <div class ="modal fade" id="shutdownModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel2">
- <div class="modal-dialog" role="document">
- <div class="modal-content">
- <div class="modal-header">
- <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
- <h4 class="modal-title" id="myModalLabel2">{{lang_shutdownButton}}</h4>
- </div>
- <div class="modal-body">
- <div>{{lang_shutdownCheck}}</div>
- {{lang_shutdownIn}} <input name="s-minutes" title="{{lang_shutdownIn}}" type="number" value="0" min="0" pattern="\d+"> {{lang_minutes}}
- </div>
- <div class="modal-footer">
- <button type="button" class="btn btn-default" data-dismiss="modal">{{lang_cancel}}</button>
- <button type="submit" {{perms.action.shutdown.disabled}} name="action" value="shutdown" class="btn btn-danger">
- <span class="glyphicon glyphicon-off"></span>
- {{lang_shutdownButton}}
- </button>
- </div>
- </div>
- </div>
- </div>
-</form>
-
-
-<script type="application/javascript">
- var $dataTable;
-
- document.addEventListener("DOMContentLoaded", function() {
-
- $dataTable = $("#dataTable");
- markCheckedRows();
- // Handle change of checkboxes in table
- $('input:checkbox').change(function() {
- var $this = $(this);
- //give each checkbox the function to mark the row (in green)
- if ($this.is(':checked')) {
- markRows($this.closest("tr"), true);
- $this.closest("td").data("sort-value", 1);
- } else {
- markRows($this.closest("tr"), false);
- $this.closest("td").data("sort-value", 0);
- }
-
- //if all are checked, change the selectAll-Button to unselectAll. if one is not checked, change unselectAll to selectAll
- var unchecked = $dataTable.find("input:checkbox:not(:checked)").length;
- var checked = $dataTable.find("input:checkbox:checked").length;
- if (unchecked === 0) {
- $('#selectAllButton').hide();
- $('#unselectAllButton').show();
- } else if (checked === 0) {
- $('#selectAllButton').show();
- $('#unselectAllButton').hide();
- }
-
- //if no client is selected, disable the shutdown/reboot button, and enable them if a client is selected
- $('#rebootButton').prop('disabled', checked === 0 || '{{perms.action.reboot.disabled}}' === 'disabled');
- $('#shutdownButton').prop('disabled', checked === 0 || '{{perms.action.shutdown.disabled}}' === 'disabled');
- });
- // Propagate click on column with checkbox to checkbox
- $('.checkboxColumn').click(function(e) {
- if (e.target === this) {
- $(this).find('input:checkbox').click();
- }
- });
- // Arm the (de)select all buttons
- $('#selectAllButton').click(function() { selectAllRows(true); });
- $('#unselectAllButton').click(function() { selectAllRows(false); });
- });
-
- // Check all checkboxes, change selectAll button, make shutdown/reboot button enabled as clients will certainly be selected
- function selectAllRows(selected) {
- var $box = $dataTable.find('input:checkbox');
- if ($box.length === 0) return;
- if (selected) {
- $box = $box.filter(':not(:checked)');
- } else {
- $box = $box.filter(':checked');
- }
- if ($box.length === 0) return;
- $box.prop('checked', !!selected).trigger('change');
- }
-
- // mark all previous checked rows (used when loading site), enable (de)select all if list is not empty
- function markCheckedRows() {
- var $checked = $dataTable.find("input:checkbox:checked");
- markRows($checked.closest("tr"), true);
- var $unchecked = $dataTable.find("input:checkbox:not(:checked)");
- markRows($unchecked.closest("tr"), false);
- if($unchecked.length === 0) {
- $('#selectAllButton').hide();
- $('#unselectAllButton').show();
- }
- if ($unchecked.length > 0 || $checked.length > 0) {
- $('.select-button').prop('disabled', false);
- }
- }
-
- function markRows($rows, marked) {
- if (marked) {
- $rows.addClass('active');
- } else {
- $rows.removeClass('active');
- }
- }
-
-</script> \ No newline at end of file
diff --git a/modules-available/rebootcontrol/templates/exec-enter-command.html b/modules-available/rebootcontrol/templates/exec-enter-command.html
new file mode 100644
index 00000000..8bf81605
--- /dev/null
+++ b/modules-available/rebootcontrol/templates/exec-enter-command.html
@@ -0,0 +1,43 @@
+<h2>{{lang_execRemoteCommand}}</h2>
+
+<form method="post" action="?do=rebootcontrol" id="list-form">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="show" value="exec">
+ <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}}
+ <input type="hidden" name="uuid[]" value="{{machineuuid}}">
+ </td>
+ <td>{{clientip}}</td>
+ <td>{{state}}</td>
+ </tr>
+ {{/clients}}
+ </tbody>
+ </table>
+
+ <h3>{{lang_enterCommand}}</h3>
+
+ <div>
+ <label for="script-text">{{lang_scriptOrCommand}}</label>
+ <textarea id="script-text" class="form-control" name="script" rows="10"></textarea>
+ </div>
+ <div class="text-right slx-space">
+ <button type="submit" class="btn btn-primary" name="action" value="exec">
+ <span class="glyphicon glyphicon-play"></span>
+ {{lang_remoteExec}}
+ </button>
+ </div>
+</form> \ No newline at end of file
diff --git a/modules-available/rebootcontrol/templates/header.html b/modules-available/rebootcontrol/templates/header.html
index e171ccd6..47d97714 100644
--- a/modules-available/rebootcontrol/templates/header.html
+++ b/modules-available/rebootcontrol/templates/header.html
@@ -1,34 +1,9 @@
<div class="page-header">
- <button type="button" id="settingsButton" class="btn btn-default pull-right" data-toggle="modal" data-target="#settingsModal"><span class="glyphicon glyphicon-cog"></span> {{lang_settings}}</button>
- <h1>{{lang_rebootControl}}</h1>
-</div>
-
-<div>
- <label>{{lang_location}}:
- <select id="locationDropdown" class="form-control" onchange="selectLocation()">
- {{#locations}}
- <option value="{{locationid}}" {{disabled}} {{#selected}}selected{{/selected}}>{{locationpad}} {{locationname}}</option>
- {{/locations}}
- </select>
- </label>
- <div class="pull-right">
- <button type="button" id="shutdownButton" class="btn btn-danger action-button" data-toggle="modal" data-target="#shutdownModal" disabled>
- <span class="glyphicon glyphicon-off"></span>
- {{lang_shutdownButton}}
- </button>
- <button type="button" id="rebootButton" class="btn btn-warning action-button" data-toggle="modal" data-target="#rebootModal" disabled>
- <span class="glyphicon glyphicon-repeat"></span>
- {{lang_rebootButton}}
- </button>
- <button type="button" id="selectAllButton" class="btn btn-primary select-button" disabled>
- <span class="glyphicon glyphicon-check"></span>
- {{lang_selectall}}
- </button>
- <button type="button" id="unselectAllButton" class="btn btn-default select-button collapse" disabled>
- <span class="glyphicon glyphicon-unchecked"></span>
- {{lang_unselectall}}
- </button>
- </div>
+ <button type="button" id="settingsButton" class="btn btn-default pull-right" data-toggle="modal" data-target="#settingsModal">
+ <span class="glyphicon glyphicon-cog"></span>
+ {{lang_settings}}
+ </button>
+ <h1>{{lang_moduleHeading}}</h1>
</div>
<div id="settingsModal" class="modal fade" role="dialog">
@@ -40,40 +15,96 @@
<h4 class="modal-title"><b>{{lang_settings}}</b></h4>
</div>
<div class="modal-body">
- <p>{{lang_pubKey}}</p>
- <pre id="pubkey">{{pubKey}}</pre>
+ <label for="pubkey">{{lang_pubKey}}</label>
+ <pre id="pubkey">{{pubkey}}</pre>
<p>{{lang_newKeypairExplanation}}</p>
- </div>
- <div class="modal-footer">
- <button {{perms.newkeypair.disabled}} class="btn btn-danger pull-right" onclick="generateNewKeypair()" type="button">
+ <div class="checkbox">
+ <input {{perms.newkeypair.disabled}} type="checkbox" id="keypair-confirm">
+ <label for="keypair-confirm">{{lang_keypairConfirmCheck}}</label>
+ </div>
+ <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 slx-space"></div>
</div>
+ <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}}>
+ <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_save}}
+ </button>
+ <div class="clearfix"></div>
+ </div>
+ </form>
</div>
</div>
</div>
<script type="application/javascript">
-
- // Change Location when selected in Dropdown Menu
- function selectLocation() {
- var dropdown = $("#locationDropdown");
- var location = dropdown.val();
- window.location.replace("?do=rebootcontrol&location="+location);
- }
-
- function generateNewKeypair() {
- if (!confirm('{{lang_confirmNewKeypair}}'))
+document.addEventListener('DOMContentLoaded', function() {
+ var $btn = $('#keypair-button');
+ var $chk = $('#keypair-confirm');
+ $chk.prop('checked', false); // Firefox helpfully keeping state on F5
+ $btn.click(function() {
+ if (!$chk.is(':checked')) {
+ var $p = $chk.parent();
+ $p.fadeOut(100, function () {
+ $p.fadeIn(75);
+ });
return;
+ }
+ $btn.prop('disabled', true);
$.ajax({
url: '?do=rebootcontrol',
type: 'POST',
data: { action: "generateNewKeypair", token: TOKEN },
success: function(value) {
$('#pubkey').text(value);
+ },
+ fail: function() {
+ $('#pubkey').text('Error');
+ $btn.prop('disabled', false);
}
});
- }
+ });
+});
+
-</script> \ No newline at end of file
+</script>
diff --git a/modules-available/rebootcontrol/templates/jumphost-edit.html b/modules-available/rebootcontrol/templates/jumphost-edit.html
new file mode 100644
index 00000000..7a79dc86
--- /dev/null
+++ b/modules-available/rebootcontrol/templates/jumphost-edit.html
@@ -0,0 +1,42 @@
+<h2>{{lang_editJumpHost}}</h2>
+
+<form method="post" action="?do=rebootcontrol">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="show" value="jumphost">
+ <input type="hidden" name="hostid" value="{{hostid}}">
+ <div class="list-group">
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-md-9 col-sm-7">
+ <label for="host">{{lang_host}}</label>
+ <input required id="host" class="form-control" type="text" name="host" value="{{host}}">
+ </div>
+ <div class="col-md-3 col-sm-5">
+ <label for="port">{{lang_port}}</label>
+ <input required id="port" class="form-control" type="number" name="port" value="{{port}}" min="1" max="65535">
+ </div>
+ </div>
+ </div>
+ <div class="list-group-item">
+ <label for="username">{{lang_username}}</label>
+ <input required id="username" class="form-control" type="text" name="username" value="{{username}}">
+ </div>
+ <div class="list-group-item">
+ <label for="sshkey">{{lang_privkey}}</label>
+ <textarea required id="sshkey" class="form-control" name="sshkey" rows="8">{{sshkey}}</textarea>
+ </div>
+ <div class="list-group-item">
+ <label for="script">{{lang_wakeupScript}}</label>
+ <textarea required id="script" class="form-control" name="script" rows="8">{{script}}</textarea>
+ <div class="slx-smallspace"></div>
+ <p>{{lang_wakeScriptHelp}}</p>
+ </div>
+ </div>
+ <div class="buttonbar text-right">
+ <button type="reset" class="btn btn-default">{{lang_reset}}</button>
+ <button type="submit" class="btn btn-primary" name="action" value="save">
+ <span class="glyphicon glyphicon-floppy-disk"></span>
+ {{lang_save}}
+ </button>
+ </div>
+</form> \ No newline at end of file
diff --git a/modules-available/rebootcontrol/templates/jumphost-list.html b/modules-available/rebootcontrol/templates/jumphost-list.html
new file mode 100644
index 00000000..083480b8
--- /dev/null
+++ b/modules-available/rebootcontrol/templates/jumphost-list.html
@@ -0,0 +1,63 @@
+<h2>{{lang_wolReachability}}</h2>
+
+<h3>{{lang_jumpHosts}}</h3>
+
+<form method="post" action="?do=rebootcontrol">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="show" value="jumphost">
+ <input type="hidden" name="action" value="list">
+ <table class="table">
+ <thead>
+ <tr>
+ <th>{{lang_host}}</th>
+ <th class="slx-smallcol">{{lang_numAssignedSubnets}}</th>
+ <th class="slx-smallcol">{{lang_reachable}}</th>
+ <th class="slx-smallcol"></th>
+ </tr>
+ </thead>
+ <tbody>
+ {{#jumpHosts}}
+ <tr>
+ <td>{{host}}:{{port}}</td>
+ <td class="text-nowrap text-right">
+ <a class="btn btn-xs btn-default {{perms.jumphost.assign-subnet.disabled}}"
+ href="?do=rebootcontrol&amp;show=jumphost&amp;what=assign&amp;id={{hostid}}">
+ <span class="glyphicon glyphicon-tasks"></span>
+ </a>
+ <span class="badge">{{subnetCount}}</span>
+ </td>
+ <td class="text-nowrap text-center">
+ {{#reachable}}
+ <span class="glyphicon glyphicon-ok text-success"></span>
+ {{/reachable}}
+ {{^reachable}}
+ <span class="glyphicon glyphicon-remove text-danger"></span>
+ {{/reachable}}
+ <button class="btn btn-xs btn-default btn-check-jumphost" type="submit" name="checkid" value="{{hostid}}"
+ {{perms.jumphost.edit.disabled}}>
+ {{lang_check}}
+ </button>
+ </td>
+ <td class="text-nowrap text-center">
+ <a class="btn btn-xs btn-default {{perms.jumphost.edit.disabled}}"
+ href="?do=rebootcontrol&amp;show=jumphost&amp;what=edit&amp;id={{hostid}}">
+ <span class="glyphicon glyphicon-edit"></span>
+ </a>
+ <button type="submit" name="deleteid" value="{{hostid}}" class="btn btn-xs btn-danger"
+ data-confirm="#confirm-delete-host" data-title="{{host}}" {{perms.jumphost.edit.disabled}}>
+ <span class="glyphicon glyphicon-trash"></span>
+ </button>
+ </td>
+ </tr>
+ {{/jumpHosts}}
+ </tbody>
+ </table>
+</form>
+<div class="buttonbar text-right">
+ <a class="btn btn-success" href="?do=rebootcontrol&amp;show=jumphost&amp;what=edit&amp;id=new">
+ <span class="glyphicon glyphicon-plus"></span>
+ {{lang_new}}
+ </a>
+</div>
+
+<div class="hidden" id="confirm-delete-host">{{lang_hostDeleteConfirm}}</div> \ No newline at end of file
diff --git a/modules-available/rebootcontrol/templates/jumphost-subnets.html b/modules-available/rebootcontrol/templates/jumphost-subnets.html
new file mode 100644
index 00000000..9b418667
--- /dev/null
+++ b/modules-available/rebootcontrol/templates/jumphost-subnets.html
@@ -0,0 +1,28 @@
+<h2>{{lang_jumpHost}} {{host}} - {{lang_assignedSubnets}}</h2>
+
+<form method="post" action="?do=rebootcontrol">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="show" value="jumphost">
+ <input type="hidden" name="action" value="assign">
+ <input type="hidden" name="hostid" value="{{hostid}}">
+ <div class="list-group">
+ <div class="list-group-item">
+ {{#list}}
+ <div class="row">
+ <div class="col-md-12">
+ <div class="checkbox">
+ <input id="check-{{subnetid}}" type="checkbox" name="subnet[{{subnetid}}]" {{checked}}>
+ <label for="check-{{subnetid}}">{{cidr}}</label>
+ </div>
+ </div>
+ </div>
+ {{/list}}
+ <div class="text-right">
+ <button type="submit" class="btn btn-primary">
+ <span class="glyphicon glyphicon-floppy-disk"></span>
+ {{lang_save}}
+ </button>
+ </div>
+ </div>
+ </div>
+</form> \ No newline at end of file
diff --git a/modules-available/rebootcontrol/templates/status-checkconnection.html b/modules-available/rebootcontrol/templates/status-checkconnection.html
new file mode 100644
index 00000000..da1177e7
--- /dev/null
+++ b/modules-available/rebootcontrol/templates/status-checkconnection.html
@@ -0,0 +1,47 @@
+<h3>{{lang_checkingJumpHost}}: {{host}} – {{timestamp_s}}</h3>
+
+<div class="clearfix"></div>
+<div class="collapse alert alert-success" id="result-ok">
+ <span class="glyphicon glyphicon-check"></span>
+ {{lang_hostReachable}}
+</div>
+<div class="collapse alert alert-warning" id="result-error">
+ <span class="glyphicon glyphicon-remove"></span>
+ {{lang_hostNonZeroExit}}
+</div>
+<div class="collapse alert alert-danger" id="result-unreach">
+ <span class="glyphicon glyphicon-remove"></span>
+ {{lang_hostNotReachable}}
+</div>
+
+<div class="collapse" id="log-wrapper">
+ <label for="log-output">{{lang_checkOutputLabel}}</label>
+ <pre id="log-output"></pre>
+</div>
+
+<div data-tm-id="{{taskId}}" data-tm-log="error" data-tm-callback="updateStatus">{{lang_checkingJumpHost}}</div>
+<script type="application/javascript">
+ function updateStatus(task) {
+ if (!task || !task.data || !task.data.result || !task.data.result['{{host}}'])
+ return;
+ var status = task.data.result['{{host}}'];
+ var log = '';
+ if (status.stderr) log += status.stderr + "\n";
+ if (status.stdout) log += status.stdout + "\n";
+ showErrorLog(log);
+ if (task.statusCode === 'TASK_FINISHED' || task.statusCode === 'TASK_ERROR') {
+ if (status.exitCode === 0) {
+ $('#result-ok').show();
+ } else if (status.exitCode > 0) {
+ $('#result-error').show();
+ } else {
+ $('#result-unreach').show();
+ }
+ }
+ }
+ function showErrorLog(log) {
+ if (!log) return;
+ $('#log-output').text(log);
+ $('#log-wrapper').show();
+ }
+</script>
diff --git a/modules-available/rebootcontrol/templates/status-exec.html b/modules-available/rebootcontrol/templates/status-exec.html
new file mode 100644
index 00000000..a3efef5f
--- /dev/null
+++ b/modules-available/rebootcontrol/templates/status-exec.html
@@ -0,0 +1,76 @@
+<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>
+
+<div class="list-group">
+ <div class="list-group-item">
+<div class="row">
+ <div class="col-md-6 col-sm-8 col-xs-12 slx-bold">{{lang_host}}</div>
+ <div class="col-md-4 col-sm-2 col-xs-6 slx-bold">{{lang_status}}</div>
+ <div class="col-md-2 col-sm-2 col-xs-6 slx-bold text-right">{{lang_exitCode}}</div>
+</div>
+ </div>
+
+{{#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">
+ <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>
+ <div class="stdout collapse">
+ <i>{{lang_stdout}}</i>
+ <pre></pre>
+ </div>
+ <div class="stderr collapse">
+ <i>{{lang_stderr}}</i>
+ <pre></pre>
+ </div>
+</div>
+{{/clients}}
+</div>
+
+<script><!--
+
+var ignoreHosts = {};
+
+function updateStatus(task) {
+ if (!task || !task.data || !task.data.result)
+ return;
+ for (var host in task.data.result) {
+ if (!task.data.result.hasOwnProperty(host) || ignoreHosts[host])
+ continue;
+ updateStatusClient(host, task.data.result[host]);
+ }
+}
+
+function updateStatusClient(id, status) {
+ var $p = $('#client-' + id);
+ if ($p.length === 0)
+ return;
+ $p.find('.state').text(status.state);
+ if (status.stdout) $p.find('.stdout').show().find('pre').text(status.stdout);
+ if (status.stderr) $p.find('.stderr').show().find('pre').text(status.stderr);
+ if (status.state === 'DONE' || status.state === 'ERROR' || status.state === 'TIMEOUT') {
+ $p.find('.state').addClass((status.state === 'DONE') ? 'text-success' : 'text-danger');
+ if (status.exitCode >= 0) {
+ $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);
+ }
+ }
+}
+
+//--></script> \ No newline at end of file
diff --git a/modules-available/rebootcontrol/templates/status.html b/modules-available/rebootcontrol/templates/status-reboot.html
index c05b2fad..34971845 100644
--- a/modules-available/rebootcontrol/templates/status.html
+++ b/modules-available/rebootcontrol/templates/status-reboot.html
@@ -1,37 +1,36 @@
-<div>
- <form class="form-inline">
- <b>{{lang_location}}: {{locationName}}</b>
- <input type="hidden" name="do" value="rebootcontrol">
- <input type="hidden" name="location" value="{{locationId}}">
- <button type="submit" class="btn btn-primary pull-right"><span class="glyphicon glyphicon-arrow-left"></span> {{lang_back}}</button>
- </form>
-</div>
+<h3>{{action}} – {{timestamp_s}}</h3>
-<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>
+{{#locations}}
+<div class="loc">{{name}}</div>
+{{/locations}}
+<div class="clearfix slx-space"></div>
- <tbody>
- {{#clients}}
- <tr>
- <td>{{hostname}}{{^hostname}}{{machineuuid}}{{/hostname}}</td>
- <td>{{clientip}}</td>
- <td id="status-{{machineuuid}}"></td>
- </tr>
- {{/clients}}
- </tbody>
- </table>
-</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>
-<div data-tm-id="{{taskId}}" data-tm-log="error" data-tm-callback="updateStatus"></div>
+ <tbody>
+ {{#clients}}
+ <tr>
+ <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>
+ <span id="text-{{machineuuid}}"></span>
+ </td>
+ </tr>
+ {{/clients}}
+ </tbody>
+</table>
+
+<div data-tm-id="{{id}}" data-tm-log="error" data-tm-callback="updateStatus"></div>
<script type="application/javascript">
statusStrings = {
@@ -48,11 +47,12 @@
function updateStatus(task) {
if (!task || !task.data || !task.data.clientStatus)
return;
+ stillActive = 25;
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..70517f84
--- /dev/null
+++ b/modules-available/rebootcontrol/templates/status-wol.html
@@ -0,0 +1,82 @@
+<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" data-tm-log="messages">{{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>
+ {{#machineuuid}}
+ <a href="?do=statistics&amp;uuid={{machineuuid}}">
+ {{hostname}}{{^hostname}}{{machineuuid}}{{/hostname}}
+ </a>
+ {{/machineuuid}}
+ {{^machineuuid}}
+ {{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>
+
+<a class="text-muted" href="#debug-out" data-toggle="collapse">Debug</a>
+<pre id="debug-out" class="collapse"></pre>
+
+<script>
+function wolCallback(task) {
+ 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>
diff --git a/modules-available/rebootcontrol/templates/subnet-edit.html b/modules-available/rebootcontrol/templates/subnet-edit.html
new file mode 100644
index 00000000..570865c7
--- /dev/null
+++ b/modules-available/rebootcontrol/templates/subnet-edit.html
@@ -0,0 +1,78 @@
+<!-- subnetid, start, end, fixed, isdirect, lastdirectcheck, lastseen, seencount -->
+
+<form method="post" action="?do=rebootcontrol">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="show" value="subnet">
+ <input type="hidden" name="id" value="{{subnetid}}">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ {{lang_editSubnet}}: <b>{{cidr}}</b> ({{start_s}}&thinsp;-&thinsp;{{end_s}})
+ </div>
+ <div class="list-group">
+ <div class="list-group-item">
+ <div class="checkbox">
+ <input id="fixed_cb" type="checkbox" name="fixed" {{#fixed}}checked{{/fixed}} {{perms.subnet.flag.disabled}}>
+ <label for="fixed_cb">{{lang_fixSubnetSettings}}</label>
+ </div>
+ <div class="slx-space"></div>
+ <p>{{lang_fixSubnetDesc}}</p>
+ </div>
+ <div class="list-group-item {{^fixed}}collapse{{/fixed}} subnet-option">
+ <div class="checkbox">
+ <input id="direct_cb" type="checkbox" name="isdirect" {{#isdirect}}checked{{/isdirect}} {{perms.subnet.flag.disabled}}>
+ <label for="direct_cb">{{lang_reachableFromServer}}</label>
+ </div>
+ <div class="slx-space"></div>
+ <p>{{lang_reachableFromServerDesc}}</p>
+ </div>
+ <div class="list-group-item {{perms.jumphost.view.hidden}}">
+ <label>{{lang_assignedJumpHosts}}</label>
+ {{#jumpHosts}}
+ <div class="row">
+ <div class="col-md-12">
+ <div class="checkbox">
+ <input id="jhb{{hostid}}" type="checkbox" name="jumphost[{{hostid}}]" {{checked}}
+ {{perms.jumphost.assign-subnet.disabled}}>
+ <label for="jhb{{hostid}}">{{host}}:{{port}}</label>
+ </div>
+ </div>
+ </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}}
+ </button>
+ </div>
+ </div>
+</form>
+<script><!--
+document.addEventListener('DOMContentLoaded', function() {
+ var $overrides = $('.subnet-option');
+ var $cb = $('#fixed_cb');
+ $cb.change(function() {
+ if ($cb.is(':checked')) {
+ $overrides.show();
+ } else {
+ $overrides.hide();
+ }
+ }).change();
+
+});
+//--></script>
diff --git a/modules-available/rebootcontrol/templates/subnet-list.html b/modules-available/rebootcontrol/templates/subnet-list.html
new file mode 100644
index 00000000..2bc9208f
--- /dev/null
+++ b/modules-available/rebootcontrol/templates/subnet-list.html
@@ -0,0 +1,78 @@
+<h2>{{lang_wolReachability}}</h2>
+
+<h3>{{lang_subnets}}</h3>
+
+<p>{{lang_subnetsDescription}}</p>
+
+<p>{{lang_wolMachineSupportText}}</p>
+
+<table class="table">
+ <thead>
+ <tr>
+ <th>{{lang_subnet}}</th>
+ <th class="slx-smallcol">{{lang_isFixed}}</th>
+ <th class="slx-smallcol">{{lang_isDirect}}</th>
+ <th class="slx-smallcol">{{lang_wolReachability}}</th>
+ <th class="slx-smallcol">{{lang_lastseen}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{#subnets}}
+ <tr>
+ <td>
+ <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"><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">
+ <div class="list-group">
+ <div class="list-group-item">
+ <label>{{lang_addNewSubnet}}</label>
+ <div class="row">
+ <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 col-sm-6">
+ <button class="btn btn-primary" name="action" value="add">
+ <span class="glyphicon glyphicon-floppy-disk"></span>
+ {{lang_add}}
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+</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 063ba949..dcb04450 100644
--- a/modules-available/rebootcontrol/templates/task-list.html
+++ b/modules-available/rebootcontrol/templates/task-list.html
@@ -2,9 +2,9 @@
<table class="table">
<thead>
<tr>
- <th>{{lang_mode}}</th>
+ <th>{{lang_when}}</th>
+ <th>{{lang_task}}</th>
<th>{{lang_location}}</th>
- <th>{{lang_time}}</th>
<th>{{lang_clientCount}}</th>
<th>{{lang_status}}</th>
</tr>
@@ -12,19 +12,23 @@
<tbody>
{{#list}}
<tr>
- <td>
- <a href="?do=rebootcontrol&amp;taskid={{taskId}}">{{mode}}</a>
+ <td class="text-nowrap">
+ {{timestamp_s}}
</td>
- <td>
- {{locationName}}
+ <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>
- {{time}}
+ {{#locations}}
+ <div class="loc">{{name}}</div>
+ {{/locations}}
+ <div class="clearfix"></div>
</td>
- <td>
- {{clientCount}}
+ <td class="text-nowrap">
+ {{clients}}
</td>
- <td>
+ <td class="text-nowrap">
{{status}}
</td>
</tr>