summaryrefslogtreecommitdiffstats
path: root/modules-available/runmode
diff options
context:
space:
mode:
authorSimon Rettberg2017-07-06 13:11:54 +0200
committerSimon Rettberg2017-07-06 13:11:54 +0200
commitff6e86e8e5db2728d3b34c10f561cfdb533afa87 (patch)
treebc9d492397689a4e95473840a5023972aa97af95 /modules-available/runmode
parent[summernote] Make auto-loading dep (diff)
downloadslx-admin-ff6e86e8e5db2728d3b34c10f561cfdb533afa87.tar.gz
slx-admin-ff6e86e8e5db2728d3b34c10f561cfdb533afa87.tar.xz
slx-admin-ff6e86e8e5db2728d3b34c10f561cfdb533afa87.zip
[runmode] New module for managing special boot modes of clients
Diffstat (limited to 'modules-available/runmode')
-rw-r--r--modules-available/runmode/config.json4
-rw-r--r--modules-available/runmode/inc/runmode.inc.php185
-rw-r--r--modules-available/runmode/install.inc.php36
-rw-r--r--modules-available/runmode/page.inc.php115
-rw-r--r--modules-available/runmode/style.css36
-rw-r--r--modules-available/runmode/templates/machine-selector.html127
6 files changed, 503 insertions, 0 deletions
diff --git a/modules-available/runmode/config.json b/modules-available/runmode/config.json
new file mode 100644
index 00000000..e3c07d48
--- /dev/null
+++ b/modules-available/runmode/config.json
@@ -0,0 +1,4 @@
+{
+ "dependencies": [ "statistics", "js_selectize" ],
+ "permission":"0"
+}
diff --git a/modules-available/runmode/inc/runmode.inc.php b/modules-available/runmode/inc/runmode.inc.php
new file mode 100644
index 00000000..5b59f1c1
--- /dev/null
+++ b/modules-available/runmode/inc/runmode.inc.php
@@ -0,0 +1,185 @@
+<?php
+
+class RunMode
+{
+
+ private static $moduleConfigs = array();
+
+ /**
+ * Get runmode config for a specific module
+ *
+ * @param string $module name of module
+ * @return RunModeModuleConfig|false
+ */
+ private static function getModuleConfig($module)
+ {
+ if (isset(self::$moduleConfigs[$module]))
+ return self::$moduleConfigs[$module];
+ if (Module::get($module) === false)
+ return false;
+ $file = 'modules/' . $module . '/hooks/runmode/config.json';
+ if (!file_exists($file))
+ return false;
+ return (self::$moduleConfigs[$module] = new RunModeModuleConfig($file));
+ }
+
+ public static function setRunMode($machineuuid, $moduleId, $modeId, $modeData)
+ {
+ // - Check if module provides runmode config at all
+ $config = self::getModuleConfig($moduleId);
+ if ($config === false)
+ return false;
+ // - Check if machine exists
+ $machine = Statistics::getMachine($machineuuid, Machine::NO_DATA);
+ if ($machine === false)
+ return false;
+ // - Add/replace entry in runmode table
+ if (is_null($modeId)) {
+ Database::exec('DELETE FROM runmode WHERE machineuuid = :machineuuid', compact('machineuuid'));
+ } else {
+ Database::exec('INSERT INTO runmode (machineuuid, module, modeid, modedata)'
+ . ' VALUES (:uuid, :module, :modeid, :modedata)'
+ . ' ON DUPLICATE KEY UPDATE module = VALUES(module), modeid = VALUES(modeid), modedata = VALUES(modedata)', array(
+ 'uuid' => $machineuuid,
+ 'module' => $moduleId,
+ 'modeid' => $modeId,
+ 'modedata' => $modeData,
+ ));
+ }
+ return true;
+ }
+
+ /**
+ * @param string|\Module $module
+ * @return array
+ */
+ public static function getForModule($module, $groupByModeId = false)
+ {
+ if (is_object($module)) {
+ $module = $module->getIdentifier();
+ }
+ $res = Database::simpleQuery('SELECT machineuuid, modeid, modedata FROM runmode WHERE module = :module',
+ compact('module'));
+ $ret = array();
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ if ($groupByModeId) {
+ if (!isset($ret[$row['modeid']])) {
+ $ret[$row['modeid']] = array();
+ }
+ $ret[$row['modeid']][] = $row;
+ } else {
+ $ret[$row['machineuuid']] = $row;
+ }
+ }
+ return $ret;
+ }
+
+ /**
+ * @param string|\Module $module
+ * @param string $modeId
+ * @param bool $detailed whether to return meta data about machine, not just machineuuid
+ * @return array
+ */
+ public static function getForMode($module, $modeId, $detailed = false)
+ {
+ if (is_object($module)) {
+ $module = $module->getIdentifier();
+ }
+ if ($detailed) {
+ $sel = ', m.hostname, m.clientip, m.macaddr, m.locationid';
+ $join = 'INNER JOIN machine m USING (machineuuid)';
+ } else {
+ $join = $sel = '';
+ }
+ $res = Database::simpleQuery(
+ "SELECT r.machineuuid, r.modedata $sel
+ FROM runmode r $join
+ WHERE module = :module AND modeid = :modeId",
+ compact('module', 'modeId'));
+ $ret = array();
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ if ($detailed && empty($row['hostname'])) {
+ $row['hostname'] = $row['clientip'];
+ }
+ $ret[] = $row;
+ }
+ return $ret;
+ }
+
+ /**
+ * Get display name of a module's mode. If the module doesn't have a getModeName
+ * method configured, the modeId is simply returned. Otherwise the return value of
+ * that method is passed through. getModeName by contract should return false if
+ * the module doesn't think the given modeId exists.
+ *
+ * @param string|\Module $module
+ * @param string $modeId
+ * @return string|bool mode name if known, modeId as fallback, or false if mode is not known by module
+ */
+ public static function getModeName($module, $modeId)
+ {
+ if (is_object($module)) {
+ $module = $module->getIdentifier();
+ }
+ $conf = self::getModuleConfig($module);
+ if ($conf === false || $conf->getModeName === false || !Module::isAvailable($module))
+ return $modeId;
+ return call_user_func($conf->getModeName, $modeId);
+ }
+
+}
+
+/* *\
+|* Helper classes *|
+\* */
+
+/**
+ * Class RunModeModuleConfig represents desired config of a runmode
+ */
+class RunModeModuleConfig
+{
+ /**
+ * @var string|false
+ */
+ public $systemdDefaultTarget = false;
+ /**
+ * @var string[]
+ */
+ public $systemdDisableTargets = [];
+ /**
+ * @var string[]
+ */
+ public $systemdEnableTargets = [];
+ /**
+ * @var string Name of function that turns a modeId into a string
+ */
+ public $getModeName = false;
+ /**
+ * @var bool Consider this a normal client that should e.g. be shown in client statistics by default
+ */
+ public $isClient = false;
+
+ public function __construct($file)
+ {
+ $data = json_decode(file_get_contents($file), true);
+ if (!is_array($data))
+ return;
+ $this->loadType($data, 'systemdDefaultTarget', 'string');
+ $this->loadType($data, 'systemdDisableTargets', 'array');
+ $this->loadType($data, 'systemdEnableTargets', 'array');
+ $this->loadType($data, 'getModeName', 'string');
+ $this->loadType($data, 'isClient', 'string');
+ }
+
+ private function loadType($data, $key, $type)
+ {
+ if (!isset($data[$key]))
+ return false;
+ if (is_string($type) && gettype($data[$key]) !== $type)
+ return false;
+ if (is_array($type) && !in_array(gettype($data[$key]), $type))
+ return false;
+ $this->{$key} = $data[$key];
+ return true;
+ }
+}
diff --git a/modules-available/runmode/install.inc.php b/modules-available/runmode/install.inc.php
new file mode 100644
index 00000000..962a0cc9
--- /dev/null
+++ b/modules-available/runmode/install.inc.php
@@ -0,0 +1,36 @@
+<?php
+
+$res = array();
+
+$res[] = tableCreate('runmode', '
+ `machineuuid` char(36) CHARACTER SET ascii NOT NULL,
+ `module` varchar(30) CHARACTER SET ascii NOT NULL,
+ `modeid` varchar(60) CHARACTER SET ascii NOT NULL,
+ `modedata` blob DEFAULT NULL,
+ PRIMARY KEY (`machineuuid`),
+ KEY `module` (`module`,`modeid`)
+');
+
+if (!tableExists('machine')) {
+ // Cannot add constraint yet
+ $res[] = UPDATE_RETRY;
+} else {
+ $c = tableGetContraints('runmode', 'machineuuid', 'machine', 'machineuuid');
+ if ($c === false)
+ finalResponse(UPDATE_FAILED, 'Cannot get constraints of runmode table: ' . Database::lastError());
+ if (empty($c)) {
+ $alter = Database::exec('ALTER TABLE runmode ADD FOREIGN KEY (machineuuid) REFERENCES machine (machineuuid)
+ ON DELETE CASCADE ON UPDATE CASCADE');
+ if ($alter === false)
+ finalResponse(UPDATE_FAILED, 'Cannot add machineuuid constraint to runmode table: ' . Database::lastError());
+ $res[] = UPDATE_DONE;
+ }
+}
+
+// Create response for browser
+
+if (in_array(UPDATE_DONE, $res)) {
+ finalResponse(UPDATE_DONE, 'Tables created successfully');
+}
+
+finalResponse(UPDATE_NOOP, 'Everything already up to date'); \ No newline at end of file
diff --git a/modules-available/runmode/page.inc.php b/modules-available/runmode/page.inc.php
new file mode 100644
index 00000000..f3e7d024
--- /dev/null
+++ b/modules-available/runmode/page.inc.php
@@ -0,0 +1,115 @@
+<?php
+
+class Page_RunMode extends Page
+{
+
+ /**
+ * Called before any page rendering happens - early hook to check parameters etc.
+ */
+ protected function doPreprocess()
+ {
+ User::load();
+ if (!User::isLoggedIn()) {
+ Message::addError('main.no-permission');
+ Util::redirect('?do=main');
+ }
+ $action = Request::post('action', false, 'string');
+ if ($action !== false) {
+ $this->handleAction($action);
+ Util::redirect('?do=runmode');
+ }
+ }
+
+ private function handleAction($action)
+ {
+ if ($action === 'save-mode') {
+ $machines = array_filter(Request::post('machines', [], 'array'), 'is_string');
+ $module = Request::post('module', false, 'string');
+ $modeId = Request::post('modeid', false, 'string');
+ // TODO Validate
+ foreach ($machines as $machine) {
+ Database::exec("INSERT IGNORE INTO runmode (machineuuid, module, modeid)
+ VALUES (:machine, :module, :modeId)", compact('machine', 'module', 'modeId'));
+ }
+ Database::exec('DELETE FROM runmode
+ WHERE module = :module AND modeid = :modeId AND machineuuid NOT IN (:machines)',
+ compact('module', 'modeId', 'machines'));
+ Util::redirect('?do=runmode&module=' . $module . '&modeid=' . $modeId);
+ }
+ }
+
+ protected function doRender()
+ {
+ $moduleId = Request::get('module', false, 'string');
+ if ($moduleId !== false) {
+ $this->renderModule($moduleId);
+ return;
+ }
+ // TODO
+ Message::addInfo('OMGhai2U');
+ }
+
+ private function renderModule($moduleId)
+ {
+ $module = Module::get($moduleId);
+ if ($module === false) {
+ Message::addError('main.no-such-module', $moduleId);
+ Util::redirect('?do=runmode');
+ }
+ $modeId = Request::get('modeid', false, 'string');
+ if ($modeId !== false) {
+ $this->renderModuleMode($module, $modeId);
+ return;
+ }
+ // TODO
+ Message::addError('main.parameter-missing', 'modeid');
+ Util::redirect('?do=runmode');
+ }
+
+ /**
+ * @param \Module $module
+ * @param string $modeId
+ */
+ private function renderModuleMode($module, $modeId)
+ {
+ $moduleId = $module->getIdentifier();
+ $modeName = RunMode::getModeName($moduleId, $modeId);
+ if ($modeName === false) {
+ Message::addError('invalid-modeid', $modeId);
+ Util::redirect('?do=runmode');
+ }
+ Render::addTemplate('machine-selector', [
+ 'module' => $moduleId,
+ 'modeid' => $modeId,
+ 'moduleName' => $module->getDisplayName(),
+ 'modeName' => $modeName,
+ 'machines' => json_encode(RunMode::getForMode($module, $modeId, true))
+ ]);
+ }
+
+ protected function doAjax()
+ {
+ $action = Request::any('action', false, 'string');
+
+ if ($action === 'getmachines') {
+ $query = Request::get('query', false, 'string');
+
+ $result = Database::simpleQuery('SELECT m.machineuuid, m.macaddr, m.clientip, m.hostname, m.locationid, '
+ . 'r.module, r.modeid '
+ . 'FROM machine m '
+ . 'LEFT JOIN runmode r USING (machineuuid) '
+ . 'WHERE machineuuid LIKE :query '
+ . ' OR macaddr LIKE :query '
+ . ' OR clientip LIKE :query '
+ . ' OR hostname LIKE :query '
+ . ' LIMIT 100', ['query' => "%$query%"]);
+
+ $returnObject = [
+ 'machines' => $result->fetchAll(PDO::FETCH_ASSOC)
+ ];
+
+ echo json_encode($returnObject);
+ }
+ }
+
+} \ No newline at end of file
diff --git a/modules-available/runmode/style.css b/modules-available/runmode/style.css
new file mode 100644
index 00000000..655292db
--- /dev/null
+++ b/modules-available/runmode/style.css
@@ -0,0 +1,36 @@
+/* select popup */
+.machine-entry {
+ width: 99%;
+ width: calc(100% - 5px);
+ border: 1px solid #999;
+ border-radius: 5px;
+ margin: 2px;
+ padding: 2px 4px;
+}
+
+/* in case it is already in the sketchboard */
+.machine-entry.used {
+ color: #666;
+}
+
+.machine-entry table {
+ font-size: 12px;
+ margin-bottom: -5px;
+ width: 100%;
+}
+.machine-entry table tr {
+ border-top: 1px solid #bbb;
+}
+
+.machine-entry-header {
+ font-weight: bolder;
+ font-size: 18px;
+}
+
+.used .mode {
+ color: #f00;
+}
+
+.selectize-dropdown-content {
+ max-height : 600px;
+}
diff --git a/modules-available/runmode/templates/machine-selector.html b/modules-available/runmode/templates/machine-selector.html
new file mode 100644
index 00000000..95b255b5
--- /dev/null
+++ b/modules-available/runmode/templates/machine-selector.html
@@ -0,0 +1,127 @@
+<h1>{{lang_assignRunmodeToMachine}}</h1>
+<h2>{{moduleName}} // {{modeName}}</h2>
+<p>{{lang_assignMachineIntroText}}</p>
+
+<div class="hidden">
+ {{#machines}}
+ <div id="qex-{{machineuuid}}">{{hostname}}</div>
+ {{/machines}}
+</div>
+
+<h4>{{lang_addNewMachines}}</h4>
+<form method="post" action="?do=runmode">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="action" value="save-mode">
+ <input type="hidden" name="module" value="{{module}}" id="i-module">
+ <input type="hidden" name="modeid" value="{{modeid}}" id="i-modeid">
+ <select id="machine-sel" name="machines[]" multiple>
+ </select>
+ <div class="buttonbar">
+ <button type="submit" class="btn btn-primary">{{lang_save}}</button>
+ </div>
+</form>
+
+<script type="application/javascript"><!--
+
+const MODULE = document.getElementById('i-module').value;
+const MODE_ID = document.getElementById('i-modeid').value;
+
+function makeCombinedField(machineArray) {
+ machineArray.forEach(function (v, i, a) {
+ machineArray[i].combined = (v.machineuuid + " " + v.hostname + " " + v.clientip + " " + v.macaddr + " " + v.macaddr.replace(/-/g, ':')).toLocaleLowerCase();
+ machineArray[i].isUsed = v.module && v.module.length && (v.module !== MODULE || v.modeid !== MODE_ID);
+ });
+ return machineArray;
+}
+
+var queryCache = {};
+
+
+function filterCache(key, query) {
+ return queryCache[key].filter(function (el) {
+ return -1 !== el.combined.indexOf(query);
+ });
+}
+
+function loadMachines(query, callback) {
+ if (query.length < 2) {
+ callback();
+ return;
+ }
+ query = query.toLocaleLowerCase();
+ // See if we have a previous query in our cache that is a superset for this one
+ for (var k in queryCache) {
+ if (query.indexOf(k) !== -1) {
+ callback(filterCache(k, query));
+ return;
+ }
+ }
+ $.ajax({
+ url: '?do=runmode&action=getmachines&query=' + encodeURIComponent(query),
+ type: 'GET',
+ dataType: 'json',
+ error: function () {
+ callback();
+ },
+ success: function (json) {
+ var machines = makeCombinedField(json.machines);
+ // Server cuts off at 100, so only cache if it contains less entries, as
+ // the new, more specific query could return previously removed results.
+ if (machines.length < 100) {
+ queryCache[query] = machines;
+ }
+ callback(machines);
+ }
+ });
+}
+
+function renderMachineOption(item, escape) {
+ var extraClass = '';
+ var usedRow = '';
+ if (item.isUsed) {
+ usedRow = '<tr class="mode"><td>Mode:</td><td>' + escape(item.module + ' // ' + item.modeid) + '</td></tr>';
+ extraClass = 'used';
+ }
+ item.hostname || (item.hostname = item.clientip);
+ return '<div class="machine-entry ' + extraClass +'">'
+ + ' <div class="machine-body">'
+ + ' <div class="machine-entry-header"> ' + escape(item.hostname) + '</div>'
+ + ' <table>'
+ + '<tr><td>UUID:</td> <td>' + escape(item.machineuuid) + '</td></tr>'
+ + '<tr><td>MAC: </td> <td>' + escape(item.macaddr) + '</td></tr>'
+ + '<tr><td>IP: </td> <td>' + escape(item.clientip) + '</td></tr>'
+ + usedRow
+ + ' </table>'
+ + ' </div>'
+ + '</div>';
+}
+
+function renderMachineSelected(item, escape) {
+ item.hostname || (item.hostname = item.clientip);
+ var extra = '';
+ if (item.isUsed) {
+ extra = '<span class="glyphicon glyphicon-warning-sign text-danger"></span> '
+ }
+ return '<div>' + extra + escape(item.hostname) + '<div class="small">' + escape(item.clientip + ' - ' + item.macaddr) + '</div>'
+ + '</div>';
+}
+
+document.addEventListener('DOMContentLoaded', function () {
+ var old = {{{machines}}} || [];
+ var $box = $('#machine-sel').selectize({
+ options: old,
+ items: old.map(function(x) { return x.machineuuid; }),
+ plugins: ["remove_button"],
+ valueField: 'machineuuid',
+ searchField: "combined",
+ openOnFocus: false,
+ create: false,
+ render: {option: renderMachineOption, item: renderMachineSelected},
+ load: loadMachines,
+ maxItems: null,
+ sortField: 'hostname',
+ sortDirection: 'asc'
+ });
+});
+
+//--></script> \ No newline at end of file