CLI interface: initial import

This commit is contained in:
Thomas Gelf 2013-10-22 12:26:45 +00:00
parent d1e61a1826
commit a25cd80ec0
15 changed files with 1879 additions and 35 deletions

View File

@ -0,0 +1,60 @@
<?php
namespace Icinga\Clicommands;
use Icinga\Cli\Command;
use Icinga\Cli\Documentation;
/**
* Help for modules, commands and actions
*
* The help command shows help for a given command, module and also for a
* given module's command or a specific command's action.
*
* Usage: icingaweb help [<module>] [<command> [<action>]]
*/
class HelpCommand extends Command
{
protected $defaultActionName = 'show';
/**
* Show help for modules, commands and actions [default]
*
* The help command shows help for a given command, module and also for a
* given module's command or a specific command's action.
*
* Usage: icingaweb help [<module>] [<command> [<action>]]
*/
public function showAction()
{
$module = null;
$command = null;
$action = null;
$loader = $this->app->cliLoader();
$command = $this->params->shift();
if ($loader->hasCommand($command)) {
$action = $this->params->shift();
if (! $loader->getCommandInstance($command)->hasActionName($action)) {
$action = null;
}
} else {
if ($loader->hasModule($command)) {
$module = $command;
$command = $this->params->shift();
if ($loader->hasModuleCommand($module, $command)) {
$action = $this->params->shift();
$mod = $loader->getModuleCommandInstance($module, $command);
if (! $mod->hasActionName($action)) {
$action = null;
}
} else {
$command = null;
}
} else {
$command = null;
}
}
echo $this->docs()->usage($module, $command, $action);
}
}

View File

@ -0,0 +1,165 @@
<?php
namespace Icinga\Clicommands;
use Icinga\Cli\Command;
/**
* List and handle modules
*
* The module command allows you to handle your IcingaWeb modules
*
* Usage: icingaweb module [<action>] [<modulename>]
*/
class ModuleCommand extends Command
{
protected $modules;
public function init()
{
$this->modules = $this->app->getModuleManager();
}
/**
* List all enabled modules
*
* If you are interested in all installed modules pass 'installed' (or
* even --installed) as a command parameter. If you enable --verbose even
* more details will be shown
*
* Usage: icingaweb module list [installed] [--verbose]
*/
public function listAction()
{
if ($type = $this->params->shift()) {
if (! in_array($type, array('enabled', 'installed'))) {
return $this->showUsage();
}
} else {
$type = 'enabled';
$this->params->shift('enabled');
if ($this->params->shift('installed')) {
$type = 'installed';
}
}
if ($this->hasRemainingParams()) {
return $this->showUsage();
}
if ($type === 'enabled') {
$modules = $this->modules->listEnabledModules();
} else {
$modules = $this->modules->listInstalledModules();
}
if (empty($modules)) {
echo "There are no modules installed\n";
return;
}
if ($this->isVerbose) {
printf("%-14s %-9s DIRECTORY\n", 'MODULE', 'STATE');
} else {
printf("%-14s %-9s\n", 'MODULE', 'STATE');
}
foreach ($modules as $module) {
if ($this->isVerbose) {
$dir = ' ' . $this->modules->getModuleDir($module);
} else {
$dir = '';
}
printf(
"%-14s %-9s%s\n",
$module,
($type === 'enabled' || $this->modules->hasEnabled($module))
? 'enabled'
: 'disabled',
$dir
);
}
echo "\n";
}
/**
* Enable a given module
*
* Usage: icingaweb module enable <module-name>
*/
public function enableAction()
{
if (! $module = $this->params->shift()) {
$module = $this->params->shift('module');
}
if (! $module || $this->hasRemainingParams()) {
return $this->showUsage();
}
$this->modules->enableModule($module);
}
/**
* Disable a given module
*
* Usage: icingaweb module disable <module-name>
*/
public function disableAction()
{
if (! $module = $this->params->shift()) {
$module = $this->params->shift('module');
}
if (! $module || $this->hasRemainingParams()) {
return $this->showUsage();
}
$this->modules->disableModule($module);
}
/**
* Search for a given module
*
* Does a lookup against your configured IcingaWeb app stores and tries to
* find modules matching your search string
*
* Usage: icingaweb module search <search-string>
*/
public function searchAction()
{
$this->fail("Not implemented yet");
}
/**
* Install a given module
*
* Downloads a given module or installes a module from a given archive
*
* Usage: icingaweb module install <module-name>
* icingaweb module install </path/to/archive.tar.gz>
*/
public function installAction()
{
$this->fail("Not implemented yet");
}
/**
* Remove a given module
*
* Removes the given module from your disk. Module configuration will be
* preserved
*
* Usage: icingaweb module remove <module-name>
*/
public function removeAction()
{
$this->fail("Not implemented yet");
}
/**
* Purge a given module
*
* Removes the given module from your disk. Also wipes configuration files
* and other data stored and/or generated by this module
*
* Usage: icingaweb module remove <module-name>
*/
public function purgeAction()
{
$this->fail("Not implemented yet");
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace Icinga\Clicommands;
use Icinga\Cli\Command;
use Icinga\Application\TranslationHelper;
/**
* Translation command
*
* This command provides different utilities useful for translators. It
* allows to add new languages and also to refresh existing translations. All
* functionality is available for core components and for modules.
*
* This is another parapragh.
*/
class TranslationCommand extends Command
{
protected $translator;
public function init()
{
$this->translator = new TranslationHelper(
$this->application,
$this->params->get('locale', 'C'),
$this->params->get('module', 'monitoring') // bullshit. NULL?
);
}
/**
* Refresh translation catalogs
*
* Extracts all translatable strings for a given module (or core) from the
* Icingaweb source code, adds those to the existing catalog for the given
* locale and marks obsolete translations.
*
* Usage: icingaweb translation refresh --module <modulename> --locale <lc_LC>
*/
public function refreshAction()
{
$this->translator->createTemporaryFileList()->extractTexts();
}
}

15
bin/icingaweb Executable file
View File

@ -0,0 +1,15 @@
#!/usr/bin/php
<?php
use Icinga\Application\Cli;
use Icinga\Cli\Params as CliParams;
use Icinga\Application\Benchmark;
set_include_path(
realpath(dirname(__FILE__) . '/../library/')
. ':' . get_include_path()
);
require_once 'Icinga/Application/Cli.php';
$app = Cli::start(dirname(__FILE__) . '/../config/')->dispatch();

View File

@ -1,67 +1,158 @@
<?php <?php
// {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}}
/**
* This file is part of Icinga 2 Web.
*
* Icinga 2 Web - Head for multiple monitoring backends.
* Copyright (C) 2013 Icinga Development Team
*
* 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; either version 2
* of the License, or (at your option) any later version.
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* @copyright 2013 Icinga Development Team <info@icinga.org>
* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2
* @author Icinga Development Team <info@icinga.org>
*/
// {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Application; namespace Icinga\Application;
use Icinga\Application\Platform;
use Icinga\Application\ApplicationBootstrap;
use Icinga\Application\Modules\Manager as ModuleManager;
use Icinga\Cli\Params;
use Icinga\Cli\Loader;
use Icinga\Cli\Screen;
use Icinga\Application\Benchmark;
use Icinga\Exception\ProgrammingError;
// @codingStandardsIgnoreStart // @codingStandardsIgnoreStart
require_once dirname(__FILE__). '/ApplicationBootstrap.php'; require_once dirname(__FILE__) . '/ApplicationBootstrap.php';
require_once dirname(__FILE__). '/../Exception/ProgrammingError.php'; require_once dirname(__FILE__). '/../Exception/ProgrammingError.php';
// @codingStandardsIgnoreStop // @codingStandardsIgnoreStop
use Icinga\Exception\ProgrammingError;
/**
* Bootstrapping on cli environment
*/
class Cli extends ApplicationBootstrap class Cli extends ApplicationBootstrap
{ {
protected $isCli = true; protected $isCli = true;
protected $params;
protected $showBenchmark = false;
protected $watchTimeout;
protected $cliLoader;
protected function bootstrap() protected function bootstrap()
{ {
$this->assertRunningOnCli(); $this->assertRunningOnCli();
return $this->setupConfig() return $this->setupConfig()
->setupErrorHandling() ->setupErrorHandling()
->setupTimezone(); ->setupResourceFactory()
->setupModules()
->parseParams();
}
public function cliLoader()
{
if ($this->cliLoader === null) {
$this->cliLoader = new Loader($this);
}
return $this->cliLoader;
}
/**
* Setup module loader
*
* TODO: This can be removed once broken bootstrapping has been fixed
* Loading the module manager and enabling all modules have former
* been two different tasks. CLI does NOT enable any module by default.
*
* @return self
*/
protected function setupModules()
{
$this->moduleManager = new ModuleManager($this, $this->getConfigDir('enabledModules'));
return $this;
}
/**
* Getter for module manager
*
* TODO: This can also be removed once fixed. Making everything private
* made this duplication necessary
*
* @return ModuleManager
*/
public function getModuleManager()
{
return $this->moduleManager;
}
protected function parseParams()
{
$this->params = Params::parse();
if ($this->params->shift('help')) {
$this->params->unshift('help');
}
$watch = $this->params->shift('watch');
if ($watch === true) {
$watch = 5;
}
if (preg_match('~^\d+$~', $watch)) {
$this->watchTimeout = (int) $watch;
}
$this->showBenchmark = (bool) $this->params->shift('benchmark');
return $this;
}
public function getParams()
{
return $this->params;
}
public function dispatch()
{
Benchmark::measure('Dispatching CLI command');
if ($this->watchTimeout === null) {
$this->dispatchOnce();
} else {
$this->dispatchEndless();
}
}
protected function dispatchOnce()
{
$loader = new Loader($this);
$loader->parseParams();
$loader->dispatch();
Benchmark::measure('All done');
if ($this->showBenchmark) {
Benchmark::dump();
}
}
protected function dispatchEndless()
{
$loader = new Loader($this);
$loader->parseParams();
$screen = Screen::instance();
while (true) {
Benchmark::measure('Watch mode - loop begins');
echo $screen->clear();
$params = clone($this->params);
$loader->dispatch();
Benchmark::measure('Dispatch done');
if ($this->showBenchmark) {
Benchmark::dump();
}
Benchmark::reset();
$this->params = $params;
sleep($this->watchTimeout);
}
} }
/** /**
* Fail if Icinga has not been called on CLI * Fail if Icinga has not been called on CLI
* *
* @throws \Exception * @throws ProgrammingError
* @return void
*/ */
private function assertRunningOnCli() private function assertRunningOnCli()
{ {
if (Platform::isCli()) { if (Platform::isCli()) {
return; return;
} }
throw new ProgrammingError('Icinga is not running on CLI'); throw new ProgrammingError('Icinga is not running on CLI');
} }
} }

View File

@ -0,0 +1,102 @@
<?php
namespace Icinga\Cli;
use Icinga\Cli\Loader;
use Icinga\Cli\Screen;
use Icinga\Application\ApplicationBootstrap as App;
use Exception;
abstract class Command
{
protected $app;
protected $docs;
protected $params;
protected $screen;
protected $isVerbose;
protected $isDebugging;
protected $moduleName;
protected $commandName;
protected $actionName;
protected $defaultActionName = 'default';
public function __construct(App $app, $moduleName, $commandName, $actionName, $initialize = true)
{
$this->app = $app;
$this->moduleName = $moduleName;
$this->commandName = $commandName;
$this->actionName = $actionName;
$this->params = $app->getParams();
$this->screen = Screen::instance($app);
$this->isVerbose = $this->params->shift('verbose', false);
$this->isDebuging = $this->params->shift('debug', false);
if ($initialize) {
$this->init();
}
}
public function hasRemainingParams()
{
return $this->params->count() > 0;
}
public function fail($msg)
{
throw new Exception($msg);
}
public function getDefaultActionName()
{
return $this->defaultActionName;
}
public function hasDefaultActionName()
{
return $this->hasActionName($this->defaultActionName);
}
public function hasActionName($name)
{
$actions = $this->listActions();
return in_array($name, $actions);
}
public function listActions()
{
$actions = array();
foreach (get_class_methods($this) as $method) {
if (preg_match('~^([A-Za-z0-9]+)Action$~', $method, $m)) {
$actions[] = $m[1];
}
}
sort($actions);
return $actions;
}
public function docs()
{
if ($this->docs === null) {
$this->docs = new Documentation($this->app);
}
return $this->docs;
}
public function showUsage($action = null)
{
if ($action === null) {
$action = $this->actionName;
}
echo $this->docs()->usage(
$this->moduleName,
$this->commandName,
$action
);
}
public function init()
{
}
}

View File

@ -0,0 +1,140 @@
<?php
namespace Icinga\Cli;
use Icinga\Application\ApplicationBootstrap as App;
use Icinga\Cli\Documentation\CommentParser;
use ReflectionClass;
use ReflectionMethod;
class Documentation
{
protected $icinga;
public function __construct(App $app)
{
$this->app = $app;
$this->loader = $app->cliLoader();
}
public function usage($module = null, $command = null, $action = null)
{
if ($module !== null) {
return $this->moduleUsage($module, $command, $action);
}
if ($command !== null) {
return $this->commandUsage($command, $action);
}
return $this->globalUsage();
}
public function globalUsage()
{
$d = "USAGE: icingaweb [module] <command> [action] [options]\n\n"
. "Available commands:\n\n";
foreach ($this->loader->listCommands() as $command) {
$obj = $this->loader->getCommandInstance($command);
$d .= sprintf(
" %-14s %s\n",
$command,
$this->getClassTitle($obj)
);
}
$d .= "\nAvailable modules:\n\n";
foreach ($this->loader->listModules() as $module) {
$d .= ' ' . $module . "\n";
}
$d .= "\nGlobal options:\n\n"
. " --verbose Be verbose\n"
. " --debug Show debug output\n"
. " --benchmark Show benchmark summary\n"
. " --watch [s] Refresh output each <s> seconds (default: 5)\n"
;
$d .= "\nShow help on a specific command : icingaweb help <command>"
. "\nShow help on a specific module : icingaweb help <module>"
. "\n";
return $d;
}
public function moduleUsage($module, $command = null, $action = null)
{
$commands = $this->loader->listModuleCommands($module);
if (empty($commands)) {
return "The '$module' module does not provide any CLI commands\n";
}
$d = '';
if ($command) {
$obj = $this->loader->getModuleCommandInstance($module, $command);
}
if ($command === null) {
$d = "USAGE: icingaweb $module <command> [<action>] [options]\n\n"
. "Available commands:\n\n";
foreach ($commands as $command) {
$d .= ' ' . $command . "\n";
}
$d .= "\nShow help on a specific command: icingaweb help $module <command>\n";
} elseif ($action === null) {
$d .= $this->showCommandActions($obj, $command);
} else {
$d .= $this->getMethodDocumentation($obj, $action);
}
return $d;
}
protected function showCommandActions($command, $name)
{
$actions = $command->listActions();
$d = $this->getClassDocumentation($command)
. "Available actions:\n\n";
foreach ($actions as $action) {
$d .= sprintf(
" %-14s %s\n",
$action,
$this->getMethodTitle($command, $action)
);
}
$d .= "\nShow help on a specific action: icingaweb help $name <action>\n";
return $d;
}
public function commandUsage($command, $action = null)
{
$obj = $this->loader->getCommandInstance($command);
$d = "\n";
if ($action === null) {
$d .= $this->showCommandActions($obj, $command);
} else {
$d .= $this->getMethodDocumentation($obj, $action);
}
return $d;
}
protected function getClassTitle($class)
{
$ref = new ReflectionClass($class);
$comment = new CommentParser($ref->getDocComment());
return $comment->getTitle();
}
protected function getClassDocumentation($class)
{
$ref = new ReflectionClass($class);
$comment = new CommentParser($ref->getDocComment());
return $comment->dump();
}
protected function getMethodTitle($class, $method)
{
$ref = new ReflectionMethod($class, $method . 'Action');
$comment = new CommentParser($ref->getDocComment());
return $comment->getTitle();
}
protected function getMethodDocumentation($class, $method)
{
$ref = new ReflectionMethod($class, $method . 'Action');
$comment = new CommentParser($ref->getDocComment());
return $comment->dump();
}
}

View File

@ -0,0 +1,77 @@
<?php
namespace Icinga\Cli\Documentation;
class CommentParser
{
protected $raw;
protected $plain;
protected $title;
protected $paragraphs = array();
public function __construct($raw)
{
$this->raw = $raw;
if ($raw) {
$this->parse();
}
}
public function getTitle()
{
return $this->title;
}
protected function parse()
{
$plain = $this->raw;
// Strip comment start /**
$plain = preg_replace('~^/\s*\*\*\n~s', '', $plain);
// Strip comment end */
$plain = preg_replace('~\n\s*\*/\s*~s', "\n", $plain);
$p = null;
foreach (preg_split('~\n~', $plain) as $line) {
// Strip * at line start
$line = preg_replace('~^\s*\*\s?~', '', $line);
$line = rtrim($line);
if ($this->title === null) {
$this->title = $line;
continue;
}
if ($p === null && empty($this->paragraphs)) {
$p = & $this->paragraphs[];
}
if ($line === '') {
if ($p !== null) {
$p = & $this->paragraphs[];
}
continue;
}
if ($p === null) {
$p = $line;
} else {
if (substr($line, 0, 2) === ' ') {
$p .= "\n" . $line;
} else {
$p .= ' ' . $line;
}
}
}
if ($p === null) {
array_pop($this->paragraphs);
}
}
public function dump()
{
$res = $this->title . "\n" . str_repeat('=', strlen($this->title)) . "\n\n";
foreach ($this->paragraphs as $p) {
$res .= wordwrap($p, 72) . "\n\n";
}
return $res;
}
}

View File

@ -0,0 +1,399 @@
<?php
namespace Icinga\Cli;
use Icinga\Application\ApplicationBootstrap as App;
use Icinga\Exception\ProgrammingError;
use Icinga\Cli\Params;
use Icinga\Cli\Screen;
use Icinga\Cli\Documentation;
use Exception;
/**
*
*/
class Loader
{
protected $app;
protected $docs;
protected $commands;
protected $modules;
protected $moduleCommands = array();
protected $coreAppDir;
protected $screen;
protected $moduleName;
protected $commandName;
protected $actionName; // Should this better be moved to the Command?
/**
* [$command] = $class;
*/
protected $commandClassMap = array();
/**
* [$command] = $file;
*/
protected $commandFileMap = array();
/**
* [$module][$command] = $class;
*/
protected $moduleClassMap = array();
/**
* [$module][$command] = $file;
*/
protected $moduleFileMap = array();
protected $commandInstances = array();
protected $moduleInstances = array();
protected $lastSuggestions = array();
public function __construct(App $app)
{
$this->app = $app;
$this->coreAppDir = ICINGA_APPDIR . '/clicommands';
}
/**
* Screen shortcut
*
* @return Screen
*/
protected function screen()
{
if ($this->screen === null) {
$this->screen = Screen::instance();
}
return $this->screen;
}
/**
* Documentation shortcut
*
* @return Documentation
*/
protected function docs()
{
if ($this->docs === null) {
$this->docs = new Documentation($this->app);
}
return $this->docs;
}
/**
* Show given message and exit
*
* @param string $msg message to show
*/
public function fail($msg)
{
printf("%s: %s\n", $this->screen()->colorize('ERROR', 'red'), $msg);
exit(1);
}
public function getCommandInstance($command)
{
if (! array_key_exists($command, $this->commandInstances)) {
$this->assertCommandExists($command);
require_once $this->commandFileMap[$command];
$className = $this->commandClassMap[$command];
$this->commandInstances[$command] = new $className(
$this->app,
null,
$command,
null,
false
);
}
return $this->commandInstances[$command];
}
public function getModuleCommandInstance($module, $command)
{
if (! array_key_exists($command, $this->moduleInstances[$module])) {
$this->assertModuleCommandExists($module, $command);
require_once $this->moduleFileMap[$module][$command];
$className = $this->moduleClassMap[$module][$command];
$this->moduleInstances[$module][$command] = new $className(
$this->app,
$module,
$command,
null,
false
);
}
return $this->moduleInstances[$module][$command];
}
public function showLastSuggestions()
{
if (! empty($this->lastSuggestions)) {
foreach ($this->lastSuggestions as & $s) {
$s = $this->screen()->colorize($s, 'lightblue');
}
printf(
"Did you mean %s?\n",
implode(" or ", $this->lastSuggestions)
);
}
}
public function parseParams(Params $params = null)
{
if ($params === null) {
$params = $this->app->getParams();
}
$first = $params->shift();
if (! $first) {
return;
}
$found = $this->resolveName($first);
if (! $found) {
$msg = "There is no such module or command: '$first'";
printf("%s: %s\n", $this->screen()->colorize('ERROR', 'red'), $msg);
$this->showLastSuggestions();
echo "\n";
}
$obj = null;
if ($this->hasCommand($found)) {
$this->commandName = $found;
$obj = $this->getCommandInstance($this->commandName);
} elseif ($this->hasModule($found)) {
$this->moduleName = $found;
$command = $this->resolveModuleCommandName($found, $params->shift());
if ($command) {
$this->commandName = $command;
$obj = $this->getModuleCommandInstance(
$this->moduleName,
$this->commandName
);
}
}
if ($obj !== null) {
$action = $this->resolveObjectActionName(
$obj,
$params->getStandalone()
);
if ($obj->hasActionName($action)) {
$this->actionName = $action;
$params->shift();
} elseif ($obj->hasDefaultActionName()) {
$this->actionName = $obj->getDefaultActionName();
}
}
return $this;
}
public function handleParams(Params $params = null)
{
$this->parseParams($params);
$this->dispatch();
}
public function dispatch()
{
if ($this->commandName === null) {
echo $this->docs()->usage($this->moduleName);
return false;
} elseif ($this->actionName === null) {
echo $this->docs()->usage($this->moduleName, $this->commandName);
return false;
}
try {
if ($this->moduleName) {
$this->app->getModuleManager()->loadModule($this->moduleName);
$obj = $this->getModuleCommandInstance(
$this->moduleName,
$this->commandName
);
} else {
$obj = $this->getCommandInstance($this->commandName);
}
$obj->init();
return $obj->{$this->actionName . 'Action'}();
} catch (Exception $e) {
$this->fail($e->getMessage());
}
}
protected function searchMatch($needle, $haystack)
{
$stack = $haystack;
$search = $needle;
$this->lastSuggestions = array();
while (strlen($search) > 0) {
$len = strlen($search);
foreach ($stack as & $s) {
$s = substr($s, 0, $len);
}
$res = array_keys($stack, $search, true);
if (count($res) === 1) {
$found = $haystack[$res[0]];
if (substr($found, 0, strlen($needle)) === $needle) {
return $found;
} else {
return false;
}
} elseif (count($res) > 1) {
foreach ($res as $key) {
$this->lastSuggestions[] = $haystack[$key];
}
return false;
}
$search = substr($search, 0, -1);
}
return false;
}
public function resolveName($name)
{
return $this->searchMatch(
$name,
array_merge($this->listCommands(), $this->listModules())
);
}
public function resolveCommandName($name)
{
return $this->searchMatch($name, $this->listCommands());
}
public function resolveModuleName($name)
{
return $this->searchMatch($name, $this->listModules());
}
public function resolveModuleCommandName($module, $name)
{
return $this->searchMatch($name, $this->listModuleCommands($module));
}
public function resolveObjectActionName($obj, $name)
{
return $this->searchMatch($name, $obj->listActions());
}
protected function assertModuleExists($module)
{
if (! $this->hasModule($module)) {
throw new ProgrammingError(
sprintf('There is no such module: %s', $module)
);
}
}
protected function assertCommandExists($command)
{
if (! $this->hasCommand($command)) {
throw new ProgrammingError(
sprintf('There is no such command: %s', $command)
);
}
}
protected function assertModuleCommandExists($module, $command)
{
$this->assertModuleExists($module);
if (! $this->hasModuleCommand($module, $command)) {
throw new ProgrammingError(
sprintf("The module '%s' has no such command: %s", $module, $command)
);
}
}
public function hasCommand($name)
{
return in_array($name, $this->listCommands());
}
public function hasModule($name)
{
return in_array($name, $this->listModules());
}
public function hasModuleCommand($module, $name)
{
return in_array($name, $this->listModuleCommands($module));
}
public function listModules()
{
if ($this->modules === null) {
$this->modules = array();
$this->modules = $this->app->getModuleManager()->listEnabledModules();
sort($this->modules);
}
return $this->modules;
}
protected function retrieveCommandsFromDir($dirname)
{
$commands = array();
if (! @file_exists($dirname) || ! is_readable($dirname)) {
return $commands;
}
$base = opendir($dirname);
if ($base === false) {
return $commands;
}
while (false !== ($dir = readdir($base))) {
if ($dir[0] === '.') {
continue;
}
if (preg_match('~^([A-Za-z0-9]+)Command\.php$~', $dir, $m)) {
$cmd = strtolower($m[1]);
$commands[] = $cmd;
}
}
sort($commands);
return $commands;
}
public function listCommands()
{
if ($this->commands === null) {
$this->commands = array();
$ns = 'Icinga\\Clicommands\\';
$this->commands = $this->retrieveCommandsFromDir($this->coreAppDir);
foreach ($this->commands as $cmd) {
$this->commandClassMap[$cmd] = $ns . ucfirst($cmd) . 'Command';
$this->commandFileMap[$cmd] = $this->coreAppDir . '/' . ucfirst($cmd) . 'Command.php';
}
}
return $this->commands;
}
public function listModuleCommands($module)
{
if (! array_key_exists($module, $this->moduleCommands)) {
$ns = 'Icinga\\Module\\' . ucfirst($module) . '\\Clicommands\\';
$this->assertModuleExists($module);
$manager = $this->app->getModuleManager();
$manager->enableModule($module);
$dir = $manager->getModuleDir($module) . '/application/clicommands';
$this->moduleCommands[$module] = $this->retrieveCommandsFromDir($dir);
$this->moduleInstances[$module] = array();
foreach ($this->moduleCommands[$module] as $cmd) {
$this->moduleClassMap[$module][$cmd] = $ns . ucfirst($cmd) . 'Command';
$this->moduleFileMap[$module][$cmd] = $dir . '/' . ucfirst($cmd) . 'Command.php';
}
}
return $this->moduleCommands[$module];
}
}

View File

@ -0,0 +1,116 @@
<?php
namespace Icinga\Cli;
class Params
{
protected $program;
protected $standalone = array();
protected $params = array();
public function __construct($argv)
{
$this->program = array_shift($argv);
for ($i = 0; $i < count($argv); $i++) {
if (substr($argv[$i], 0, 2) === '--') {
$key = substr($argv[$i], 2);
if (! isset($argv[$i + 1]) || substr($argv[$i + 1], 0, 2) === '--') {
$this->params[$key] = true;
} else {
$this->params[$key] = $argv[++$i];
}
} else {
$this->standalone[] = $argv[$i];
}
}
}
public function getStandalone($pos = 0, $default = null)
{
if (isset($this->standalone[$pos])) {
return $this->standalone[$pos];
}
return $default;
}
public function count()
{
return count($this->standalone) + count($this->params);
}
public function getParams()
{
return $this->params;
}
public function __get($key)
{
return $this->get($key);
}
public function has($key)
{
return array_key_exists($key, $this->params);
}
public function get($key, $default = null)
{
if ($this->has($key)) {
return $this->params[$key];
}
return $default;
}
public function set($key, $value)
{
$this->params[$key] = $value;
return $this;
}
public function remove($keys = array())
{
if (! is_array($keys)) {
$keys = array($keys);
}
foreach ($keys as $key) {
if (array_key_exists($key, $this->params)) {
unset($this->params[$key]);
}
}
return $this;
}
public function without($keys = array())
{
$params = clone($this);
return $params->remove($keys);
}
public function shift($key = null, $default = null)
{
if ($key === null) {
if (count($this->standalone) > 0) {
return array_shift($this->standalone);
}
return $default;
}
$result = $this->get($key, $default);
$this->remove($key);
return $result;
}
public function unshift($key)
{
array_unshift($this->standalone, $key);
return $this;
}
public static function parse($argv = null)
{
if ($argv === null) {
$argv = $GLOBALS['argv'];
}
$params = new self($argv);
return $params;
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Icinga\Cli;
use Icinga\Cli\Screen\AnsiScreen;
class Screen
{
protected static $instance;
public function instance()
{
if (self::$instance === null) {
self::$instance = new AnsiScreen();
}
return self::$instance;
}
}

View File

@ -0,0 +1,158 @@
<?php
namespace Icinga\Cli\Screen;
use Icinga\Cli\Screen;
// @see http://en.wikipedia.org/wiki/ANSI_escape_code
class AnsiScreen extends Screen
{
protected $isUtf8;
protected $fgColors = array(
'black' => '30',
'darkgray' => '1;30',
'red' => '31',
'lightred' => '1;31',
'green' => '32',
'lightgreen' => '1;32',
'brown' => '33',
'yellow' => '1;33',
'blue' => '34',
'lightblue' => '1;34',
'purple' => '35',
'lightpurple' => '1;35',
'cyan' => '36',
'lightcyan' => '1;36',
'lightgray' => '37',
'white' => '1;37',
);
protected $bgColors = array(
'black' => '40',
'red' => '41',
'green' => '42',
'brown' => '43',
'blue' => '44',
'purple' => '45',
'cyan' => '46',
'lightgray' => '47',
);
public function __construct()
{
}
public function getColumns()
{
$cols = (int) getenv('COLUMNS');
if (! $cols) {
// stty -a ?
$cols = (int) exec('tput cols');
}
if (! $cols) {
$cols = 80;
}
return $cols;
}
public function getRows()
{
$rows = (int) getenv('ROWS');
if (! $rows) {
// stty -a ?
$rows = (int) exec('tput rows');
}
if (! $rows) {
$rows = 25;
}
return $rows;
}
public function hasUtf8()
{
if ($this->isUtf8 === null) {
// null should equal 0 here, however seems to equal '' on some systems:
$current = setlocale(LC_ALL, 0);
$parts = preg_split('/;/', $current);
$lc_parts = array();
foreach ($parts as $part) {
if (strpos($part, '=') === false) {
continue;
}
list($key, $val) = preg_split('/=/', $part, 2);
$lc_parts[$key] = $val;
}
$this->isUtf8 = array_key_exists('LC_CTYPE', $lc_parts)
&& preg_match('~\.UTF-8$~i', $lc_parts['LC_CTYPE']);
}
return $this->isUtf8;
}
public function clear()
{
return "\033[2J" // Clear the whole screen
. "\033[1;1H" // Move the cursor to row 1, column 1
. "\033[1S"; // Scroll whole page up by 1 line (why?)
}
protected function fgColor($color)
{
if (! array_key_exists($color, $this->fgColors)) {
throw new \Exception(sprintf('There is no such foreground color: %s', $color));
}
return $this->fgColors[$color];
}
protected function bgColor($color)
{
if (! array_key_exists($color, $this->bgColors)) {
throw new \Exception(sprintf('There is no such background color: %s', $color));
}
return $this->bgColors[$color];
}
protected function startColor($fgColor = null, $bgColor = null)
{
$escape = "ESC[";
$parts = array();
if ($fgColor !== null
&& $bgColor !== null
&& ! array_key_exists($bgColor, $this->bgColors)
&& array_key_exists($bgColor, $this->fgColors)
&& array_key_exists($fgColor, $this->bgColors)
) {
$parts[] = '7'; // reverse video, negative image
$parts[] = $this->bgColor($fgColor);
$parts[] = $this->fgColor($bgColor);
} else {
if ($fgColor !== null) {
$parts[] = $this->fgColor($fgColor);
}
if ($bgColor !== null) {
$parts[] = $this->bgColor($bgColor);
}
}
if (empty($parts)) {
return '';
}
return "\033[" . implode(';', $parts) . 'm';
}
public function underline($text)
{
return "\033[4m"
. $text
. "\033[0m"; // Reset color codes
}
public function colorize($text, $fgColor = null, $bgColor = null)
{
return $this->startColor($fgColor, $bgColor)
. $text
. "\033[0m"; // Reset color codes
}
}

View File

@ -0,0 +1,280 @@
<?php
namespace Icinga\Module\Monitoring\Clicommands;
use Icinga\Module\Monitoring\Backend;
use Icinga\Module\Monitoring\Cli\CliUtils;
use Icinga\Util\Format;
use Icinga\Cli\Command;
use Icinga\File\Csv;
/**
* List and filter monitored objects
*
* This command allows you to search and visualize your monitored objects in
* different ways.
*
* USAGE
*
* icingaweb monitoring list [<type>] [options]
*
* OPTIONS
*
* --verbose Show detailled output
* --showsql Dump generated SQL query (DB backend only)
*
* --format <csv|json|<custom>>
* Dump columns in the given format. <custom> format allows $column$
* placeholders, e.g. --format '$host$: $service$'
*
* --<column> [filter]
* Filter given column by optional filter. Boolean (1/0) columns are true
* if no filter value is given.
*
* EXAMPLES
*
* icingaweb monitoring list --unhandled
* icingaweb monitoring list --host local* --service *disk*
* icingaweb monitoring list --format '$host_name$: $service_description$'
*/
class ListCommand extends Command
{
protected $backend;
protected $dumpSql;
protected $defaultActionName = 'status';
public function init()
{
$this->backend = Backend::createBackend($this->params->shift('backend'));
$this->dumpSql = $this->params->shift('showsql');
}
protected function getQuery($table, $columns)
{
$limit = $this->params->shift('limit');
$format = $this->params->shift('format');
if ($format !== null) {
if ($this->params->has('columns')) {
$columnParams = preg_split(
'/,/',
$this->params->shift('columns')
);
$columns = array();
foreach ($columnParams as $col) {
if (false !== ($pos = strpos($col, '='))) {
$columns[substr($col, 0, $pos)] = substr($col, $pos + 1);
} else {
$columns[] = $col;
}
}
}
}
$query = $this->backend->select()->from($table, $columns);
if ($limit) {
$query->limit($limit, $this->params->shift('offset'));
}
foreach ($this->params->getParams() as $col => $filter) {
$query->where($col, $filter);
}
// $query->applyFilters($this->params->getParams());
if ($this->dumpSql) {
echo wordwrap($query->dump(), 72);
exit;
}
if ($format !== null) {
$this->showFormatted($query, $format, $columns);
}
return $query;
}
protected function showFormatted($query, $format, $columns)
{
switch($format) {
case 'json':
echo json_encode($query->fetchAll());
break;
case 'csv':
Csv::fromQuery($query)->dump();
break;
default:
preg_match_all('~\$([a-z0-9_-]+)\$~', $format, $m);
$words = array();
foreach ($columns as $key => $col) {
if (is_numeric($key)) {
if (in_array($col, $m[1])) {
$words[] = $col;
}
} else {
if (in_array($key, $m[1])) {
$words[] = $key;
}
}
}
foreach ($query->fetchAll() as $row) {
$output = $format;
foreach ($words as $word) {
$output = preg_replace(
'~\$' . $word . '\$~',
$row->{$word},
$output
);
}
echo $output . "\n";
}
}
exit;
}
public function statusAction()
{
$columns = array(
'host_name',
'host_state',
'host_output',
'host_handled',
'host_acknowledged',
'host_in_downtime',
'service_description',
'service_state',
'service_acknowledged',
'service_in_downtime',
'service_handled',
'service_output',
'service_last_state_change'
);
$query = $this->getQuery('status', $columns)
->order('host_name');
echo $this->renderQuery($query);
}
protected function renderQuery($query)
{
$out = '';
$last_host = null;
$screen = $this->screen;
$utils = new CliUtils($screen);
$maxCols = $screen->getColumns();
$rows = $query->fetchAll();
$count = $query->count();
$count = count($rows);
for ($i = 0; $i < $count; $i++) {
$row = & $rows[$i];
$utils->setHostState($row->host_state);
if (! array_key_exists($i + 1, $rows)
|| $row->host_name !== $rows[$i + 1]->host_name
) {
$lastService = true;
} else {
$lastService = false;
}
$hostUnhandled = ! ($row->host_state == 0 || $row->host_handled);
if ($row->host_name !== $last_host) {
if (isset($row->service_description)) {
$out .= "\n";
}
$hostTxt = $utils->shortHostState();
if ($hostUnhandled) {
$out .= $utils->hostStateBackground(
sprintf(' %s ', $utils->shortHostState())
);
} else {
$out .= sprintf(
'%s %s ',
$utils->hostStateBackground(' '),
$utils->shortHostState()
);
}
$out .= sprintf(
" %s%s: %s\n",
$screen->underline($row->host_name),
$screen->colorize($utils->objectStateFlags('host', $row), 'lightblue'),
$row->host_output
);
if (isset($row->services_ok)) {
$out .= sprintf(
"%d services, %d problems (%d unhandled), %d OK\n",
$row->services_cnt,
$row->services_problem,
$row->services_problem_unhandled,
$row->services_ok
);
}
}
$last_host = $row->host_name;
if (! isset($row->service_description)) {
continue;
}
$utils->setServiceState($row->service_state);
$serviceUnhandled = ! (
$row->service_state == 0 || $row->service_handled
);
if ($lastService) {
$straight = ' ';
$leaf = '└';
} else {
$straight = '│';
$leaf = '├';
}
$out .= $utils->hostStateBackground(' ');
if ($serviceUnhandled) {
$out .= $utils->serviceStateBackground(
sprintf(' %s ', $utils->shortServiceState())
);
$emptyBg = ' ';
$emptySpace = '';
} else {
$out .= sprintf(
'%s %s ',
$utils->serviceStateBackground(' '),
$utils->shortServiceState()
);
$emptyBg = ' ';
$emptySpace = ' ';
}
$emptyLine = "\n"
. $utils->hostStateBackground(' ')
. $utils->serviceStateBackground($emptyBg)
. $emptySpace
. ' ' . $straight . ' ';
$wrappedOutput = wordwrap(
preg_replace('~\@{3,}~', '@@@', $row->service_output),
$maxCols - 13
) . "\n";
$out .= sprintf(
" %1s─ %s%s (since %s)",
$leaf,
$screen->underline($row->service_description),
$screen->colorize($utils->objectStateFlags('service', $row), 'lightblue'),
Format::timeSince($row->service_last_state_change)
);
if ($this->isVerbose) {
$out .= $emptyLine . preg_replace(
'/\n/',
$emptyLine,
$wrappedOutput
) . "\n";
} else {
$out .= "\n";
}
}
$out .= "\n";
return $out;
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace Icinga\Module\Monitoring\Clicommands;
use Icinga\Protocol\Nrpe\Connection;
use Icinga\Cli\Command;
use Exception;
/**
* NRPE
*/
class NrpeCommand extends Command
{
protected $defaultActionName = 'check';
/**
* Execute an NRPE command
*
* This command will execute an NRPE check, fire it against the given host
* and also pass through all your parameters. Output will be shown, exit
* code respected.
*
* USAGE
*
* icingaweb monitoring nrpe <host> <command> [--ssl] [nrpe options]
*
* EXAMPLE
*
* icingaweb monitoring nrpe 127.0.0.1 CheckMEM --ssl --MaxWarn 80% \
* --MaxCrit 90% --type physical
*/
public function checkAction()
{
$host = $this->params->shift();
if (! $host) {
echo $this->showUsage();
exit(3);
}
$command = $this->params->shift(null, '_NRPE_CHECK');
$port = $this->params->shift('port', 5666);
try {
$nrpe = new Connection($host, $port);
if ($this->params->shift('ssl')) {
$nrpe->useSsl();
}
$args = array();
foreach ($this->params->getParams() as $k => $v) {
$args[] = $k . '=' . $v;
}
echo $nrpe->sendCommand($command, $args) . "\n";
exit($nrpe->getLastReturnCode());
} catch (Exception $e) {
echo $e->getMessage() . "\n";
exit(3);
}
}
}

View File

@ -0,0 +1,122 @@
<?php
namespace Icinga\Module\Monitoring\Cli;
use Icinga\Cli\Screen;
class CliUtils
{
protected $hostColors = array(
0 => array('black', 'lightgreen'),
1 => array('black', 'lightred'),
2 => array('black', 'brown'),
99 => array('black', 'lightgray'),
);
protected $serviceColors = array(
0 => array('black', 'lightgreen'),
1 => array('black', 'yellow'),
2 => array('black', 'lightred'),
3 => array('black', 'lightpurple'),
99 => array('black', 'lightgray'),
);
protected $hostStates = array(
0 => 'UP',
1 => 'DOWN',
2 => 'UNREACHABLE',
99 => 'PENDING',
);
protected $serviceStates = array(
0 => 'OK',
1 => 'WARNING',
2 => 'CRITICAL',
3 => 'UNKNOWN',
99 => 'PENDING',
);
protected $screen;
protected $hostState;
protected $serviceState;
public function __construct(Screen $screen)
{
$this->screen = $screen;
}
public function setHostState($state)
{
$this->hostState = $state;
}
public function setServiceState($state)
{
$this->serviceState = $state;
}
public function shortHostState($state = null)
{
if ($state === null) {
$state = $this->hostState;
}
return sprintf('%-4s', substr($this->hostStates[$state], 0, 4));
}
public function shortServiceState($state = null)
{
if ($state === null) {
$state = $this->serviceState;
}
return sprintf('%-4s', substr($this->serviceStates[$state], 0, 4));
}
public function hostStateBackground($text, $state = null)
{
if ($state === null) {
$state = $this->hostState;
}
return $this->screen->colorize(
$text,
$this->hostColors[$state][0],
$this->hostColors[$state][1]
);
}
public function serviceStateBackground($text, $state = null)
{
if ($state === null) {
$state = $this->serviceState;
}
return $this->screen->colorize(
$text,
$this->serviceColors[$state][0],
$this->serviceColors[$state][1]
);
}
public function objectStateFlags($type, & $row)
{
$extra = array();
if ($row->{$type . '_in_downtime'}) {
if ($this->screen->hasUtf8()) {
$extra[] = 'DOWNTIME ⌚';
} else {
$extra[] = 'DOWNTIME';
}
}
if ($row->{$type . '_acknowledged'}) {
if ($this->screen->hasUtf8()) {
$extra[] = 'ACK ✓';
} else {
$extra[] = 'ACK';
}
}
if (empty($extra)) {
$extra = '';
} else {
$extra = sprintf(' [ %s ]', implode(', ', $extra));
}
return $extra;
}
}