summaryrefslogtreecommitdiffstats
path: root/modules-available/vmstore
diff options
context:
space:
mode:
Diffstat (limited to 'modules-available/vmstore')
-rw-r--r--modules-available/vmstore/baseconfig/getconfig.inc.php4
-rw-r--r--modules-available/vmstore/hooks/main-warning.inc.php2
-rw-r--r--modules-available/vmstore/inc/vmstorebenchmark.inc.php84
-rw-r--r--modules-available/vmstore/lang/de/messages.json6
-rw-r--r--modules-available/vmstore/lang/de/module.json4
-rw-r--r--modules-available/vmstore/lang/de/permissions.json1
-rw-r--r--modules-available/vmstore/lang/de/template-tags.json10
-rw-r--r--modules-available/vmstore/lang/en/messages.json6
-rw-r--r--modules-available/vmstore/lang/en/module.json4
-rw-r--r--modules-available/vmstore/lang/en/permissions.json1
-rw-r--r--modules-available/vmstore/lang/en/template-tags.json10
-rw-r--r--modules-available/vmstore/page.inc.php270
-rw-r--r--modules-available/vmstore/permissions/permissions.json3
-rw-r--r--modules-available/vmstore/templates/benchmark-imgselect.html59
-rw-r--r--modules-available/vmstore/templates/benchmark-nothing.html7
-rw-r--r--modules-available/vmstore/templates/benchmark-result.html141
-rw-r--r--modules-available/vmstore/templates/page-vmstore.html3
17 files changed, 607 insertions, 8 deletions
diff --git a/modules-available/vmstore/baseconfig/getconfig.inc.php b/modules-available/vmstore/baseconfig/getconfig.inc.php
index 3bad16e1..d239a3d7 100644
--- a/modules-available/vmstore/baseconfig/getconfig.inc.php
+++ b/modules-available/vmstore/baseconfig/getconfig.inc.php
@@ -1,5 +1,8 @@
<?php
+/** @var ?string $uuid */
+/** @var ?string $ip */
+
// VMStore path and type
$vmstore = Property::getVmStoreConfig();
if (is_array($vmstore) && isset($vmstore['storetype'])) {
@@ -21,6 +24,7 @@ if (is_array($vmstore) && isset($vmstore['storetype'])) {
ConfigHolder::add("SLX_VM_NFS_OPTS", $vmstore['cifsopts']);
}
break;
+ default:
}
}
diff --git a/modules-available/vmstore/hooks/main-warning.inc.php b/modules-available/vmstore/hooks/main-warning.inc.php
index ca2d1382..50d81ac8 100644
--- a/modules-available/vmstore/hooks/main-warning.inc.php
+++ b/modules-available/vmstore/hooks/main-warning.inc.php
@@ -4,7 +4,7 @@
* Hook for main page: Show warning if vmstore not configured yet; set "warning" flag if so
*/
-if (!is_array(Property::getVmStoreConfig())) {
+if (empty(Property::getVmStoreConfig())) {
Message::addError('vmstore.vmstore-not-configured', true); // Always specify module prefix since this is running in main
$needSetup = true; // Set $needSetup to true if you want a warning badge to appear in the menu
}
diff --git a/modules-available/vmstore/inc/vmstorebenchmark.inc.php b/modules-available/vmstore/inc/vmstorebenchmark.inc.php
new file mode 100644
index 00000000..b819ef8a
--- /dev/null
+++ b/modules-available/vmstore/inc/vmstorebenchmark.inc.php
@@ -0,0 +1,84 @@
+<?php
+
+class VmStoreBenchmark
+{
+
+ const PROP_LIST_KEY = 'vmstore.benchmark';
+
+ /**
+ * @param string[] $machineUuids List of UUIDs
+ * @return void
+ */
+ public static function prepareSelectDialog(array $uuids)
+ {
+ Module::isAvailable('rebootcontrol');
+ User::assertPermission('.vmstore.benchmark');
+ $uuids = array_values(Request::post('uuid', Request::REQUIRED, 'array'));
+ $machines = RebootUtils::getFilteredMachineList($uuids, '.vmstore.benchmark');
+ if ($machines === false)
+ return;
+ $machines = array_column($machines, 'machineuuid');
+ $id = Property::addToList(self::PROP_LIST_KEY,
+ json_encode(['machines' => $machines]), 60);
+ Util::redirect('?do=vmstore&show=benchmark&action=select&id=' . $id);
+ }
+
+ /**
+ * @param string $image relative path/name of image
+ * @param string $serverOrMode IP address of DNBD3 server, OR 'auto' for all servers known to client, or 'nfs' for NFS
+ * @param int $start timestamp when the clients should start
+ * @return ?string taskId, or null on error
+ */
+ public static function start(string $id, array $machineUuids, string $image, string $serverOrMode, int &$start): ?string
+ {
+ Module::isAvailable('rebootcontrol');
+ $clients = Database::queryAll('SELECT machineuuid, clientip FROM machine WHERE machineuuid IN (:uuids)',
+ ['uuids' => $machineUuids]);
+ if (empty($clients)) {
+ ErrorHandler::traceError('Cannot start benchmark: No matching clients');
+ }
+ // The more clients we have, the longer it takes to SSH into all of them.
+ // As of 2022, RemoteExec processes 4 clients in parallel
+ $start = ceil(count($clients) / 4 + 5 + time());
+ if ($serverOrMode === 'nfs') {
+ $modeOption = '--nfs';
+ } elseif ($serverOrMode === 'auto') {
+ $modeOption = '';
+ } else {
+ $modeOption = "--servers '$serverOrMode'";
+ }
+ // We fork off the benchmark into the background, and collect the results with another RemoteExec job
+ // when we're done. This is because RemoteExec only does four concurrent SSH connections, so if we wanted to
+ // do this the easy, synchronous way, we never could run more than four tests at the same time.
+ $command = <<<COMMAND
+(
+ exec &> /dev/null < /dev/null
+ setsid
+ while true; do
+ echo 3 > /proc/sys/vm/drop_caches
+ sleep 1
+ done &
+ flush=\$!
+ image_speedcheck --start $start --console $modeOption --file "$image" > "/tmp/speedcheck-$id"
+ kill \$flush
+) &
+COMMAND;
+ $task = RebootControl::runScript($clients, $command);
+ return $task['id'] ?? null;
+ }
+
+ /**
+ * @return array{cpu: array, net: array}
+ */
+ public static function parseBenchLine(string $line): array
+ {
+ $out = ['cpu' => [], 'net' => []];
+ foreach (explode(',', $line) as $elem) {
+ $elem = explode('+', $elem);
+ $out['net'][] = ['x' => (int)$elem[0], 'y' => (int)$elem[1]];
+ //$out['cpu'][] = ['x' => $elem[0], 'y' => $elem[2]];
+ }
+ return $out;
+ }
+
+} \ No newline at end of file
diff --git a/modules-available/vmstore/lang/de/messages.json b/modules-available/vmstore/lang/de/messages.json
index 993d355d..86aa780e 100644
--- a/modules-available/vmstore/lang/de/messages.json
+++ b/modules-available/vmstore/lang/de/messages.json
@@ -1,3 +1,9 @@
{
+ "benchmark-already-started": "Benchmark bereits gestartet",
+ "benchmark-failed": "Benchmark fehlgeschlagen",
+ "dnbd3-failed": "DNBD3-Verbindung fehlgeschlagen",
+ "invalid-benchmark-job": "Ung\u00fcltige Benchmark-ID: {{0}}",
+ "invalid-dnbd3-server-id": "Ung\u00fcltige DNBD3 Server-ID: {{0}}",
+ "select-image-first": "Bitte zuerst ein Image ausw\u00e4hlen",
"vmstore-not-configured": "Es ist noch kein Speicherort f\u00fcr die Virtuellen Maschinen festgelegt."
} \ No newline at end of file
diff --git a/modules-available/vmstore/lang/de/module.json b/modules-available/vmstore/lang/de/module.json
index 87be6cae..80241434 100644
--- a/modules-available/vmstore/lang/de/module.json
+++ b/modules-available/vmstore/lang/de/module.json
@@ -1,4 +1,8 @@
{
+ "dnbd3-all-loadbalance": "Alle DNBD3-Server nutzen (load balancing)",
+ "menu_benchmark": "Benchmark",
+ "menu_edit": "Bearbeiten",
"module_name": "VM Speicherort",
+ "page-title-benchmark": "Netzwerk Benchmark",
"page_title": "Speicherort f\u00fcr VMs festlegen"
} \ No newline at end of file
diff --git a/modules-available/vmstore/lang/de/permissions.json b/modules-available/vmstore/lang/de/permissions.json
index 1f8d18d7..ffc3be39 100644
--- a/modules-available/vmstore/lang/de/permissions.json
+++ b/modules-available/vmstore/lang/de/permissions.json
@@ -1,3 +1,4 @@
{
+ "benchmark": "Darf Benchmarks starten.",
"edit": "Den verwendeten VM-Speicher konfigurieren."
} \ No newline at end of file
diff --git a/modules-available/vmstore/lang/de/template-tags.json b/modules-available/vmstore/lang/de/template-tags.json
index 8b6661c2..4b848e79 100644
--- a/modules-available/vmstore/lang/de/template-tags.json
+++ b/modules-available/vmstore/lang/de/template-tags.json
@@ -1,8 +1,13 @@
{
+ "lang_benchmark": "Benchmark",
+ "lang_benchmarkMainPageText": "Um ein Benchmark mit einem oder mehreren Rechnern zu starten, w\u00e4hlen Sie die entsprechenden Ger\u00e4te in der Listenansicht der Client-Statistiken aus.",
+ "lang_benchmarkResult": "Ergebnis",
+ "lang_benchmarkSecondsReminaing": "Sekunden, die f\u00fcr den Test verbleiben",
"lang_cifsHelp1": "Ben\u00f6tigt wird ein CIFS-Share, z.B. von einem Windows Server, der f\u00fcr\r\nden Satellitenserver schreibbar, und f\u00fcr die Arbeitsstationen lesbar\r\nist.",
"lang_cifsHelp2": "Geben Sie f\u00fcr den Satellitenserver einen User mit Lese- und\r\nSchreibberechtigungen an. F\u00fcr die Clients sollte ein User angegeben\r\nwerden, der nur Leseberechtigungen auf dem Share besitzt. Am einfachsten\r\nerreichen Sie dies, indem Sie passwortlosen Gastzugriff mit Leserechten\r\nauf die Freigabe erlauben.",
"lang_cifsHelp3": "Wenn exklusiv DNBD3 verwendet wird, k\u00f6nnen Sie den passwortlosen\r\nGastzugriff deaktivieren und die Zeile \"Nur-Lese-Zugangsdaten\" leer\r\nlassen. Dies erh\u00f6ht die Sicherheit.",
"lang_configure": "Konfigurieren",
+ "lang_image": "Image",
"lang_internal": "Intern",
"lang_nfsHelp1": "Ben\u00f6tigt wird ein NFSv4\/3-Share, der f\u00fcr den Satellitenserver schreibbar, und f\u00fcr die Arbeitsstationen lesbar ist. Beispielkonfiguration auf dem NFS-Server, wenn der Satellitenserver die Adresse 1.2.3.4 hat:",
"lang_nfsHelp2": "Alternative Konfiguration mittels all_squash. In diesem Fall muss das Verzeichnis auf dem Server dem Benutzer mit der uid 1234 geh\u00f6ren:",
@@ -12,6 +17,11 @@
"lang_optionalMountOptions": "Zu verwendende Mount-Optionen (optional):",
"lang_readOnly": "Nur-Lese-Zugangsdaten",
"lang_readWrite": "Lese\/Schreib-Zugangsdaten",
+ "lang_selectImage": "Image f\u00fcr den Test ausw\u00e4hlen",
+ "lang_selectServerOrNfs": "Quelle f\u00fcr Lesetest ausw\u00e4hlen",
+ "lang_size": "Gr\u00f6\u00dfe",
+ "lang_start": "Start",
+ "lang_users": "Aktuelle Verbindungen",
"lang_vmLocation": "VM Speicherort",
"lang_vmLocationChoose": "Bitte w\u00e4hlen Sie, wo die Images der Virtuellen Maschinen gespeichert werden sollen.",
"lang_vmLocationConfiguration": "VM Speicherort wird konfiguriert",
diff --git a/modules-available/vmstore/lang/en/messages.json b/modules-available/vmstore/lang/en/messages.json
index 9ac360eb..0b935c94 100644
--- a/modules-available/vmstore/lang/en/messages.json
+++ b/modules-available/vmstore/lang/en/messages.json
@@ -1,3 +1,9 @@
{
+ "benchmark-already-started": "Benchmark already started",
+ "benchmark-failed": "Benchmark failed",
+ "dnbd3-failed": "DNBD3 connection failed",
+ "invalid-benchmark-job": "Invalid benchmark ID: {{0}}",
+ "invalid-dnbd3-server-id": "Invalid DNBD3 server ID: {{0}}",
+ "select-image-first": "Please select an image first",
"vmstore-not-configured": "A location for the virtual machine is not set yet."
} \ No newline at end of file
diff --git a/modules-available/vmstore/lang/en/module.json b/modules-available/vmstore/lang/en/module.json
index a424640e..12e5167b 100644
--- a/modules-available/vmstore/lang/en/module.json
+++ b/modules-available/vmstore/lang/en/module.json
@@ -1,4 +1,8 @@
{
+ "dnbd3-all-loadbalance": "Use all DNBD3 servers (load balancing)",
+ "menu_benchmark": "Benchmark",
+ "menu_edit": "Edit",
"module_name": "VM Storage Location",
+ "page-title-benchmark": "Network benchmark",
"page_title": "Setting VM Storage Location"
} \ No newline at end of file
diff --git a/modules-available/vmstore/lang/en/permissions.json b/modules-available/vmstore/lang/en/permissions.json
index 6d34014a..fb5d56a5 100644
--- a/modules-available/vmstore/lang/en/permissions.json
+++ b/modules-available/vmstore/lang/en/permissions.json
@@ -1,3 +1,4 @@
{
+ "benchmark": "May start benchmarks.",
"edit": "Configure VM storage to use."
} \ No newline at end of file
diff --git a/modules-available/vmstore/lang/en/template-tags.json b/modules-available/vmstore/lang/en/template-tags.json
index b1d53db1..348c59fc 100644
--- a/modules-available/vmstore/lang/en/template-tags.json
+++ b/modules-available/vmstore/lang/en/template-tags.json
@@ -1,8 +1,13 @@
{
+ "lang_benchmark": "Benchmark",
+ "lang_benchmarkMainPageText": "To start a benchmark, select one or more clients in the list view of the Client Statistics.",
+ "lang_benchmarkResult": "Results",
+ "lang_benchmarkSecondsReminaing": "Seconds remaining",
"lang_cifsHelp1": "Requires a CIFS\/SMB share that's writable for the satellite server and read-only for the clients (if not using DNBD3).",
"lang_cifsHelp2": "Please provide user credentials with read\/write permissions which will be used by the server. For the clients, user credentials that allow read-only access is required. You could also enable passwordless guest login for read-only access.",
"lang_cifsHelp3": "If you want to use DNBD3 in exclusive mode, you can leave the read only credentials empty, to prevent people from browsing the share.",
"lang_configure": "Configure",
+ "lang_image": "Image",
"lang_internal": "Internal",
"lang_nfsHelp1": "An NFSv4\/3-Share is required. It should be readable by all the workstations, and writable for the satellite server. An example, assuming the satellite server has IP address 1.2.3.4:",
"lang_nfsHelp2": "Alternate configuration using all_squash. The exported directory should be owned (and be writable) by the user with uid 1234.",
@@ -12,6 +17,11 @@
"lang_optionalMountOptions": "Mount options to use (optional):",
"lang_readOnly": "Read-only Access",
"lang_readWrite": "Read\/Write Access",
+ "lang_selectImage": "Select image for testing",
+ "lang_selectServerOrNfs": "Select source for reading",
+ "lang_size": "Size",
+ "lang_start": "Start",
+ "lang_users": "Current connections",
"lang_vmLocation": "VM Storage Location",
"lang_vmLocationChoose": "Please choose where the images of virtual machines will be stored.",
"lang_vmLocationConfiguration": "VM location is configured",
diff --git a/modules-available/vmstore/page.inc.php b/modules-available/vmstore/page.inc.php
index 1e0cc619..9d7f16c2 100644
--- a/modules-available/vmstore/page.inc.php
+++ b/modules-available/vmstore/page.inc.php
@@ -2,12 +2,28 @@
class Page_VmStore extends Page
{
- private $mountTask = false;
+ /**
+ * @var ?string
+ */
+ private $mountTask = null;
protected function doPreprocess()
{
User::load();
+ if (User::hasPermission('edit')) {
+ Dashboard::addSubmenu('?do=vmstore', Dictionary::translate('menu_edit'));
+ }
+ if (User::hasPermission('benchmark')) {
+ Dashboard::addSubmenu('?do=vmstore&show=benchmark', Dictionary::translate('menu_benchmark'));
+ }
+
+ if (Request::any('show') === 'benchmark') {
+ User::assertPermission('benchmark');
+ $this->benchmarkDoPreprocess();
+ return;
+ }
+
User::assertPermission('edit');
$action = Request::post('action');
@@ -19,10 +35,15 @@ class Page_VmStore extends Page
protected function doRender()
{
+ if (Request::any('show') === 'benchmark') {
+ $this->benchmarkDoRender();
+ return;
+ }
+
$action = Request::post('action');
- if ($action === 'setstore' && !Taskmanager::isFailed($this->mountTask)) {
+ if ($action === 'setstore' && !Taskmanager::isFailed(Taskmanager::status($this->mountTask))) {
Render::addTemplate('mount', array(
- 'task' => $this->mountTask['id']
+ 'task' => $this->mountTask
));
return;
}
@@ -50,19 +71,256 @@ class Page_VmStore extends Page
Util::redirect('?do=VmStore');
}
// Validate syntax of nfs/cifs
- if ($storetype === 'nfs' && !preg_match('#^\S+:\S+$#is', $vmstore['nfsaddr'])) {
+ if ($storetype === 'nfs' && !preg_match('#^\S+:\S+$#i', $vmstore['nfsaddr'])) {
Message::addError('main.value-invalid', 'nfsaddr', $vmstore['nfsaddr']);
Util::redirect('?do=VmStore');
}
$vmstore['cifsaddr'] = str_replace('\\', '/', $vmstore['cifsaddr']);
- if ($storetype === 'cifs' && !preg_match('#^//\S+/.+$#is', $vmstore['cifsaddr'])) {
+ if ($storetype === 'cifs' && !preg_match('#^//\S+/.+$#i', $vmstore['cifsaddr'])) {
Message::addError('main.value-invalid', 'nfsaddr', $vmstore['nfsaddr']);
Util::redirect('?do=VmStore');
}
$this->mountTask = Trigger::mount($vmstore);
- if ($this->mountTask !== false) {
+ if ($this->mountTask !== null) {
TaskmanagerCallback::addCallback($this->mountTask, 'manualMount', $vmstore);
}
}
+ private function benchmarkDoPreprocess()
+ {
+ if (!Module::isAvailable('rebootcontrol')) {
+ ErrorHandler::traceError('rebootcontrol module not enabled');
+ }
+ Render::setTitle(Dictionary::translate('page-title-benchmark'));
+ if (Request::post('action') === 'start') {
+ $this->benchmarkActionStart();
+ }
+ }
+
+ private function benchmarkDoRender()
+ {
+ switch (Request::get('action')) {
+ case 'select':
+ $this->benchmarkShowImageSelect();
+ break;
+ case 'result':
+ $this->benchmarkShowResult();
+ break;
+ default:
+ Render::addTemplate('benchmark-nothing');
+ }
+ }
+
+ private function getJobFromId(int $id): ?array
+ {
+ $data = Property::getListEntry(VmStoreBenchmark::PROP_LIST_KEY, $id);
+ if ($data !== null) {
+ $data = json_decode($data, true);
+ }
+ if (!is_array($data) || !isset($data['machines'])) {
+ Message::addError('invalid-benchmark-job', $id);
+ return null;
+ }
+ return $data;
+ }
+
+ private function benchmarkActionStart()
+ {
+ Module::isAvailable('dnbd3');
+ $id = Request::post('id', Request::REQUIRED, 'int');
+ $data = $this->getJobFromId($id);
+ if ($data === null)
+ return;
+ if (isset($data['task'])) {
+ if ($data['task'] === 'inprogress') {
+ // Let's hope the proper ID gets written in a short while
+ sleep(1);
+ }
+ Util::redirect('?do=vmstore&show=benchmark&action=result&id=' . $id);
+ }
+ $selectedServer = Request::post('server', 'auto', 'string');
+ if ($selectedServer === 'nfs' || !Dnbd3::isEnabled()) {
+ $selectedServer = 'nfs';
+ } elseif ($selectedServer !== 'auto') {
+ $ip = Dnbd3::getServer($selectedServer);
+ if ($ip === false) {
+ Message::addError('invalid-dnbd3-server-id', $selectedServer);
+ return;
+ }
+ $selectedServer = $ip['clientip'];
+ }
+ $data['image'] = Request::post('image', Request::REQUIRED, 'string');
+ // Save once first to minimize race window
+ $data['task'] = 'inprogress';
+ Property::updateListEntry(VmStoreBenchmark::PROP_LIST_KEY, $id, json_encode($data), 30);
+ $start = 0;
+ $data['task'] = VmStoreBenchmark::start($id, $data['machines'], $data['image'], $selectedServer, $start);
+ if ($data['task'] === null) {
+ $data['task'] = 'failed';
+ } else {
+ // Test is 2x 30 seconds
+ $data['expected'] = $start + 64;
+ }
+ error_log('Saving: ' . json_encode($data));
+ Property::updateListEntry(VmStoreBenchmark::PROP_LIST_KEY, $id, json_encode($data), 30);
+ Util::redirect('?do=vmstore&show=benchmark&action=result&id=' . $id);
+ }
+
+ private function benchmarkShowImageSelect()
+ {
+ $id = Request::get('id', Request::REQUIRED, 'int');
+ $data = $this->getJobFromId($id);
+ if ($data === null)
+ return;
+ if (isset($data['task'])) {
+ Message::addWarning('benchmark-already-started');
+ Util::redirect('?do=vmstore&show=benchmark&action=result&id=' . $id);
+ }
+ Module::isAvailable('dnbd3');
+ $lookup = Dnbd3::getActiveServers();
+ $list = Dnbd3Rpc::getStatsMulti(array_keys($lookup), [Dnbd3Rpc::QUERY_IMAGES]);
+ if (empty($list)) {
+ Message::addError('dnbd3-failed');
+ Util::redirect('?do=vmstore');
+ }
+ $images = [];
+ foreach ($list as $json) {
+ foreach ($json['images'] as $img) {
+ $name = $img['name'] . ':' . $img['rid'];
+ if (!isset($images[$name])) {
+ $images[$name] = [
+ 'users' => 0,
+ 'size' => $img['size'],
+ 'size_s' => Util::readableFileSize($img['size'], 1),
+ 'name' => $name,
+ 'id' => count($images)
+ ];
+ }
+ $images[$name]['users'] += $img['users'];
+ }
+ }
+ $servers = [];
+ if (Dnbd3::isEnabled()) {
+ $servers[] = ['idx' => 'auto',
+ 'server' => Dictionary::translate('dnbd3-all-loadbalance')];
+ foreach ($lookup as $ip => $idx) {
+ $servers[] = ['idx' => $idx, 'server' => $ip];
+ }
+ }
+ if (!Dnbd3::isEnabled() || Dnbd3::hasNfsFallback()) {
+ $servers[] = ['idx' => 'nfs', 'server' => 'NFS'];
+ }
+ $servers[0]['checked'] = 'checked';
+ ArrayUtil::sortByColumn($images, 'users', SORT_DESC, SORT_NUMERIC);
+ Module::isAvailable('js_stupidtable');
+ Render::addTemplate('benchmark-imgselect', [
+ 'id' => $id,
+ 'list' => array_values($images),
+ 'servers' => $servers,
+ ]);
+ }
+
+ private function benchmarkShowResult()
+ {
+ $id = Request::get('id', Request::REQUIRED, 'int');
+ $data = $this->getJobFromId($id);
+ if ($data === null)
+ return;
+ if (!isset($data['task'])) {
+ Message::addWarning('select-image-first');
+ Util::redirect('?do=vmstore&show=benchmark&action=select&id=' . $id);
+ }
+ if ($data['task'] === 'failed') {
+ Message::addError('benchmark-failed');
+ return;
+ }
+ $remaining = 0;
+ if ($data['task'] !== 'done') {
+ $remaining = ($data['expected'] ?? 0) - time();
+ if ($remaining < 0) {
+ $remaining = 0;
+ }
+ $this->processRunningBenchmark($id, $data, $remaining === 0);
+ $refresh = $remaining;
+ Util::clamp($refresh, 2, 64);
+ }
+ $args = [
+ 'id' => $id,
+ 'result' => json_encode($data['result'] ?? []),
+ 'wanted' => json_encode($data['machines']),
+ ];
+ if ($remaining > 0) {
+ $args['remaining'] = $remaining;
+ $args['refresh'] = $refresh ?? 60;
+ }
+ Module::isAvailable('js_chart');
+ Render::addTemplate('benchmark-result', $args);
+ }
+
+ private function processRunningBenchmark(int $id, array &$data, bool $timeout)
+ {
+ Module::isAvailable('rebootcontrol');
+ $changed = false;
+
+ $active = array_filter($data['machines'], function ($e) use ($data) { return !isset($data['result'][$e]); });
+ if (empty($active)) {
+ $timeout = true;
+ } else {
+ if ($timeout) {
+ // cat everything for easier troubleshooting
+ $command = <<<EOF
+cat "/tmp/speedcheck-$id"
+EOF;
+ } else {
+ $command = <<<EOF
+grep -q '^Seq:' "/tmp/speedcheck-$id" && cat "/tmp/speedcheck-$id"
+EOF;
+ }
+ $task = RebootControl::runScript($active, $command);
+ $task = Taskmanager::waitComplete($task, 4000);
+ if ($task === false) {
+ $data['task'] = 'failed';
+ return;
+ }
+ if (!isset($data['result'])) {
+ $data['result'] = [];
+ }
+ $res =& $task['data'];
+ foreach ($res['result'] as $uuid => $out) {
+ if (isset($data['result'][$uuid]))
+ continue;
+ error_log(json_encode($out));
+ // Not finished, ignore
+ if (($out['state'] !== 'DONE' || $out['exitCode'] !== 0) && !$timeout)
+ continue;
+ $changed = true;
+ unset($client);
+ $client = ['machineuuid' => $uuid];
+ $data['result'][$uuid] =& $client;
+ if (preg_match_all("/^\+(\w{3}):(\d+),(.*)$/m", $out['stdout'], $modes, PREG_SET_ORDER)) {
+ foreach ($modes as $mode) {
+ $client[$mode[1]] = [
+ 'start' => $mode[2],
+ 'values' => VmStoreBenchmark::parseBenchLine($mode[3]),
+ ];
+ }
+ } else {
+ $client['stderr'] = substr($out['stderr'], 0, 4000)
+ . "\nStatus: {$out['state']}, ExitCode: {$out['exitCode']}";
+ $client['stdout'] = substr($out['stdout'], 0, 4000);
+ }
+ $m = Database::queryFirst('SELECT clientip, hostname FROM machine WHERE machineuuid = :uuid',
+ ['uuid' => $uuid]);
+ $client['name'] = empty($m['hostname']) ? $m['clientip'] : $m['hostname'];
+ }
+ }
+ if (count($data['result']) === count($data['machines']) || $timeout) {
+ $data['task'] = 'done';
+ $changed = true;
+ }
+ if ($changed) {
+ Property::updateListEntry(VmStoreBenchmark::PROP_LIST_KEY, $id, json_encode($data), 30);
+ }
+ }
+
} \ No newline at end of file
diff --git a/modules-available/vmstore/permissions/permissions.json b/modules-available/vmstore/permissions/permissions.json
index 8303fd02..0617c673 100644
--- a/modules-available/vmstore/permissions/permissions.json
+++ b/modules-available/vmstore/permissions/permissions.json
@@ -1,5 +1,8 @@
{
"edit": {
"location-aware": false
+ },
+ "benchmark": {
+ "location-aware": true
}
} \ No newline at end of file
diff --git a/modules-available/vmstore/templates/benchmark-imgselect.html b/modules-available/vmstore/templates/benchmark-imgselect.html
new file mode 100644
index 00000000..be81aa3e
--- /dev/null
+++ b/modules-available/vmstore/templates/benchmark-imgselect.html
@@ -0,0 +1,59 @@
+<div class="page-header">
+ <h1>{{lang_benchmark}}</h1>
+</div>
+
+<form role="form" method="post" action="?do=vmstore">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="show" value="benchmark">
+ <input type="hidden" name="id" value="{{id}}">
+
+ <h4>{{lang_selectServerOrNfs}}</h4>
+ {{#servers}}
+ <div class="radio">
+ <input type="radio" id="s-{{idx}}" name="server" value="{{idx}}" {{checked}}>
+ <label for="s-{{idx}}">{{server}}</label>
+ </div>
+ {{/servers}}
+
+ <div class="slx-space"></div>
+
+ <h4>{{lang_selectImage}}</h4>
+ <div>
+ <table class="table table-condensed stupidtable">
+ <thead>
+ <tr>
+ <th data-sort="string">{{lang_image}}</th>
+ <th class="slx-smallcol" data-sort="int" data-sort-default="desc">{{lang_users}}</th>
+ <th class="slx-smallcol" data-sort="int" data-sort-default="desc">{{lang_size}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{#list}}
+ <tr>
+ <td>
+ <div class="radio radio-inline">
+ <input type="radio" id="r-{{id}}" name="image" value="{{name}}">
+ <label for="r-{{id}}">{{name}}</label>
+ </div>
+ </td>
+ <td class="text-right">{{users}}</td>
+ <td class="text-right" data-sort-value="{{size}}">{{size_s}}</td>
+ </tr>
+ {{/list}}
+ </tbody>
+ </table>
+ </div>
+
+ <div class="slx-space"></div>
+
+ <div style="position:fixed;bottom:0;right:0;padding:8px;background:#fff;width:100%;border-top:1px solid #ddd">
+ <div class="buttonbar text-right">
+ <button type="submit" name="action" value="start" class="btn btn-primary">
+ <span class="glyphicon glyphicon-play"></span>
+ {{lang_start}}
+ </button>
+ </div>
+ </div>
+
+
+</form> \ No newline at end of file
diff --git a/modules-available/vmstore/templates/benchmark-nothing.html b/modules-available/vmstore/templates/benchmark-nothing.html
new file mode 100644
index 00000000..aeef9187
--- /dev/null
+++ b/modules-available/vmstore/templates/benchmark-nothing.html
@@ -0,0 +1,7 @@
+<div class="page-header">
+ <h1>{{lang_benchmark}}</h1>
+</div>
+
+<div class="alert alert-info">
+ {{lang_benchmarkMainPageText}}
+</div> \ No newline at end of file
diff --git a/modules-available/vmstore/templates/benchmark-result.html b/modules-available/vmstore/templates/benchmark-result.html
new file mode 100644
index 00000000..28f31f12
--- /dev/null
+++ b/modules-available/vmstore/templates/benchmark-result.html
@@ -0,0 +1,141 @@
+<h1>{{lang_benchmark}}</h1>
+
+<h2>{{lang_benchmarkResult}}</h2>
+
+{{#remaining}}
+<div class="alert alert-info">
+ {{lang_benchmarkSecondsReminaing}}: <span id="remaining-seconds">{{remaining}}</span>
+</div>
+{{/remaining}}
+
+<div id="graphs"></div>
+
+<div id="errors"></div>
+
+<script>
+ document.addEventListener('DOMContentLoaded', function() {
+ var result = {{{result}}};
+ var clients = {{{wanted}}};
+ var graphs = {};
+ function formatBytes(val) {
+ return Math.floor(val / 1024 / 1024) + "\u2009MiB/s";
+ }
+ function renderX(val, index) {
+ return Math.floor(val / 1000) + '\u2009s';
+ }
+ function makeGraph(typeKey, resourceKey, caption) {
+ var uuid;
+ var ds = [];
+ var gmin = 0, rmax = 0;
+ var colors = [];
+ var cnt = 0;
+ for (uuid in result) {
+ if (!result[uuid][typeKey]) {
+ delete result[uuid];
+ continue;
+ }
+ if (gmin === 0 || result[uuid][typeKey].start < gmin) {
+ gmin = result[uuid][typeKey].start;
+ }
+ cnt++;
+ }
+ if (cnt === 1) {
+ colors.push('rgb(0, 128, 0)');
+ } else {
+ for (i = 0; i < cnt; ++i) {
+ colors.push('rgb(0, 128, ' + (i / (cnt - 1)) * 255 + ')');
+ }
+ }
+ var v, i, o, idx;
+ var sums = [];
+ for (uuid in result) {
+ o = result[uuid][typeKey].start - gmin; // Adjust according to earliest client
+ v = result[uuid][typeKey].values[resourceKey];
+ for (i = 0; i < v.length; ++i) {
+ v[i].x += o;
+ if (cnt > 1) {
+ idx = Math.round(v[i].x / 250);
+ if (sums[idx]) {
+ sums[idx] += v[i].y | 0;
+ } else {
+ sums[idx] = v[i].y | 0;
+ }
+ }
+ }
+ if (v[v.length-1].x > rmax) rmax = v[v.length-1].x; // Get max value
+ ds.push({data: v, label: result[uuid].name, borderColor: colors[ds.length], fill: false});
+ }
+ if (cnt > 1) {
+ ds.push({data: sums, label: 'Sum', borderColor: '#c00'});
+ }
+ if (!graphs[typeKey]) {
+ var $e = $('#graphs');
+ var $c = $('<canvas style="width:100%;height:250px">');
+ $e.append($('<h3>').text(caption));
+ $e.append($c);
+ var ls = [];
+ for (i = 0; i <= rmax; i += 250) ls.push(i); // Generate steps for graph
+ graphs[typeKey] = new Chart($c[0].getContext('2d'), {data: {datasets: ds, labels: ls}, type: 'scatter', options: {
+ animation: false,
+ responsive: true,
+ borderWidth: 2,
+ pointBorderWidth: 0,
+ showLine: true,
+ scales: { y: { ticks: { callback: formatBytes }}, x: { ticks: { callback: renderX }, max: rmax } },
+ plugins: {
+ tooltip: { callbacks: { label: function(context) {
+ if (context.parsed.y !== null) {
+ return context.dataset.label + ": " + formatBytes(context.parsed.y);
+ }
+ return context.dataset.label;
+ }
+ }},
+ legend: { position: 'left'}
+ }
+ }});
+ } else {
+ graphs[typeKey].data.datasets = ds;
+ graphs[typeKey].update();
+ }
+ }
+
+ var $err = $('#errors');
+ for (var uuid in result) {
+ if (result[uuid].stdout || result[uuid].stderr) {
+ var $frame = $('<div class="panel panel-body">');
+ $frame.append($('<h5>').text(result[uuid].name));
+ if (result[uuid].stdout) {
+ $frame.append($('<label>').text('stdout'));
+ $frame.append($('<pre>').text(result[uuid].stdout));
+ }
+ if (result[uuid].stderr) {
+ $frame.append($('<label>').text('stderr'));
+ $frame.append($('<pre>').text(result[uuid].stderr));
+ }
+ $err.append($frame);
+ }
+ }
+
+ makeGraph('SEQ', 'net', 'Sequential Reads');
+ makeGraph('RND', 'net', 'Random 1M');
+
+ {{#refresh}}
+ setTimeout(function() {
+ window.location.reload();
+ }, {{refresh}} * 1000);
+ {{#remaining}}
+ var remaining = {{remaining}};
+ function updateRemainingCounter() {
+ if (remaining > 0) {
+ setTimeout(updateRemainingCounter, 1000);
+ } else {
+ window.location.reload();
+ }
+ $('#remaining-seconds').text(remaining--);
+ }
+ updateRemainingCounter();
+ {{/remaining}}
+ {{/refresh}}
+
+ });
+</script> \ No newline at end of file
diff --git a/modules-available/vmstore/templates/page-vmstore.html b/modules-available/vmstore/templates/page-vmstore.html
index 0e1ad601..fa222631 100644
--- a/modules-available/vmstore/templates/page-vmstore.html
+++ b/modules-available/vmstore/templates/page-vmstore.html
@@ -1,10 +1,11 @@
+<h1>{{lang_vmLocation}}</h1>
+
<form role="form" method="post" action="?do=VmStore">
<input type="text" name="prevent_autofill" id="prevent_autofill" value="" style="position:absolute;top:-2000px" tabindex="-1">
<input type="password" name="password_fake" id="password_fake" value="" style="position:absolute;top:-2000px" tabindex="-1">
<input type="hidden" name="token" value="{{token}}">
<input type="hidden" name="action" value="setstore">
- <h1>{{lang_vmLocation}}</h1>
<p>{{lang_vmLocationChoose}} <a class="btn btn-default" data-toggle="modal" data-target="#help-store"><span class="glyphicon glyphicon-question-sign"></span></a></p>