summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimon Rettberg2019-12-10 18:18:28 +0100
committerSimon Rettberg2019-12-10 18:18:28 +0100
commitb497d4a7200591e0b7b91948120cbfa395f5c92f (patch)
tree6e384142228882f422a3766c8e8598034f323dc2
parent[rebootcontrol] Fix jumphost script template (missing %) (diff)
downloadslx-admin-b497d4a7200591e0b7b91948120cbfa395f5c92f.tar.gz
slx-admin-b497d4a7200591e0b7b91948120cbfa395f5c92f.tar.xz
slx-admin-b497d4a7200591e0b7b91948120cbfa395f5c92f.zip
[statistics/rebootcontrol] Add remote exec UI
-rw-r--r--modules-available/rebootcontrol/inc/rebootcontrol.inc.php16
-rw-r--r--modules-available/rebootcontrol/inc/rebootqueries.inc.php29
-rw-r--r--modules-available/rebootcontrol/inc/rebootutils.inc.php75
-rw-r--r--modules-available/rebootcontrol/page.inc.php29
-rw-r--r--modules-available/rebootcontrol/pages/exec.inc.php57
-rw-r--r--modules-available/rebootcontrol/pages/task.inc.php4
-rw-r--r--modules-available/rebootcontrol/templates/exec-enter-command.html41
-rw-r--r--modules-available/rebootcontrol/templates/status-exec.html55
-rw-r--r--modules-available/rebootcontrol/templates/status-wol.html2
-rw-r--r--modules-available/statistics/page.inc.php4
-rw-r--r--modules-available/statistics/pages/list.inc.php2
-rw-r--r--modules-available/statistics/templates/clientlist.html6
12 files changed, 262 insertions, 58 deletions
diff --git a/modules-available/rebootcontrol/inc/rebootcontrol.inc.php b/modules-available/rebootcontrol/inc/rebootcontrol.inc.php
index 489b0252..667c8bbd 100644
--- a/modules-available/rebootcontrol/inc/rebootcontrol.inc.php
+++ b/modules-available/rebootcontrol/inc/rebootcontrol.inc.php
@@ -21,7 +21,7 @@ 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);
@@ -501,4 +501,18 @@ class RebootControl
$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 c0c479bd..00000000
--- a/modules-available/rebootcontrol/inc/rebootqueries.inc.php
+++ /dev/null
@@ -1,29 +0,0 @@
-<?php
-
-class RebootQueries
-{
-
- /**
- * 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;
- }
-
-} \ 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/page.inc.php b/modules-available/rebootcontrol/page.inc.php
index 764a3d7b..cf87a3b3 100644
--- a/modules-available/rebootcontrol/page.inc.php
+++ b/modules-available/rebootcontrol/page.inc.php
@@ -71,31 +71,10 @@ class Page_RebootControl extends Page
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]);
- }
- }
- // See if anything is left
- if (!is_array($actualClients) || empty($actualClients)) {
- Message::addError('no-clients-selected');
+ $actualClients = RebootUtils::getFilteredMachineList($requestedClients, 'action.' . $action);
+ if ($actualClients === false)
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;
- });
+ RebootUtils::sortRunningFirst($actualClients);
if ($action === 'shutdown') {
$mode = 'SHUTDOWN';
$minutes = Request::post('s-minutes', 0, 'int');
@@ -144,7 +123,7 @@ class Page_RebootControl extends Page
$clients = Request::post('clients');
if (is_array($clients)) {
// XXX No permission check here, should we consider this as leaking sensitive information?
- $machines = RebootQueries::getMachinesByUuid(array_values($clients), false, ['machineuuid', 'state']);
+ $machines = RebootUtils::getMachinesByUuid(array_values($clients), false, ['machineuuid', 'state']);
$ret = [];
foreach ($machines as $machine) {
switch ($machine['state']) {
diff --git a/modules-available/rebootcontrol/pages/exec.inc.php b/modules-available/rebootcontrol/pages/exec.inc.php
new file mode 100644
index 00000000..58053072
--- /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 = 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/task.inc.php b/modules-available/rebootcontrol/pages/task.inc.php
index e52eb981..691fd9e2 100644
--- a/modules-available/rebootcontrol/pages/task.inc.php
+++ b/modules-available/rebootcontrol/pages/task.inc.php
@@ -82,9 +82,9 @@ class SubPage
// Output
if ($type === RebootControl::TASK_REBOOTCTL) {
- $job['clients'] = RebootQueries::getMachinesByUuid(ArrayUtil::flattenByKey($job['clients'], 'machineuuid'));
+ $job['clients'] = RebootUtils::getMachinesByUuid(ArrayUtil::flattenByKey($job['clients'], 'machineuuid'));
} elseif ($type === RebootControl::TASK_EXEC) {
- $details = RebootQueries::getMachinesByUuid(ArrayUtil::flattenByKey($job['clients'], 'machineuuid'), true);
+ $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']];
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/status-exec.html b/modules-available/rebootcontrol/templates/status-exec.html
new file mode 100644
index 00000000..140de02b
--- /dev/null
+++ b/modules-available/rebootcontrol/templates/status-exec.html
@@ -0,0 +1,55 @@
+
+<div data-tm-id="{{id}}" data-tm-log="error" data-tm-callback="updateStatus">{{lang_executingRemotely}}</div>
+
+<div class="slx-space"></div>
+
+<div class="row">
+ <div class="col-md-5 slx-bold">{{lang_host}}</div>
+ <div class="col-md-5 slx-bold">{{lang_status}}</div>
+ <div class="col-md-2 slx-bold text-right">{{lang_exitCode}}</div>
+</div>
+
+{{#clients}}
+<div class="slx-space" id="client-{{machineuuid}}">
+ <div class="row">
+ <div class="col-md-5 slx-bold">{{hostname}}{{^hostname}}{{clientip}}{{/hostname}}</div>
+ <div class="col-md-5 state"></div>
+ <div class="col-md-2 text-right exitCode"></div>
+ </div>
+ <i>{{lang_stdout}}</i>
+ <pre class="stdout"></pre>
+ <i>{{lang_stderr}}</i>
+ <pre class="stderr"></pre>
+</div>
+<hr>
+{{/clients}}
+
+<script><!--
+
+var ingoreHosts = {};
+
+function updateStatus(task) {
+ if (!task || !task.data || !task.data.result)
+ return;
+ for (var host in task.data.result) {
+ if (ingoreHosts[host] || !task.data.result.hasOwnProperty(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);
+ $p.find('.stdout').text(status.stdout);
+ $p.find('.stderr').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'));
+ }
+ ingoreHosts[id] = true;
+ }
+}
+//--></script> \ No newline at end of file
diff --git a/modules-available/rebootcontrol/templates/status-wol.html b/modules-available/rebootcontrol/templates/status-wol.html
index da19b57d..3e83126c 100644
--- a/modules-available/rebootcontrol/templates/status-wol.html
+++ b/modules-available/rebootcontrol/templates/status-wol.html
@@ -4,7 +4,7 @@
<div class="clearfix slx-space"></div>
{{#tasks}}
-<div data-tm-id="{{id}}" data-tm-callback="wolCallback">{{lang_aWolJob}}</div>
+<div data-tm-id="{{.}}" data-tm-callback="wolCallback">{{lang_aWolJob}}</div>
{{/tasks}}
{{^tasks}}
<div class="alert alert-warning">
diff --git a/modules-available/statistics/page.inc.php b/modules-available/statistics/page.inc.php
index ff5a59cd..533a9bf9 100644
--- a/modules-available/statistics/page.inc.php
+++ b/modules-available/statistics/page.inc.php
@@ -74,6 +74,10 @@ class Page_Statistics extends Page
$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
diff --git a/modules-available/statistics/pages/list.inc.php b/modules-available/statistics/pages/list.inc.php
index f223dfb2..e2e7ff09 100644
--- a/modules-available/statistics/pages/list.inc.php
+++ b/modules-available/statistics/pages/list.inc.php
@@ -60,6 +60,7 @@ class SubPage
$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)) {
@@ -125,6 +126,7 @@ class SubPage
'canShutdown' => !empty($shutdownAllowedLocations),
'canDelete' => !empty($deleteAllowedLocations),
'canWol' => !empty($wolAllowedLocations),
+ 'canExec' => !empty($execAllowedLocations),
);
Render::addTemplate('clientlist', $data);
}
diff --git a/modules-available/statistics/templates/clientlist.html b/modules-available/statistics/templates/clientlist.html
index 6ff9bac7..47be4dd0 100644
--- a/modules-available/statistics/templates/clientlist.html
+++ b/modules-available/statistics/templates/clientlist.html
@@ -178,6 +178,12 @@
{{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"