diff options
author | Simon Rettberg | 2017-07-06 13:11:54 +0200 |
---|---|---|
committer | Simon Rettberg | 2017-07-06 13:11:54 +0200 |
commit | ff6e86e8e5db2728d3b34c10f561cfdb533afa87 (patch) | |
tree | bc9d492397689a4e95473840a5023972aa97af95 /modules-available | |
parent | [summernote] Make auto-loading dep (diff) | |
download | slx-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')
-rw-r--r-- | modules-available/runmode/config.json | 4 | ||||
-rw-r--r-- | modules-available/runmode/inc/runmode.inc.php | 185 | ||||
-rw-r--r-- | modules-available/runmode/install.inc.php | 36 | ||||
-rw-r--r-- | modules-available/runmode/page.inc.php | 115 | ||||
-rw-r--r-- | modules-available/runmode/style.css | 36 | ||||
-rw-r--r-- | modules-available/runmode/templates/machine-selector.html | 127 |
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 |