From a25cd80ec010e30b4ce9b7b0802f8928592f2e27 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Tue, 22 Oct 2013 12:26:45 +0000 Subject: [PATCH] CLI interface: initial import --- application/clicommands/HelpCommand.php | 60 +++ application/clicommands/ModuleCommand.php | 165 ++++++++ .../clicommands/TranslationCommand.php | 43 ++ bin/icingaweb | 15 + library/Icinga/Application/Cli.php | 161 +++++-- library/Icinga/Cli/Command.php | 102 +++++ library/Icinga/Cli/Documentation.php | 140 ++++++ .../Cli/Documentation/CommentParser.php | 77 ++++ library/Icinga/Cli/Loader.php | 399 ++++++++++++++++++ library/Icinga/Cli/Params.php | 116 +++++ library/Icinga/Cli/Screen.php | 18 + library/Icinga/Cli/Screen/AnsiScreen.php | 158 +++++++ .../application/clicommands/ListCommand.php | 280 ++++++++++++ .../application/clicommands/NrpeCommand.php | 58 +++ .../library/Monitoring/Cli/CliUtils.php | 122 ++++++ 15 files changed, 1879 insertions(+), 35 deletions(-) create mode 100644 application/clicommands/HelpCommand.php create mode 100644 application/clicommands/ModuleCommand.php create mode 100644 application/clicommands/TranslationCommand.php create mode 100755 bin/icingaweb create mode 100644 library/Icinga/Cli/Command.php create mode 100644 library/Icinga/Cli/Documentation.php create mode 100644 library/Icinga/Cli/Documentation/CommentParser.php create mode 100644 library/Icinga/Cli/Loader.php create mode 100644 library/Icinga/Cli/Params.php create mode 100644 library/Icinga/Cli/Screen.php create mode 100644 library/Icinga/Cli/Screen/AnsiScreen.php create mode 100644 modules/monitoring/application/clicommands/ListCommand.php create mode 100644 modules/monitoring/application/clicommands/NrpeCommand.php create mode 100644 modules/monitoring/library/Monitoring/Cli/CliUtils.php diff --git a/application/clicommands/HelpCommand.php b/application/clicommands/HelpCommand.php new file mode 100644 index 000000000..1701df8ba --- /dev/null +++ b/application/clicommands/HelpCommand.php @@ -0,0 +1,60 @@ +] [ []] + */ +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 [] [ []] + */ + 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); + } +} diff --git a/application/clicommands/ModuleCommand.php b/application/clicommands/ModuleCommand.php new file mode 100644 index 000000000..3dd9ab530 --- /dev/null +++ b/application/clicommands/ModuleCommand.php @@ -0,0 +1,165 @@ +] [] + */ +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 + */ + 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 + */ + 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 + */ + 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 + * icingaweb module install + */ + 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 + */ + 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 + */ + public function purgeAction() + { + $this->fail("Not implemented yet"); + } +} diff --git a/application/clicommands/TranslationCommand.php b/application/clicommands/TranslationCommand.php new file mode 100644 index 000000000..be56df543 --- /dev/null +++ b/application/clicommands/TranslationCommand.php @@ -0,0 +1,43 @@ +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 --locale + */ + public function refreshAction() + { + $this->translator->createTemporaryFileList()->extractTexts(); + } +} diff --git a/bin/icingaweb b/bin/icingaweb new file mode 100755 index 000000000..e0f5dad67 --- /dev/null +++ b/bin/icingaweb @@ -0,0 +1,15 @@ +#!/usr/bin/php +dispatch(); + diff --git a/library/Icinga/Application/Cli.php b/library/Icinga/Application/Cli.php index 72ae07850..ed8674c4c 100644 --- a/library/Icinga/Application/Cli.php +++ b/library/Icinga/Application/Cli.php @@ -1,67 +1,158 @@ - * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 - * @author Icinga Development Team - */ // {{{ICINGA_LICENSE_HEADER}}} 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 -require_once dirname(__FILE__). '/ApplicationBootstrap.php'; +require_once dirname(__FILE__) . '/ApplicationBootstrap.php'; require_once dirname(__FILE__). '/../Exception/ProgrammingError.php'; // @codingStandardsIgnoreStop -use Icinga\Exception\ProgrammingError; - -/** - * Bootstrapping on cli environment - */ class Cli extends ApplicationBootstrap { protected $isCli = true; + protected $params; + + protected $showBenchmark = false; + + protected $watchTimeout; + + protected $cliLoader; + protected function bootstrap() { $this->assertRunningOnCli(); - return $this->setupConfig() - ->setupErrorHandling() - ->setupTimezone(); + ->setupErrorHandling() + ->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 * - * @throws \Exception + * @throws ProgrammingError + * @return void */ private function assertRunningOnCli() { if (Platform::isCli()) { return; } - throw new ProgrammingError('Icinga is not running on CLI'); } } diff --git a/library/Icinga/Cli/Command.php b/library/Icinga/Cli/Command.php new file mode 100644 index 000000000..3553305cb --- /dev/null +++ b/library/Icinga/Cli/Command.php @@ -0,0 +1,102 @@ +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() + { + } +} + diff --git a/library/Icinga/Cli/Documentation.php b/library/Icinga/Cli/Documentation.php new file mode 100644 index 000000000..858cdfeee --- /dev/null +++ b/library/Icinga/Cli/Documentation.php @@ -0,0 +1,140 @@ +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] [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 seconds (default: 5)\n" + ; + $d .= "\nShow help on a specific command : icingaweb help " + . "\nShow help on a specific module : icingaweb help " + . "\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 [] [options]\n\n" + . "Available commands:\n\n"; + foreach ($commands as $command) { + $d .= ' ' . $command . "\n"; + } + $d .= "\nShow help on a specific command: icingaweb help $module \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 \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(); + } +} diff --git a/library/Icinga/Cli/Documentation/CommentParser.php b/library/Icinga/Cli/Documentation/CommentParser.php new file mode 100644 index 000000000..8b88dfcd9 --- /dev/null +++ b/library/Icinga/Cli/Documentation/CommentParser.php @@ -0,0 +1,77 @@ +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; + } +} diff --git a/library/Icinga/Cli/Loader.php b/library/Icinga/Cli/Loader.php new file mode 100644 index 000000000..d87e7eb56 --- /dev/null +++ b/library/Icinga/Cli/Loader.php @@ -0,0 +1,399 @@ +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]; + } +} diff --git a/library/Icinga/Cli/Params.php b/library/Icinga/Cli/Params.php new file mode 100644 index 000000000..1fc47a150 --- /dev/null +++ b/library/Icinga/Cli/Params.php @@ -0,0 +1,116 @@ +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; + } +} diff --git a/library/Icinga/Cli/Screen.php b/library/Icinga/Cli/Screen.php new file mode 100644 index 000000000..9b0f5afd0 --- /dev/null +++ b/library/Icinga/Cli/Screen.php @@ -0,0 +1,18 @@ + '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 + } +} diff --git a/modules/monitoring/application/clicommands/ListCommand.php b/modules/monitoring/application/clicommands/ListCommand.php new file mode 100644 index 000000000..5bba2211b --- /dev/null +++ b/modules/monitoring/application/clicommands/ListCommand.php @@ -0,0 +1,280 @@ +] [options] + * + * OPTIONS + * + * --verbose Show detailled output + * --showsql Dump generated SQL query (DB backend only) + * + * --format > + * Dump columns in the given format. format allows $column$ + * placeholders, e.g. --format '$host$: $service$' + * + * -- [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; + } +} + diff --git a/modules/monitoring/application/clicommands/NrpeCommand.php b/modules/monitoring/application/clicommands/NrpeCommand.php new file mode 100644 index 000000000..0d49c37a2 --- /dev/null +++ b/modules/monitoring/application/clicommands/NrpeCommand.php @@ -0,0 +1,58 @@ + [--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); + } + } +} + diff --git a/modules/monitoring/library/Monitoring/Cli/CliUtils.php b/modules/monitoring/library/Monitoring/Cli/CliUtils.php new file mode 100644 index 000000000..fd894aeb9 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Cli/CliUtils.php @@ -0,0 +1,122 @@ + 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; + } + +}