summaryrefslogtreecommitdiffstats
path: root/library/Director/Core/CoreApi.php
diff options
context:
space:
mode:
Diffstat (limited to 'library/Director/Core/CoreApi.php')
-rw-r--r--library/Director/Core/CoreApi.php940
1 files changed, 940 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;
+ }
+}