diff options
Diffstat (limited to '')
-rw-r--r-- | library/Director/Core/CoreApi.php | 940 | ||||
-rw-r--r-- | library/Director/Core/DeploymentApiInterface.php | 75 | ||||
-rw-r--r-- | library/Director/Core/Json.php | 34 | ||||
-rw-r--r-- | library/Director/Core/LegacyDeploymentApi.php | 466 | ||||
-rw-r--r-- | library/Director/Core/RestApiClient.php | 276 | ||||
-rw-r--r-- | library/Director/Core/RestApiResponse.php | 149 | ||||
-rw-r--r-- | library/Director/CoreBeta/ApiStream.php | 57 | ||||
-rw-r--r-- | library/Director/CoreBeta/Stream.php | 17 | ||||
-rw-r--r-- | library/Director/CoreBeta/StreamContext.php | 89 | ||||
-rw-r--r-- | library/Director/CoreBeta/StreamContextSslOptions.php | 52 |
10 files changed, 2155 insertions, 0 deletions
diff --git a/library/Director/Core/CoreApi.php b/library/Director/Core/CoreApi.php new file mode 100644 index 0000000..ea10916 --- /dev/null +++ b/library/Director/Core/CoreApi.php @@ -0,0 +1,940 @@ +<?php + +namespace Icinga\Module\Director\Core; + +use Exception; +use Icinga\Exception\NotFoundError; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Hook\DeploymentHook; +use Icinga\Module\Director\IcingaConfig\IcingaConfig; +use Icinga\Module\Director\Objects\IcingaObject; +use Icinga\Module\Director\Objects\IcingaCommand; +use Icinga\Module\Director\Objects\DirectorDeploymentLog; +use Icinga\Module\Director\Objects\IcingaZone; +use Icinga\Web\Hook; +use RuntimeException; + +class CoreApi implements DeploymentApiInterface +{ + protected $client; + + protected $initialized = false; + + /** @var Db */ + protected $db; + + public function __construct(RestApiClient $client) + { + $this->client = $client; + } + + // Todo: type + public function setDb(Db $db) + { + $this->db = $db; + return $this; + } + + /** + * @return string|null + */ + public function getVersion() + { + return $this->parseVersion($this->getRawVersion()); + } + + public function enableWorkaroundForConnectionIssues() + { + $version = $this->getVersion(); + + if ($version === null || + ((version_compare($version, '2.8.2', '>=') && version_compare($version, '2.10.2', '<'))) + ) { + $this->client->disconnect(); + $this->client->setKeepAlive(false); + } + } + + /** + * @return string|null + */ + public function getRawVersion() + { + try { + return $this->client()->get('')->getRaw('version'); + } catch (Exception $exception) { + return null; + } + } + + /** + * @param $version + * @return string|null + */ + protected function parseVersion($version) + { + if ($version === null) { + return null; + } + + if (preg_match('/^[rv]?(\d\.\d+\.\d+)/', $version, $match)) { + return $match[1]; + } else { + return null; + } + } + + public function getObjects($pluralType, $attrs = array(), $ignorePackage = null) + { + $params = (object) []; + if ($ignorePackage) { + $params->filter = 'obj.package!="' . $ignorePackage . '"'; + } + + if (! empty($attrs)) { + $params->attrs = $attrs; + } + + return $this->client()->get( + 'objects/' . urlencode(strtolower($pluralType)), + $params + )->getResult('name'); + } + + public function onEvent($callback, $raw = false) + { + $this->client->onEvent($callback, $raw); + + return $this; + } + + public function getObject($name, $pluraltype, $attrs = array()) + { + $params = (object) array( + ); + + if (! empty($attrs)) { + $params->attrs = $attrs; + } + $url = 'objects/' . urlencode(strtolower($pluraltype)) . '/' . rawurlencode($name) . '?all_joins=1'; + $res = $this->client()->get($url, $params)->getResult('name'); + + // TODO: check key, throw + return $res[$name]; + } + + /** + * Get a PKI ticket for CSR auto-signing + * + * @param string $cn The host’s common name for which the ticket should be generated + * + * @return string|null + */ + public function getTicket($cn) + { + $r = $this->client()->post( + 'actions/generate-ticket', + ['cn' => $cn] + ); + if (! $r->succeeded()) { + throw new RuntimeException($r->getErrorMessage()); + } + + $ticket = $r->getRaw('ticket'); + if ($ticket === null) { + // RestApiResponse::succeeded() returns true if Icinga 2 reports an error in the results key, e.g. + // { + // "results": [ + // { + // "code": 500.0, + // "status": "Ticket salt is not configured in ApiListener object" + // } + // ] + // } + throw new RuntimeException($r->getRaw('status', 'Ticket is empty')); + } + + return $ticket; + } + + public function checkHostNow($host) + { + $filter = 'host.name == "' . $host . '"'; + + return $this->client()->post( + 'actions/reschedule-check?filter=' . rawurlencode($filter), + (object) array( + 'type' => 'Host' + ) + ); + } + + public function checkServiceNow($host, $service) + { + $filter = 'host.name == "' . $host . '" && service.name == "' . $service . '"'; + $this->client()->post( + 'actions/reschedule-check?filter=' . rawurlencode($filter), + (object) array( + 'type' => 'Service' + ) + ); + } + + public function acknowledgeHostProblem($host, $author, $comment) + { + $filter = 'host.name == "' . $host . '"'; + return $this->client()->post( + 'actions/acknowledge-problem?type=Host&filter=' . rawurlencode($filter), + (object) array( + 'author' => $author, + 'comment' => $comment + ) + ); + } + + public function removeHostAcknowledgement($host) + { + $filter = 'host.name == "' . $host . '"'; + return $this->client()->post( + 'actions/remove-acknowledgement?type=Host&filter=' . rawurlencode($filter) + ); + } + + public function reloadNow() + { + try { + $this->client()->post('actions/restart-process'); + + return true; + } catch (Exception $e) { + return $e->getMessage(); + } + } + + public function getHostOutput($host) + { + try { + $object = $this->getObject($host, 'hosts'); + } catch (Exception $e) { + return 'Unable to fetch the requested object'; + } + if (isset($object->attrs->last_check_result)) { + return $object->attrs->last_check_result->output; + } else { + return '(no check result available)'; + } + } + + public function checkHostAndWaitForResult($host, $timeout = 10) + { + $object = $this->getObject($host, 'hosts'); + if (isset($object->attrs->last_check_result)) { + $oldOutput = $object->attrs->last_check_result->output; + } else { + $oldOutput = ''; + } + + $now = microtime(true); + $this->checkHostNow($host); + + while (true) { + try { + $object = $this->getObject($host, 'hosts'); + if (isset($object->attrs->last_check_result)) { + $res = $object->attrs->last_check_result; + if ($res->execution_start > $now || $res->output !== $oldOutput) { + return $res; + } + } else { + // no check result available + } + } catch (Exception $e) { + // Unable to fetch the requested object + throw new RuntimeException(sprintf( + 'Unable to fetch the requested host "%s"', + $host + )); + } + if (microtime(true) > ($now + $timeout)) { + break; + } + + usleep(50000); + } + + return false; + } + + public function checkServiceAndWaitForResult($host, $service, $timeout = 10) + { + $now = microtime(true); + $this->checkServiceNow($host, $service); + + while (true) { + try { + $object = $this->getObject("$host!$service", 'services'); + if (isset($object->attrs->last_check_result)) { + $res = $object->attrs->last_check_result; + if ($res->execution_start > $now) { + return $res; + } + } else { + // no check result available + } + } catch (Exception $e) { + // Unable to fetch the requested object + throw new RuntimeException(sprintf( + 'Unable to fetch the requested service "%s" on "%s"', + $service, + $host + )); + } + if (microtime(true) > ($now + $timeout)) { + break; + } + + usleep(150000); + } + + return false; + } + + public function getServiceOutput($host, $service) + { + try { + $object = $this->getObject($host . '!' . $service, 'services'); + } catch (\Exception $e) { + return 'Unable to fetch the requested object'; + } + if (isset($object->attrs->last_check_result)) { + return $object->attrs->last_check_result->output; + } else { + return '(no check result available)'; + } + } + + public function supportsRuntimeCreationFor(IcingaObject $object) + { + $valid = array('host'); + return in_array($object->getShortTableName(), $valid); + } + + protected function assertRuntimeCreationSupportFor(IcingaObject $object) + { + if (!$this->supportsRuntimeCreationFor($object)) { + throw new RuntimeException(sprintf( + 'Object creation at runtime is not supported for "%s"', + $object->getShortTableName() + )); + } + } + + // Note: this is for testing purposes only, NOT production-ready + public function createObjectAtRuntime(IcingaObject $object) + { + $this->assertRuntimeCreationSupportFor($object); + + $key = $object->getShortTableName(); + + $command = sprintf( + "f = function() {\n" + . ' existing = get_%s("%s")' + . "\n if (existing) { return false }" + . "\n%s\n}\nInternal.run_with_activation_context(f)\n", + $key, + $object->get('object_name'), + (string) $object + ); + + return $this->runConsoleCommand($command)->getSingleResult(); + } + + public function getConstants() + { + $constants = array(); + $command = 'var constants = []; +for (k => v in globals) { + if (typeof(v) in [String, Number, Boolean]) { + res = { name = k, value = v } + constants.add({name = k, value = v}) + } +}; +constants +'; + + foreach ($this->runConsoleCommand($command)->getSingleResult() as $row) { + $constants[$row->name] = $row->value; + } + + return $constants; + } + + public function runConsoleCommand($command) + { + return $this->client()->post( + 'console/execute-script', + array('command' => $command) + ); + } + + public function getConstant($name) + { + $constants = $this->getConstants(); + if (array_key_exists($name, $constants)) { + return $constants[$name]; + } + + return null; + } + + public function getTypes() + { + return $this->client()->get('types')->getResult('name'); + } + + public function getType($type) + { + $res = $this->client()->get('types', array('name' => $type))->getResult('name'); + return $res[$type]; // TODO: error checking + } + + public function getStatus() + { + return $this->client()->get('status')->getResult('name'); + } + + public function listObjects($type, $pluralType) + { + // TODO: more abstraction needed + // TODO: autofetch and cache pluraltypes + try { + $result = $this->client()->get( + 'objects/' . $pluralType, + array( + 'attrs' => array('__name') + ) + )->getResult('name'); + } catch (NotFoundError $e) { + $result = []; + } + + return array_keys($result); + } + + public function getPackages() + { + return $this->client()->get('config/packages')->getResult('name'); + } + + public function getActiveStageName() + { + return current($this->listPackageStages($this->getPackageName(), true)); + } + + protected function getPackageName() + { + return $this->db->settings()->get('icinga_package_name'); + } + + public function getActiveChecksum(Db $conn) + { + $db = $conn->getDbAdapter(); + $stage = $this->getActiveStageName(); + if (! $stage) { + return null; + } + + $query = $db->select()->from( + array('l' => 'director_deployment_log'), + array('checksum' => $conn->dbHexFunc('l.config_checksum')) + )->where('l.stage_name = ?', $stage); + + return $db->fetchOne($query); + } + + protected function getDirectorObjects($type, $plural, $map) + { + $attrs = array_merge( + array_keys($map), + array('package', 'templates', 'active') + ); + + $objects = array(); + $result = $this->getObjects($plural, $attrs, $this->getPackageName()); + foreach ($result as $name => $row) { + $attrs = $row->attrs; + + $properties = array( + 'object_name' => $name, + 'object_type' => 'external_object' + ); + + foreach ($map as $key => $prop) { + if (property_exists($attrs, $key)) { + $properties[$prop] = $attrs->$key; + } + } + + $objects[$name] = IcingaObject::createByType($type, $properties, $this->db); + } + + return $objects; + } + + /** + * @return IcingaZone[] + */ + public function getZoneObjects() + { + return $this->getDirectorObjects('Zone', 'zones', [ + 'parent' => 'parent', + 'global' => 'is_global', + ]); + } + + public function getUserObjects() + { + return $this->getDirectorObjects('User', 'users', [ + 'display_name' => 'display_name', + 'email' => 'email', + 'groups' => 'groups', + 'vars' => 'vars', + ]); + } + + protected function buildEndpointZoneMap() + { + $zones = $this->getObjects('zones', ['endpoints'], $this->getPackageName()); + $zoneMap = array(); + + foreach ($zones as $name => $zone) { + if (! is_array($zone->attrs->endpoints)) { + continue; + } + foreach ($zone->attrs->endpoints as $endpoint) { + $zoneMap[$endpoint] = $name; + } + } + + return $zoneMap; + } + + public function getEndpointObjects() + { + $zoneMap = $this->buildEndpointZoneMap(); + $objects = $this->getDirectorObjects('Endpoint', 'endpoints', [ + 'host' => 'host', + 'port' => 'port', + 'log_duration' => 'log_duration', + ]); + + foreach ($objects as $object) { + $name = $object->object_name; + if (array_key_exists($name, $zoneMap)) { + $object->zone = $zoneMap[$name]; + } + } + + return $objects; + } + + public function getHostObjects() + { + $params = [ + 'display_name' => 'display_name', + 'address' => 'address', + 'address6' => 'address6', + 'templates' => 'imports', + 'groups' => 'groups', + 'vars' => 'vars', + 'check_command' => 'check_command', + 'max_check_attempts' => 'max_check_attempts', + 'check_period' => 'check_period', + 'check_interval' => 'check_interval', + 'retry_interval' => 'retry_interval', + 'enable_notifications' => 'enable_notifications', + 'enable_active_checks' => 'enable_active_checks', + 'enable_passive_checks' => 'enable_passive_checks', + 'enable_event_handler' => 'enable_event_handler', + 'enable_flapping' => 'enable_flapping', + 'enable_perfdata' => 'enable_perfdata', + 'event_command' => 'event_command', + 'volatile' => 'volatile', + 'zone' => 'zone', + 'command_endpoint' => 'command_endpoint', + 'notes' => 'notes', + 'notes_url' => 'notes_url', + 'action_url' => 'action_url', + 'icon_image' => 'icon_image', + 'icon_image_alt' => 'icon_image_alt', + ]; + + if (version_compare($this->getVersion(), '2.8.0', '>=')) { + $params['flapping_threshold_high'] = 'flapping_threshold_high'; + $params['flapping_threshold_low'] = 'flapping_threshold_low'; + } + + return $this->getDirectorObjects('Host', 'hosts', $params); + } + + public function getHostGroupObjects() + { + return $this->getDirectorObjects('HostGroup', 'hostgroups', [ + 'display_name' => 'display_name', + ]); + } + + public function getUserGroupObjects() + { + return $this->getDirectorObjects('UserGroup', 'usergroups', [ + 'display_name' => 'display_name', + ]); + } + + /** + * @return IcingaCommand[] + */ + public function getCheckCommandObjects() + { + return $this->getSpecificCommandObjects('Check'); + } + + /** + * @return IcingaCommand[] + */ + public function getNotificationCommandObjects() + { + return $this->getSpecificCommandObjects('Notification'); + } + + /** + * @return IcingaCommand[] + */ + public function getEventCommandObjects() + { + return $this->getSpecificCommandObjects('Event'); + } + + /** + * @return IcingaCommand[] + */ + public function getSpecificCommandObjects($type) + { + IcingaCommand::setPluginDir($this->getConstant('PluginDir')); + + $objects = $this->getDirectorObjects('Command', "${type}Commands", [ + 'arguments' => 'arguments', + // 'env' => 'env', + 'timeout' => 'timeout', + 'command' => 'command', + 'vars' => 'vars', + ]); + foreach ($objects as $obj) { + $obj->methods_execute = "Plugin$type"; + } + + return $objects; + } + + public function listPackageStages($name, $active = null) + { + $packages = $this->getPackages(); + $found = array(); + + if (array_key_exists($name, $packages)) { + $package = $packages[$name]; + $current = $package->{'active-stage'}; + foreach ($package->stages as $stage) { + if ($active === null) { + $found[] = $stage; + } elseif ($active === true) { + if ($current === $stage) { + $found[] = $stage; + } + } elseif ($active === false) { + if ($current !== $stage) { + $found[] = $stage; + } + } + } + } + + return $found; + } + + public function collectLogFiles(Db $db) + { + $existing = $this->listPackageStages($this->getPackageName()); + $missing = []; + $empty = []; + foreach (DirectorDeploymentLog::getUncollected($db) as $deployment) { + $stage = $deployment->get('stage_name'); + if (! in_array($stage, $existing)) { + $missing[] = $deployment; + continue; + } + + try { + $availableFiles = $this->listStageFiles($stage); + } catch (Exception $e) { + // Could not collect stage files. Doesn't matter, let's try next time + continue; + } + + if (in_array('startup.log', $availableFiles) + && in_array('status', $availableFiles) + ) { + if ($this->getStagedFile($stage, 'status') === '0') { + $deployment->set('startup_succeeded', 'y'); + } else { + $deployment->set('startup_succeeded', 'n'); + } + $deployment->set('startup_log', $this->shortenStartupLog( + $this->getStagedFile($stage, 'startup.log') + )); + } else { + // Stage seems to be incomplete, let's try again next time + $empty[] = $deployment; + continue; + } + $deployment->set('stage_collected', 'y'); + + $deployment->store(); + + /** @var DeploymentHook[] $hooks */ + $hooks = Hook::all('director/Deployment'); + foreach ($hooks as $hook) { + $hook->onCollect($deployment); + } + } + + foreach ($missing as $deployment) { + $deployment->set('stage_collected', 'n'); + $deployment->store(); + } + + $running = DirectorDeploymentLog::getRelatedToActiveStage($this, $db); + if ($running !== null) { + foreach ($empty as $deployment) { + if ($deployment->getDeploymentTimestamp() < $running->getDeploymentTimestamp()) { + $deployment->set('stage_collected', 'n'); + $deployment->store(); + $this->deleteStage($this->getPackageName(), $deployment->get('stage_name')); + } + } + } + } + + public function wipeInactiveStages(Db $db) + { + $uncollected = DirectorDeploymentLog::getUncollected($db); + $packageName = $this->getPackageName(); + foreach ($this->listPackageStages($packageName, false) as $stage) { + if (array_key_exists($stage, $uncollected)) { + continue; + } + $this->client()->delete($this->prepareStageUrl($packageName, $stage)); + } + } + + public function listStageFiles($stage, $packageName = null) + { + if ($packageName === null) { + $packageName = $this->getPackageName(); + } + return array_keys( + $this->client() + ->get($this->prepareStageUrl($packageName, $stage)) + ->getResult('name', ['type' => 'file']) + ); + } + + public function getStagedFile($stage, $file, $packageName = null) + { + if ($packageName === null) { + $packageName = $this->getPackageName(); + } + return $this->client() + ->getRaw($this->prepareFileUrl($packageName, $stage, $file)); + } + + public function hasPackage($name) + { + $modules = $this->getPackages(); + return \array_key_exists($name, $modules); + } + + public function createPackage($name) + { + return $this->client()->post($this->preparePackageUrl($name))->succeeded(); + } + + public function deletePackage($name) + { + return $this->client()->delete($this->preparePackageUrl($name))->succeeded(); + } + + public function assertPackageExists($name) + { + if (! $this->hasPackage($name)) { + if (! $this->createPackage($name)) { + throw new RuntimeException(sprintf( + 'Failed to create the package "%s" through the REST API', + $name + )); + } + } + + return $this; + } + + public function deleteStage($packageName, $stageName) + { + $this->client()->delete( + $this->prepareStageUrl($packageName, $stageName) + )->succeeded(); + } + + /** + * @throws Exception + */ + public function stream() + { + $allTypes = array( + 'CheckResult', + 'StateChange', + 'Notification', + 'AcknowledgementSet', + 'AcknowledgementCleared', + 'CommentAdded', + 'CommentRemoved', + 'DowntimeAdded', + 'DowntimeRemoved', + 'DowntimeTriggered' + ); + + $queue = 'director-rand'; + + $url = sprintf('events?queue=%s&types=%s', $queue, implode('&types=', $allTypes)); + + $this->client()->request('post', $url, null, false, true); + } + + /** + * @param IcingaConfig $config + * @param Db $db + * @param null $packageName + * @return DirectorDeploymentLog + * @throws \Icinga\Module\Director\Exception\DuplicateKeyException + */ + public function dumpConfig(IcingaConfig $config, Db $db, $packageName = null) + { + if ($packageName === null) { + $packageName = $db->settings()->get('icinga_package_name'); + } + $start = microtime(true); + /** @var DirectorDeploymentLog $deployment */ + $deployment = DirectorDeploymentLog::create(array( + // 'config_id' => $config->id, + // 'peer_identity' => $endpoint->object_name, + 'peer_identity' => $this->client->getPeerIdentity(), + 'start_time' => date('Y-m-d H:i:s'), + 'config_checksum' => $config->getChecksum(), + 'last_activity_checksum' => $config->getLastActivityChecksum() + // 'triggered_by' => Util::getUsername(), + // 'username' => Util::getUsername(), + // 'module_name' => $moduleName, + )); + + /** @var DeploymentHook[] $hooks */ + $hooks = Hook::all('director/Deployment'); + foreach ($hooks as $hook) { + $hook->beforeDeploy($deployment); + } + + $this->assertPackageExists($packageName); + + $response = $this->client()->post('config/stages/' . \rawurlencode($packageName), [ + 'files' => $config->getFileContents() + ]); + + $duration = (int) ((microtime(true) - $start) * 1000); + // $deployment->duration_ms = $duration; + $deployment->set('duration_dump', $duration); + + $succeeded = 'n'; + if ($response->succeeded()) { + if ($stage = $response->getResult('stage', ['package' => $packageName])) { // Status? + $deployment->set('stage_name', key($stage)); + $succeeded = 'y'; + } + } + $deployment->set('dump_succeeded', $succeeded); + $deployment->store($db); + + if ($succeeded === 'y') { + foreach ($hooks as $hook) { + $hook->triggerSuccessfulDump($deployment); + } + } + + return $deployment; + } + + protected function shortenStartupLog($log) + { + $logLen = strlen($log); + if ($logLen < 1024 * 60) { + return $log; + } + + $part = substr($log, 0, 1024 * 20); + $parts = explode("\n", $part); + array_pop($parts); + $begin = implode("\n", $parts) . "\n\n"; + + $part = substr($log, -1024 * 20); + $parts = explode("\n", $part); + array_shift($parts); + $end = "\n\n" . implode("\n", $parts); + + return $begin . sprintf( + '[..] %d bytes removed by Director [..]', + $logLen - (strlen($begin) + strlen($end)) + ) . $end; + } + + protected function preparePackageUrl($packageName) + { + return 'config/packages/' . \rawurlencode($packageName); + } + + protected function prepareStageUrl($packageName, $stage) + { + return \sprintf( + 'config/stages/%s/%s', + \rawurlencode($packageName), + \rawurlencode($stage) + ); + } + + protected function prepareFileUrl($packageName, $stage, $file) + { + return \sprintf( + 'config/files/%s/%s/%s', + \rawurlencode($packageName), + \rawurlencode($stage), + \rawurlencode($file) + ); + } + + protected function client() + { + if ($this->initialized === false) { + $this->initialized = true; + $this->enableWorkaroundForConnectionIssues(); + } + + return $this->client; + } +} diff --git a/library/Director/Core/DeploymentApiInterface.php b/library/Director/Core/DeploymentApiInterface.php new file mode 100644 index 0000000..026f0fd --- /dev/null +++ b/library/Director/Core/DeploymentApiInterface.php @@ -0,0 +1,75 @@ +<?php + +namespace Icinga\Module\Director\Core; + +use Icinga\Module\Director\Db; +use Icinga\Module\Director\IcingaConfig\IcingaConfig; + +/** + * Interface to a deployment API of the monitoring configuration + * + * @package Icinga\Module\Director\Core + */ +interface DeploymentApiInterface +{ + /** + * Collecting log files from the deployment system + * and write them into the database. + * + * @param Db $db + */ + public function collectLogFiles(Db $db); + + /** + * Cleanup old stages that are collected and not active + * + * @param Db $db + */ + public function wipeInactiveStages(Db $db); + + /** + * Returns the active configuration stage + * + * @return string + */ + public function getActiveStageName(); + + /** + * List files in a named stage + * + * @param string $stage name of the stage + * @return string[] + */ + public function listStageFiles($stage); + + /** + * Retrieve a raw file from the named stage + * + * @param string $stage Stage name + * @param string $file Relative file path + * + * @return string + */ + public function getStagedFile($stage, $file); + + /** + * Explicitly delete a stage + * + * @param string $packageName + * @param string $stageName + * + * @return bool + */ + public function deleteStage($packageName, $stageName); + + /** + * Deploy the config and activate it + * + * @param IcingaConfig $config + * @param Db $db + * @param string $packageName + * + * @return mixed + */ + public function dumpConfig(IcingaConfig $config, Db $db, $packageName = null); +} diff --git a/library/Director/Core/Json.php b/library/Director/Core/Json.php new file mode 100644 index 0000000..507349c --- /dev/null +++ b/library/Director/Core/Json.php @@ -0,0 +1,34 @@ +<?php + +namespace Icinga\Module\Director\Core; + +use Icinga\Module\Director\Exception\JsonEncodeException; + +class Json +{ + public static function encode($mixed, $flags = null) + { + if ($flags === null) { + $result = \json_encode($mixed); + } else { + $result = \json_encode($mixed, $flags); + } + + if ($result === false && json_last_error() !== JSON_ERROR_NONE) { + throw JsonEncodeException::forLastJsonError(); + } + + return $result; + } + + public static function decode($string) + { + $result = \json_decode($string); + + if ($result === null && json_last_error() !== JSON_ERROR_NONE) { + throw JsonEncodeException::forLastJsonError(); + } + + return $result; + } +} diff --git a/library/Director/Core/LegacyDeploymentApi.php b/library/Director/Core/LegacyDeploymentApi.php new file mode 100644 index 0000000..7287c4a --- /dev/null +++ b/library/Director/Core/LegacyDeploymentApi.php @@ -0,0 +1,466 @@ +<?php + +namespace Icinga\Module\Director\Core; + +use Exception; +use Icinga\Exception\IcingaException; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\IcingaConfig\IcingaConfig; +use Icinga\Module\Director\Objects\DirectorDeploymentLog; + +/** + * Legacy DeploymentApi for Icinga 1.x configuration deployment + * + * @package Icinga\Module\Director\Core + */ +class LegacyDeploymentApi implements DeploymentApiInterface +{ + protected $db; + protected $deploymentPath; + protected $activationScript; + + protected $dir_mode; + protected $file_mode; + + public function __construct(Db $db) + { + $this->db = $db; + $settings = $this->db->settings(); + $this->deploymentPath = $settings->deployment_path_v1; + $this->activationScript = $settings->activation_script_v1; + + $this->dir_mode = base_convert($settings->get('deployment_file_mode_v1', '2775'), 8, 10); + $this->file_mode = base_convert($settings->get('deployment_dir_mode_v1', '0664'), 8, 10); + } + + /** + * TODO: merge in common class + * @inheritdoc + */ + public function collectLogFiles(Db $db) + { + $packageName = $db->settings()->get('icinga_package_name'); + $existing = $this->listPackageStages($packageName); + + foreach (DirectorDeploymentLog::getUncollected($db) as $deployment) { + $stage = $deployment->get('stage_name'); + if (! in_array($stage, $existing)) { + continue; + } + + try { + $availableFiles = $this->listStageFiles($stage); + } catch (Exception $e) { + // Could not collect stage files. Doesn't matter, let's try next time + continue; + } + + if (in_array('startup.log', $availableFiles) + && in_array('status', $availableFiles) + ) { + $status = $this->getStagedFile($stage, 'status'); + $status = trim($status); + if ($status === '0') { + $deployment->set('startup_succeeded', 'y'); + } else { + $deployment->set('startup_succeeded', 'n'); + } + $deployment->set('startup_log', $this->shortenStartupLog( + $this->getStagedFile($stage, 'startup.log') + )); + } else { + // Stage seems to be incomplete, let's try again next time + continue; + } + $deployment->set('stage_collected', 'y'); + + $deployment->store(); + } + } + + /** + * TODO: merge in common class + * @inheritdoc + */ + public function wipeInactiveStages(Db $db) + { + $uncollected = DirectorDeploymentLog::getUncollected($db); + $packageName = $db->settings()->get('icinga_package_name'); + $currentStage = $this->getActiveStageName(); + + // try to expire old deployments + foreach ($uncollected as $name => $deployment) { + /** @var DirectorDeploymentLog $deployment */ + if ($deployment->get('dump_succeeded') === 'n' + || $deployment->get('startup_succeeded') === null + ) { + $start_time = strtotime($deployment->start_time); + + // older than an hour and no startup + if ($start_time + 3600 < time()) { + $deployment->set('startup_succeeded', 'n'); + $deployment->set('startup_log', 'Activation timed out...'); + $deployment->store(); + } + } + } + + foreach ($this->listPackageStages($packageName) as $stage) { + if (array_key_exists($stage, $uncollected) + && $uncollected[$stage]->get('startup_succeeded') === null + ) { + continue; + } elseif ($stage === $currentStage) { + continue; + } else { + $this->deleteStage($packageName, $stage); + } + } + } + + /** @inheritdoc */ + public function getActiveStageName() + { + $this->assertDeploymentPath(); + + $path = $this->deploymentPath . DIRECTORY_SEPARATOR . 'active'; + + if (file_exists($path)) { + if (is_link($path)) { + $linkTarget = readlink($path); + $linkTargetDir = dirname($linkTarget); + $linkTargetName = basename($linkTarget); + + if ($linkTargetDir === $this->deploymentPath || $linkTargetDir === '.') { + return $linkTargetName; + } else { + throw new IcingaException( + 'Active stage link pointing to a invalid target: %s -> %s', + $path, + $linkTarget + ); + } + } else { + throw new IcingaException('Active stage is not a symlink: %s', $path); + } + } else { + return false; + } + } + + /** @inheritdoc */ + public function listStageFiles($stage) + { + $path = $this->getStagePath($stage); + if (! is_dir($path)) { + throw new IcingaException('Deployment stage "%s" does not exist at: %s', $stage, $path); + } + return $this->listDirectoryContents($path); + } + + /** @inheritdoc */ + public function listPackageStages($packageName) + { + $this->assertPackageName($packageName); + $this->assertDeploymentPath(); + + $dh = @opendir($this->deploymentPath); + if ($dh === null) { + throw new IcingaException('Can not list contents of %s', $this->deploymentPath); + } + + $stages = array(); + while ($file = readdir($dh)) { + if ($file === '.' || $file === '..') { + continue; + } elseif (is_dir($this->deploymentPath . DIRECTORY_SEPARATOR . $file) + && substr($file, 0, 9) === 'director-' + ) { + $stages[] = $file; + } + } + + return $stages; + } + + /** @inheritdoc */ + public function getStagedFile($stage, $file) + { + $path = $this->getStagePath($stage); + + $filePath = $path . DIRECTORY_SEPARATOR . $file; + + if (! file_exists($filePath)) { + throw new IcingaException('Could not find file %s', $filePath); + } else { + return file_get_contents($filePath); + } + } + + /** @inheritdoc */ + public function deleteStage($packageName, $stageName) + { + $this->assertPackageName($packageName); + $this->assertDeploymentPath(); + + $path = $this->getStagePath($stageName); + + static::rrmdir($path); + } + + /** @inheritdoc */ + public function dumpConfig(IcingaConfig $config, Db $db, $packageName = null) + { + if ($packageName === null) { + $packageName = $db->settings()->get('icinga_package_name'); + } + $this->assertPackageName($packageName); + $this->assertDeploymentPath(); + + $start = microtime(true); + $deployment = DirectorDeploymentLog::create(array( + // 'config_id' => $config->id, + // 'peer_identity' => $endpoint->object_name, + 'peer_identity' => $this->deploymentPath, + 'start_time' => date('Y-m-d H:i:s'), + 'config_checksum' => $config->getChecksum(), + 'last_activity_checksum' => $config->getLastActivityChecksum() + // 'triggered_by' => Util::getUsername(), + // 'username' => Util::getUsername(), + // 'module_name' => $moduleName, + )); + + $stage_name = 'director-' .date('Ymd-His'); + $deployment->set('stage_name', $stage_name); + + try { + $succeeded = $this->deployStage($stage_name, $config->getFileContents()); + if ($succeeded === true) { + $succeeded = $this->activateStage($stage_name); + } + } catch (Exception $e) { + $deployment->set('dump_succeeded', 'n'); + $deployment->set('startup_log', $e->getMessage()); + $deployment->set('startup_succeeded', 'n'); + $deployment->store($db); + throw $e; + } + + $duration = (int) ((microtime(true) - $start) * 1000); + $deployment->set('duration_dump', $duration); + + $deployment->set('dump_succeeded', $succeeded === true ? 'y' : 'n'); + + $deployment->store($db); + return $succeeded; + } + + /** + * Deploy a new stage, and write all files to it + * + * @param string $stage Name of the stage + * @param array $files Array of files, $fileName => $content + * + * @return bool Success status + * + * @throws IcingaException When something could not be accessed + */ + protected function deployStage($stage, $files) + { + $path = $this->deploymentPath . DIRECTORY_SEPARATOR . $stage; + + if (file_exists($path)) { + throw new IcingaException('Stage "%s" does already exist at: ', $stage, $path); + } else { + $this->mkdir($path); + + foreach ($files as $file => $content) { + $fullPath = $path . DIRECTORY_SEPARATOR . $file; + $this->mkdir(dirname($fullPath), true); + + $fh = @fopen($fullPath, 'w'); + if ($fh === null) { + throw new IcingaException('Could not open file "%s" for writing.', $fullPath); + } + chmod($fullPath, $this->file_mode); + + fwrite($fh, $content); + fclose($fh); + } + + return true; + } + } + + /** + * Starts activation of + * + * Note: script should probably fork to background? + * + * @param string $stage Stage to activate + * + * @return bool + * + * @throws IcingaException For an execution error + */ + protected function activateStage($stage) + { + if ($this->activationScript === null || trim($this->activationScript) === '') { + // skip activation, could be done by external cron worker + return true; + } else { + $command = sprintf('%s %s 2>&1', escapeshellcmd($this->activationScript), escapeshellarg($stage)); + $output = null; + $rc = null; + exec($command, $output, $rc); + $output = join("\n", $output); + if ($rc !== 0) { + throw new IcingaException("Activation script did exit with return code %d:\n\n%s", $rc, $output); + } + return true; + } + } + + /** + * Recursively dump directory contents, with relative path + * + * @param string $path Absolute path to read from + * @param int $depth Internal counter + * + * @return string[] + * + * @throws IcingaException When directory could not be opened + */ + protected function listDirectoryContents($path, $depth = 0) + { + $dh = @opendir($path); + if ($dh === null) { + throw new IcingaException('Can not list contents of %s', $path); + } + + $files = array(); + while ($file = readdir($dh)) { + $fullPath = $path . DIRECTORY_SEPARATOR . $file; + if ($file === '.' || $file === '..') { + continue; + } elseif (is_dir($fullPath)) { + $subdirFiles = $this->listDirectoryContents($fullPath, $depth + 1); + foreach ($subdirFiles as $subFile) { + $files[] = $file . DIRECTORY_SEPARATOR . $subFile; + } + } else { + $files[] = $file; + } + } + + if ($depth === 0) { + sort($files); + } + + return $files; + } + + /** + * Assert that only the director module is interacted with + * + * @param string $packageName + * @throws IcingaException When another module is requested + */ + protected function assertPackageName($packageName) + { + if ($packageName !== 'director') { + throw new IcingaException('Does not supported different modules!'); + } + } + + /** + * Assert the deployment path to be configured, existing, and writeable + * + * @throws IcingaException + */ + protected function assertDeploymentPath() + { + if ($this->deploymentPath === null) { + throw new IcingaException('Deployment path is not configured for legacy config!'); + } elseif (! is_dir($this->deploymentPath)) { + throw new IcingaException('Deployment path is not a directory: %s', $this->deploymentPath); + } elseif (! is_writeable($this->deploymentPath)) { + throw new IcingaException('Deployment path is not a writeable: %s', $this->deploymentPath); + } + } + + /** + * TODO: avoid code duplication: copied from CoreApi + * + * @param string $log The log contents to shorten + * @return string + */ + protected function shortenStartupLog($log) + { + $logLen = strlen($log); + if ($logLen < 1024 * 60) { + return $log; + } + + $part = substr($log, 0, 1024 * 20); + $parts = explode("\n", $part); + array_pop($parts); + $begin = implode("\n", $parts) . "\n\n"; + + $part = substr($log, -1024 * 20); + $parts = explode("\n", $part); + array_shift($parts); + $end = "\n\n" . implode("\n", $parts); + + return $begin . sprintf( + '[..] %d bytes removed by Director [..]', + $logLen - (strlen($begin) + strlen($end)) + ) . $end; + } + + /** + * Return the full path of a stage + * + * @param string $stage Name of the stage + * + * @return string + */ + public function getStagePath($stage) + { + $this->assertDeploymentPath(); + return $this->deploymentPath . DIRECTORY_SEPARATOR . $stage; + } + + /** + * @from https://php.net/manual/de/function.rmdir.php#108113 + * @param $dir + */ + protected static function rrmdir($dir) + { + foreach (glob($dir . '/*') as $file) { + if (is_dir($file)) { + static::rrmdir($file); + } else { + unlink($file); + } + } + + rmdir($dir); + } + + protected function mkdir($path, $recursive = false) + { + if (! file_exists($path)) { + if ($recursive) { + $this->mkdir(dirname($path)); + } + + try { + mkdir($path); + chmod($path, $this->dir_mode); + } catch (Exception $e) { + throw new IcingaException('Could not create path "%s": %s', $path, $e->getMessage()); + } + } + } +} diff --git a/library/Director/Core/RestApiClient.php b/library/Director/Core/RestApiClient.php new file mode 100644 index 0000000..b0854ff --- /dev/null +++ b/library/Director/Core/RestApiClient.php @@ -0,0 +1,276 @@ +<?php + +namespace Icinga\Module\Director\Core; + +use Icinga\Application\Benchmark; +use RuntimeException; + +class RestApiClient +{ + protected $version = 'v1'; + + protected $peer; + + protected $port; + + protected $user; + + protected $pass; + + protected $curl; + + protected $readBuffer = ''; + + protected $onEvent; + + protected $onEventWantsRaw; + + protected $keepAlive = true; + + public function __construct($peer, $port = 5665, $cn = null) + { + $this->peer = $peer; + $this->port = $port; + } + + // TODO: replace with Web2 CA trust resource plus cert and get rid + // of user/pass or at least strongly advise against using it + public function setCredentials($user, $pass) + { + $this->user = $user; + $this->pass = $pass; + + return $this; + } + + public function onEvent($callback, $raw = false) + { + $this->onEventWantsRaw = $raw; + $this->onEvent = $callback; + + return $this; + } + + public function getPeerIdentity() + { + return $this->peer; + } + + public function setKeepAlive($keepAlive = true) + { + $this->keepAlive = (bool) $keepAlive; + + return $this; + } + + protected function url($url) + { + return sprintf('https://%s:%d/%s/%s', $this->peer, $this->port, $this->version, $url); + } + + /** + * @param $method + * @param $url + * @param null $body + * @param bool $raw + * @param bool $stream + * @return RestApiResponse + */ + public function request($method, $url, $body = null, $raw = false, $stream = false) + { + if (function_exists('curl_version')) { + return $this->curlRequest($method, $url, $body, $raw, $stream); + } else { + throw new RuntimeException( + 'No CURL extension detected, it must be installed and enabled' + ); + } + } + + protected function curlRequest($method, $url, $body = null, $raw = false, $stream = false) + { + $auth = sprintf('%s:%s', $this->user, $this->pass); + $headers = [ + 'Host: ' . $this->getPeerIdentity(), + ]; + + if (! $this->keepAlive) { + // This fails on Icinga 2.9: + // $headers[] = 'Connection: close'; + } + + if (! $raw) { + $headers[] = 'Accept: application/json'; + } + + if ($body !== null) { + $body = Json::encode($body); + $headers[] = 'Content-Type: application/json'; + } + + $curl = $this->curl(); + $opts = [ + CURLOPT_URL => $this->url($url), + CURLOPT_HTTPHEADER => $headers, + CURLOPT_USERPWD => $auth, + CURLOPT_CUSTOMREQUEST => strtoupper($method), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CONNECTTIMEOUT => 3, + + // TODO: Fix this! + CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_SSL_VERIFYPEER => false, + ]; + + if ($body !== null) { + $opts[CURLOPT_POSTFIELDS] = $body; + } + + if ($stream) { + $opts[CURLOPT_WRITEFUNCTION] = [$this, 'readPart']; + $opts[CURLOPT_TCP_NODELAY] = 1; + } + + curl_setopt_array($curl, $opts); + // TODO: request headers, validate status code + + Benchmark::measure('Rest Api, sending ' . $url); + $res = curl_exec($curl); + if ($res === false) { + $error = curl_error($curl); + $this->disconnect(); + + throw new RuntimeException("CURL ERROR: $error"); + } + + $statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); + if ($statusCode === 401) { + $this->disconnect(); + throw new RuntimeException( + 'Unable to authenticate, please check your API credentials' + ); + } + + Benchmark::measure('Rest Api, got response'); + if (! $this->keepAlive) { + $this->disconnect(); + } + + if ($stream) { + return $this; + } + + if ($raw) { + return $res; + } else { + return RestApiResponse::fromJsonResult($res); + } + } + + /** + * @param resource $curl + * @param $data + * @return int + */ + protected function readPart($curl, &$data) + { + $length = strlen($data); + $this->readBuffer .= $data; + $this->processEvents(); + return $length; + } + + public function get($url, $body = null) + { + return $this->request('get', $url, $body); + } + + public function getRaw($url, $body = null) + { + return $this->request('get', $url, $body, true); + } + + public function post($url, $body = null) + { + return $this->request('post', $url, $body); + } + + public function put($url, $body = null) + { + return $this->request('put', $url, $body); + } + + public function delete($url, $body = null) + { + return $this->request('delete', $url, $body); + } + + /** + * @return resource + */ + protected function curl() + { + if ($this->curl === null) { + $this->curl = curl_init(sprintf('https://%s:%d', $this->peer, $this->port)); + if (! $this->curl) { + throw new RuntimeException('CURL INIT ERROR: ' . curl_error($this->curl)); + } + } + + return $this->curl; + } + + protected function processEvents() + { + $offset = 0; + while (false !== ($pos = strpos($this->readBuffer, "\n", $offset))) { + if ($pos === $offset) { + // echo "Got empty line $offset / $pos\n"; + $offset = $pos + 1; + continue; + } + $this->processReadBuffer($offset, $pos); + + $offset = $pos + 1; + } + + if ($offset > 0) { + $this->readBuffer = substr($this->readBuffer, $offset + 1); + } + + // echo "REMAINING: " . strlen($this->readBuffer) . "\n"; + } + + protected function processReadBuffer($offset, $pos) + { + if ($this->onEvent === null) { + return; + } + + $func = $this->onEvent; + $str = substr($this->readBuffer, $offset, $pos); + // printf("Processing %s bytes\n", strlen($str)); + + if ($this->onEventWantsRaw) { + $func($str); + } else { + $func(Json::decode($str)); + } + } + + public function disconnect() + { + if ($this->curl !== null) { + if (is_resource($this->curl)) { + @curl_close($this->curl); + } + + $this->curl = null; + } + } + + public function __destruct() + { + $this->disconnect(); + } +} diff --git a/library/Director/Core/RestApiResponse.php b/library/Director/Core/RestApiResponse.php new file mode 100644 index 0000000..523ed35 --- /dev/null +++ b/library/Director/Core/RestApiResponse.php @@ -0,0 +1,149 @@ +<?php + +namespace Icinga\Module\Director\Core; + +use Icinga\Exception\IcingaException; +use Icinga\Exception\NotFoundError; + +class RestApiResponse +{ + protected $errorMessage; + + protected $results; + + protected function __construct() + { + } + + public static function fromJsonResult($json) + { + $response = new static; + return $response->parseJsonResult($json); + } + + public static function fromErrorMessage($error) + { + $response = new static; + $response->errorMessage = $error; + + return $response; + } + + public function getResult($desiredKey, $filter = array()) + { + return $this->extractResult($this->results, $desiredKey, $filter); + } + + public function getRaw($key = null, $default = null) + { + if ($key === null) { + return $this->results; + } elseif (isset($this->results[0]) && property_exists($this->results[0], $key)) { + return $this->results[0]->$key; + } else { + return $default; + } + } + + public function getSingleResult() + { + if ($this->isErrorCode($this->results[0]->code)) { + throw new IcingaException( + $this->results[0]->status + ); + } else { + return $this->results[0]->result; + } + } + + protected function isErrorCode($code) + { + $code = (int) ceil($code); + return $code >= 400; + } + + protected function extractResult($results, $desiredKey, $filter = array()) + { + $response = array(); + foreach ($results as $result) { + foreach ($filter as $key => $val) { + if (! property_exists($result, $key)) { + continue; + } + if ($result->$key !== $val) { + continue; + } + } + if (! property_exists($result, $desiredKey)) { + continue; + } + + $response[$result->$desiredKey] = $result; + } + return $response; + } + + public function getErrorMessage() + { + return $this->errorMessage; + } + + public function succeeded() + { + return $this->errorMessage === null; + } + + protected function parseJsonResult($json) + { + $result = @json_decode($json); + if ($result === null) { + $this->setJsonError(); + // <h1>Bad Request</h1><p><pre>bad version</pre></p> + throw new IcingaException( + 'Parsing JSON result failed: ' + . $this->errorMessage + . ' (Got: ' . substr($json, 0, 60) . ')' + ); + } + if (property_exists($result, 'error')) { + if (property_exists($result, 'status')) { + if ((int) $result->error === 404) { + throw new NotFoundError($result->status); + } else { + throw new IcingaException('API request failed: ' . $result->status); + } + } else { + throw new IcingaException('API request failed: ' . var_export($result, 1)); + } + } + + $this->results = $result->results; // TODO: Check if set + return $this; + } + + // TODO: just return json_last_error_msg() for PHP >= 5.5.0 + protected function setJsonError() + { + switch (json_last_error()) { + case JSON_ERROR_DEPTH: + $this->errorMessage = 'The maximum stack depth has been exceeded'; + break; + case JSON_ERROR_CTRL_CHAR: + $this->errorMessage = 'Control character error, possibly incorrectly encoded'; + break; + case JSON_ERROR_STATE_MISMATCH: + $this->errorMessage = 'Invalid or malformed JSON'; + break; + case JSON_ERROR_SYNTAX: + $this->errorMessage = 'Syntax error'; + break; + case JSON_ERROR_UTF8: + $this->errorMessage = 'Malformed UTF-8 characters, possibly incorrectly encoded'; + break; + default: + $this->errorMessage = 'An error occured when parsing a JSON string'; + } + + return $this; + } +} diff --git a/library/Director/CoreBeta/ApiStream.php b/library/Director/CoreBeta/ApiStream.php new file mode 100644 index 0000000..478fd40 --- /dev/null +++ b/library/Director/CoreBeta/ApiStream.php @@ -0,0 +1,57 @@ +<?php + +namespace Icinga\Module\Director\CoreBeta; + +use Exception; + +class ApiClient extends Stream +{ + protected $port; + + public static function create($peer, $port = 5665) + { + $stream = new static(); + } + + protected function createClientConnection() + { + $context = $this->createSslContext(); + if ($context === false) { + echo "Unable to set SSL options\n"; + return false; + } + + $conn = stream_socket_client( + 'ssl://' . $this->peername . ':' . $this->peerport, + $errno, + $errstr, + 15, + STREAM_CLIENT_CONNECT, + $context + ); + + return $conn; + } + + protected function createSslContext() + { + $local = 'ssl://' . $this->local; + $context = stream_context_create(); + + // Hack, we need key and cert: + $certfile = preg_replace('~\..+$~', '', $this->certname) . '.combi'; + + $options = array( + 'ssl' => array( + 'verify_host' => true, + 'cafile' => $this->ssldir . '/ca.crt', + 'local_cert' => $this->ssldir . '/' . $certfile, + 'CN_match' => 'monitor1', + ) + ); + + $result = stream_context_set_option($context, $options); + + return $context; + } +} diff --git a/library/Director/CoreBeta/Stream.php b/library/Director/CoreBeta/Stream.php new file mode 100644 index 0000000..5add9a3 --- /dev/null +++ b/library/Director/CoreBeta/Stream.php @@ -0,0 +1,17 @@ +<?php + +namespace Icinga\Module\Director\CoreBeta; + +abstract class Stream +{ + protected $stream; + + protected $buffer = ''; + + protected $bufferLength = 0; + + protected function __construct($stream) + { + $this->stream = $stream; + } +} diff --git a/library/Director/CoreBeta/StreamContext.php b/library/Director/CoreBeta/StreamContext.php new file mode 100644 index 0000000..4844b79 --- /dev/null +++ b/library/Director/CoreBeta/StreamContext.php @@ -0,0 +1,89 @@ +<?php + +namespace Icinga\Module\Director\CoreBeta; + +use Icinga\Exception\ProgrammingError; + +class StreamContext +{ + protected $options = array(); + + public function ssl() + { + if ($this->ssl === null) { + $this->ssl = new StreamContextSslOptions(); + } + + return $this->ssl; + } + + public function isSsl() + { + return $this->ssl !== null; + } + + public function setCA(CA $ca) + { + // $this->options + } + + protected function createSslContext() + { + $local = 'ssl://' . $this->local; + $context = stream_context_create(); + + // Hack, we need key and cert: + $certfile = preg_replace('~\..+$~', '', $this->certname) . '.combi'; + + $options = array( + 'ssl' => array( + 'verify_host' => true, + 'cafile' => $this->ssldir . '/ca.crt', + 'local_cert' => $this->ssldir . '/' . $certfile, + 'CN_match' => 'monitor1', + ) + ); + + $result = stream_context_set_option($context, $options); + + return $context; + } + + public function setContextOptions($options) + { + if (array_key_exists('ssl', $options)) { + throw new ProgrammingError('Direct access to ssl options is not allowed'); + } + } + + protected function reallySetContextOptions($options) + { + if ($this->context === null) { + $this->options = $options; + } else { + stream_context_set_option($this->context, $options); + } + } + + protected function lazyContext() + { + if ($this->context === null) { + $this->context = stream_context_create(); + $this->setContextOptions($this->getOptions()); + + // stream_context_set_option($this->context + if ($this->isSsl()) { + $this->options['ssl'] = $this->ssl()->getOptions(); + } + + $result = stream_context_set_option($this->context, $this->options); + } + + return $this->context; + } + + public function getRawContext() + { + return $this->lazyContext(); + } +} diff --git a/library/Director/CoreBeta/StreamContextSslOptions.php b/library/Director/CoreBeta/StreamContextSslOptions.php new file mode 100644 index 0000000..d01d4a5 --- /dev/null +++ b/library/Director/CoreBeta/StreamContextSslOptions.php @@ -0,0 +1,52 @@ +<?php + +namespace Icinga\Module\Director\CoreBeta; + +use Icinga\Exception\ProgrammingError; + +class StreamContextSslOptions +{ + protected $options = array( + 'verify_peer' => true, + ); + + public function setCA(CA $ca) + { + $this->ca = $ca; + } + + public function capturePeerCert($capture = true) + { + $this->options['capture_peer_cert'] = (bool) $capture; + return $this; + } + + public function capturePeerChain($capture = true) + { + $this->options['capture_peer_chain'] = (bool) $capture; + return $this; + } + + public function setCiphers($ciphers) + { + $this->options['ciphers'] = $ciphers; + return $this; + } + + public function setPeerName($name) + { + if (version_compare(PHP_VERSION, '5.6.0') >= 0) { + $this->options['peer_name'] = $name; + $this->options['verify_peer_name'] = true; + } else { + $this->options['CN_match'] = $name; + } + return $this; + } + + public function getOptions() + { + // TODO: Fail on missing cert + return $this->options; + } +} |