<?php
class Page_VmStore extends Page
{
/**
* @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');
if ($action === 'setstore') {
$this->setStore();
}
}
protected function doRender()
{
if (Request::any('show') === 'benchmark') {
$this->benchmarkDoRender();
return;
}
$action = Request::post('action');
if ($action === 'setstore' && !Taskmanager::isFailed(Taskmanager::status($this->mountTask))) {
Render::addTemplate('mount', array(
'task' => $this->mountTask
));
return;
}
$vmstore = Property::getVmStoreConfig();
if (isset($vmstore['storetype'])) {
$vmstore['pre-' . $vmstore['storetype']] = 'checked';
}
Render::addTemplate('page-vmstore', $vmstore);
}
private function setStore()
{
$vmstore = array();
foreach (array('storetype', 'nfsaddr', 'nfsopts', 'cifsaddr', 'cifsuser', 'cifspasswd', 'cifsuserro', 'cifspasswdro', 'cifsopts') as $key) {
$vmstore[$key] = trim(Request::post($key, '', 'string'));
// Remove rw setting
if ($key === 'cifsopts' || $key === 'nfsopts') {
$vmstore[$key] = preg_replace('/\s+,\s+/', ',', $vmstore[$key]);
$vmstore[$key] = preg_replace('/^rw,|,rw$/', '', str_replace(',rw,', ',', $vmstore[$key]));
}
}
$storetype = $vmstore['storetype'];
if (!in_array($storetype, array('internal', 'nfs', 'cifs'))) {
Message::addError('main.value-invalid', 'type', $storetype);
Util::redirect('?do=VmStore');
}
// Validate syntax of nfs/cifs
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+/.+$#i', $vmstore['cifsaddr'])) {
Message::addError('main.value-invalid', 'nfsaddr', $vmstore['nfsaddr']);
Util::redirect('?do=VmStore');
}
$this->mountTask = Trigger::mount($vmstore);
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);
}
}
}