diff options
Diffstat (limited to 'modules-available/vmstore/page.inc.php')
-rw-r--r-- | modules-available/vmstore/page.inc.php | 270 |
1 files changed, 264 insertions, 6 deletions
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 |