DaPortal

<?php //$Id$
//Copyright (c) 2015-2016 Pierre Pronchery <khorben@defora.org>
//This file is part of DeforaOS Web DaPortal
//
//This program is free software: you can redistribute it and/or modify
//it under the terms of the GNU General Public License as published by
//the Free Software Foundation, version 3 of the License.
//
//This program is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//GNU General Public License for more details.
//
//You should have received a copy of the GNU General Public License
//along with this program. If not, see <http://www.gnu.org/licenses/>.
//SaltModule
class SaltModule extends Module
{
//public
//methods
//SaltModule::call
public function call(Engine $engine, Request $request, $internal = 0)
{
//XXX should be saved in the constructor
$this->engine = $engine;
$action = $request->getAction();
if(!is_string($action) || strlen($action) === 0)
$action = 'default';
if($internal)
switch($action)
{
case 'actions':
return $this->$action($request);
default:
return FALSE;
}
$method = 'call'.$action;
if(!method_exists($this, $method))
return new ErrorResponse(_('Invalid action'),
Response::$CODE_ENOENT);
return $this->$method($request);
}
//protected
//properties
protected $engine;
//methods
//accessors
//SaltModule::canReboot
protected function canReboot(Request $request = NULL, $hostname = FALSE,
&$error = FALSE)
{
$credentials = $this->engine->getCredentials();
if(!$credentials->isAdmin())
{
$error = _('Permission denied');
return Response::$CODE_EPERM;
}
if($request !== NULL && $request->isIdempotent())
{
$error = _('Confirmation required');
return Response::$CODE_EROFS;
}
//let Salt decide
return Response::$CODE_SUCCESS;
}
//SaltModule::canServiceReload
protected function canServiceReload(Request $request = NULL,
$hostname = FALSE, &$error = FALSE)
{
return $this->canServiceRestart($request, $hostname, $error);
}
//SaltModule::canServiceRestart
protected function canServiceRestart(Request $request = NULL,
$hostname = FALSE, &$error = FALSE)
{
$credentials = $this->engine->getCredentials();
if(!$credentials->isAdmin())
{
$error = _('Permission denied');
return Response::$CODE_EPERM;
}
if($request !== NULL && $request->isIdempotent())
{
$error = _('Confirmation required');
return Response::$CODE_EROFS;
}
//let Salt decide
return Response::$CODE_SUCCESS;
}
//SaltModule::canServiceStart
protected function canServiceStart(Request $request = NULL,
$hostname = FALSE, &$error = FALSE)
{
return $this->canServiceRestart($request, $hostname, $error);
}
//SaltModule::canServiceStop
protected function canServiceStop(Request $request = NULL,
$hostname = FALSE, &$error = FALSE)
{
return $this->canServiceRestart($request, $hostname, $error);
}
//SaltModule::canShutdown
protected function canShutdown(Request $request = NULL,
$hostname = FALSE, &$error = FALSE)
{
return $this->canReboot($request, $hostname, $error);
}
//SaltModule::canUpgrade
protected function canUpgrade(Request $request = NULL,
$hostname = FALSE, &$error = FALSE)
{
$credentials = $this->engine->getCredentials();
if(!$credentials->isAdmin())
{
$error = _('Permission denied');
return Response::$CODE_EPERM;
}
if($request !== NULL && $request->isIdempotent())
{
$error = _('Confirmation required');
return Response::$CODE_EROFS;
}
//let Salt decide
return Response::$CODE_SUCCESS;
}
//actions
//SaltModule::actions
protected function actions($request)
{
return array();
}
//calls
//SaltModule::callDefault
protected function callDefault(Request $request)
{
$title = _('Salt administration');
$hostname = $request->get('host');
$page = new Page(array('title' => $title));
$page->append('title', array('stock' => 'monitor',
'text' => $title));
$vbox = $page->append('vbox');
if(!is_string($hostname) || strlen($hostname) == 0)
$this->_defaultList($vbox);
else
$this->_defaultHost($request, $vbox, $hostname);
return new PageResponse($page);
}
private function _defaultForm(PageElement $page, $hostname = FALSE)
{
//XXX forms are blocks by default in HTML
$form = $page->append('form', array('idempotent' => TRUE,
'request' => $this->getRequest()));
$hbox = $form->append('hbox');
$hbox->append('entry', array('name' => 'host',
'placeholder' => _('Hostname (or glob)'),
'value' => $hostname));
$hbox->append('button', array('type' => 'submit',
'text' => _('Monitor')));
}
private function _defaultHost(Request $request, PageElement $page,
$host)
{
$salt = array('uptime' => array('title' => _('Uptime'),
'helper' => 'Uptime',
'error' => _('Could not obtain uptime')),
'upgrades' => array('title' => _('Package upgrades'),
'helper' => 'UpgradeList',
'error' => _('Could not list package upgrades'),
'args' => array('hostname')),
'services' => array('title' => _('Services'),
'helper' => 'ServiceListEnabled',
'error' => _('Could not list the services'),
'args' => array('hostname'),
'render' => 'ServiceList'),
'status' => array('title' => _('Statistics'),
'helper' => 'StatusAll',
'error' => _('Could not obtain statistics')));
$page->append('title', array('text' => $host));
$this->_defaultHostToolbar($page, $host);
//gather the data
$count = 0;
foreach($salt as $s => $v)
{
$helper = 'helperSalt'.$v['helper'];
if(($data = $this->$helper($host)) === FALSE
|| !is_object($data))
continue;
$count = max($count, $this->_defaultHostCount($data));
$salt[$s]['data'] = $data;
}
$view = $page->append('iconview');
$url = $this->engine->getURL($request);
foreach($salt as $s => $v)
{
$link = new PageElement('link', array(
'url' => $url.'#'.$s, 'text' => $v['title']));
if(!isset($v['data']))
{
$view->append('row', array(
'icon' => new PageElement('image',
array('stock' => 'error')),
'label' => $link));
$page->append('dialog', array('type' => 'error',
'title' => $v['title'],
'text' => $v['error']));
continue;
}
$view->append('row', array(
'icon' => new PageElement('image', array(
'stock' => 'monitor')),
'label' => $link));
$render = isset($v['render'])
? $v['render'] : $v['helper'];
$render = 'render'.$render;
$args = isset($v['args']) ? $v['args'] : array();
if($count > 1)
$this->_defaultHostRenderMultiple($page, $s,
$host, $v['title'], $v['data'],
array($this, $render), $args);
else
$this->_defaultHostRender($page, $s, $host,
$v['title'], $v['data'],
array($this, $render), $args);
}
}
private function _defaultHostCount($data)
{
$hostnames = array();
if(!is_array($data))
$data = array($data);
foreach($data as $hosts)
if(is_object($hosts))
foreach($hosts as $hostname => $h)
$hostnames[$hostname] = FALSE;
return count($hostnames);
}
private function _defaultHostRender(PageElement $page, $id, $host,
$title, $data, $callback, $params = array())
{
if(!is_array($data))
$data = array($data);
foreach($data as $hosts)
if(is_object($hosts))
foreach($hosts as $hostname => $h)
{
$page->append('title', array(
'id' => $id,
'text' => $title));
$args = array();
foreach($params as $param)
$args[$param] = $$param;
$callback($page, $h, $args);
}
}
private function _defaultHostRenderMultiple(PageElement $page, $id,
$host, $title, $data, $callback, $params = array())
{
$page->append('title', array('id' => $id, 'text' => $title));
$columns = array('title' => '', 'status' => '');
$page = $page->append('treeview', array('columns' => $columns));
if(!is_array($data))
$data = array($data);
foreach($data as $hosts)
if(is_object($hosts))
foreach($hosts as $hostname => $h)
{
$request = $this->getRequest(FALSE,
array('host' => $hostname));
$link = new PageElement('link', array(
'request' => $request,
'text' => $hostname));
$row = $page->append('row', array(
'title' => $link));
$p = new PageElement('vbox');
$args = array();
foreach($params as $param)
$args[$param] = $$param;
$callback($p, $h, $args);
$row->set('status', $p);
}
}
private function _defaultHostToolbar(PageElement $page, $hostname)
{
$toolbar = $page->append('toolbar');
$request = $this->getRequest(FALSE, array('host' => $hostname));
$toolbar->append('button', array('stock' => 'refresh',
'text' => _('Refresh'), 'request' => $request));
$request = $this->getRequest('reboot', array(
'host' => $hostname));
$toolbar->append('button', array('stock' => 'refresh',
'text' => _('Reboot'), 'request' => $request));
$request = $this->getRequest('shutdown', array(
'host' => $hostname));
$toolbar->append('button', array('stock' => 'logout',
'text' => _('Shutdown'),
'request' => $request));
$this->_defaultForm($toolbar, $hostname);
}
private function _defaultList(PageElement $page)
{
$this->_defaultListToolbar($page);
if(($data = $this->helperSaltPing()) === FALSE)
{
$error = _('Could not list hosts');
$page->append('dialog', array(
'type' => 'error', 'text' => $error));
return;
}
$view = $page->append('iconview');
if(!is_array($data))
$data = array($data);
$rows = array();
foreach($data as $d)
foreach($d as $hostname => $value)
{
$request = $this->getRequest(FALSE,
array('host' => $hostname));
$link = new PageElement('link', array(
'request' => $request,
'text' => $hostname));
$stock = ($value === TRUE)
? 'server' : 'warning';
$title = is_string($value) ? FALSE : $value;
$icon = new PageElement('image', array(
'stock' => $stock));
$rows[$hostname] = array('icon' => $icon,
'title' => $title, 'label' => $link);
}
sort($rows);
foreach($rows as $row)
$view->append('row', $row);
}
private function _defaultListToolbar(PageElement $page)
{
$toolbar = $page->append('toolbar');
$request = $this->getRequest();
$toolbar->append('button', array('stock' => 'refresh',
'text' => _('Refresh'),
'request' => $request));
$this->_defaultForm($toolbar);
}
//SaltModule::callReboot
protected function callReboot(Request $request)
{
return $this->helperAction($request);
}
//SaltModule::callServiceReload
protected function callServiceReload(Request $request)
{
return $this->helperAction($request, array('service'));
}
//SaltModule::callServiceRestart
protected function callServiceRestart(Request $request)
{
return $this->helperAction($request, array('service'));
}
//SaltModule::callServiceStart
protected function callServiceStart(Request $request)
{
return $this->helperAction($request, array('service'));
}
//SaltModule::callServiceStop
protected function callServiceStop(Request $request)
{
return $this->helperAction($request, array('service'));
}
//SaltModule::callShutdown
protected function callShutdown(Request $request)
{
return $this->helperAction($request);
}
//SaltModule::callUpgrade
protected function callUpgrade(Request $request)
{
return $this->helperAction($request);
}
//helpers
//SaltModule::helperAction
protected function helperAction(Request $request, $args = array())
{
$action = $request->getAction();
if(($hostname = $request->get('host')) === FALSE)
{
$error = _('Unknown host');
$page = new PageElement('dialog', array(
'type' => 'error', 'text' => $error));
return new PageResponse($page, Response::$CODE_ENOENT);
}
$method = 'can'.$action;
if(method_exists($this, $method)
&& ($code = $this->$method($request, $hostname,
$error)) !== Response::$CODE_SUCCESS)
return $this->_actionError($request, $hostname, $action,
$code, $error);
$method = 'helperSalt'.$action;
if(!method_exists($this, $method))
{
$error = _('Unsupported action');
$page = new PageElement('dialog', array(
'type' => 'error', 'text' => $error));
return new PageResponse($page,
Response::$CODE_ENOENT);
}
$a = array($hostname);
foreach($args as $arg)
$a[] = $request->get($arg);
if(call_user_func_array(array($this, $method), $a) === FALSE)
{
$error = sprintf(_('Could not %s'), $action);
$page = new PageElement('dialog', array(
'type' => 'error', 'text' => $error));
return new PageResponse($page,
Response::$CODE_EUNKNOWN);
}
$message = sprintf(_('%s successful'), ucfirst($action));
$dialog = new PageElement('dialog', array('type' => 'info',
'title' => $hostname, 'text' => $message));
$r = $this->getRequest(FALSE, array('host' => $hostname));
$dialog->append('button', array('stock' => 'back',
'request' => $r, 'text' => _('Back')));
return new PageResponse($dialog);
}
private function _actionError($request, $hostname, $action, $code,
$error)
{
switch($code)
{
case Response::$CODE_EROFS:
$error .= "\n";
$format = _('Do you really want to %s %s?');
$error .= sprintf($format, $action, $hostname);
$dialog = new PageElement('dialog', array(
'type' => 'question',
'text' => $error));
$form = $dialog->append('form', array(
'request' => $request));
$r = $this->getRequest(FALSE, array(
'host' => $hostname));
$form->append('button', array(
'stock' => 'cancel',
'request' => $r,
'text' => _('Cancel')));
$form->append('button', array(
'type' => 'submit',
'text' => ucfirst($action)));
return new PageResponse($dialog);
default:
$dialog = new PageElement('dialog', array(
'type' => 'error',
'text' => $error));
return new PageResponse($dialog);
}
}
//SaltModule::helperSalt
protected function helperSalt($hostname = FALSE, $command = 'test.ping',
$args = FALSE, $options = FALSE)
{
$salt = $this->configGet('salt') ?: 'salt';
$options = is_array($options)
? $options : array('--out=json', '--static');
$hostname = (is_string($hostname) && strlen($hostname))
? $hostname : '*';
$args = is_array($args) ? $args : array();
$cmd = escapeshellarg($salt);
foreach($options as $option)
$cmd .= ' '.escapeshellarg($option);
$cmd .= ' '.escapeshellarg($hostname);
$cmd .= ' '.escapeshellarg($command);
foreach($args as $arg)
$cmd .= ' '.escapeshellarg($arg);
exec($cmd, $output, $res);
if($res != 0)
//something went really wrong, or finally right, or this
//is not salt, or it is not installed
return FALSE;
if(($data = json_decode(implode($output, "\n"))) === NULL)
return FALSE;
return $data;
}
//SaltModule::helperSaltDiskusage
protected function helperSaltDiskusage($hostname)
{
return $this->helperSalt($hostname, 'status.diskusage');
}
//SaltModule::helperSaltLoadavg
protected function helperSaltLoadavg($hostname)
{
return $this->helperSalt($hostname, 'status.loadavg');
}
//SaltModule::helperSaltNetdev
protected function helperSaltNetdev($hostname)
{
return $this->helperSalt($hostname, 'status.netdev');
}
//SaltModule::helperSaltPing
protected function helperSaltPing($hostname = FALSE)
{
return $this->helperSalt($hostname, 'test.ping');
}
//SaltModule::helperSaltReboot
protected function helperSaltReboot($hostname)
{
return $this->helperSalt($hostname, 'system.reboot');
}
//SaltModule::helperSaltServiceList
protected function helperSaltServiceList($hostname)
{
return $this->helperSalt($hostname, 'service.get_all');
}
//SaltModule::helperSaltServiceListEnabled
protected function helperSaltServiceListEnabled($hostname)
{
return $this->helperSalt($hostname, 'service.get_enabled');
}
//SaltModule::helperSaltServiceReload
protected function helperSaltServiceReload($hostname, $service)
{
return $this->helperSalt($hostname, 'service.reload',
array($service));
}
//SaltModule::helperSaltServiceRestart
protected function helperSaltServiceRestart($hostname, $service)
{
return $this->helperSalt($hostname, 'service.restart',
array($service));
}
//SaltModule::helperSaltServiceStart
protected function helperSaltServiceStart($hostname, $service)
{
return $this->helperSalt($hostname, 'service.start',
array($service));
}
//SaltModule::helperSaltServiceStatus
protected function helperSaltServiceStatus($hostname, $service)
{
return $this->helperSalt($hostname, 'service.status',
array($service));
}
//SaltModule::helperSaltServiceStop
protected function helperSaltServiceStop($hostname, $service)
{
return $this->helperSalt($hostname, 'service.stop',
array($service));
}
//SaltModule::helperSaltShutdown
protected function helperSaltShutdown($hostname)
{
return $this->helperSalt($hostname, 'system.shutdown');
}
//SaltModule::helperSaltStatusAll
protected function helperSaltStatusAll($hostname)
{
return $this->helperSalt($hostname, 'status.all_status');
}
//SaltModule::helperSaltUptime
protected function helperSaltUptime($hostname)
{
return $this->helperSalt($hostname, 'status.uptime');
}
//SaltModule::helperSaltUpgrade
protected function helperSaltUpgrade($hostname)
{
return $this->helperSalt($hostname, 'pkg.upgrade');
}
//SaltModule::helperSaltUpgradeList
protected function helperSaltUpgradeList($hostname)
{
return $this->helperSalt($hostname, 'pkg.list_upgrades');
}
//rendering
//SaltModule::renderDiskusage
private function renderDiskusage(PageElement $page, $data,
$args = array())
{
if(is_string($data))
{
$page->append('dialog', array('type' => 'error',
'text' => $data));
return;
}
foreach($data as $vol => $voldata)
{
if($voldata->total == 0)
continue;
$capacity = 100 - ($voldata->available
/ $voldata->total * 100);
$progress = $page->append('progress', array(
'text' => $vol.': '.round($capacity).'%',
'min' => 0, 'max' => 100, 'high' => 75,
'value' => $capacity));
$progress->append('label', array(
'text' => ' '.$vol));
}
}
//SaltModule::renderLoadavg
protected function renderLoadavg(PageElement $page, $data,
$args = array())
{
foreach($data as $key => $value)
$page->append('label', array(
'text' => $key.': '.$value));
}
//SaltModule::renderNetdev
protected function renderNetdev(PageElement $page, $data,
$args = array())
{
if(is_string($data))
{
$page->append('dialog', array('type' => 'error',
'text' => $data));
return;
}
foreach($data as $name => $interface)
{
$title = $name;
$vbox = $page->append('vbox');
$vbox->append('title', array('text' => $title));
foreach($interface as $key => $value)
$vbox->append('label', array(
'text' => $key.': '.$value));
}
}
//SaltModule::renderServiceList
protected function renderServiceList(PageElement $page, $data,
$args = array())
{
$calls = array(
'reload' => array('text' => _('Reload'),
'stock' => 'media-previous'),
'start' => array('text' => _('Start'),
'stock' => 'media-play'),
'stop' => array('text' => _('Stop'),
'stock' => 'media-stop'),
'restart' => array('text' => _('Restart'),
'stock' => 'media-loop'));
if(!isset($args['hostname']))
return;
$hostname = $args['hostname'];
$columns = array('service' => '', 'actions' => '');
$view = $page->append('treeview', array('columns' => $columns,
'alternate' => TRUE));
foreach($data as $service)
{
$actions = FALSE;
foreach($calls as $call => $p)
{
$c = 'canService'.$call;
if($this->$c(NULL, $hostname, $error)
!== Response::$CODE_SUCCESS)
continue;
$r = $this->getRequest('service'.$call, array(
'host' => $hostname,
'service' => $service));
if($actions === FALSE)
$actions = new PageElement('hbox');
$actions->append('button', array(
'stock' => $p['stock'],
'request' => $r,
'text' => $p['text']));
}
$view->append('row', array('service' => $service,
'actions' => $actions));
}
}
//SaltModule::renderStatusAll
protected function renderStatusAll(PageElement $page, $data,
$args = array())
{
if(!($data instanceof Traversable) && !is_object($data))
return;
foreach($data as $key => $value)
{
$vbox = new PageElement('vbox');
$append = TRUE;
switch($key)
{
case 'diskusage':
$title = _('Disk usage');
$vbox->append('title', array(
'text' => $title));
$this->renderDiskusage($vbox, $value);
break;
case 'loadavg':
$title = _('Load average');
$vbox->append('title', array(
'text' => $title));
$this->renderLoadavg($vbox, $value);
break;
case 'netdev':
$title = _('Network interfaces');
$vbox->append('title', array(
'text' => $title));
$this->renderNetdev($vbox, $value);
break;
default:
$append = FALSE;
break;
}
if($append)
$page->append($vbox);
}
}
//SaltModule::renderUptime
protected function renderUptime(PageElement $page, $data,
$args = array())
{
$page->append('label', array('text' => $data));
}
//SaltModule::renderUpgradeList
protected function renderUpgradeList(PageElement $page, $data,
$args = array())
{
if(!isset($args['hostname']))
return;
if(is_string($data))
{
$page->append('dialog', array('type' => 'error',
'text' => $data));
return;
}
$hostname = $args['hostname'];
$page = $page->append('vbox');
if(($count = count((array)$data)) == 0)
{
$message = _('There is no package to update.');
$page->append('dialog', array('type' => 'info',
'title' => _('System up to date'),
'text' => $message));
return;
}
$message = sprintf(_('%u package upgrade(s) are available'),
$count);
$dialog = $page->append('dialog', array('type' => 'warning',
'text' => $message));
$page = $dialog->append('expander', array(
'title' => _('Details')));
foreach($data as $key => $value)
$page->append('label', array(
'text' => $key.': '.$value));
$request = $this->getRequest('upgrade', array(
'host' => $hostname));
$dialog->append('button', array('stock' => 'submit',
'request' => $request,
'text' => _('Upgrade')));
}
}
?>