diff options
author | Simon Rettberg | 2016-06-06 18:30:49 +0200 |
---|---|---|
committer | Simon Rettberg | 2016-06-06 18:30:49 +0200 |
commit | a6b4976e8592b054fc7cefde379ccc6e9d79c324 (patch) | |
tree | 6f9734c28fb35747736a0243c34617dd0183064f | |
parent | [statistics] Show lspci info in machine details (diff) | |
download | slx-admin-a6b4976e8592b054fc7cefde379ccc6e9d79c324.tar.gz slx-admin-a6b4976e8592b054fc7cefde379ccc6e9d79c324.tar.xz slx-admin-a6b4976e8592b054fc7cefde379ccc6e9d79c324.zip |
[install] Work on install mechanism so modules can independently install/update tables
-rw-r--r-- | install.php | 240 | ||||
-rw-r--r-- | script/install.js | 108 |
2 files changed, 348 insertions, 0 deletions
diff --git a/install.php b/install.php new file mode 100644 index 00000000..e579dd52 --- /dev/null +++ b/install.php @@ -0,0 +1,240 @@ +<?php + +/* + * Modular installation system. + * + * Since most of slx-admin is now modularized, it might not make too much sense to have + * static central SQL dumps of the database scheme. Tables usually belong to specific modules, + * so they should only be created if the module is actually in use. + * Thus, we have a modular install system where modules should create (and update) database + * tables that belong to them. + * The system should not be abused to modify tables of other modules by adding or changing + * columns, which could lead to a maintenance nightmare over time. + * + * The install/update mechanism could also be used to setup other requirements, like directories, + * permissions etc. Each module's install hook should report back the status of the module's + * requirements. For examples see some modules like baseconfig, main, statistics, ... + * + * Warning: Be careful which classes/functionality of slx-admin you use in your scripts, since + * they might depend on some tables that do not exist yet. ;) + */ + +/** + * Report back the update status to the browser/client and terminate execution. + * This has to be called by an update module at some point to signal the result + * of its execution. + * + * @param $status one of the UPDATE_* status codes + * @param string $message Human readable description of the status (optional) + */ +function finalResponse($status, $message = '') +{ + if (!DIRECT_MODE && AJAX) { + echo json_encode(array('status' => $status, 'message' => $message)); + } else { + echo 'STATUS=', $status, "\n"; + echo 'MESSAGE=', $message; + } + exit; +} + +define('UPDATE_DONE', 'UPDATE_DONE'); // Process completed successfully. This is a success return code. +define('UPDATE_NOOP', 'UPDATE_NOOP'); // Nothing had to be done, everything is up to date. This is also a success code. +define('UPDATE_RETRY', 'UPDATE_RETRY'); // Install/update process failed, but should be retried later. +define('UPDATE_FAILED', 'UPDATE_FAILED'); // Fatal error occured, retry will not resolve the issue. + +/* + * Helper functions for dealing with the database + */ + +function tableHasColumn($table, $column) +{ + $table = preg_replace('/\W/', '', $table); + $column = preg_replace('/\W/', '', $column); + $res = Database::simpleQuery("DESCRIBE `$table`", array(), true); + if ($res !== false) { + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + if ((is_array($column) && in_array($row['Field'], $column)) || (is_string($column) && $row['Field'] === $column)) + return true; + } + } + return false; +} + +function tableDropColumn($table, $column) +{ + $table = preg_replace('/\W/', '', $table); + $column = preg_replace('/\W/', '', $column); + $res = Database::simpleQuery("DESCRIBE `$table`", array(), true); + if ($res !== false) { + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + if ((is_array($column) && in_array($row['Field'], $column)) || (is_string($column) && $row['Field'] === $column)) + Database::exec("ALTER TABLE `$table` DROP `{$row['Field']}`"); + } + } +} + +function tableExists($table) +{ + $res = Database::simpleQuery("SHOW TABLES", array(), true); + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + if ($row['Tables_in_openslx'] === $table) + return true; + } + return false; +} + +/* + * Rest of install script.... + */ + +if (!isset($_SERVER['REMOTE_ADDR']) || isset($_REQUEST['direct'])) { + define('DIRECT_MODE', true); +} else { + define('DIRECT_MODE', false); +} + +define('AJAX', ((isset($_REQUEST['async'])) || (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'))); + +error_reporting(E_ALL); +chdir(dirname($_SERVER['SCRIPT_FILENAME'])); + +// Autoload classes from ./inc which adhere to naming scheme <lowercasename>.inc.php +spl_autoload_register(function ($class) { + $file = 'inc/' . preg_replace('/[^a-z0-9]/', '', mb_strtolower($class)) . '.inc.php'; + if (!file_exists($file)) + return; + require_once $file; +}); + +if (!is_readable('config.php')) { + finalResponse(UPDATE_FAILED, 'config.php does not exist!'); +} + +require_once 'config.php'; + +if (CONFIG_SQL_PASS === '%MYSQL_OPENSLX_PASS%') { + finalResponse(UPDATE_FAILED, 'mysql credentials not configured yet!'); +} + +// Explicitly connect to the database so it won't call Util::traceError() on failure +if (!Database::init(true)) { + finalResponse(UPDATE_RETRY, 'Connecting to the database failed'); +} + +// Good to go so far + +/** + * @param \Module $module + * @return bool + */ +function hasUpdateScript($module) +{ + return is_readable($module->getDir() . '/install.inc.php'); +} + +function runUpdateScript($module) +{ + require_once $module->getDir() . '/install.inc.php'; +} + +// Build dependency tree +Module::init(); +$modules = Module::getEnabled(); +if (empty($modules)) { + finalResponse(UPDATE_NOOP, 'No active modules, nothing to do'); +} + +if (DIRECT_MODE) { + // + // Direct mode - plain + $new = array(); + foreach ($modules as $entry) { + if (hasUpdateScript($entry)) { + $new[] = $entry; + } + } + $modules = $new; + if (empty($modules)) { + finalResponse(UPDATE_NOOP, 'No modules with install scripts, nothing to do'); + } + // Get array where the key maps a module identifier to the next module object + $assoc = array(); + $count = count($modules); + for ($i = 0; $i < $count; ++$i) { + $assoc[$modules[$i]->getIdentifier()] = $modules[($i + 1) % $count]; + } + + if (!empty($argv[1])) { + $last = $argv[1]; + } else { + $last = Request::any('last', '', 'string'); + } + if (!empty($last) && isset($assoc[$last])) { + $module = $assoc[$last]; + } + if (!isset($module)) { + $module = $modules[0]; + } + echo 'MODULE=', $module->getIdentifier(), "\n"; + runUpdateScript($module); +} else { + // + // Interactive web based mode + $mod = Request::any('module', false, 'string'); + if ($mod !== false) { + // Execute specific module + $module = Module::get($mod, true); + if ($module === false) { + finalResponse(UPDATE_NOOP, 'Given module does not exist!'); + } + if (!hasUpdateScript($module)) { + finalResponse(UPDATE_NOOP, 'Given module has no install script'); + } + runUpdateScript($module); + finalResponse(UPDATE_DONE, 'Module did not report status; assuming OK'); + } + // Show the page that shows status and triggers jobs + echo <<<HERE +<!DOCTYPE html> +<html> + <head> + <title>Install/Update SLXadmin</title> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <style type="text/css"> + body, html { + color: #000; + background: #fff; + } + table, tr, td { + border: 1px solid #ccc; + border-collapse: collapse; + padding: 3px; + } + button { + font-weight: bold; + padding: 8px; + } + </style> + </head> + <body> + <h1>Modules</h1> + <table> + <tr><th>Module</th><th>Status</th></tr> +HERE; + foreach ($modules as $module) { + $id = $module->getIdentifier(); + echo "<tr><td class=\"id-col\">{$id}</td><td id=\"mod-{$id}\">Waiting...</td></tr>"; + } + echo <<<HERE + </table> + <br><br> + <button onclick="slxRunInstall()">Install/Upgrade</button> + <script src="script/jquery.js"></script> + <script src="script/install.js"></script> + </body> +</html> +HERE; + +}
\ No newline at end of file diff --git a/script/install.js b/script/install.js new file mode 100644 index 00000000..2dec46d4 --- /dev/null +++ b/script/install.js @@ -0,0 +1,108 @@ +var onceOnlyGoddammit = false; +var slxModules = {}; +var slxTries = {}; +var slxCurrent = false; + +function slxRunInstall() { + if (onceOnlyGoddammit) + return; + onceOnlyGoddammit = true; + var first = false; + list = $('.id-col').each(function () { + var id = $(this).text(); + slxModules[id] = 'IDLE'; + slxTries[id] = 0; + if (first === false) { + first = id; + } + }); + if (first !== false) { + slxRun(first); + } +} + +function makeCallback(callback, userData) { + return function (firstParam) { + callback(this, userData, firstParam); + }; +} + +function slxRun(moduleName) { + var dest = $('#mod-' + moduleName); + if (dest.length !== 1 || typeof slxModules[moduleName] === 'undefined') { + alert('No such module: ' + moduleName); + return; + } + if (slxModules[moduleName] === 'IDLE' || slxModules[moduleName] === 'UPDATE_RETRY') { + if (slxTries[moduleName]++ > 3) + return; + slxModules[moduleName] = 'WORKING'; + dest.text('Working.....'); + slxCurrent = moduleName; + $.post('install.php', {module: moduleName}, makeCallback(slxDone, moduleName), 'json') + .always(makeCallback(slxTrigger, moduleName)); + } +} + +var slxDone = function (elem, moduleName, jsonReply) { + if (!jsonReply) { + jsonReply = {}; + } + if (!jsonReply.status) { + jsonReply.status = 'UPDATE_FAILED'; + jsonReply.message = 'Unknown/no status code received from server'; + } + var status = jsonReply.status; + if (jsonReply.message) { + status = status + ' (' + jsonReply.message + ')'; + } + console.log('D'); + console.log(elem); + slxModules[moduleName] = jsonReply.status; + $('#mod-' + moduleName).text(status); + if (jsonReply.status === 'UPDATE_NOOP' || jsonReply.status === 'UPDATE_DONE') { + $('#mod-' + moduleName).css('color', '#0c0'); + } + console.log('E'); +} + +var slxTrigger = function (elem, moduleName) { + if (slxModules[moduleName] === 'WORKING') { + slxModules[moduleName] = 'UPDATE_FAILED'; + $(elem).text('UPDATE_FAILED (No response from server)'); + } + if (slxCurrent === moduleName) { + slxCurrent = false; + slxRunNext(moduleName); + } +} + +function slxRunNext(lastModule) { + var next = false; + var first = false; + for (var key in slxModules) { + if (!slxModules.hasOwnProperty(key)) + continue; + // + if (slxTries[key] < 3 && (slxModules[key] === 'IDLE' || slxModules[key] === 'UPDATE_RETRY')) { + if (next === true) { + next = key; + break; + } + if (first === false) { + first = key; + } + } + if (next === false && key === lastModule) { + next = true; + } + } + if (next === false || next === true) { + next = first; + } + if (next !== false) { + slxRun(next); + } else { + alert('Done.'); + } +}
\ No newline at end of file |