summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--apis/cron.inc.php22
-rw-r--r--inc/arrayutil.inc.php24
-rw-r--r--inc/iputil.inc.php72
-rw-r--r--inc/permission.inc.php2
-rw-r--r--inc/session.inc.php27
-rw-r--r--inc/taskmanagercallback.inc.php11
-rw-r--r--modules-available/js_ip/clientscript.js (renamed from modules-available/locations/clientscript.js)19
-rw-r--r--modules-available/js_ip/config.json7
-rw-r--r--modules-available/locations/pages/locations.inc.php1
-rw-r--r--modules-available/locations/templates/locations.html12
-rw-r--r--modules-available/rebootcontrol/clientscript.js27
-rw-r--r--modules-available/rebootcontrol/hooks/cron.inc.php236
-rw-r--r--modules-available/rebootcontrol/inc/rebootcontrol.inc.php481
-rw-r--r--modules-available/rebootcontrol/inc/rebootqueries.inc.php58
-rw-r--r--modules-available/rebootcontrol/inc/rebootutils.inc.php75
-rw-r--r--modules-available/rebootcontrol/install.inc.php49
-rw-r--r--modules-available/rebootcontrol/lang/de/messages.json16
-rw-r--r--modules-available/rebootcontrol/lang/de/module.json6
-rw-r--r--modules-available/rebootcontrol/lang/de/permissions.json15
-rw-r--r--modules-available/rebootcontrol/lang/de/template-tags.json73
-rw-r--r--modules-available/rebootcontrol/lang/en/module.json4
-rw-r--r--modules-available/rebootcontrol/lang/en/template-tags.json2
-rw-r--r--modules-available/rebootcontrol/page.inc.php228
-rw-r--r--modules-available/rebootcontrol/pages/exec.inc.php57
-rw-r--r--modules-available/rebootcontrol/pages/jumphost.inc.php208
-rw-r--r--modules-available/rebootcontrol/pages/subnet.inc.php156
-rw-r--r--modules-available/rebootcontrol/pages/task.inc.php150
-rw-r--r--modules-available/rebootcontrol/permissions/permissions.json27
-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.html41
-rw-r--r--modules-available/rebootcontrol/templates/header.html100
-rw-r--r--modules-available/rebootcontrol/templates/jumphost-edit.html42
-rw-r--r--modules-available/rebootcontrol/templates/jumphost-list.html65
-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.html63
-rw-r--r--modules-available/rebootcontrol/templates/status-reboot.html (renamed from modules-available/rebootcontrol/templates/status.html)63
-rw-r--r--modules-available/rebootcontrol/templates/status-wol.html52
-rw-r--r--modules-available/rebootcontrol/templates/subnet-edit.html70
-rw-r--r--modules-available/rebootcontrol/templates/subnet-list.html53
-rw-r--r--modules-available/rebootcontrol/templates/task-header.html4
-rw-r--r--modules-available/rebootcontrol/templates/task-list.html22
-rw-r--r--modules-available/statistics/api.inc.php24
-rw-r--r--modules-available/statistics/lang/de/template-tags.json1
-rw-r--r--modules-available/statistics/lang/en/template-tags.json1
-rw-r--r--modules-available/statistics/page.inc.php64
-rw-r--r--modules-available/statistics/pages/list.inc.php6
-rw-r--r--modules-available/statistics/templates/clientlist.html12
49 files changed, 2455 insertions, 582 deletions
diff --git a/apis/cron.inc.php b/apis/cron.inc.php
index 75d7f132..d00c179c 100644
--- a/apis/cron.inc.php
+++ b/apis/cron.inc.php
@@ -62,9 +62,25 @@ function getJobStatus($id)
}
// Hooks by other modules
-function handleModule($file)
+/**
+ * @param Hook $hook
+ */
+function handleModule($hook)
{
- include_once $file;
+ global $cron_log_text;
+ $cron_log_text = '';
+ include_once $hook->file;
+ if (!empty($cron_log_text)) {
+ EventLog::info('CronJob ' . $hook->moduleId . ' finished.', $cron_log_text);
+ }
+}
+
+$cron_log_text = '';
+function cron_log($text)
+{
+ // XXX: Enable this code for debugging -- make this configurable some day
+ //global $cron_log_text;
+ //$cron_log_text .= $text . "\n";
}
$blocked = Property::getList(CRON_KEY_BLOCKED);
@@ -93,7 +109,7 @@ foreach (Hook::load('cron') as $hook) {
$value = $hook->moduleId . '|' . time();
Property::addToList(CRON_KEY_STATUS, $value, 30);
try {
- handleModule($hook->file);
+ handleModule($hook);
} catch (Exception $e) {
// Logging
EventLog::failure('Cronjob for module ' . $hook->moduleId . ' has crashed. Check the php or web server error log.', $e->getMessage());
diff --git a/inc/arrayutil.inc.php b/inc/arrayutil.inc.php
new file mode 100644
index 00000000..ec6e2a5f
--- /dev/null
+++ b/inc/arrayutil.inc.php
@@ -0,0 +1,24 @@
+<?php
+
+class ArrayUtil
+{
+
+ /**
+ * Take an array of arrays, take given key from each sub-array and return
+ * new array with just those corresponding values.
+ * @param array $list
+ * @param string $key
+ * @return array
+ */
+ public static function flattenByKey($list, $key)
+ {
+ $ret = [];
+ foreach ($list as $item) {
+ if (array_key_exists($key, $item)) {
+ $ret[] = $item[$key];
+ }
+ }
+ return $ret;
+ }
+
+} \ No newline at end of file
diff --git a/inc/iputil.inc.php b/inc/iputil.inc.php
new file mode 100644
index 00000000..5cdd583c
--- /dev/null
+++ b/inc/iputil.inc.php
@@ -0,0 +1,72 @@
+<?php
+
+class IpUtil
+{
+
+ public static function rangeToCidr($start, $end)
+ {
+ $value = (int)$start ^ (int)$end;
+ if (!self::isAllOnes($value))
+ return 'NOT SUBNET: ' . long2ip($start) . '-' . long2ip($end);
+ $ones = self::countOnes($value);
+ return long2ip($start) . '/' . (32 - $ones);
+ }
+
+ public static function isValidSubnetRange($start, $end)
+ {
+ return self::isAllOnes((int)$start ^ (int)$end);
+ }
+
+ /**
+ * Return number of one bits required to represent
+ * this number. Assumes given number is 2^n - 1.
+ */
+ private static function countOnes($value)
+ {
+ // This is log(value) / log(2)
+ // It should actually be $value + 1, but floating point errors
+ // start to happen either way at higher values, so with
+ // the round() thrown in, it doesn't matter...
+ return round(log($value) / 0.69314718055995);
+ }
+
+ /**
+ * Is the given number just ones if converted to
+ * binary (ignoring leading zeros)?
+ */
+ private static function isAllOnes($value)
+ {
+ return ($value & ($value + 1)) === 0;
+ }
+
+ /**
+ * Parse network range in CIDR notion, return
+ * ['start' => (int), 'end' => (int)] representing
+ * the according start and end addresses as integer
+ * values. Returns false on malformed input.
+ * @param string $cidr 192.168.101/24, 1.2.3.4/16, ...
+ * @return array|false start and end address, false on error
+ */
+ public static function parseCidr($cidr)
+ {
+ $parts = explode('/', $cidr);
+ if (count($parts) !== 2)
+ return false;
+ $ip = $parts[0];
+ $bits = $parts[1];
+ if (!is_numeric($bits) || $bits < 0 || $bits > 32)
+ return false;
+ $dots = substr_count($ip, '.');
+ if ($dots < 3) {
+ $ip .= str_repeat('.0', 3 - $dots);
+ }
+ $ip = ip2long($ip);
+ if ($ip === false)
+ return false;
+ $bits = pow(2, 32 - $bits) - 1;
+ if (PHP_INT_SIZE === 4)
+ return ['start' => sprintf('%u', $ip & ~$bits), 'end' => sprintf('%u', $ip | $bits)];
+ return ['start' => $ip & ~$bits, 'end' => $ip | $bits];
+ }
+
+}
diff --git a/inc/permission.inc.php b/inc/permission.inc.php
index 3a7bdc36..7dd011bb 100644
--- a/inc/permission.inc.php
+++ b/inc/permission.inc.php
@@ -37,7 +37,7 @@ class Permission
continue;
$temp =& $temp[$sub];
}
- $temp = ['disabled' => 'disabled', 'readonly' => 'readonly'];
+ $temp = ['disabled' => 'disabled', 'readonly' => 'readonly', 'hidden' => 'hidden'];
}
if (!$one && !is_null($noneAvailDisabled)) {
$array[$noneAvailDisabled] = [
diff --git a/inc/session.inc.php b/inc/session.inc.php
index c08c8c4a..6204c98c 100644
--- a/inc/session.inc.php
+++ b/inc/session.inc.php
@@ -46,17 +46,22 @@ class Session
public static function get($key)
{
- if (!isset(self::$data[$key])) return false;
- return self::$data[$key];
+ if (!isset(self::$data[$key]) || !is_array(self::$data[$key])) return false;
+ return self::$data[$key][0];
}
- public static function set($key, $value)
+ /**
+ * @param string $key key of entry
+ * @param mixed $value data to store for key, false = delete
+ * @param int|false $validMinutes validity in minutes, or false = forever
+ */
+ public static function set($key, $value, $validMinutes = false)
{
if (self::$data === false) Util::traceError('Tried to set session data with no active session');
if ($value === false) {
unset(self::$data[$key]);
} else {
- self::$data[$key] = $value;
+ self::$data[$key] = [$value, $validMinutes === false ? false : time() + $validMinutes * 60];
}
}
@@ -99,7 +104,19 @@ class Session
return false;
}
self::$data = @unserialize(@file_get_contents($sessionfile));
- if (self::$data === false) return false;
+ if (self::$data === false)
+ return false;
+ $now = time();
+ $save = false;
+ foreach (array_keys(self::$data) as $key) {
+ if (self::$data[$key][1] !== false && self::$data[$key][1] < $now) {
+ unset(self::$data[$key]);
+ $save = true;
+ }
+ }
+ if ($save) {
+ self::save();
+ }
return true;
}
diff --git a/inc/taskmanagercallback.inc.php b/inc/taskmanagercallback.inc.php
index 2be9fe73..21bece38 100644
--- a/inc/taskmanagercallback.inc.php
+++ b/inc/taskmanagercallback.inc.php
@@ -201,5 +201,14 @@ class TaskmanagerCallback
$mod->activate(1, false);
MiniLinux::linuxDownloadCallback($task, $args);
}
-
+
+ public static function rbcConnCheck($task, $args)
+ {
+ $mod = Module::get('rebootcontrol');
+ if ($mod === false)
+ return;
+ $mod->activate(1, false);
+ RebootControl::connectionCheckCallback($task, $args);
+ }
+
}
diff --git a/modules-available/locations/clientscript.js b/modules-available/js_ip/clientscript.js
index ad3e6c43..930292b1 100644
--- a/modules-available/locations/clientscript.js
+++ b/modules-available/js_ip/clientscript.js
@@ -1,3 +1,5 @@
+'use strict';
+
function ip2long(IP) {
var i = 0;
IP = IP.match(/^([1-9]\d*|0[0-7]*|0x[\da-f]+)(?:\.([1-9]\d*|0[0-7]*|0x[\da-f]+))?(?:\.([1-9]\d*|0[0-7]*|0x[\da-f]+))?(?:\.([1-9]\d*|0[0-7]*|0x[\da-f]+))?$/i);
@@ -23,8 +25,10 @@ function long2ip(a) {
}
function cidrToRange(cidr) {
- var range = [2];
+ var range = [];
cidr = cidr.split('/');
+ if (cidr.length !== 2)
+ return false;
var cidr_1 = parseInt(cidr[1]);
if (cidr_1 <= 0 || cidr_1 > 32)
return false;
@@ -50,14 +54,11 @@ function slxAttachCidr() {
return;
t.removeClass('cidrmagic');
s.focusout(function () {
- var val = s.val();
- if (val.match(/^[0-9]+\.[0-9]+(\.[0-9]+(\.[0-9]+)?)?\/[0-9]{2}$/)) {
- var res = cidrToRange(val);
- if (res === false)
- return;
- s.val(res[0]);
- e.val(res[1]);
- }
+ var res = cidrToRange(s.val());
+ if (res === false)
+ return;
+ s.val(res[0]);
+ e.val(res[1]);
});
});
}
diff --git a/modules-available/js_ip/config.json b/modules-available/js_ip/config.json
new file mode 100644
index 00000000..96c02bce
--- /dev/null
+++ b/modules-available/js_ip/config.json
@@ -0,0 +1,7 @@
+{
+ "dependencies": [],
+ "scripts": [
+ "clientscript.js"
+ ],
+ "client-plugin": true
+} \ No newline at end of file
diff --git a/modules-available/locations/pages/locations.inc.php b/modules-available/locations/pages/locations.inc.php
index a8cd2e63..54f44554 100644
--- a/modules-available/locations/pages/locations.inc.php
+++ b/modules-available/locations/pages/locations.inc.php
@@ -242,6 +242,7 @@ class SubPage
// depends on permissions in the according modules, not this one
Permission::addGlobalTags($data['perms'], NULL, ['subnets.edit', 'location.add']);
Render::addTemplate('locations', $data);
+ Module::isAvailable('js_ip'); // For CIDR magic
}
private static function propagateFields(&$locationList, $defaultValue, $name, $class)
diff --git a/modules-available/locations/templates/locations.html b/modules-available/locations/templates/locations.html
index f30ebcab..e2224bc0 100644
--- a/modules-available/locations/templates/locations.html
+++ b/modules-available/locations/templates/locations.html
@@ -209,7 +209,7 @@ function slxOpenLocation(e, lid) {
tr.append(td);
$(e).closest('tr').addClass('active slx-bold').after(tr);
td.load('?do=Locations&page=details&action=showlocation&locationid=' + lid, function() {
- slxAttachCidr();
+ if (slxAttachCidr) slxAttachCidr();
scollIntoView(tr);
});
slxLastLocation = tr;
@@ -230,18 +230,14 @@ function scollIntoView(el) {
function slxAddSubnetRow(e, lid) {
var tr = $('#loc-sub-' + lid);
- tr.before('<tr id="row' + slxAddCounter + '" class="cidrmagic">\
+ tr.before('<tr class="cidrmagic">\
<td>#</td>\
<td><input class="form-control cidrstart" type="text" name="newstartaddr[' + slxAddCounter + ']" pattern="\\d{1,3}\.\\d{1,3}\.\\d{1,3}\.\\d{1,3}"></td>\
<td><input class="form-control cidrend" type="text" name="newendaddr[' + slxAddCounter + ']" pattern="\\d{1,3}\.\\d{1,3}\.\\d{1,3}\.\\d{1,3}"></td>\
- <td class="text-center"><button class="btn btn-default btn-sm" type="button" onclick="removeNewSubnetRow(' + slxAddCounter + ')"><span class="glyphicon glyphicon-remove"></span></button></td>\
+ <td class="text-center"><button class="btn btn-default btn-sm" type="button" onclick="$(this).closest(\'tr\').remove()"><span class="glyphicon glyphicon-remove"></span></button></td>\
</tr>');
slxAddCounter++;
- slxAttachCidr();
-}
-
-function removeNewSubnetRow(r) {
- $("#row"+r).remove();
+ if (slxAttachCidr) slxAttachCidr();
}
function deleteSubnetWarning(locid) {
diff --git a/modules-available/rebootcontrol/clientscript.js b/modules-available/rebootcontrol/clientscript.js
new file mode 100644
index 00000000..80be2533
--- /dev/null
+++ b/modules-available/rebootcontrol/clientscript.js
@@ -0,0 +1,27 @@
+var stillActive = true;
+document.addEventListener('DOMContentLoaded', function() {
+ var clients = [];
+ $('.machineuuid').each(function() { clients.push($(this).data('uuid')); });
+ if (clients.length === 0)
+ return;
+ function updateClientStatus() {
+ if (!stillActive) return;
+ stillActive = false;
+ setTimeout(updateClientStatus, 5000);
+ $.ajax({
+ url: "?do=rebootcontrol",
+ method: "POST",
+ dataType: 'json',
+ data: { token: TOKEN, action: "clientstatus", clients: clients }
+ }).done(function(data) {
+ console.log(data);
+ if (!data)
+ return;
+ for (var e in data) {
+ $('#status-' + e).prop('class', 'glyphicon ' + data[e]);
+ if (!stillActive) $('#spinner-' + e).remove();
+ }
+ });
+ }
+ setTimeout(updateClientStatus, 1000);
+}); \ No newline at end of file
diff --git a/modules-available/rebootcontrol/hooks/cron.inc.php b/modules-available/rebootcontrol/hooks/cron.inc.php
new file mode 100644
index 00000000..e387e055
--- /dev/null
+++ b/modules-available/rebootcontrol/hooks/cron.inc.php
@@ -0,0 +1,236 @@
+<?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');
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ RebootControl::wakeViaJumpHost($row, '255.255.255.255', [['macaddr' => '00:11:22:33:44:55']]);
+ }
+}
+
+/*
+ * Client reachability test -- can be disabled
+ */
+if (mt_rand(1, 2) !== 1 || Property::get(RebootControl::KEY_AUTOSCAN_DISABLED))
+ return;
+
+class Stuff
+{
+ public static $subnets;
+}
+
+function destSawPw($destTask, $destMachine, $passwd)
+{
+ return strpos($destTask['data']['result'][$destMachine['machineuuid']]['stdout'], "passwd=$passwd") !== false;
+}
+
+function spawnDestinationListener($dstid, &$destMachine, &$destTask, &$destDeadline)
+{
+ $destMachines = Stuff::$subnets[$dstid];
+ cron_log(count($destMachines) . ' potential destination machines for subnet ' . $dstid);
+ shuffle($destMachines);
+ $destMachines = array_slice($destMachines, 0, 3);
+ $destTask = $destMachine = false;
+ $destDeadline = 0;
+ foreach ($destMachines as $machine) {
+ cron_log("Trying to use {$machine['clientip']} as listener for " . long2ip($machine['bcast']));
+ $destTask = RebootControl::runScript([$machine], "echo 'Running-MARK'\nbusybox timeout -t 8 jawol -v -l", 10);
+ Taskmanager::release($destTask);
+ $destDeadline = time() + 10;
+ if (!Taskmanager::isRunning($destTask))
+ continue;
+ sleep(2); // Wait a bit and re-check job is running; only then proceed with this host
+ $destTask = Taskmanager::status($destTask);
+ cron_log("....is {$destTask['statusCode']} {$machine['machineuuid']}");
+ if (Taskmanager::isRunning($destTask)
+ && strpos($destTask['data']['result'][$machine['machineuuid']]['stdout'], 'Running-MARK') !== false) {
+ $destMachine = $machine;
+ break; // GOOD TO GO
+ }
+ cron_log(print_r($destTask, true));
+ cron_log("Dest isn't running or didn't have MARK in output, trying another one...");
+ }
+}
+
+function testClientToClient($srcid, $dstid)
+{
+ $sourceMachines = Stuff::$subnets[$srcid];
+ // Start listener on destination
+ spawnDestinationListener($dstid, $destMachine, $destTask, $destDeadline);
+ if ($destMachine === false || !Taskmanager::isRunning($destTask))
+ return false; // No suitable dest-host found
+ // Find a source host
+ $passwd = sprintf('%02x:%02x:%02x:%02x:%02x:%02x', mt_rand(0, 255), mt_rand(0, 255),
+ mt_rand(0, 255), mt_rand(0, 255), mt_rand(0, 255), mt_rand(0, 255));
+ shuffle($sourceMachines);
+ $sourceMachines = array_slice($sourceMachines, 0, 3);
+ cron_log("Running sending task on "
+ . implode(', ', array_map(function($item) { return $item['clientip']; }, $sourceMachines)));
+ $sourceTask = RebootControl::wakeViaClient($sourceMachines, $destMachine['macaddr'], $destMachine['bcast'], $passwd);
+ Taskmanager::release($sourceTask);
+ if (!Taskmanager::isRunning($sourceTask)) {
+ cron_log('Failed to launch task for source hosts...');
+ return false;
+ }
+ cron_log('Waiting for testing tasks to finish...');
+ // Loop as long as destination task and source task is running and we didn't see the pw at destination yet
+ while (Taskmanager::isRunning($destTask) && Taskmanager::isRunning($sourceTask)
+ && !destSawPw($destTask, $destMachine, $passwd) && $destDeadline > time()) {
+ $sourceTask = Taskmanager::status($sourceTask);
+ usleep(250000);
+ $destTask = Taskmanager::status($destTask);
+ }
+ cron_log($destTask['data']['result'][$destMachine['machineuuid']]['stdout']);
+ // Final moment: did dest see the packets from src? Determine this by looking for the generated password
+ if (destSawPw($destTask, $destMachine, $passwd))
+ return 1; // Found pw
+ return 0; // Nothing :-(
+}
+
+function testServerToClient($dstid)
+{
+ spawnDestinationListener($dstid, $destMachine, $destTask, $destDeadline);
+ if ($destMachine === false || !Taskmanager::isRunning($destTask))
+ return false; // No suitable dest-host found
+ $passwd = sprintf('%02x:%02x:%02x:%02x:%02x:%02x', mt_rand(0, 255), mt_rand(0, 255),
+ mt_rand(0, 255), mt_rand(0, 255), mt_rand(0, 255), mt_rand(0, 255));
+ cron_log('Sending WOL packets from Sat Server...');
+ $task = RebootControl::wakeDirectly($destMachine['macaddr'], $destMachine['bcast'], $passwd);
+ usleep(200000);
+ $destTask = Taskmanager::status($destTask);
+ if (!destSawPw($destTask, $destMachine, $passwd) && !Taskmanager::isTask($task))
+ return false;
+ cron_log('Waiting for receive on destination...');
+ $task = Taskmanager::status($task);
+ if (!destSawPw($destTask, $destMachine, $passwd)) {
+ $task = Taskmanager::waitComplete($task, 2000);
+ $destTask = Taskmanager::status($destTask);
+ }
+ cron_log($destTask['data']['result'][$destMachine['machineuuid']]['stdout']);
+ if (destSawPw($destTask, $destMachine, $passwd))
+ return 1;
+ return 0;
+}
+
+/**
+ * Take test result, turn into "next check" timestamp
+ */
+function resultToTime($result)
+{
+ if ($result === false) {
+ // Temporary failure -- couldn't run at least one destination and one source task
+ $next = 7200; // 2 hours
+ } elseif ($result === 0) {
+ // Test finished, subnet not reachable
+ $next = 86400 * 7; // a week
+ } else {
+ // Test finished, reachable
+ $next = 86400 * 30; // a month
+ }
+ return time() + round($next * mt_rand(90, 133) / 100);
+}
+
+/*
+ *
+ */
+
+// First, cleanup: delete orphaned subnets that don't exist anymore, or don't have any clients using our server
+$cutoff = strtotime('-180 days');
+Database::exec('DELETE FROM reboot_subnet WHERE fixed = 0 AND lastseen < :cutoff', ['cutoff' => $cutoff]);
+
+// Get machines running, group by subnet
+$cutoff = time() - 301; // Really only the ones that didn't miss the most recent update
+$res = Database::simpleQuery("SELECT s.subnetid, s.end AS bcast, m.machineuuid, m.clientip, m.macaddr
+ FROM reboot_subnet s
+ INNER JOIN machine m ON (
+ (m.state = 'IDLE' OR m.state = 'OCCUPIED')
+ AND
+ (m.lastseen >= $cutoff)
+ AND
+ (INET_ATON(m.clientip) BETWEEN s.start AND s.end)
+ )");
+
+//cron_log('Machine: ' . $res->rowCount());
+
+if ($res->rowCount() === 0)
+ return;
+
+Stuff::$subnets = [];
+while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ if (!isset(Stuff::$subnets[$row['subnetid']])) {
+ Stuff::$subnets[$row['subnetid']] = [];
+ }
+ Stuff::$subnets[$row['subnetid']][] = $row;
+}
+
+$task = Taskmanager::submit('DummyTask', []);
+$task = Taskmanager::waitComplete($task, 4000);
+if (!Taskmanager::isFinished($task)) {
+ cron_log('Task manager down. Doing nothing.');
+ return; // No :-(
+}
+unset($task);
+
+/*
+ * Try server to client
+ */
+
+$res = Database::simpleQuery("SELECT subnetid FROM reboot_subnet
+ WHERE subnetid IN (:active) AND nextdirectcheck < UNIX_TIMESTAMP() AND fixed = 0
+ ORDER BY nextdirectcheck ASC LIMIT 10", ['active' => array_keys(Stuff::$subnets)]);
+cron_log('Direct checks: ' . $res->rowCount() . ' (' . implode(', ', array_keys(Stuff::$subnets)) . ')');
+while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $dst = (int)$row['subnetid'];
+ cron_log('Direct check for subnetid ' . $dst);
+ $result = testServerToClient($dst);
+ $next = resultToTime($result);
+ if ($result === false) {
+ Database::exec('UPDATE reboot_subnet
+ SET nextdirectcheck = :nextcheck
+ WHERE subnetid = :dst', ['nextcheck' => $next, 'dst' => $dst]);
+ } else {
+ Database::exec('UPDATE reboot_subnet
+ SET nextdirectcheck = :nextcheck, isdirect = :isdirect
+ WHERE subnetid = :dst', ['nextcheck' => $next, 'isdirect' => $result, 'dst' => $dst]);
+ }
+}
+
+/*
+ * Try client to client
+ */
+
+// Query all possible combos
+$combos = [];
+foreach (Stuff::$subnets as $src => $_) {
+ $src = (int)$src;
+ foreach (Stuff::$subnets as $dst => $_) {
+ $dst = (int)$dst;
+ if ($src !== $dst) {
+ $combos[] = [$src, $dst];
+ }
+ }
+}
+
+// Check subnet to subnet
+if (count($combos) > 0) {
+ $res = Database::simpleQuery("SELECT ss.subnetid AS srcid, sd.subnetid AS dstid
+ FROM reboot_subnet ss
+ INNER JOIN reboot_subnet sd ON ((ss.subnetid, sd.subnetid) IN (:combos) AND sd.fixed = 0)
+ LEFT JOIN reboot_subnet_x_subnet sxs ON (ss.subnetid = sxs.srcid AND sd.subnetid = sxs.dstid)
+ WHERE sxs.nextcheck < UNIX_TIMESTAMP() OR sxs.nextcheck IS NULL
+ ORDER BY sxs.nextcheck ASC
+ LIMIT 10", ['combos' => $combos]);
+ cron_log('C2C checks: ' . $res->rowCount());
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $src = (int)$row['srcid'];
+ $dst = (int)$row['dstid'];
+ $result = testClientToClient($src, $dst);
+ $next = resultToTime($result);
+ Database::exec('INSERT INTO reboot_subnet_x_subnet (srcid, dstid, reachable, nextcheck)
+ VALUES (:srcid, :dstid, :reachable, :nextcheck)
+ ON DUPLICATE KEY UPDATE ' . ($result === false ? '' : 'reachable = VALUES(reachable),') . ' nextcheck = VALUES(nextcheck)',
+ ['srcid' => $src, 'dstid' => $dst, 'reachable' => (int)$result, 'nextcheck' => $next]);
+ }
+}
diff --git a/modules-available/rebootcontrol/inc/rebootcontrol.inc.php b/modules-available/rebootcontrol/inc/rebootcontrol.inc.php
index ec4b84ed..667c8bbd 100644
--- a/modules-available/rebootcontrol/inc/rebootcontrol.inc.php
+++ b/modules-available/rebootcontrol/inc/rebootcontrol.inc.php
@@ -5,9 +5,14 @@ class RebootControl
const KEY_TASKLIST = 'rebootcontrol.tasklist';
+ const KEY_AUTOSCAN_DISABLED = 'rebootcontrol.disable.scan';
+
const REBOOT = 'REBOOT';
const KEXEC_REBOOT = 'KEXEC_REBOOT';
const SHUTDOWN = 'SHUTDOWN';
+ const TASK_REBOOTCTL = 'TASK_REBOOTCTL';
+ const TASK_WOL = 'WAKE_ON_LAN';
+ const TASK_EXEC = 'REMOTE_EXEC';
/**
* @param string[] $uuids List of machineuuids to reboot
@@ -16,70 +21,498 @@ class RebootControl
*/
public static function reboot($uuids, $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($list, $mode, $minutes)
{
$task = Taskmanager::submit("RemoteReboot", array(
"clients" => $list,
"mode" => $mode,
"minutes" => $minutes,
- "locationId" => $locationId,
"sshkey" => SSHKey::getPrivateKey(),
"port" => 9922, // Hard-coded, must match mgmt-sshd module
));
if (!Taskmanager::isFailed($task)) {
- Property::addToList(RebootControl::KEY_TASKLIST, $locationId . '/' . $task["id"], 60 * 24);
+ self::addTask($task['id'], self::TASK_REBOOTCTL, $list, $task['id'], ['action' => $mode]);
}
return $task;
}
+ private static function extractLocationIds($from, &$to)
+ {
+ if (is_numeric($from)) {
+ $to[$from] = true;
+ return;
+ }
+ if (!is_array($from))
+ return;
+ $allnum = true;
+ foreach ($from as $k => $v) {
+ if (is_numeric($k) && is_numeric($v))
+ continue;
+ $allnum = false;
+ if (is_numeric($k) && is_array($v)) {
+ self::extractLocationIds($v, $to);
+ } else {
+ $k = strtolower($k);
+ if ($k === 'locationid' || $k === 'locationids' || $k === 'location' || $k === 'locations' || $k === 'lid' || $k === 'lids') {
+ self::extractLocationIds($v, $to);
+ } elseif ($k === 'client' || $k === 'clients' || $k === 'machine' || $k === 'machines') {
+ if (is_array($v)) {
+ self::extractLocationIds($v, $to);
+ }
+ }
+ }
+ }
+ if ($allnum) {
+ foreach ($from as $v) {
+ $to[$v] = true;
+ }
+ }
+ }
+
+ private static function addTask($id, $type, $clients, $taskIds, $other = false)
+ {
+ $lids = ArrayUtil::flattenByKey($clients, 'locationid');
+ $lids = array_unique($lids);
+ $newClients = [];
+ foreach ($clients as $c) {
+ $d = ['clientip' => $c['clientip']];
+ if (isset($c['machineuuid'])) {
+ $d['machineuuid'] = $c['machineuuid'];
+ }
+ $newClients[] = $d;
+ }
+ if (!is_array($taskIds)) {
+ $taskIds = [$taskIds];
+ }
+ $data = [
+ 'id' => $id,
+ 'type' => $type,
+ 'locations' => $lids,
+ 'clients' => $newClients,
+ 'tasks' => $taskIds,
+ ];
+ if (is_array($other)) {
+ $data += $other;
+ }
+ Property::addToList(RebootControl::KEY_TASKLIST, json_encode($data), 20);
+ }
+
/**
* @param int[]|null $locations filter by these locations
- * @return array list of active tasks for reboots/shutdowns.
+ * @return array|false list of active tasks for reboots/shutdowns.
*/
- public static function getActiveTasks($locations = null)
+ public static function getActiveTasks($locations = null, $id = null)
{
- if (is_array($locations) && in_array(0,$locations)) {
+ if (is_array($locations) && in_array(0, $locations)) {
$locations = null;
}
$list = Property::getList(RebootControl::KEY_TASKLIST);
$return = [];
foreach ($list as $entry) {
- $p = explode('/', $entry, 2);
- if (count($p) !== 2) {
+ $p = json_decode($entry, true);
+ if (!is_array($p) || !isset($p['id'])) {
Property::removeFromList(RebootControl::KEY_TASKLIST, $entry);
continue;
}
- if (is_array($locations) && !in_array($p[0], $locations)) // Ignore
+ if (is_array($locations) && is_array($p['locations']) && array_diff($p['locations'], $locations) !== [])
+ continue; // Not allowed
+ if ($id !== null) {
+ if ($p['id'] === $id)
+ return $p;
continue;
- $id = $p[1];
- $task = Taskmanager::status($id);
- if (!Taskmanager::isTask($task)) {
+ }
+ $valid = empty($p['tasks']);
+ if (!$valid) {
+ // Validate at least one task is still valid
+ foreach ($p['tasks'] as $task) {
+ $task = Taskmanager::status($task);
+ if (Taskmanager::isTask($task)) {
+ $p['status'] = $task['statusCode'];
+ $valid = true;
+ break;
+ }
+ }
+ }
+ if (!$valid) {
Property::removeFromList(RebootControl::KEY_TASKLIST, $entry);
continue;
}
- $return[] = [
- 'taskId' => $task['id'],
- 'locationId' => $task['data']['locationId'],
- 'time' => $task['data']['time'],
- 'mode' => $task['data']['mode'],
- 'clientCount' => count($task['data']['clients']),
- 'status' => $task['statusCode'],
- ];
+ $return[] = $p;
}
+ if ($id !== null)
+ return false;
return $return;
}
-} \ 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
+ */
+ public static function runScript($clients, $command, $timeout = 5, $privkey = false)
+ {
+ $task = self::runScriptInternal($clients, $command, $timeout, $privkey);
+ if (!Taskmanager::isFailed($task)) {
+ self::addTask($task['id'], self::TASK_EXEC, $clients, $task['id']);
+ }
+ return $task;
+ }
+
+ private static function runScriptInternal(&$clients, $command, $timeout = 5, $privkey = false)
+ {
+ $valid = [];
+ $invalid = [];
+ foreach ($clients as $client) {
+ 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)]);
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ 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]);
+ }
+
+ /**
+ * @param array $sourceMachines list of source machines. array of [clientip, machineuuid] entries
+ * @param string $bcast directed broadcast address to send to
+ * @param string|string[] $macaddr destination mac address(es)
+ * @param string $passwd optional WOL password, mac address or ipv4 notation
+ * @return array|false task struct, false on error
+ */
+ public static function wakeViaClient($sourceMachines, $macaddr, $bcast = false, $passwd = false)
+ {
+ $command = 'jawol';
+ if (!empty($bcast)) {
+ $command .= " -d '$bcast'";
+ }
+ if (!empty($passwd)) {
+ $command .= " -p '$passwd'";
+ }
+ if (is_array($macaddr)) {
+ $macaddr = implode("' '", $macaddr);
+ }
+ $command .= " '$macaddr'";
+ // Yes there is one zero missing from the usleep -- that's the whole point: we prefer 100ms sleeps
+ return self::runScriptInternal($sourceMachines,
+ "for i in 1 1 0; do $command; usleep \${i}00000 2> /dev/null || sleep \$i; done");
+ }
+
+ /**
+ * @param string|string[] $macaddr destination mac address(es)
+ * @param string $bcast directed broadcast address to send to
+ * @param string $passwd optional WOL password, mac address or ipv4 notation
+ * @return array|false task struct, false on error
+ */
+ public static function wakeDirectly($macaddr, $bcast = false, $passwd = false)
+ {
+ if (!is_array($macaddr)) {
+ $macaddr = [$macaddr];
+ }
+ return Taskmanager::submit('WakeOnLan', [
+ 'ip' => $bcast,
+ 'password' => $passwd,
+ 'macs' => $macaddr,
+ ]);
+ }
+
+ public static function wakeViaJumpHost($jumphost, $bcast, $clients)
+ {
+ $hostid = $jumphost['hostid'];
+ $macs = ArrayUtil::flattenByKey($clients, 'macaddr');
+ if (empty($macs)) {
+ error_log('Called wakeViaJumpHost without clients');
+ return false;
+ }
+ $macs = "'" . implode("' '", $macs) . "'";
+ $macs = str_replace('-', ':', $macs);
+ $script = str_replace(['%IP%', '%MACS%'], [$bcast, $macs], $jumphost['script']);
+ $task = RebootControl::runScriptInternal($_ = [[
+ 'clientip' => $jumphost['host'],
+ 'port' => $jumphost['port'],
+ 'username' => $jumphost['username'],
+ ]], $script, 6, $jumphost['sshkey']);
+ if ($task !== false && isset($task['id'])) {
+ TaskmanagerCallback::addCallback($task, 'rbcConnCheck', $hostid);
+ }
+ return $task;
+ }
+
+ /**
+ * @param array $list list of clients containing each keys 'macaddr' and 'clientip'
+ * @return string id of this job
+ */
+ public static function wakeMachines($list)
+ {
+ /* TODO: Refactor mom's spaghetti
+ * Now that I figured out what I want, do something like this:
+ * 1) Group clients by subnet
+ * 2) Only after step 1, start to collect possible ways to wake up clients for each subnet that's not empty
+ * 3) Habe some priority list for the methods, extend Taskmanager to have "negative dependency"
+ * i.e. submit task B with task A as parent task, but only launch task B if task A failed.
+ * If task A succeeded, mark task B as FINISHED immediately without actually running it.
+ * (or introduce new statusCode for this?)
+ */
+ $errors = '';
+ $tasks = [];
+ $bad = $unknown = [];
+ // Need all subnets...
+ $subnets = [];
+ $res = Database::simpleQuery('SELECT subnetid, start, end, isdirect FROM reboot_subnet');
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $row += [
+ 'jumphosts' => [],
+ 'direct' => [],
+ 'indirect' => [],
+ ];
+ $subnets[$row['subnetid']] = $row;
+ }
+ // Get all jump hosts
+ $jumphosts = [];
+ $res = Database::simpleQuery('SELECT jh.hostid, host, port, username, sshkey, script, jh.reachable,
+ Group_Concat(jxs.subnetid) AS subnets1, Group_Concat(sxs.dstid) AS subnets2
+ FROM reboot_jumphost jh
+ LEFT JOIN reboot_jumphost_x_subnet jxs ON (jh.hostid = jxs.hostid)
+ LEFT JOIN reboot_subnet s ON (INET_ATON(jh.host) BETWEEN s.start AND s.end)
+ LEFT JOIN reboot_subnet_x_subnet sxs ON (sxs.srcid = s.subnetid AND sxs.reachable <> 0)
+ GROUP BY jh.hostid');
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ if ($row['subnets1'] === null && $row['subnets2'] === null)
+ continue;
+ $nets = explode(',', $row['subnets1'] . ',' . $row['subnets2']);
+ foreach ($nets as $net) {
+ if (empty($net) || !isset($subnets[$net]))
+ continue;
+ $subnets[$net]['jumphosts'][$row['hostid']] = $row['hostid'];
+ }
+ $row['jobs'] = [];
+ $jumphosts[] = $row;
+ }
+ // Group by subnet
+ foreach ($list as $client) {
+ $ip = sprintf('%u', ip2long($client['clientip']));
+ //$client['numip'] = $ip;
+ unset($subnet);
+ $subnet = false;
+ foreach ($subnets as &$sn) {
+ if ($sn['start'] <= $ip && $sn['end'] >= $ip) {
+ $subnet =& $sn;
+ break;
+ }
+ }
+ $ok = false;
+ if (!$ok && $subnet === false) {
+ $unknown[] = $client;
+ $ok = true;
+ }
+ if (!$ok && $subnet['isdirect']) {
+ // Directly reachable
+ $subnet['direct'][] = $client;
+ $ok = true;
+ }
+ if (!$ok && !empty($subnet['jumphosts'])) {
+ foreach ($subnet['jumphosts'] as $hostid) {
+ if ($jumphosts[$hostid]['reachable'] != 0) {
+ $jumphosts[$hostid]['jobs'][$subnet['end']][] = $client;
+ $ok = true;
+ break;
+ }
+ }
+ }
+ if (!$ok) {
+ // find clients in same subnet, or reachable ones
+ self::findMachinesForSubnet($subnet);
+ if (empty($subnet['dclients']) && empty($subnet['iclients'])) {
+ // Nothing found -- cannot wake this host
+ $bad[] = $client;
+ } else {
+ // Found suitable indirect host
+ $subnet['indirect'][] = $client;
+ }
+ }
+ }
+ unset($subnet);
+ // Batch process
+ // First, via jump host
+ foreach ($jumphosts as $jh) {
+ foreach ($jh['jobs'] as $bcast => $clients) {
+ $errors .= 'Via jumphost ' . $jh['host'] . ': ' . implode(', ', ArrayUtil::flattenByKey($clients, 'clientip')) . "\n";
+ $task = self::wakeViaJumpHost($jh, $bcast, $clients);
+ if (Taskmanager::isFailed($task)) {
+ // TODO: Figure out $subnet from $bcast and queue as indirect
+ // (rather, overhaul this whole spaghetti code)
+ $errors .= ".... FAILED TO LAUNCH TASK ON JUMPHOST!\n";
+ }
+ }
+ }
+ // Server or client
+ foreach ($subnets as $subnet) {
+ if (!empty($subnet['direct'])) {
+ // Can wake directly
+ if (!self::wakeGroup('From server', $tasks, $errors, null, $subnet['direct'], $subnet['end'])) {
+ if (!empty($subnet['dclients']) || !empty($subnet['iclients'])) {
+ $errors .= "Re-queueing clients for indirect wakeup\n";
+ $subnet['indirect'] = array_merge($subnet['indirect'], $subnet['direct']);
+ }
+ }
+ }
+ if (!empty($subnet['indirect'])) {
+ // Can wake indirectly
+ $ok = false;
+ if (!empty($subnet['dclients'])) {
+ $ok = true;
+ if (!self::wakeGroup('in same subnet', $tasks, $errors, $subnet['dclients'], $subnet['indirect'])) {
+ if (!empty($subnet['iclients'])) {
+ $errors .= "Re-re-queueing clients for indirect wakeup\n";
+ $ok = false;
+ }
+ }
+ }
+ if (!$ok && !empty($subnet['iclients'])) {
+ $ok = self::wakeGroup('across subnets', $tasks, $errors, $subnet['dclients'], $subnet['indirect'], $subnet['end']);
+ }
+ if (!$ok) {
+ $errors .= "I'm all out of ideas.\n";
+ }
+ }
+ }
+ if (!empty($bad)) {
+ $ips = ArrayUtil::flattenByKey($bad, 'clientip');
+ $errors .= "**** WARNING ****\nNo way to send WOL packets to the following machines:\n" . implode("\n", $ips) . "\n";
+ }
+ if (!empty($unknown)) {
+ $ips = ArrayUtil::flattenByKey($unknown, 'clientip');
+ $errors .= "**** WARNING ****\nThe following clients do not belong to a known subnet (bug?)\n" . implode("\n", $ips) . "\n";
+ }
+ $id = Util::randomUuid();
+ self::addTask($id, self::TASK_WOL, $list, $tasks, ['log' => $errors]);
+ return $id;
+ }
+
+ private static function wakeGroup($type, &$tasks, &$errors, $via, $clients, $bcast = false)
+ {
+ $macs = ArrayUtil::flattenByKey($clients, 'macaddr');
+ $ips = ArrayUtil::flattenByKey($clients, 'clientip');
+ if ($via !== null) {
+ $srcips = ArrayUtil::flattenByKey($via, 'clientip');
+ $errors .= 'Via ' . implode(', ', $srcips) . ' ';
+ }
+ $errors .= $type . ': ' . implode(', ', $ips);
+ if ($bcast !== false) {
+ $errors .= ' (UDP to ' . long2ip($bcast) . ')';
+ }
+ $errors .= "\n";
+ if ($via === null) {
+ $task = self::wakeDirectly($macs, $bcast);
+ } else {
+ $task = self::wakeViaClient($via, $macs, $bcast);
+ }
+ if ($task !== false && isset($task['id'])) {
+ $tasks[] = $task['id'];
+ }
+ if (Taskmanager::isFailed($task)) {
+ $errors .= ".... FAILED TO START ACCORDING TASK!\n";
+ return false;
+ }
+ return true;
+ }
+
+ private static function findMachinesForSubnet(&$subnet)
+ {
+ if (isset($subnet['dclients']))
+ return;
+ $cutoff = time() - 302;
+ // Get clients from same subnet first
+ $subnet['dclients'] = Database::queryAll("SELECT machineuuid, clientip FROM machine
+ WHERE state IN ('IDLE', 'OCCUPIED') AND INET_ATON(clientip) BETWEEN :start AND :end AND lastseen > :cutoff
+ LIMIT 3",
+ ['start' => $subnet['start'], 'end' => $subnet['end'], 'cutoff' => $cutoff]);
+ $subnet['iclients'] = [];
+ if (!empty($subnet['dclients']))
+ return;
+ // If none, get clients from other subnets known to be able to reach this one
+ $subnet['iclients'] = Database::queryAll("SELECT m.machineuuid, m.clientip FROM reboot_subnet_x_subnet sxs
+ INNER JOIN reboot_subnet s ON (s.subnetid = sxs.srcid AND sxs.dstid = :subnetid)
+ INNER JOIN machine m ON (INET_ATON(m.clientip) BETWEEN s.start AND s.end AND state IN ('IDLE', 'OCCUPIED') AND m.lastseen > :cutoff)
+ LIMIT 20", ['subnetid' => $subnet['subnetid'], 'cutoff' => $cutoff]);
+ shuffle($subnet['iclients']);
+ $subnet['iclients'] = array_slice($subnet['iclients'], 0, 3);
+ }
+
+ public static function prepareExec()
+ {
+ User::assertPermission('action.exec');
+ $uuids = array_values(Request::post('uuid', Request::REQUIRED, 'array'));
+ $machines = RebootUtils::getFilteredMachineList($uuids, 'action.exec');
+ if ($machines === false)
+ return;
+ RebootUtils::sortRunningFirst($machines);
+ $id = mt_rand();
+ Session::set('exec-' . $id, $machines, 60);
+ Session::save();
+ Util::redirect('?do=rebootcontrol&show=exec&what=prepare&id=' . $id);
+ }
+
+}
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..99235e8a
--- /dev/null
+++ b/modules-available/rebootcontrol/inc/rebootutils.inc.php
@@ -0,0 +1,75 @@
+<?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($list, $assoc = false, $columns = ['machineuuid', 'hostname', 'clientip', 'state', 'locationid'])
+ {
+ if (empty($list))
+ return array();
+ if (is_array($columns)) {
+ $columns = implode(',', $columns);
+ }
+ $res = Database::simpleQuery("SELECT $columns FROM machine
+ WHERE machineuuid IN (:list)", compact('list'));
+ if (!$assoc)
+ return $res->fetchAll(PDO::FETCH_ASSOC);
+ $ret = [];
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $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(&$clients)
+ {
+ 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($requestedClients, $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/install.inc.php b/modules-available/rebootcontrol/install.inc.php
new file mode 100644
index 00000000..0aedfa20
--- /dev/null
+++ b/modules-available/rebootcontrol/install.inc.php
@@ -0,0 +1,49 @@
+<?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[] = 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');
+
+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..32da10b1 100644
--- a/modules-available/rebootcontrol/lang/de/messages.json
+++ b/modules-available/rebootcontrol/lang/de/messages.json
@@ -1,4 +1,18 @@
{
+ "invalid-cidr": "Ung\u00fcltige CIDR-Angabe: {{0}}",
+ "invalid-port": "Ung\u00fcltiger Port: {{0}}",
+ "invalid-subnet": "Ung\u00fcltiges Subnetz: {{0}}",
+ "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-updated": "Subnetz aktualisiert",
+ "unknown-exec-job": "Unbekannte Job-ID",
+ "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..24dae3f1 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": "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..a5ae8c9c 100644
--- a/modules-available/rebootcontrol/lang/de/permissions.json
+++ b/modules-available/rebootcontrol/lang/de/permissions.json
@@ -1,5 +1,14 @@
{
- "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.wol": "Client per WOL starten.",
+ "jumphost.assign-subnet": "Einem Sprung-Host ein Subnetz zuweisen.",
+ "jumphost.edit": "Einen Sprung-Host bearbeiten.",
+ "jumphost.view": "Liste der Sprung-Hosts sehen.",
+ "newkeypair": "Neues Schl\u00fcsselpaar generieren.",
+ "subnet.edit": "Subnetze hinzuf\u00fcgen\/entfernen.",
+ "subnet.flag": "Eigenschaften eines Subnetzs bearbeiten.",
+ "subnet.view": "Liste der Subnetzs sehen.",
+ "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..3dd5a3e6 100644
--- a/modules-available/rebootcontrol/lang/de/template-tags.json
+++ b/modules-available/rebootcontrol/lang/de/template-tags.json
@@ -1,39 +1,74 @@
{
+ "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_connecting": "Verbinde...",
+ "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 ideses Subnetz nicht mehr automatisch ermittelt. Sie k\u00f6nnen in diesem Fall selber festlegen, ob das Subnetz WOL-Pakete vom Satelliten-Server empfangen kann. Au\u00dferdem wird das Subnetz bei Setzen dieser Option nicht mehr automatisch aus der Datenbank gel\u00f6scht, wenn 6 Monate lang kein Client in diesem Subnetz gesehen wurde.",
+ "lang_fixSubnetSettings": "Subnetz-Einstellungen manuell festlegen",
"lang_genNew": "Neues Schl\u00fcsselpaar generieren",
+ "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": "Vom Satellit erreichbar",
+ "lang_isFixed": "Manuell konfiguriert",
+ "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": "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 Satelliten-Server",
+ "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_saveWolAutoDiscover": "Auto-Erkennung ein\/ausschalten",
+ "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 Satelliten-Server bekannte Subnetze. Damit WOL \u00fcber Subnetz-Grenzen hinaus funktioniert, muss bekannt sein, in welche Netze \"Directed Broadcasts\" gesendet werden k\u00f6nnen, bzw. f\u00fcr welche Netze ein \"Sprung-Host\" existiert. Diese Liste wird sich automatisch f\u00fcllen, wenn Clients gestartet werden. Au\u00dferdem wird automatisch ermittelt, welche Netze mittels \"Directed Broadcasts\" erreichbar sind, sofern diese Funktion nicht oben unter \"Einstellungen\" deaktiviert wird.",
+ "lang_task": "Task",
+ "lang_taskListIntro": "Hier sehen Sie eine Liste k\u00fcrzlich gestarteter Aufgaben, wie z.B. WOL-Aktionen, das Neustarten oder Herunterfahren von Clients, etc.",
+ "lang_wakeScriptHelp": "Dieses Script wird auf dem Sprung-Host ausgef\u00fchrt, um den\/die gew\u00fcnschten Maschinen aufzuwecken. Es wird unter der Standard-Shell des oben angegebenen Benutzers ausgef\u00fchrt. Das Script kann zwei spezielle Platzhalter enthalten, die vor dem Ausf\u00fchren des Scripts vom Satellitenserver ersetzt werden: %MACS% ist eine durch Leerzeichen getrennte Liste von MAC-Adressen, die aufzuwecken sind. Das Tool \"wakeonlan\" unterst\u00fctzt direkt mehrere MAC-Adressen, sodass der Platzhalter %MACS% direkt als Kommandozeilenargument verwendet werden kann. Das Tool \"etherwake\" hingegen kann pro Aufruf immer nur einen Host aufwecken, weshalb eine for-Schleife notwendig ist. Au\u00dferdem wird der Platzhalter %IP% ersetzt, welcher je nach Ziel entweder \"255.255.255.255\" ist, oder bei einem netz\u00fcbergreifenden WOL-Paket die \"directed broadcast address\" des Zielnetzes. Netz\u00fcbergreifende WOL-Pakete werden vom \"etherwake\" nicht unterst\u00fctzt.",
+ "lang_wakeupScript": "Aufweck-Script",
+ "lang_wolAutoDiscoverCheck": "WOL-Erreichbarkeit von Subnetzs automatisch ermitteln",
+ "lang_wolDiscoverDescription": "Ist diese Option aktiv, ermitteln Server und Clients automatisch, welche Netze von wo mittels WOL erreichbar sind.",
+ "lang_wolDiscoverHeading": "Automatische WOL-Ermittlung",
+ "lang_wolMachineSupportText": "Sind die technischen Voraussetzungen erf\u00fcllt, dass ein WOL-Paket den gew\u00fcnschten Rechner erreichen kann, ist es weiterhin erforderlich, dass der Rechner mittels BIOS und evtl. vorhandenem Betriebssystem so konfiguriert wird, dass er auch auf WOL-Pakete reagiert. Zum einen muss die Funktion im BIOS aktiviert sein. Hier ist auch darauf zu achten, ob es eine zus\u00e4tzliche Einstellung gibt, die die normale Bootreihenfolge \u00fcberschreibt, und dass diese wie gew\u00fcnscht konfiguriert wird. Ist WOL im BIOS aktiviert, kann das Betriebssystem die Funktionalit\u00e4t noch per Software ein- und ausschalten. Unter Windows erfolgt dies im Ger\u00e4temanager in den Eigenschaften der Netzwerkkarte. Dies ist relevant, wenn parallel zu bwLehrpool noch ein Windows von der lokaler Platte betrieben wird. Unter Linux kann die WOL-Funktion mit dem ethtool beeinflusst werden. bwLehrpool aktiviert WOL automatisch bei jedem Boot.",
+ "lang_wolReachability": "WOL-Erreichbarkeit"
} \ 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..47d91e98 100644
--- a/modules-available/rebootcontrol/lang/en/module.json
+++ b/modules-available/rebootcontrol/lang/en/module.json
@@ -1,4 +1,4 @@
{
- "module_name": "Reboot Control",
- "page_title": "Reboot Control"
+ "module_name": "WakeOnLAN",
+ "page_title": "WakeOnLAN"
} \ 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..9c7830aa 100644
--- a/modules-available/rebootcontrol/lang/en/template-tags.json
+++ b/modules-available/rebootcontrol/lang/en/template-tags.json
@@ -9,9 +9,11 @@
"lang_genNew": "Generate new keypair",
"lang_ip": "IP",
"lang_kexecRebootCheck": "Quick reboot straight to bwLehrpool (kexec)",
+ "lang_keypairConfirmCheck": "I'm sure",
"lang_location": "Location",
"lang_minutes": " Minutes",
"lang_mode": "Mode",
+ "lang_moduleHeading": "WakeOnLAN",
"lang_newKeypairExplanation": "You can create a new keypair, which will replace the old one. Please note that after doing so, you cannot poweroff or reboot clients that are already running, since they still use the old key. They have to be rebooted manually first.",
"lang_off": "Off",
"lang_on": "On",
diff --git a/modules-available/rebootcontrol/page.inc.php b/modules-available/rebootcontrol/page.inc.php
index 3a438504..cf87a3b3 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,76 @@ 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', true));
+ }
+ if (User::hasPermission('subnet.*')) {
+ Dashboard::addSubmenu('?do=rebootcontrol&show=subnet', Dictionary::translate('subnets', true));
+ }
- $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);
+ Property::set(RebootControl::KEY_AUTOSCAN_DISABLED, !$enabled);
+ if ($enabled) {
+ Message::addInfo('woldiscover-enabled');
} else {
- $locationId = $actualClients[$idx]['locationid'];
+ Message::addInfo('woldiscover-disabled');
}
- }
- // 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"]);
- } else {
- Util::redirect("?do=rebootcontrol");
+ $section = 'subnet'; // For redirect below
}
}
+ if (Request::isPost()) {
+ Util::redirect('?do=rebootcontrol' . ($section ? '&show=' . $section : ''));
+ } elseif ($section === false) {
+ Util::redirect('?do=rebootcontrol&show=task');
+ }
+ }
+
+ 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"]);
+ }
+ return;
}
/**
@@ -81,94 +98,49 @@ 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]);
- }
-
- }
+ // Always show public key (it's public, isn't it?)
+ $data = [
+ 'pubkey' => SSHKey::getPublicKey(),
+ 'wol_auto_checked' => Property::get(RebootControl::KEY_AUTOSCAN_DISABLED) ? '' : 'checked',
+ ];
+ Permission::addGlobalTags($data['perms'], null, ['newkeypair', 'woldiscover']);
+ Render::addTemplate('header', $data);
+
+ if ($this->haveSubpage) {
+ SubPage::doRender();
+ return;
}
}
- 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..0c40c313
--- /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()
+ {
+ $id = Request::post('id', Request::REQUIRED, 'int');
+ $machines = Session::get('exec-' . $id);
+ if (!is_array($machines)) {
+ Message::addError('unknown-exec-job', $id);
+ return;
+ }
+ $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;
+ }
+ 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..7d1877d2
--- /dev/null
+++ b/modules-available/rebootcontrol/pages/jumphost.inc.php
@@ -0,0 +1,208 @@
+<?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;
+ }
+ }
+
+ private static function execCheckConnection($hostid)
+ {
+ $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 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');
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $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 = [];
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $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..45151954
--- /dev/null
+++ b/modules-available/rebootcontrol/pages/subnet.inc.php
@@ -0,0 +1,156 @@
+<?php
+
+class SubPage
+{
+
+ public static function doPreprocess()
+ {
+ $action = Request::post('action', false, 'string');
+ if ($action === 'add') {
+ self::addSubnet();
+ } elseif ($action === 'edit') {
+ self::editSubnet();
+ }
+ }
+
+ /*
+ * POST
+ */
+
+ private static function addSubnet()
+ {
+ User::assertPermission('subnet.edit');
+ $cidr = Request::post('cidr', Request::REQUIRED, 'string');
+ $range = IpUtil::parseCidr($cidr);
+ if ($range === false) {
+ Message::addError('invalid-cidr', $cidr);
+ return;
+ }
+ $ret = Database::exec('INSERT INTO reboot_subnet (start, end, fixed, isdirect)
+ VALUES (:start, :end, 1, 0)', [
+ 'start' => $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 AND', ['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');
+ }
+
+ /*
+ * 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 = [];
+ $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');
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $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';
+ }
+ $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 = [];
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $row['checked'] = $row['subnetid'] === null ? '' : 'checked';
+ $jh[] = $row;
+ }
+ $subnet['jumpHosts'] = $jh;
+ // Get list of all subnets that can broadcast into this one
+ $res = Database::simpleQuery('SELECT s.start, s.end FROM reboot_subnet s
+ INNER JOIN reboot_subnet_x_subnet sxs ON (s.subnetid = sxs.srcid AND sxs.dstid = :id AND sxs.reachable = 1)
+ ORDER BY s.start ASC', ['id' => $id]);
+ $sn = [];
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $sn[] = ['cidr' => IpUtil::rangeToCidr($row['start'], $row['end'])];
+ }
+ $subnet['sourceNets'] = $sn;
+ Permission::addGlobalTags($subnet['perms'], null, ['subnet.flag', 'jumphost.view', 'jumphost.assign-subnet']);
+ Render::addTemplate('subnet-edit', $subnet);
+ }
+
+ 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..691fd9e2
--- /dev/null
+++ b/modules-available/rebootcontrol/pages/task.inc.php
@@ -0,0 +1,150 @@
+<?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()
+ {
+ $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 {
+ Util::traceError('oopsie');
+ }
+ Render::addTemplate('status-' . $template, $job);
+ }
+
+ private static function showTaskList()
+ {
+ Render::addTemplate('task-header');
+ // Append list of active reboot/shutdown tasks
+ $allowedLocs = User::getAllowedLocations("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']);
+ }
+ }
+ unset($entry);
+ Render::addTemplate('task-list', ['list' => $active]);
+ }
+ }
+
+ private static function expandLocationIds(&$lids)
+ {
+ foreach ($lids as &$locid) {
+ if ($locid === 0) {
+ $name = '-';
+ } else {
+ $name = Location::getName($locid);
+ }
+ $locid = ['id' => $locid, 'name' => $name];
+ }
+ $lids = array_values($lids);
+ }
+
+ public static function doAjax()
+ {
+
+ }
+
+}
+
+
+// Remove when we require >= 7.3.0
+if (!function_exists('array_key_first')) {
+ function array_key_first(array $arr) {
+ foreach($arr as $key => $unused) {
+ return $key;
+ }
+ return NULL;
+ }
+}
diff --git a/modules-available/rebootcontrol/permissions/permissions.json b/modules-available/rebootcontrol/permissions/permissions.json
index a058ffbf..5416a482 100644
--- a/modules-available/rebootcontrol/permissions/permissions.json
+++ b/modules-available/rebootcontrol/permissions/permissions.json
@@ -2,10 +2,37 @@
"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
}
} \ 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..5916e2a8
--- /dev/null
+++ b/modules-available/rebootcontrol/templates/exec-enter-command.html
@@ -0,0 +1,41 @@
+<h2>{{lang_execRemoteCommand}}</h2>
+
+<table class="table table-hover stupidtable" id="dataTable">
+ <thead>
+ <tr>
+ <th data-sort="string">{{lang_client}}</th>
+ <th data-sort="ipv4">{{lang_ip}}</th>
+ <th data-sort="string">
+ {{lang_status}}
+ </th>
+ </tr>
+ </thead>
+
+ <tbody>
+ {{#clients}}
+ <tr>
+ <td>{{hostname}}{{^hostname}}{{machineuuid}}{{/hostname}}</td>
+ <td>{{clientip}}</td>
+ <td>{{state}}</td>
+ </tr>
+ {{/clients}}
+ </tbody>
+</table>
+
+<h3>{{lang_enterCommand}}</h3>
+
+<form method="post" action="?do=rebootcontrol" id="list-form">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="show" value="exec">
+ <input type="hidden" name="id" value="{{id}}">
+ <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..6d38b939 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,73 @@
<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"></div>
+ </div>
+ <div class="modal-body">
+ <label>{{lang_wolDiscoverHeading}}</label>
+ <form method="post" action="?do=rebootcontrol">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="action" value="toggle-wol">
+ <div class="checkbox">
+ <input {{perms.woldiscover.disabled}} id="wol-auto-discover" type="checkbox" name="enabled" {{wol_auto_checked}}>
+ <label for="wol-auto-discover">{{lang_wolAutoDiscoverCheck}}</label>
+ </div>
+ <div class="slx-space"></div>
+ <p>{{lang_wolDiscoverDescription}}</p>
+ <button {{perms.woldiscover.disabled}} class="btn btn-primary pull-right"
+ onclick="generateNewKeypair()" type="submit">
+ <span class="glyphicon glyphicon-floppy-disk"></span>
+ {{lang_saveWolAutoDiscover}}
+ </button>
+ <div class="clearfix"></div>
+ </form>
+ </div>
+ <div class="modal-body">
</div>
</div>
</div>
</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
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..6023c872
--- /dev/null
+++ b/modules-available/rebootcontrol/templates/jumphost-list.html
@@ -0,0 +1,65 @@
+<h2>{{lang_settings}}</h2>
+
+<h3>{{lang_wolReachability}}</h3>
+
+<h4>{{lang_jumpHosts}}</h4>
+
+<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..e31d95ea
--- /dev/null
+++ b/modules-available/rebootcontrol/templates/status-checkconnection.html
@@ -0,0 +1,47 @@
+<h3>{{lang_checkingJumpHost}}: {{host}}</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..403b7fca
--- /dev/null
+++ b/modules-available/rebootcontrol/templates/status-exec.html
@@ -0,0 +1,63 @@
+<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">{{hostname}}{{^hostname}}{{clientip}}{{/hostname}}</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;
+ }
+}
+
+//--></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..240c4387 100644
--- a/modules-available/rebootcontrol/templates/status.html
+++ b/modules-available/rebootcontrol/templates/status-reboot.html
@@ -1,37 +1,35 @@
-<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}}</h3>
+{{#locations}}
+<div class="loc">{{name}}</div>
+{{/locations}}
+<div class="clearfix slx-space"></div>
-<div>
- <table class="table table-hover stupidtable" id="dataTable">
- <thead>
- <tr>
- <th data-sort="string">{{lang_client}}</th>
- <th data-sort="ipv4">{{lang_ip}}</th>
- <th data-sort="string">
- {{lang_status}}
- </th>
- </tr>
- </thead>
+<table class="table table-hover stupidtable" id="dataTable">
+ <thead>
+ <tr>
+ <th data-sort="string">{{lang_client}}</th>
+ <th data-sort="ipv4">{{lang_ip}}</th>
+ <th data-sort="string">
+ {{lang_status}}
+ </th>
+ </tr>
+ </thead>
- <tbody>
- {{#clients}}
- <tr>
- <td>{{hostname}}{{^hostname}}{{machineuuid}}{{/hostname}}</td>
- <td>{{clientip}}</td>
- <td id="status-{{machineuuid}}"></td>
- </tr>
- {{/clients}}
- </tbody>
- </table>
-</div>
+ <tbody>
+ {{#clients}}
+ <tr>
+ <td>{{hostname}}{{^hostname}}{{machineuuid}}{{/hostname}}</td>
+ <td>{{clientip}}</td>
+ <td>
+ <span id="status-{{machineuuid}}" class="machineuuid" data-uuid="{{machineuuid}}"></span>
+ <span id="text-{{machineuuid}}"></span>
+ </td>
+ </tr>
+ {{/clients}}
+ </tbody>
+</table>
-<div data-tm-id="{{taskId}}" data-tm-log="error" data-tm-callback="updateStatus"></div>
+<div data-tm-id="{{id}}" data-tm-log="error" data-tm-callback="updateStatus"></div>
<script type="application/javascript">
statusStrings = {
@@ -48,11 +46,12 @@
function updateStatus(task) {
if (!task || !task.data || !task.data.clientStatus)
return;
+ stillActive = true;
var clientStatus = task.data.clientStatus;
for (var uuid in clientStatus) {
if (!clientStatus.hasOwnProperty(uuid))
continue;
- var $s = $("#status-" + uuid);
+ var $s = $("#text-" + uuid);
var status = clientStatus[uuid];
if ($s.data('state') === status)
continue;
diff --git a/modules-available/rebootcontrol/templates/status-wol.html b/modules-available/rebootcontrol/templates/status-wol.html
new file mode 100644
index 00000000..3e83126c
--- /dev/null
+++ b/modules-available/rebootcontrol/templates/status-wol.html
@@ -0,0 +1,52 @@
+{{#locations}}
+<div class="loc">{{name}}</div>
+{{/locations}}
+<div class="clearfix slx-space"></div>
+
+{{#tasks}}
+<div data-tm-id="{{.}}" data-tm-callback="wolCallback">{{lang_aWolJob}}</div>
+{{/tasks}}
+{{^tasks}}
+<div class="alert alert-warning">
+ <span class="glyphicon glyphicon-exclamation-sign"></span>
+ {{lang_noTasksForJob}}
+</div>
+{{/tasks}}
+
+<pre>{{log}}</pre>
+
+<table class="table table-hover stupidtable" id="dataTable">
+ <thead>
+ <tr>
+ <th data-sort="string">{{lang_client}}</th>
+ <th data-sort="ipv4">{{lang_ip}}</th>
+ <th data-sort="string">
+ {{lang_status}}
+ </th>
+ </tr>
+ </thead>
+
+ <tbody>
+ {{#clients}}
+ <tr>
+ <td>{{hostname}}{{^hostname}}{{machineuuid}}{{^machineuuid}}{{clientip}}{{/machineuuid}}{{/hostname}}</td>
+ <td>{{clientip}}</td>
+ {{#machineuuid}}
+ <td>
+ <span id="status-{{machineuuid}}" class="machineuuid" data-uuid="{{machineuuid}}"></span>
+ <span id="spinner-{{machineuuid}}" class="glyphicon glyphicon-refresh slx-rotation">
+ </td>
+ {{/machineuuid}}
+ {{^machineuuid}}
+ <td></td>
+ {{/machineuuid}}
+ </tr>
+ {{/clients}}
+ </tbody>
+</table>
+
+<script><!--
+function wolCallback(task) {
+ stillActive = true;
+}
+//--></script> \ No newline at end of file
diff --git a/modules-available/rebootcontrol/templates/subnet-edit.html b/modules-available/rebootcontrol/templates/subnet-edit.html
new file mode 100644
index 00000000..d8173863
--- /dev/null
+++ b/modules-available/rebootcontrol/templates/subnet-edit.html
@@ -0,0 +1,70 @@
+<!-- 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>
+ <div class="list-group-item">
+ <label>{{lang_reachableFrom}}</label>
+ {{#sourceNets}}
+ <div>{{cidr}}</div>
+ {{/sourceNets}}
+ </div>
+ </div>
+ <div class="panel-footer text-right">
+ <button type="submit" class="btn btn-primary" name="action" value="edit" {{perms.subnet.flag.disabled}}>
+ <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..bf6cee1c
--- /dev/null
+++ b/modules-available/rebootcontrol/templates/subnet-list.html
@@ -0,0 +1,53 @@
+<!-- subnetid, start, end, fixed, isdirect, lastdirectcheck, lastseen, seencount -->
+
+<h2>{{lang_subnets}}</h2>
+
+<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>
+ <a href="?do=rebootcontrol&show=subnet&what=subnet&id={{subnetid}}">{{cidr}}</a>
+ </td>
+ <td class="text-center">{{#fixed}}<span class="glyphicon glyphicon-lock"></span>{{/fixed}}</td>
+ <td class="text-center">{{#isdirect}}<span class="glyphicon glyphicon-ok"></span>{{/isdirect}}</td>
+ <td class="text-right">{{jumphostcount}} / {{sourcecount}}</td>
+ <td class="{{lastseen_class}}">{{lastseen_s}}</td>
+ </tr>
+ {{/subnets}}
+ </tbody>
+</table>
+
+<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..5ab75675 100644
--- a/modules-available/rebootcontrol/templates/task-list.html
+++ b/modules-available/rebootcontrol/templates/task-list.html
@@ -2,9 +2,8 @@
<table class="table">
<thead>
<tr>
- <th>{{lang_mode}}</th>
+ <th>{{lang_task}}</th>
<th>{{lang_location}}</th>
- <th>{{lang_time}}</th>
<th>{{lang_clientCount}}</th>
<th>{{lang_status}}</th>
</tr>
@@ -12,19 +11,20 @@
<tbody>
{{#list}}
<tr>
- <td>
- <a href="?do=rebootcontrol&amp;taskid={{taskId}}">{{mode}}</a>
+ <td class="text-nowrap">
+ <a href="?do=rebootcontrol&amp;show=task&amp;what=task&amp;taskid={{id}}">{{type}}</a>
+ <div class="small">{{action}}</div>
</td>
<td>
- {{locationName}}
+ {{#locations}}
+ <div class="loc">{{name}}</div>
+ {{/locations}}
+ <div class="clearfix"></div>
</td>
- <td>
- {{time}}
+ <td class="text-nowrap">
+ {{clients}}
</td>
- <td>
- {{clientCount}}
- </td>
- <td>
+ <td class="text-nowrap">
{{status}}
</td>
</tr>
diff --git a/modules-available/statistics/api.inc.php b/modules-available/statistics/api.inc.php
index 1cf98ae4..64925791 100644
--- a/modules-available/statistics/api.inc.php
+++ b/modules-available/statistics/api.inc.php
@@ -158,6 +158,30 @@ if ($type{0} === '~') {
}
}
+ // Inform WOL (rebootcontrol) module about subnet size
+ if (Module::get('rebootcontrol') !== false) {
+ $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)(pow(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);
+ }
+ }
+ }
+ }
+
// Write statistics data
} else if ($type === '~runstate') {
diff --git a/modules-available/statistics/lang/de/template-tags.json b/modules-available/statistics/lang/de/template-tags.json
index 1e92aeed..68056a7a 100644
--- a/modules-available/statistics/lang/de/template-tags.json
+++ b/modules-available/statistics/lang/de/template-tags.json
@@ -115,6 +115,7 @@
"lang_usageState": "Zustand",
"lang_uuid": "UUID",
"lang_virtualCores": "Virtuelle Kerne",
+ "lang_wakeOnLan": "WakeOnLan",
"lang_when": "Wann",
"lang_withBadSectors": "Clients mit potentiell defekten Festplatten (mehr als 10 defekte Sektoren)"
} \ No newline at end of file
diff --git a/modules-available/statistics/lang/en/template-tags.json b/modules-available/statistics/lang/en/template-tags.json
index 781bceb1..c38f3350 100644
--- a/modules-available/statistics/lang/en/template-tags.json
+++ b/modules-available/statistics/lang/en/template-tags.json
@@ -114,6 +114,7 @@
"lang_usageState": "State",
"lang_uuid": "UUID",
"lang_virtualCores": "Virtual cores",
+ "lang_wakeOnLan": "WakeOnLan",
"lang_when": "When",
"lang_withBadSectors": "Clients with potentially bad HDDs (more than 10 reallocated sectors)"
} \ No newline at end of file
diff --git a/modules-available/statistics/page.inc.php b/modules-available/statistics/page.inc.php
index 68649d91..533a9bf9 100644
--- a/modules-available/statistics/page.inc.php
+++ b/modules-available/statistics/page.inc.php
@@ -72,6 +72,12 @@ class Page_Statistics extends Page
$this->rebootControl(true);
} elseif ($action === 'shutdownmachines') {
$this->rebootControl(false);
+ } elseif ($action === 'wol') {
+ $this->wol();
+ } elseif ($action === 'prepare-exec') {
+ if (Module::isAvailable('rebootcontrol')) {
+ RebootControl::prepareExec();
+ }
}
// Make sure we don't render any content for POST requests - should be handled above and then
@@ -79,6 +85,23 @@ class Page_Statistics extends Page
Util::redirect('?do=statistics');
}
+ private function wol()
+ {
+ if (!Module::isAvailable('rebootcontrol'))
+ return;
+ $ids = Request::post('uuid', [], 'array');
+ $ids = array_values($ids);
+ if (empty($ids)) {
+ Message::addError('main.parameter-empty', 'uuid');
+ return;
+ }
+ $this->getAllowedMachines(".rebootcontrol.action.wol", $ids, $allowedMachines);
+ if (empty($allowedMachines))
+ return;
+ $taskid = RebootControl::wakeMachines($allowedMachines);
+ Util::redirect('?do=rebootcontrol&show=task&what=task&taskid=' . $taskid);
+ }
+
/**
* @param bool $reboot true = reboot, false = shutdown
*/
@@ -92,12 +115,31 @@ class Page_Statistics extends Page
Message::addError('main.parameter-empty', 'uuid');
return;
}
- $allowedLocations = User::getAllowedLocations(".rebootcontrol.action." . ($reboot ? 'reboot' : 'shutdown'));
+ $this->getAllowedMachines(".rebootcontrol.action." . ($reboot ? 'reboot' : 'shutdown'), $ids, $allowedMachines);
+ if (empty($allowedMachines))
+ return;
+ if ($reboot && Request::post('kexec', false)) {
+ $action = RebootControl::KEXEC_REBOOT;
+ } elseif ($reboot) {
+ $action = RebootControl::REBOOT;
+ } else {
+ $action = RebootControl::SHUTDOWN;
+ }
+ $task = RebootControl::execute($allowedMachines, $action, 0);
+ if (Taskmanager::isTask($task)) {
+ Util::redirect("?do=rebootcontrol&show=task&what=task&taskid=" . $task["id"]);
+ }
+ }
+
+ private function getAllowedMachines($permission, $ids, &$allowedMachines)
+ {
+ $allowedLocations = User::getAllowedLocations($permission);
if (empty($allowedLocations)) {
Message::addError('main.no-permission');
Util::redirect('?do=statistics');
}
- $res = Database::simpleQuery('SELECT machineuuid, clientip, locationid FROM machine WHERE machineuuid IN (:ids)', compact('ids'));
+ $res = Database::simpleQuery('SELECT machineuuid, clientip, macaddr, locationid FROM machine
+ WHERE machineuuid IN (:ids)', compact('ids'));
$ids = array_flip($ids);
$allowedMachines = [];
$seenLocations = [];
@@ -114,24 +156,6 @@ class Page_Statistics extends Page
if (!empty($ids)) {
Message::addWarning('unknown-machine', implode(', ', array_keys($ids)));
}
- if (!empty($allowedMachines)) {
- if (count($seenLocations) === 1) {
- $locactionId = (int)array_keys($seenLocations)[0];
- } else {
- $locactionId = 0;
- }
- if ($reboot && Request::post('kexec', false)) {
- $action = RebootControl::KEXEC_REBOOT;
- } elseif ($reboot) {
- $action = RebootControl::REBOOT;
- } else {
- $action = RebootControl::SHUTDOWN;
- }
- $task = RebootControl::execute($allowedMachines, $action, 0, $locactionId);
- if (Taskmanager::isTask($task)) {
- Util::redirect("?do=rebootcontrol&taskid=" . $task["id"]);
- }
- }
}
private function deleteMachines()
diff --git a/modules-available/statistics/pages/list.inc.php b/modules-available/statistics/pages/list.inc.php
index d1c9f2e9..e2e7ff09 100644
--- a/modules-available/statistics/pages/list.inc.php
+++ b/modules-available/statistics/pages/list.inc.php
@@ -59,6 +59,8 @@ class SubPage
$deleteAllowedLocations = User::getAllowedLocations("machine.delete");
$rebootAllowedLocations = User::getAllowedLocations('.rebootcontrol.action.reboot');
$shutdownAllowedLocations = User::getAllowedLocations('.rebootcontrol.action.reboot');
+ $wolAllowedLocations = User::getAllowedLocations('.rebootcontrol.action.wol');
+ $execAllowedLocations = User::getAllowedLocations('.rebootcontrol.action.exec');
// Only make client clickable if user is allowed to view details page
$detailsAllowedLocations = User::getAllowedLocations("machine.view-details");
while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
@@ -123,8 +125,10 @@ class SubPage
'canReboot' => !empty($rebootAllowedLocations),
'canShutdown' => !empty($shutdownAllowedLocations),
'canDelete' => !empty($deleteAllowedLocations),
+ 'canWol' => !empty($wolAllowedLocations),
+ 'canExec' => !empty($execAllowedLocations),
);
Render::addTemplate('clientlist', $data);
}
-} \ No newline at end of file
+}
diff --git a/modules-available/statistics/templates/clientlist.html b/modules-available/statistics/templates/clientlist.html
index 5420d65c..47be4dd0 100644
--- a/modules-available/statistics/templates/clientlist.html
+++ b/modules-available/statistics/templates/clientlist.html
@@ -172,6 +172,18 @@
{{lang_reboot}}
</button>
{{/canReboot}}
+ {{#canWol}}
+ <button type="submit" name="action" value="wol" class="btn btn-primary btn-machine-action">
+ <span class="glyphicon glyphicon-bell"></span>
+ {{lang_wakeOnLan}}
+ </button>
+ {{/canWol}}
+ {{#canExec}}
+ <button type="submit" name="action" value="prepare-exec" class="btn btn-primary btn-machine-action">
+ <span class="glyphicon glyphicon-play"></span>
+ {{lang_remoteExec}}
+ </button>
+ {{/canExec}}
{{/rebootcontrol}}
{{#canDelete}}
<button type="submit" name="action" value="delmachines" class="btn btn-danger btn-machine-action"