From 633e43127c6d1a96d47587eed2739bdaff6c3d4f Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Sat, 16 Nov 2013 15:42:16 +0100 Subject: Add minilinux download functionality and ipxe compilation scripts --- DB_SCRIBBLE | 7 +-- apis/download.inc.php | 96 ++++++++++++++++++++++++++++++++++------ apis/exec.inc.php | 75 +++++++++++++++++++++++++++++++ apis/getconfig.inc.php | 1 + config.php | 13 ++++-- external/build_ipxe.sh | 40 +++++++++++++++++ external/tgz/list.php | 24 ++++++++++ fakeremote/list.php | 24 ---------- inc/message.inc.php | 3 ++ inc/session.inc.php | 2 +- modules/adduser.inc.php | 16 ++++--- modules/ipxe.inc.php | 35 +++++++++++++++ modules/main.inc.php | 14 ++++-- modules/minilinux.inc.php | 86 +++++++++++++++++++++++++++++++++++ modules/sysconfig.inc.php | 29 +++++++++++- templates/download-error.html | 2 +- templates/download-progress.html | 6 +++ templates/exec-error.html | 1 + templates/exec-progress.html | 4 ++ templates/main-menu.html | 7 +-- templates/page-ipxe.html | 17 +++++++ templates/page-main.html | 9 +++- templates/page-minilinux.html | 24 ++++++++++ templates/page-tgz-list.html | 2 +- templates/txt-ipxeconfig.html | 7 +++ templates/txt-pxeconfig.html | 68 ++++++++++++++++++++++++++++ 26 files changed, 548 insertions(+), 64 deletions(-) create mode 100644 apis/exec.inc.php create mode 100755 external/build_ipxe.sh create mode 100644 external/tgz/list.php delete mode 100644 fakeremote/list.php create mode 100644 modules/ipxe.inc.php create mode 100644 modules/minilinux.inc.php create mode 100644 templates/download-progress.html create mode 100644 templates/exec-error.html create mode 100644 templates/exec-progress.html create mode 100644 templates/page-ipxe.html create mode 100644 templates/page-minilinux.html create mode 100644 templates/txt-ipxeconfig.html create mode 100644 templates/txt-pxeconfig.html diff --git a/DB_SCRIBBLE b/DB_SCRIBBLE index d4ce72a2..34bfdb00 100644 --- a/DB_SCRIBBLE +++ b/DB_SCRIBBLE @@ -50,12 +50,12 @@ INSERT INTO `setting` (`setting`, `defaultvalue`, `permissions`, `validator`, `d ('SLX_PROXY_PORT', '', 2, 'regex:/^\\d*$/', 'Der Port des zu verwendenden Proxy Servers.'), ('SLX_PROXY_TYPE', 'socks5', 2, '', 'Art des Proxys.\r\n*socks4*, *socks5*,\r\n*http-connect* (HTTP Proxy mit Unterstützung der CONNECT-Methode),\r\n*http-relay* (Klassischer HTTP Proxy)'), ('SLX_ROOT_PASS', '', 2, 'function:linuxPassword', 'Das root-Passwort des Grundsystems. Wird nur für Diagnosezwecke am Client benötigt.\r\nFeld leer lassen, um root-Logins zu verbieten.\r\n/Hinweis/: Das Passwort wird crypt $6$ gehasht, daher wir das Passwort nach dem Speichern nicht mehr lesbar sein!'), -('SLX_VM_NFS', '', 2, '', 'Serveradresse und mount point des NFS Servers, auf dem die virtuellen Maschinen liegen.\r\nBeispiel: *vm-store.example.com:/data/images*'); +('SLX_VM_NFS', '(SERVER):/srv/openslx/nfs', 2, '', 'Serveradresse und mount point des NFS Servers, auf dem die virtuellen Maschinen liegen. Um den integrierten NFS-Server zu nutzen, muss die Angabe *:/srv/openslx/mnt* lauten.\r\nAnsonsten lässt sich ein beliebiger NFS-Server angeben. Beispiel: *vm-store.example.com:/data/images*'); -CRREATE TABLE IF NOT EXISTS `setting_distro` ( +CREATE TABLE IF NOT EXISTS `setting_distro` ( `distroid` int(10) unsigned NOT NULL, `setting` varchar(28) NOT NULL, `value` text NOT NULL, @@ -77,7 +77,8 @@ CREATE TABLE IF NOT EXISTS `user` ( `phone` varchar(100) DEFAULT NULL, `email` varchar(100) DEFAULT NULL, `permissions` int(10) unsigned NOT NULL, - PRIMARY KEY (`userid`) + PRIMARY KEY (`userid`), + UNIQUE KEY `login` (`login`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/apis/download.inc.php b/apis/download.inc.php index 4be67521..b9033c6f 100644 --- a/apis/download.inc.php +++ b/apis/download.inc.php @@ -3,38 +3,106 @@ User::load(); if (!User::hasPermission('superadmin')) die('No permission'); -if (!isset($_REQUEST['type'])) die('No type'); -if (!isset($_REQUEST['file'])) die('No file'); -if (!isset($_REQUEST['id'])) die('No id'); require_once('inc/render.inc.php'); -$type = $_REQUEST['type']; +if (!isset($_REQUEST['file'])) die('No file'); +if (!isset($_REQUEST['id'])) die('No id'); $file = preg_replace('/[^a-z0-9\.\-_]/is', '', $_REQUEST['file']); $id = $_REQUEST['id']; +// Progress update only + +if (isset($_REQUEST['progress'])) { + $progress = preg_replace('/[^a-z0-9\-]/', '', $_REQUEST['progress']); + $pid = (isset($_REQUEST['pid']) ? (int)$_REQUEST['pid'] : 0); + $log = '/tmp/' . $progress . '.log'; + if (!file_exists($log)) { + echo Render::parse('download-error', array('file' => $file)); + exit(0); + } + $error = false; + $percent = 0; + $fh = fopen($log, 'r'); + while (!feof($fh)) { + $line = fgets($fh); + if (preg_match('/ ERROR (\d{3}):/', $line, $out)) { + $error = $out[1]; + break; + } + if (preg_match('/ (\d+)% /', $line, $out)) { + $percent = $out[1]; + } + } + fclose($fh); + if ($error === false && $pid > 0 && $percent != 100 && !posix_kill($pid, 0)) $error = 'Process died - ' . $line; + if ($error !== false) { + echo Render::parse('download-error', array('file' => $file, 'code' => $error)); + unlink($log); + exit(0); + } + if ($percent == 100) { + echo Render::parse('download-complete' ,array('file' => $file)); + unlink($log); + } else { + echo Render::parse('download-progress' ,array('file' => $file, 'progress' => $progress, 'id' => $id, 'percent' => $percent, 'pid' => $pid)); + } + exit(0); +} + +// Actual download request + +if (!isset($_REQUEST['type'])) die('No type'); + + +$type = $_REQUEST['type']; +$directExec = true; +$overwrite = isset($_REQUEST['exec']); + switch ($type) { case 'tgz': $remote = CONFIG_REMOTE_TGZ; $local = CONFIG_TGZ_LIST_DIR; break; +case 'ml': + $remote = CONFIG_REMOTE_ML; + $local = CONFIG_HTTP_DIR . '/default'; + $directExec = false; + $overwrite = true; + break; default: die('Invalid download type'); } @mkdir($local, 0755, true); -if (file_exists($local . '/' . $file) && !isset($_REQUEST['exec'])) { - echo Render::parse('download-overwrite', array('file' => $file, 'id' => $id, 'query' => $_SERVER['REQUEST_URI']));; +if (file_exists($local . '/' . $file) && !$overwrite) { + echo Render::parse('download-overwrite', array('file' => $file, 'id' => $id, 'query' => $_SERVER['REQUEST_URI'])); exit(0); } -$ret = Util::downloadToFile($local . '/' . $file, $remote . '/' . $file, 20, $code); -if ($ret === false || $code < 200 || $code >= 300) { - @unlink($local . '/' . $file); - echo Render::parse('download-error', array('file' => $file, 'remote' => $remote, 'code' => $code)); - exit(0); +if ($directExec) { + // Blocking inline download + $ret = Util::downloadToFile($local . '/' . $file, $remote . '/' . $file, 20, $code); + if ($ret === false || $code < 200 || $code >= 300) { + @unlink($local . '/' . $file); + echo Render::parse('download-error', array('file' => $file, 'remote' => $remote, 'code' => $code)); + exit(0); + } + + // No execution - just return dialog + echo Render::parse('download-complete', array('file' => $file)); +} else { + // Use WGET + $logfile = 'slx-' . mt_rand() . '-' . time(); + exec("wget --timeout=10 -O $local/$file -o /tmp/${logfile}.log -b $remote/$file", $retstr, $retval); + unlink("$local/${file}.md5"); + if ($retval != 0) { + echo Render::parse('download-error', array('file' => $destination, 'remote' => $source, 'code' => implode(' // ', $retstr) . ' - ' . $retval)); + exit(0); + } + $pid = 0; + foreach ($retstr as $line) if (preg_match('/ (\d+)([,\.\!\:]|$)/', $line, $out)) $pid = $out[1]; + file_put_contents("$local/${file}.lck", $pid . ' ' . $logfile); + echo Render::parse('download-progress', array('file' => $file, 'progress' => $logfile, 'id' => $id, 'percent' => '0', 'pid' => $pid)); } -// No execution - just return dialog -echo Render::parse('download-complete', array('file' => $file)); - diff --git a/apis/exec.inc.php b/apis/exec.inc.php new file mode 100644 index 00000000..94a94a00 --- /dev/null +++ b/apis/exec.inc.php @@ -0,0 +1,75 @@ + 10) array_shift($lastLines); + } + fclose($fh); + $running = ($pid == 0 || posix_kill($pid, 0)); + echo Render::parse('exec-progress', array('progress' => $progress, 'id' => $id, 'pid' => $pid, 'running' => $running, 'text' => implode('', $lastLines))); + if (!$running) unlink($log); + exit(0); +} + +// Actual download request +// type ip id + +if (!isset($_REQUEST['type'])) die('No type'); + + +$type = $_REQUEST['type']; + +switch ($type) { +case 'ipxe': + if (!isset($_REQUEST['ip'])) die('No IP given'); + $ip = preg_replace('/[^0-9\.]/', '', $_REQUEST['ip']); + $command = '/opt/openslx/build_ipxe.sh "' . CONFIG_IPXE_DIR . '/last-ip" "' . $ip . '"'; + $conf = Render::parse('txt-ipxeconfig', array( + 'SERVER' => $ip + )); + if (false === file_put_contents('/opt/openslx/ipxe/ipxelinux.ipxe', $conf)) die('Error writing iPXE Config'); + $conf = Render::parse('txt-pxeconfig', array( + 'SERVER' => $ip, + 'DEFAULT' => 'openslx' + )); + if (false === file_put_contents(CONFIG_TFTP_DIR . '/pxelinux.cfg/default', $conf)) die('Error writing PXE Menu'); + break; +default: + die('Invalid exec type'); +} + +$logfile = 'slx-' . mt_rand() . '-' . time(); +error_log('**EXEC: ' . "$command '/tmp/${logfile}.log'"); +exec("$command '/tmp/${logfile}.log'", $retstr, $retval); +if ($retval != 0) { + echo Render::parse('exec-error', array('error' => implode(' // ', $retstr) . ' - ' . $retval)); + exit(0); +} +$pid = 0; +foreach ($retstr as $line) if (preg_match('/PID: (\d+)\./', $line, $out)) $pid = $out[1]; +echo Render::parse('exec-progress', array('progress' => $logfile, 'id' => $id, 'pid' => $pid, 'running' => true)); + diff --git a/apis/getconfig.inc.php b/apis/getconfig.inc.php index a3d2bd91..6edb55be 100644 --- a/apis/getconfig.inc.php +++ b/apis/getconfig.inc.php @@ -5,5 +5,6 @@ $res = Database::simpleQuery('SELECT setting.setting, setting.defaultvalue, sett LEFT JOIN setting_global AS tbl USING (setting) ORDER BY setting ASC'); // TODO: Add setting groups and sort order while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + if (empty($row['value'])) $row['value'] = $row['defaultvalue']; echo $row['setting'] . "='" . str_replace("'", "'\"'\"'", $row['value']) . "'\n"; } diff --git a/config.php b/config.php index 5b44d1cf..ac6a9de4 100644 --- a/config.php +++ b/config.php @@ -4,7 +4,7 @@ define('CONFIG_DEBUG', true); define('CONFIG_SESSION_DIR', '/tmp/openslx'); -define('CONFIG_SESSION_TIMEOUT', 86400); +define('CONFIG_SESSION_TIMEOUT', 86400 * 3); //define('CONFIG_SQL_BACKEND', 'mysql'); //define('CONFIG_SQL_HOST', 'localhost'); @@ -13,8 +13,13 @@ define('CONFIG_SQL_USER', 'openslx'); define('CONFIG_SQL_PASS', 'geheim'); //define('CONFIG_SQL_DB', 'openslx'); -define('CONFIG_TGZ_LIST_DIR', '/tmp/configs'); -define('CONFIG_HTTP_DIR', '/tmp/active-config'); +define('CONFIG_TGZ_LIST_DIR', '/opt/openslx/configs'); -define('CONFIG_REMOTE_TGZ', 'http://127.0.0.1/fakeremote'); +define('CONFIG_REMOTE_TGZ', 'http://mltk.boot.openslx.org/tgz'); +define('CONFIG_REMOTE_ML', 'http://mltk.boot.openslx.org/update'); + +define('CONFIG_TFTP_DIR', '/srv/openslx/tftp'); +define('CONFIG_HTTP_DIR', '/srv/openslx/www/boot'); + +define('CONFIG_IPXE_DIR', '/opt/openslx/ipxe'); diff --git a/external/build_ipxe.sh b/external/build_ipxe.sh new file mode 100755 index 00000000..8cb23cd0 --- /dev/null +++ b/external/build_ipxe.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +# Call: $0 +# Self-Call: $0 --exec + +if [ $# -lt 3 ]; then + echo "Falscher Aufruf: Keine zwei Parameter angegeben!" + exit 1 +fi + +if [ "$1" != "--exec" ]; then + $0 --exec "$1" "$2" > "$3" 2>&1 & + RET=$! + echo "PID: ${RET}." + exit 0 +fi + +FILE="$2" +SERVER="$3" + +cd "/opt/openslx/ipxe/src" + +[ -e "bin/undionly.kkkpxe" ] && unlink "bin/undionly.kkkpxe" + +make bin/undionly.kkkpxe EMBED=../ipxelinux.ipxe,../pxelinux.0 + +if [ ! -e "bin/undionly.kkkpxe" -o "$(stat -c %s "bin/undionly.kkkpxe")" -lt 80000 ]; then + echo "Error compiling ipxelinux.0" + exit 1 +fi + +if ! cp "bin/undionly.kkkpxe" "/srv/openslx/tftp/ipxelinux.0"; then + echo "** Error copying ipxelinux.0 to target **" + exit 1 +fi + +echo -n "$SERVER" > "$FILE" +echo " ** SUCCESS **" +exit 0 + diff --git a/external/tgz/list.php b/external/tgz/list.php new file mode 100644 index 00000000..5c8d1c67 --- /dev/null +++ b/external/tgz/list.php @@ -0,0 +1,24 @@ +" }'; +} +echo ' ]'; +*/ + +$files = array(); +foreach (glob('./*.tgz') as $file) { + $files[] = array( + 'file' => basename($file), + 'description' => 'Eine sinnvolle Beschreibung' + ); +} + +echo json_encode($files); + diff --git a/fakeremote/list.php b/fakeremote/list.php deleted file mode 100644 index 5c8d1c67..00000000 --- a/fakeremote/list.php +++ /dev/null @@ -1,24 +0,0 @@ -" }'; -} -echo ' ]'; -*/ - -$files = array(); -foreach (glob('./*.tgz') as $file) { - $files[] = array( - 'file' => basename($file), - 'description' => 'Eine sinnvolle Beschreibung' - ); -} - -echo json_encode($files); - diff --git a/inc/message.inc.php b/inc/message.inc.php index d39f0f9b..ea6cd20d 100644 --- a/inc/message.inc.php +++ b/inc/message.inc.php @@ -16,8 +16,11 @@ $error_text = array( 'remote-timeout' => 'Konnte Ressource {{0}} nicht herunterladen', 'remote-parse-failed' => 'Parsen der empfangenen Daten fehlgeschlagen ({{0}})', 'missing-file' => 'Es wurde keine Datei ausgewählt!', + 'invalid-file' => 'Die Datei {{0}} existiert nicht!', 'upload-complete' => 'Upload von {{0}} war erfolgreich', 'upload-failed' => 'Upload von {{0}} schlug fehl!', + 'config-activated' => 'Konfiguration wurde aktiviert', + 'error-write' => 'Fehler beim Schreiben von {{0}}', ); class Message diff --git a/inc/session.inc.php b/inc/session.inc.php index 402e6cd9..3ba614f2 100644 --- a/inc/session.inc.php +++ b/inc/session.inc.php @@ -2,7 +2,7 @@ require_once('config.php'); -@mkdir(CONFIG_SESSION_DIR, 0700); +@mkdir(CONFIG_SESSION_DIR, 0700, true); @chmod(CONFIG_SESSION_DIR, 0700); if (!is_writable(CONFIG_SESSION_DIR)) die('Config error: Session Path not writable!'); diff --git a/modules/adduser.inc.php b/modules/adduser.inc.php index 04b6044f..f152643b 100644 --- a/modules/adduser.inc.php +++ b/modules/adduser.inc.php @@ -10,6 +10,9 @@ if (isset($_POST['action']) && $_POST['action'] === 'adduser') { } elseif ($_POST['pass1'] !== $_POST['pass2']) { Message::addError('password-mismatch'); Util::redirect('?do=adduser'); + } elseif (Database::queryFirst('SELECT userid FROM user LIMIT 1') !== false) { + Message::addError('adduser-disabled'); + Util::redirect('?do=session&action=login'); } else { $data = array( 'user' => $_POST['user'], @@ -22,17 +25,18 @@ if (isset($_POST['action']) && $_POST['action'] === 'adduser') { if (Database::exec('INSERT INTO user SET login = :user, passwd = :pass, fullname = :fullname, phone = :phone, email = :email', $data) != 1) { Util::traceError('Could not create new user in DB'); } - $adduser_success = true; + // Make it superadmin if first user. This method sucks as it's a race condition but hey... + $ret = Database::queryFirst('SELECT Count(*) AS num FROM user'); + if ($ret !== false && $ret['num'] == 1) { + Database::exec('UPDATE user SET permissions = 1'); + } + Message::addInfo('adduser-success'); + Util::redirect('?do=session&action=login'); } } function render_module() { - // A user was added. Show success message and bail out - if (isset($adduser_success)) { - Message::addInfo('adduser-success'); - return; - } // No user was added, check if current user is allowed to add a new user // Currently you can only add users if there is no user yet. :) if (Database::queryFirst('SELECT userid FROM user LIMIT 1') !== false) { diff --git a/modules/ipxe.inc.php b/modules/ipxe.inc.php new file mode 100644 index 00000000..869f4c72 --- /dev/null +++ b/modules/ipxe.inc.php @@ -0,0 +1,35 @@ + $out[1], + 'current' => ($out[1] == $current) + ); + } + } + Render::addTemplate('page-ipxe', array('ips' => $ips, 'token' => Session::get('token'))); +} + + diff --git a/modules/main.inc.php b/modules/main.inc.php index 007fb296..31aef55e 100644 --- a/modules/main.inc.php +++ b/modules/main.inc.php @@ -6,14 +6,20 @@ function render_module() { // Render::setTitle('abc'); - Render::openTag('h1', array('class' => 'wurst kacke')); - Render::closeTag('h1'); - if (!User::isLoggedIn()) { Render::addTemplate('page-main-guest'); return; } // Logged in here - Render::addTemplate('page-main', array('user' => User::getName())); + $ipxe = true; + $file = CONFIG_IPXE_DIR . '/last-ip'; + if (file_exists($file)) { + $last = file_get_contents($file); + exec('/bin/ip a', $ips); + foreach ($ips as $ip) { + if (preg_match("#inet $last/\d+.*scope#", $ip)) $ipxe = false; + } + } + Render::addTemplate('page-main', array('user' => User::getName(), 'ipxe' => $ipxe)); } diff --git a/modules/minilinux.inc.php b/modules/minilinux.inc.php new file mode 100644 index 00000000..3508fb43 --- /dev/null +++ b/modules/minilinux.inc.php @@ -0,0 +1,86 @@ + $files, 'token' => Session::get('token'))); +} + +function checkFile(&$files, $name) +{ + static $someId = 0; + $remote = CONFIG_REMOTE_ML . "/${name}.md5"; + $localTarget = CONFIG_HTTP_DIR . "/default/${name}"; + $local = "${localTarget}.md5"; + $localLock = "${localTarget}.lck"; + + // Maybe already in progress? + if (file_exists($localLock)) { + $data = explode(' ', file_get_contents($localLock)); + if (count($data) == 2) { + $pid = (int)$data[0]; + if (posix_kill($pid, 0)) { + $files[] = array( + 'file' => $name, + 'id' => 'id' . $someId++, + 'pid' => $pid, + 'progress' => $data[1] + ); + return true; + } else { + unlink($localLock); + } + } else { + unlink($localLock); + } + } + + // Not in progress, normal display + if (!file_exists($local) || filemtime($local) + 300 < time()) { + if (file_exists($localTarget)) { + $existingMd5 = md5_file($localTarget); + } else { + $existingMd5 = ''; + } + if (file_put_contents($local, $existingMd5) === false) { + @unlink($local); + Message::addWarning('error-write', $local); + } + } else { + $existingMd5 = file_get_contents($local); + } + $existingMd5 = strtolower(preg_replace('/[^0-9a-f]/is', '', $existingMd5)); + $remoteMd5 = Util::download($remote, 3, $code); + $remoteMd5 = strtolower(preg_replace('/[^0-9a-f]/is', '', $existingMd5)); + if ($code != 200) { + Message::addError('remote-timeout', $remote); + return false; + } + if ($existingMd5 === $remoteMd5) { + // Up to date + $files[] = array( + 'file' => $name, + 'id' => 'id' . $someId++, + ); + return true; + } + // New version on server + $files[] = array( + 'file' => $name, + 'id' => 'id' . $someId++, + 'update' => true + ); + return true; +} + diff --git a/modules/sysconfig.inc.php b/modules/sysconfig.inc.php index d5300194..f10446ed 100644 --- a/modules/sysconfig.inc.php +++ b/modules/sysconfig.inc.php @@ -27,6 +27,33 @@ if (isset($_POST['action']) && $_POST['action'] === 'upload') { Util::redirect('?do=sysconfig'); } +if (isset($_REQUEST['action']) && $_REQUEST['action'] === 'activate') { + if (!Util::verifyToken()) { + Util::redirect('?do=sysconfig'); + } + if (!User::hasPermission('superadmin')) { + Message::addError('no-permission'); + Util::redirect('?do=sysconfig'); + } + if (!isset($_REQUEST['file'])) { + Message::addError('missing-file'); + Util::redirect('?do=sysconfig'); + } + $file = preg_replace('/[^a-z0-9\-_\.]/', '', $_REQUEST['file']); + $path = CONFIG_TGZ_LIST_DIR . '/' . $file; + if (!file_exists($path)) { + Message::addError('invalid-file', $file); + Util::redirect('?do=sysconfig'); + } + mkdir(CONFIG_HTTP_DIR . '/default', 0755, true); + $linkname = CONFIG_HTTP_DIR . '/default/config.tgz'; + @unlink($linkname); + if (file_exists($linkname)) Util::traceError('Could not delete old config.tgz link!'); + if (!symlink($path, $linkname)) Util::traceError("Could not symlink to $path at $linkname!"); + Message::addSuccess('config-activated'); + Util::redirect('?do=sysconfig'); +} + function render_module() { if (!isset($_REQUEST['action'])) $_REQUEST['action'] = 'list'; @@ -52,7 +79,7 @@ function list_configs() $files = array(); foreach (glob(CONFIG_TGZ_LIST_DIR . '/*.tgz') as $file) { $files[] = array( - 'file' => $file + 'file' => basename($file) ); } Render::addTemplate('page-tgz-list', array('files' => $files, 'token' => Session::get('token'))); diff --git a/templates/download-error.html b/templates/download-error.html index ca40acee..70826234 100644 --- a/templates/download-error.html +++ b/templates/download-error.html @@ -1 +1 @@ -
Downloading {{file}} from {{remote}} failed! ({{code}})
+
Downloading {{file}} {{#remote}} from {{remote}} {{/remote}} failed! ({{code}})
diff --git a/templates/download-progress.html b/templates/download-progress.html new file mode 100644 index 00000000..81b31901 --- /dev/null +++ b/templates/download-progress.html @@ -0,0 +1,6 @@ +
+
+ {{percent}}% Complete +
+ +
diff --git a/templates/exec-error.html b/templates/exec-error.html new file mode 100644 index 00000000..0a82e1df --- /dev/null +++ b/templates/exec-error.html @@ -0,0 +1 @@ +

Ausführung fehlgeschlagen!

{{error}}
diff --git a/templates/exec-progress.html b/templates/exec-progress.html new file mode 100644 index 00000000..0583a74a --- /dev/null +++ b/templates/exec-progress.html @@ -0,0 +1,4 @@ +
+
{{text}}
+ {{#running}}{{/running}} +
diff --git a/templates/main-menu.html b/templates/main-menu.html index 021daf6d..799d92be 100644 --- a/templates/main-menu.html +++ b/templates/main-menu.html @@ -7,7 +7,7 @@ - OpenSLX Admin + OpenSLX Admin