diff options
Diffstat (limited to 'library/Icinga/Application/Modules/Manager.php')
-rw-r--r-- | library/Icinga/Application/Modules/Manager.php | 698 |
1 files changed, 698 insertions, 0 deletions
diff --git a/library/Icinga/Application/Modules/Manager.php b/library/Icinga/Application/Modules/Manager.php new file mode 100644 index 0000000..55d074d --- /dev/null +++ b/library/Icinga/Application/Modules/Manager.php @@ -0,0 +1,698 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Application\Modules; + +use Icinga\Application\ApplicationBootstrap; +use Icinga\Application\Icinga; +use Icinga\Application\Logger; +use Icinga\Data\DataArray\ArrayDatasource; +use Icinga\Data\SimpleQuery; +use Icinga\Exception\ConfigurationError; +use Icinga\Exception\SystemPermissionException; +use Icinga\Exception\ProgrammingError; +use Icinga\Exception\NotReadableError; + +/** + * Module manager that handles detecting, enabling and disabling of modules + * + * Modules can have 3 states: + * * installed, module exists but is disabled + * * enabled, module enabled and should be loaded + * * loaded, module enabled and loaded via the autoloader + * + */ +class Manager +{ + /** + * Namespace for module permissions + * + * @var string + */ + const MODULE_PERMISSION_NS = 'module/'; + + /** + * Array of all installed module's base directories + * + * @var array + */ + private $installedBaseDirs = array(); + + /** + * Array of all enabled modules base dirs + * + * @var array + */ + private $enabledDirs = array(); + + /** + * Array of all module names that have been loaded + * + * @var array + */ + private $loadedModules = array(); + + /** + * Reference to Icinga::app + * + * @var Icinga + */ + private $app; + + /** + * The directory that is used to detect enabled modules + * + * @var string + */ + private $enableDir; + + /** + * All paths to look for installed modules that can be enabled + * + * @var array + */ + private $modulePaths = array(); + + /** + * Whether we loaded all enabled modules + * + * @var bool + */ + private $loadedAllEnabledModules = false; + + /** + * Create a new instance of the module manager + * + * @param ApplicationBootstrap $app + * @param string $enabledDir Enabled modules location. The application maintains symlinks within + * the given path + * @param array $availableDirs Installed modules location + **/ + public function __construct($app, $enabledDir, array $availableDirs) + { + $this->app = $app; + $this->modulePaths = $availableDirs; + $this->enableDir = $enabledDir; + } + + /** + * Query interface for the module manager + * + * @return SimpleQuery + */ + public function select() + { + $source = new ArrayDatasource($this->getModuleInfo()); + return $source->select(); + } + + /** + * Check for enabled modules + * + * Update the internal $enabledDirs property with the enabled modules. + * + * @throws ConfigurationError If module dir does not exist, is not a directory or not readable + */ + private function detectEnabledModules() + { + if (! file_exists($parent = dirname($this->enableDir))) { + return; + } + if (! is_readable($parent)) { + throw new NotReadableError( + 'Cannot read enabled modules. Config directory "%s" is not readable', + $parent + ); + } + + if (! file_exists($this->enableDir)) { + return; + } + if (! is_dir($this->enableDir)) { + throw new NotReadableError( + 'Cannot read enabled modules. Module directory "%s" is not a directory', + $this->enableDir + ); + } + if (! is_readable($this->enableDir)) { + throw new NotReadableError( + 'Cannot read enabled modules. Module directory "%s" is not readable', + $this->enableDir + ); + } + if (($dh = opendir($this->enableDir)) !== false) { + $isPhar = substr($this->enableDir, 0, 8) === 'phar:///'; + $this->enabledDirs = array(); + while (($file = readdir($dh)) !== false) { + if ($file[0] === '.' || $file === 'README') { + continue; + } + + $link = $this->enableDir . DIRECTORY_SEPARATOR . $file; + if (! $isPhar && ! is_link($link)) { + Logger::warning( + 'Found invalid module in enabledModule directory "%s": "%s" is not a symlink', + $this->enableDir, + $link + ); + continue; + } + + $dir = $isPhar ? $link : realpath($link); + if ($dir !== false && is_dir($dir)) { + $this->enabledDirs[$file] = $dir; + } else { + $this->enabledDirs[$file] = null; + + Logger::warning( + 'Found invalid module in enabledModule directory "%s": "%s" points to non existing path "%s"', + $this->enableDir, + $link, + $dir + ); + } + + ksort($this->enabledDirs); + } + closedir($dh); + } + } + + /** + * Try to set all enabled modules in loaded sate + * + * @return $this + * @see Manager::loadModule() + */ + public function loadEnabledModules() + { + if (! $this->loadedAllEnabledModules) { + foreach ($this->listEnabledModules() as $name) { + $this->loadModule($name); + } + + $this->loadedAllEnabledModules = true; + } + + return $this; + } + + /** + * Whether we loaded all enabled modules + * + * @return bool + */ + public function loadedAllEnabledModules() + { + return $this->loadedAllEnabledModules; + } + + + /** + * Try to load the module and register it in the application + * + * @param string $name The name of the module to load + * @param mixed $basedir Optional module base directory + * + * @return $this + */ + public function loadModule($name, $basedir = null) + { + if ($this->hasLoaded($name)) { + return $this; + } + + $module = null; + if ($basedir === null) { + $module = new Module($this->app, $name, $this->getModuleDir($name)); + } else { + $module = new Module($this->app, $name, $basedir); + } + + if ($name !== 'ipl' && $name !== 'reactbundle') { + $module->register(); + } + + $this->loadedModules[$name] = $module; + return $this; + } + + /** + * Set the given module to the enabled state + * + * @param string $name The module to enable + * @param bool $force Whether to ignore unmet dependencies + * + * @return $this + * @throws ConfigurationError When trying to enable a module that is not installed + * @throws SystemPermissionException When insufficient permissions for the application exist + */ + public function enableModule($name, $force = false) + { + if (! $this->hasInstalled($name)) { + throw new ConfigurationError( + 'Cannot enable module "%s". Module is not installed.', + $name + ); + } + + if (strtolower(substr($name, 0, 18)) === 'icingaweb2-module-') { + throw new ConfigurationError( + 'Cannot enable module "%s": Directory name does not match the module\'s name.' + . ' Please rename the module to "%s" before enabling.', + $name, + substr($name, 18) + ); + } + + if ($this->hasUnmetDependencies($name)) { + if ($force) { + Logger::warning(t('Enabling module "%s" although it has unmet dependencies'), $name); + } else { + throw new ConfigurationError( + t('Module "%s" can\'t be enabled. Module has unmet dependencies'), + $name + ); + } + } + + clearstatcache(true); + $target = $this->installedBaseDirs[$name]; + $link = $this->enableDir . DIRECTORY_SEPARATOR . $name; + + if (! is_dir($this->enableDir)) { + if (!@mkdir($this->enableDir, 0777, true)) { + $error = error_get_last(); + throw new SystemPermissionException( + 'Failed to create enabledModules directory "%s" (%s)', + $this->enableDir, + $error['message'] + ); + } + + chmod($this->enableDir, 02770); + } elseif (! is_writable($this->enableDir)) { + throw new SystemPermissionException( + 'Cannot enable module "%s". Check the permissions for the enabledModules directory: %s', + $name, + $this->enableDir + ); + } + + $this->loadedAllEnabledModules = false; + + if (file_exists($link) && is_link($link)) { + return $this; + } + + if (! @symlink($target, $link)) { + $error = error_get_last(); + if (strstr($error["message"], "File exists") === false) { + throw new SystemPermissionException( + 'Cannot enable module "%s" at %s due to file system errors. ' + . 'Please check path and mounting points because this is not a permission error. ' + . 'Primary error was: %s', + $name, + $this->enableDir, + $error['message'] + ); + } + } + + $this->enabledDirs[$name] = $link; + $this->loadModule($name); + return $this; + } + + /** + * Disable the given module and remove its enabled state + * + * @param string $name The name of the module to disable + * + * @return $this + * + * @throws ConfigurationError When the module is not installed or it's not a symlink + * @throws SystemPermissionException When insufficient permissions for the application exist + */ + public function disableModule($name) + { + if (! $this->hasEnabled($name)) { + throw new ConfigurationError( + 'Cannot disable module "%s". Module is not installed.', + $name + ); + } + + if (! is_writable($this->enableDir)) { + throw new SystemPermissionException( + 'Cannot disable module "%s". Check the permissions for the enabledModules directory: %s', + $name, + $this->enableDir + ); + } + + $link = $this->enableDir . DIRECTORY_SEPARATOR . $name; + if (! is_link($link)) { + throw new ConfigurationError( + 'Cannot disable module %s at %s. ' + . 'It looks like you have installed this module manually and moved it to your module folder. ' + . 'In order to dynamically enable and disable modules, you have to create a symlink to ' + . 'the enabledModules folder.', + $name, + $this->enableDir + ); + } + + if (is_link($link)) { + if (! @unlink($link)) { + $error = error_get_last(); + throw new SystemPermissionException( + 'Cannot enable module "%s" at %s due to file system errors. ' + . 'Please check path and mounting points because this is not a permission error. ' + . 'Primary error was: %s', + $name, + $this->enableDir, + $error['message'] + ); + } + } + + unset($this->enabledDirs[$name]); + return $this; + } + + /** + * Return the directory of the given module as a string, optionally with a given sub directoy + * + * @param string $name The module name to return the module directory of + * @param string $subdir The sub directory to append to the path + * + * @return string + * + * @throws ProgrammingError When the module is not installed or existing + */ + public function getModuleDir($name, $subdir = '') + { + if ($this->hasLoaded($name)) { + return $this->getModule($name)->getBaseDir() . $subdir; + } + + if ($this->hasEnabled($name)) { + return $this->enabledDirs[$name]. $subdir; + } + + if ($this->hasInstalled($name)) { + return $this->installedBaseDirs[$name] . $subdir; + } + + throw new ProgrammingError( + 'Trying to access uninstalled module dir: %s', + $name + ); + } + + /** + * Return true when the module with the given name is installed, otherwise false + * + * @param string $name The module to check for being installed + * + * @return bool + */ + public function hasInstalled($name) + { + if (!count($this->installedBaseDirs)) { + $this->detectInstalledModules(); + } + return array_key_exists($name, $this->installedBaseDirs); + } + + /** + * Return true when the given module is in enabled state, otherwise false + * + * @param string $name The module to check for being enabled + * + * @return bool + */ + public function hasEnabled($name) + { + return array_key_exists($name, $this->enabledDirs); + } + + /** + * Return true when the module is in loaded state, otherwise false + * + * @param string $name The module to check for being loaded + * + * @return bool + */ + public function hasLoaded($name) + { + return array_key_exists($name, $this->loadedModules); + } + + /** + * Check if a module with the given name is enabled + * + * Passing a version constraint also verifies that the module's version matches. + * + * @param string $name + * @param string $version + * + * @return bool + */ + public function has($name, $version = null) + { + if (! $this->hasEnabled($name)) { + return false; + } elseif ($version === null || $version === true) { + return true; + } + + $operator = '='; + if (preg_match('/^([<>=]{1,2})\s*v?((?:[\d.]+)(?:.+)?)$/', $version, $match)) { + $operator = $match[1]; + $version = $match[2]; + } + + $modVersion = ltrim($this->getModule($name)->getVersion(), 'v'); + return version_compare($modVersion, $version, $operator); + } + + /** + * Get the currently loaded modules + * + * @return Module[] + */ + public function getLoadedModules() + { + return $this->loadedModules; + } + + /** + * Get a module + * + * @param string $name Name of the module + * @param bool $assertLoaded Whether or not to throw an exception if the module hasn't been loaded + * + * @return Module + * @throws ProgrammingError If the module hasn't been loaded + */ + public function getModule($name, $assertLoaded = true) + { + if ($this->hasLoaded($name)) { + return $this->loadedModules[$name]; + } elseif (! (bool) $assertLoaded) { + return new Module($this->app, $name, $this->getModuleDir($name)); + } + throw new ProgrammingError( + 'Can\'t access module %s because it hasn\'t been loaded', + $name + ); + } + + /** + * Return an array containing information objects for each available module + * + * Each entry has the following fields + * * name, name of the module as a string + * * path, path where the module is located as a string + * * installed, whether the module is installed or not as a boolean + * * enabled, whether the module is enabled or not as a boolean + * * loaded, whether the module is loaded or not as a boolean + * + * @return array + */ + public function getModuleInfo() + { + $info = array(); + + $installed = $this->listInstalledModules(); + foreach ($installed as $name) { + $info[$name] = (object) array( + 'name' => $name, + 'path' => $this->installedBaseDirs[$name], + 'installed' => true, + 'enabled' => $this->hasEnabled($name), + 'loaded' => $this->hasLoaded($name) + ); + } + + $enabled = $this->listEnabledModules(); + foreach ($enabled as $name) { + $info[$name] = (object) array( + 'name' => $name, + 'path' => $this->enabledDirs[$name], + 'installed' => $this->enabledDirs[$name] !== null, + 'enabled' => true, + 'loaded' => $this->hasLoaded($name) + ); + } + + return $info; + } + + /** + * Check if the given module has unmet dependencies + * + * @param string $name + * + * @return bool + */ + public function hasUnmetDependencies($name) + { + $module = $this->getModule($name, false); + + $requiredMods = $module->getRequiredModules(); + + if (isset($requiredMods['monitoring'], $requiredMods['icingadb'])) { + if (! $this->has('monitoring', $requiredMods['monitoring']) + && ! $this->has('icingadb', $requiredMods['icingadb']) + ) { + return true; + } + + unset($requiredMods['monitoring'], $requiredMods['icingadb']); + } + + foreach ($requiredMods as $moduleName => $moduleVersion) { + if (! $this->has($moduleName, $moduleVersion)) { + return true; + } + } + + $libraries = Icinga::app()->getLibraries(); + + $requiredLibs = $module->getRequiredLibraries(); + foreach ($requiredLibs as $libraryName => $libraryVersion) { + if (! $libraries->has($libraryName, $libraryVersion)) { + return true; + } + } + + return false; + } + + /** + * Return an array containing all enabled module names as strings + * + * @return array + */ + public function listEnabledModules() + { + if (count($this->enabledDirs) === 0) { + $this->detectEnabledModules(); + } + + return array_keys($this->enabledDirs); + } + + /** + * Return an array containing all loaded module names as strings + * + * @return array + */ + public function listLoadedModules() + { + return array_keys($this->loadedModules); + } + + /** + * Return an array of module names from installed modules + * + * Calls detectInstalledModules() if no module discovery has been performed yet + * + * @return array + * + * @see detectInstalledModules() + */ + public function listInstalledModules() + { + if (!count($this->installedBaseDirs)) { + $this->detectInstalledModules(); + } + + if (count($this->installedBaseDirs)) { + return array_keys($this->installedBaseDirs); + } + + return array(); + } + + /** + * Detect installed modules from every path provided in modulePaths + * + * @param array $availableDirs Installed modules location + * + * @return $this + */ + public function detectInstalledModules(array $availableDirs = null) + { + $modulePaths = $availableDirs !== null ? $availableDirs : $this->modulePaths; + foreach ($modulePaths as $basedir) { + $canonical = realpath($basedir); + if ($canonical === false) { + Logger::warning('Module path "%s" does not exist', $basedir); + continue; + } + if (!is_dir($canonical)) { + Logger::error('Module path "%s" is not a directory', $canonical); + continue; + } + if (!is_readable($canonical)) { + Logger::error('Module path "%s" is not readable', $canonical); + continue; + } + if (($dh = opendir($canonical)) !== false) { + while (($file = readdir($dh)) !== false) { + if ($file[0] === '.') { + continue; + } + if (is_dir($canonical . '/' . $file)) { + if (! array_key_exists($file, $this->installedBaseDirs)) { + $this->installedBaseDirs[$file] = $canonical . '/' . $file; + } else { + Logger::debug( + 'Module "%s" already exists in installation path "%s" and is ignored.', + $canonical . '/' . $file, + $this->installedBaseDirs[$file] + ); + } + } + } + closedir($dh); + } + } + ksort($this->installedBaseDirs); + return $this; + } + + /** + * Get the directories where to look for installed modules + * + * @return array + */ + public function getModuleDirs() + { + return $this->modulePaths; + } +} |