summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--inc/permission.inc.php2
-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/inc/rebootcontrol.inc.php68
-rw-r--r--modules-available/rebootcontrol/install.inc.php49
-rw-r--r--modules-available/rebootcontrol/lang/de/messages.json8
-rw-r--r--modules-available/rebootcontrol/lang/de/module.json4
-rw-r--r--modules-available/rebootcontrol/lang/de/permissions.json13
-rw-r--r--modules-available/rebootcontrol/lang/de/template-tags.json37
-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.php217
-rw-r--r--modules-available/rebootcontrol/pages/jumphost.inc.php167
-rw-r--r--modules-available/rebootcontrol/pages/subnet.inc.php150
-rw-r--r--modules-available/rebootcontrol/pages/task.inc.php101
-rw-r--r--modules-available/rebootcontrol/permissions/permissions.json21
-rw-r--r--modules-available/rebootcontrol/templates/_page.html184
-rw-r--r--modules-available/rebootcontrol/templates/header.html74
-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/status-checkconnection.html47
-rw-r--r--modules-available/rebootcontrol/templates/status-reboot.html (renamed from modules-available/rebootcontrol/templates/status.html)9
-rw-r--r--modules-available/rebootcontrol/templates/subnet-edit.html64
-rw-r--r--modules-available/rebootcontrol/templates/subnet-list.html58
-rw-r--r--modules-available/rebootcontrol/templates/task-list.html2
-rw-r--r--modules-available/statistics/api.inc.php24
-rw-r--r--modules-available/statistics/page.inc.php2
30 files changed, 1055 insertions, 409 deletions
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/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/inc/rebootcontrol.inc.php b/modules-available/rebootcontrol/inc/rebootcontrol.inc.php
index ec4b84ed..8a85e3ff 100644
--- a/modules-available/rebootcontrol/inc/rebootcontrol.inc.php
+++ b/modules-available/rebootcontrol/inc/rebootcontrol.inc.php
@@ -82,4 +82,72 @@ class RebootControl
return $return;
}
+ /**
+ * 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)
+ {
+ $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 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;
+ }
+ }
+ }
+ if ($privkey === false) {
+ $privkey = SSHKey::getPrivateKey();
+ }
+ $task = Taskmanager::submit('RemoteExec', [
+ 'clients' => $valid,
+ 'command' => $command,
+ 'timeoutSeconds' => $timeout,
+ 'sshkey' => $privkey,
+ 'port' => 9922, // Fallback if no port given in client struct
+ ]);
+ if (!Taskmanager::isFailed($task)) {
+ Property::addToList(RebootControl::KEY_TASKLIST, '0/' . $task["id"], 60 * 24);
+ }
+ return $task;
+ }
+
+ public static function connectionCheckCallback($task, $hostId)
+ {
+ $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]);
+ }
+
} \ 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..eb484d3e
--- /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,
+ `lastdirectcheck` 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,
+ `lastcheck` INT(10) UNSIGNED NOT NULL DEFAULT '0',
+ PRIMARY KEY (`srcid`, `dstid`),
+ KEY `lastcheck` (`lastcheck`)");
+
+$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..788e57a2 100644
--- a/modules-available/rebootcontrol/lang/de/messages.json
+++ b/modules-available/rebootcontrol/lang/de/messages.json
@@ -1,4 +1,10 @@
{
+ "invalid-port": "Ung\u00fcltiger Port: {{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",
+ "unknown-task-type": "Unbekannter Task-Typ"
} \ 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..47d91e98 100644
--- a/modules-available/rebootcontrol/lang/de/module.json
+++ b/modules-available/rebootcontrol/lang/de/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/de/permissions.json b/modules-available/rebootcontrol/lang/de/permissions.json
index 12ec4c83..fdc0b35f 100644
--- a/modules-available/rebootcontrol/lang/de/permissions.json
+++ b/modules-available/rebootcontrol/lang/de/permissions.json
@@ -1,5 +1,12 @@
{
- "action.shutdown": "Client herunterfahren.",
- "action.reboot": "Client neustarten.",
- "newkeypair": "Neues Schlüsselpaar generieren."
+ "action.reboot": "Client neustarten.",
+ "action.shutdown": "Client herunterfahren.",
+ "action.wol": "Client per WOL starten.",
+ "jumphost.assign-subnet": "Einem Sprung-Host ein Subnet zuweisen.",
+ "jumphost.edit": "Einen Sprung-Host bearbeiten.",
+ "jumphost.view": "Liste der Sprung-Hosts sehen.",
+ "newkeypair": "Neues Schl\u00fcsselpaar generieren.",
+ "subnet.edit": "Subnets hinzuf\u00fcgen\/entfernen.",
+ "subnet.flag": "Eigenschaften eines Subnets bearbeiten.",
+ "subnet.view": "Liste der Subnets sehen."
} \ 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..365a739b 100644
--- a/modules-available/rebootcontrol/lang/de/template-tags.json
+++ b/modules-available/rebootcontrol/lang/de/template-tags.json
@@ -1,39 +1,42 @@
{
"lang_activeTasks": "Laufende Jobs",
+ "lang_assignedSubnets": "Subnets",
"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_error": "Nicht erreichbar",
"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_jumpHosts": "Sprung-Hosts",
+ "lang_keypairConfirmCheck": "Ich bin sicher",
"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_online": "Online",
+ "lang_port": "Port",
+ "lang_privkey": "Geheimer Schl\u00fcssel",
"lang_pubKey": "SSH Public Key:",
- "lang_reboot": "Neustarten",
+ "lang_reachable": "Erreichbar",
"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_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_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_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..eaa3c2e6 100644
--- a/modules-available/rebootcontrol/page.inc.php
+++ b/modules-available/rebootcontrol/page.inc.php
@@ -3,7 +3,10 @@
class Page_RebootControl extends Page
{
- private $action = false;
+ /**
+ * @var bool whether we have a SubPage from the pages/ subdir
+ */
+ private $haveSubpage = false;
/**
* Called before any page rendering happens - early hook to check parameters etc.
@@ -17,62 +20,87 @@ class Page_RebootControl extends Page
Util::redirect('?do=Main'); // does not return
}
- $this->action = Request::any('action', 'show', 'string');
-
-
- if ($this->action === 'reboot' || $this->action === 'shutdown') {
+ 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));
+ }
- $requestedClients = Request::post('clients', false, 'array');
- if (!is_array($requestedClients) || empty($requestedClients)) {
- Message::addError('no-clients-selected');
- Util::redirect();
+ $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;
}
+ } else {
+ $action = Request::post('action', 'show', 'string');
- $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');
- }
- // 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 {
- $locationId = $actualClients[$idx]['locationid'];
- }
- }
- // 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');
+ if ($action === 'reboot' || $action === 'shutdown') {
+ $this->execRebootShutdown($action);
}
- $task = RebootControl::execute($actualClients, $mode, $minutes, $locationId);
- if (Taskmanager::isTask($task)) {
- Util::redirect("?do=rebootcontrol&taskid=" . $task["id"]);
+ }
+
+ if (Request::isPost()) {
+ Util::redirect('?do=rebootcontrol' . ($section ? '&show=' . $section : ''));
+ }
+ }
+
+ private function execRebootShutdown($action)
+ {
+ $requestedClients = Request::post('clients', false, 'array');
+ if (!is_array($requestedClients) || empty($requestedClients)) {
+ Message::addError('no-clients-selected');
+ return;
+ }
+
+ $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');
+ }
+ // Filter ones with no permission
+ foreach (array_keys($actualClients) as $idx) {
+ if (!User::hasPermission('action.' . $action, $actualClients[$idx]['locationid'])) {
+ Message::addWarning('locations.no-permission-location', $actualClients[$idx]['locationid']);
+ unset($actualClients[$idx]);
} else {
- Util::redirect("?do=rebootcontrol");
+ $locationId = $actualClients[$idx]['locationid'];
}
}
-
+ // See if anything is left
+ if (!is_array($actualClients) || empty($actualClients)) {
+ Message::addError('no-clients-selected');
+ return;
+ }
+ 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 ($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&show=task&what=task&taskid=" . $task["id"]);
+ }
+ return;
}
/**
@@ -81,86 +109,21 @@ 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()];
+ Permission::addGlobalTags($data['perms'], null, ['newkeypair']);
+ 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();
@@ -169,6 +132,4 @@ class Page_RebootControl extends Page
}
}
-
-
}
diff --git a/modules-available/rebootcontrol/pages/jumphost.inc.php b/modules-available/rebootcontrol/pages/jumphost.inc.php
new file mode 100644
index 00000000..111560ef
--- /dev/null
+++ b/modules-available/rebootcontrol/pages/jumphost.inc.php
@@ -0,0 +1,167 @@
+<?php
+
+class SubPage
+{
+
+ public static function doPreprocess()
+ {
+ $action = Request::post('action', false, 'string');
+ if ($action === 'save') {
+ self::saveJumpHost();
+ } elseif ($action === 'list') {
+ self::listAction();
+ }
+ }
+
+ /*
+ * POST
+ */
+
+ private static function listAction()
+ {
+ $id = Request::post('checkid', false, 'int');
+ if ($id !== false) {
+ // Check connectivity
+ self::execCheckConnection($id);
+ return;
+ }
+ }
+
+ private static function execCheckConnection($hostid)
+ {
+ $host = self::getJumpHost($hostid);
+ $script = str_replace(['%IP%', '%MACS%'], ['255.255.255.255', '00:11:22:33:44:55'], $host['script']);
+ $task = RebootControl::runScript([[
+ 'clientip' => $host['host'],
+ 'port' => $host['port'],
+ 'username' => $host['username'],
+ ]], $script, 5, $host['sshkey']);
+ if (!Taskmanager::isTask($task))
+ return;
+ TaskmanagerCallback::addCallback($task, 'rbcConnCheck', $hostid);
+ 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', $id);
+ self::execCheckConnection($id);
+ } else {
+ Message::addError('no-such-jumphost', $id);
+ }
+ }
+
+ /*
+ * Render
+ */
+
+ public static function doRender()
+ {
+ $id = Request::get('id', false, 'string');
+ if ($id !== false) {
+ self::showJumpHost($id);
+ } 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($id)
+ {
+ User::assertPermission('jumphost.edit');
+ 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);
+ }
+
+ 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..946d2d64
--- /dev/null
+++ b/modules-available/rebootcontrol/pages/subnet.inc.php
@@ -0,0 +1,150 @@
+<?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');
+ $range = [];
+ foreach (['start', 'end'] as $key) {
+ $range[$key] = Request::post($key, Request::REQUIRED, 'string');
+ $range[$key . '_l'] = ip2long($range[$key]);
+ if ($range[$key . '_l'] === false) {
+ Message::addError('invalid-ip-address', $range[$key]);
+ return;
+ }
+ }
+ if ($range['start_l'] > $range['end_l']) {
+ Message::addError('invalid-range', $range['start'], $range['end']);
+ return;
+ }
+ $ret = Database::exec('INSERT INTO reboot_subnet (start, end, fixed, isdirect)
+ VALUES (:start, :end, 1, 0)', [
+ 'start' => sprintf('%u', $range['start_l']),
+ 'end' => sprintf('%u', $range['end_l']),
+ ], 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)) {
+ $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,
+ lastdirectcheck, lastseen, seencount, Count(hxs.hostid) AS jumphostcount
+ FROM reboot_subnet
+ LEFT JOIN reboot_jumphost_x_subnet hxs USING (subnetid)
+ GROUP BY subnetid, start, end
+ ORDER BY start ASC, end DESC');
+ $deadline = strtotime('-60 days');
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $row['start_s'] = long2ip($row['start']);
+ $row['end_s'] = long2ip($row['end']);
+ $row['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);
+ Module::isAvailable('js_ip');
+ }
+
+ 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['start_s'] = long2ip($subnet['start']);
+ $subnet['end_s'] = long2ip($subnet['end']);
+ $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]);
+ $jh = [];
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $row['checked'] = $row['subnetid'] === null ? '' : 'checked';
+ $jh[] = $row;
+ }
+ $subnet['jumpHosts'] = $jh;
+ Permission::addGlobalTags($subnet['perms'], null, ['subnet.flag', 'jumphost.view', 'jumphost.assign-subnet']);
+ Render::addTemplate('subnet-edit', $subnet);
+ }
+
+ public static function doAjax()
+ {
+
+ }
+
+} \ No newline at end of file
diff --git a/modules-available/rebootcontrol/pages/task.inc.php b/modules-available/rebootcontrol/pages/task.inc.php
new file mode 100644
index 00000000..15449aaf
--- /dev/null
+++ b/modules-available/rebootcontrol/pages/task.inc.php
@@ -0,0 +1,101 @@
+<?php
+
+class SubPage
+{
+
+ public static function doPreprocess()
+ {
+
+ }
+
+ public static function doRender()
+ {
+ $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');
+ $task = Taskmanager::status($taskid);
+
+ if (!Taskmanager::isTask($task) || !isset($task['data'])) {
+ Message::addError('no-such-task', $taskid);
+ return;
+ }
+
+ $td =& $task['data'];
+ $type = Request::get('type', false, 'string');
+ if ($type === false) {
+ // Try to guess
+ if (isset($td['locationId']) || isset($td['clients'])) {
+ $type = 'reboot';
+ } elseif (isset($td['result'])) {
+ $type = 'exec';
+ }
+ }
+ if ($type === 'reboot') {
+ $data = [
+ 'taskId' => $task['id'],
+ 'locationId' => $td['locationId'],
+ 'locationName' => Location::getName($td['locationId']),
+ ];
+ $uuids = array_map(function ($entry) {
+ return $entry['machineuuid'];
+ }, $td['clients']);
+ $data['clients'] = RebootQueries::getMachinesByUuid($uuids);
+ Render::addTemplate('status-reboot', $data);
+ } elseif ($type === 'exec') {
+ $data = [
+ 'taskId' => $task['id'],
+ ];
+ Render::addTemplate('status-exec', $data);
+ } elseif ($type === 'checkhost') {
+ $ip = array_key_first($td['result']);
+ $data = [
+ 'taskId' => $task['id'],
+ 'host' => $ip,
+ ];
+ Render::addTemplate('status-checkconnection', $data);
+ } else {
+ Message::addError('unknown-task-type');
+ }
+ }
+
+ private static function showTaskList()
+ {
+ // Append list of active reboot/shutdown tasks
+ $allowedLocs = User::getAllowedLocations("*");
+ $active = RebootControl::getActiveTasks($allowedLocs);
+ if (empty($active)) {
+ Message::addInfo('no-current-tasks');
+ } else {
+ foreach ($active as &$entry) {
+ $entry['locationName'] = Location::getName($entry['locationId']);
+ }
+ unset($entry);
+ Render::addTemplate('task-list', ['list' => $active]);
+ }
+ }
+
+ 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..5983447e 100644
--- a/modules-available/rebootcontrol/permissions/permissions.json
+++ b/modules-available/rebootcontrol/permissions/permissions.json
@@ -2,10 +2,31 @@
"newkeypair": {
"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
}
} \ 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/header.html b/modules-available/rebootcontrol/templates/header.html
index e171ccd6..4f240b81 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">
@@ -41,11 +16,16 @@
</div>
<div class="modal-body">
<p>{{lang_pubKey}}</p>
- <pre id="pubkey">{{pubKey}}</pre>
+ <pre id="pubkey">{{pubkey}}</pre>
<p>{{lang_newKeypairExplanation}}</p>
+ <div class="checkbox">
+ <input {{perms.newkeypair.disabled}} type="checkbox" id="keypair-confirm">
+ <label for="keypair-confirm">{{lang_keypairConfirmCheck}}</label>
+ </div>
</div>
<div class="modal-footer">
- <button {{perms.newkeypair.disabled}} class="btn btn-danger pull-right" onclick="generateNewKeypair()" type="button">
+ <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>
@@ -55,25 +35,33 @@
</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..8453d2bb
--- /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_assignedSubnets}}</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">
+ {{subnetCount}}
+ <a class="btn btn-xs btn-default {{perms.jumphost.assign-subnet.disabled}}"
+ href="?do=rebootcontrol&amp;show=jumphost-assign&amp;id={{hostid}}">
+ <span class="glyphicon glyphicon-edit"></span>
+ </a>
+ </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;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;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/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.html b/modules-available/rebootcontrol/templates/status-reboot.html
index c05b2fad..4be95e81 100644
--- a/modules-available/rebootcontrol/templates/status.html
+++ b/modules-available/rebootcontrol/templates/status-reboot.html
@@ -1,11 +1,4 @@
-<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>{{lang_location}}: {{locationName}}</h3>
<div>
<table class="table table-hover stupidtable" id="dataTable">
diff --git a/modules-available/rebootcontrol/templates/subnet-edit.html b/modules-available/rebootcontrol/templates/subnet-edit.html
new file mode 100644
index 00000000..4c3702ba
--- /dev/null
+++ b/modules-available/rebootcontrol/templates/subnet-edit.html
@@ -0,0 +1,64 @@
+<!-- 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>{{start_s}} - {{end_s}}</b>
+ </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>
+ <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> \ No newline at end of file
diff --git a/modules-available/rebootcontrol/templates/subnet-list.html b/modules-available/rebootcontrol/templates/subnet-list.html
new file mode 100644
index 00000000..e2747316
--- /dev/null
+++ b/modules-available/rebootcontrol/templates/subnet-list.html
@@ -0,0 +1,58 @@
+<!-- subnetid, start, end, fixed, isdirect, lastdirectcheck, lastseen, seencount -->
+
+<h2>{{lang_subnets}}</h2>
+
+<p>{{lang_subnetsDescription}}</p>
+
+<table class="table">
+ <thead>
+ <tr>
+ <th>{{lang_start}}</th>
+ <th>{{lang_end}}</th>
+ <th class="slx-smallcol">{{lang_isFixed}}</th>
+ <th class="slx-smallcol">{{lang_isDirect}}</th>
+ <th class="slx-smallcol">{{lang_jumphostCount}}</th>
+ <th>{{lang_lastseen}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{#subnets}}
+ <tr>
+ <td>
+ <a href="?do=rebootcontrol&show=subnet&what=subnet&id={{subnetid}}">{{start_s}}</a>
+ </td>
+ <td>
+ <a href="?do=rebootcontrol&show=subnet&what=subnet&id={{subnetid}}">{{end_s}}</a>
+ </td>
+ <td>{{#fixed}}<span class="glyphicon glyphicon-lock"></span>{{/fixed}}</td>
+ <td>{{#isdirect}}<span class="glyphicon glyphicon-ok"></span>{{/isdirect}}</td>
+ <td>{{jumphostcount}}</td>
+ <td class="{{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 cidrmagic">
+ <label>{{lang_addNewSubnet}}</label>
+ <div class="row">
+ <div class="col-md-4">
+ <input class="form-control cidrstart" type="text" name="start" placeholder="1.2.3.4/24">
+ </div>
+ <div class="col-md-4">
+ <input class="form-control cidrend" type="text" name="end">
+ </div>
+ <div class="col-md-4 text-right">
+ <button class="btn btn-primary" name="action" value="add">
+ <span class="glyphicon glyphicon-floppy-disk"></span>
+ {{lang_create}}
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+</form> \ 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..8ae4975f 100644
--- a/modules-available/rebootcontrol/templates/task-list.html
+++ b/modules-available/rebootcontrol/templates/task-list.html
@@ -13,7 +13,7 @@
{{#list}}
<tr>
<td>
- <a href="?do=rebootcontrol&amp;taskid={{taskId}}">{{mode}}</a>
+ <a href="?do=rebootcontrol&amp;show=task&amp;what=task&amp;taskid={{taskId}}">{{mode}}</a>
</td>
<td>
{{locationName}}
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/page.inc.php b/modules-available/statistics/page.inc.php
index 68649d91..be838fc0 100644
--- a/modules-available/statistics/page.inc.php
+++ b/modules-available/statistics/page.inc.php
@@ -129,7 +129,7 @@ class Page_Statistics extends Page
}
$task = RebootControl::execute($allowedMachines, $action, 0, $locactionId);
if (Taskmanager::isTask($task)) {
- Util::redirect("?do=rebootcontrol&taskid=" . $task["id"]);
+ Util::redirect("?do=rebootcontrol&what=task&taskid=" . $task["id"]);
}
}
}