diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 13:17:31 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 13:17:31 +0000 |
commit | f66ab8dae2f3d0418759f81a3a64dc9517a62449 (patch) | |
tree | fbff2135e7013f196b891bbde54618eb050e4aaf /application/controllers | |
parent | Initial commit. (diff) | |
download | icingaweb2-module-director-f66ab8dae2f3d0418759f81a3a64dc9517a62449.tar.xz icingaweb2-module-director-f66ab8dae2f3d0418759f81a3a64dc9517a62449.zip |
Adding upstream version 1.10.2.upstream/1.10.2
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
66 files changed, 6666 insertions, 0 deletions
diff --git a/application/controllers/ApiuserController.php b/application/controllers/ApiuserController.php new file mode 100644 index 0000000..36438ae --- /dev/null +++ b/application/controllers/ApiuserController.php @@ -0,0 +1,9 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectController; + +class ApiuserController extends ObjectController +{ +} diff --git a/application/controllers/ApiusersController.php b/application/controllers/ApiusersController.php new file mode 100644 index 0000000..5597521 --- /dev/null +++ b/application/controllers/ApiusersController.php @@ -0,0 +1,9 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectsController; + +class ApiusersController extends ObjectsController +{ +} diff --git a/application/controllers/BasketController.php b/application/controllers/BasketController.php new file mode 100644 index 0000000..8733d16 --- /dev/null +++ b/application/controllers/BasketController.php @@ -0,0 +1,416 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Exception; +use gipfl\Diff\HtmlRenderer\InlineDiff; +use gipfl\Diff\PhpDiff; +use gipfl\IcingaWeb2\Link; +use gipfl\Web\Table\NameValueTable; +use gipfl\Web\Widget\Hint; +use Icinga\Date\DateFormatter; +use Icinga\Module\Director\Core\Json; +use Icinga\Module\Director\Data\Exporter; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\DirectorObject\Automation\Basket; +use Icinga\Module\Director\DirectorObject\Automation\BasketSnapshot; +use Icinga\Module\Director\DirectorObject\Automation\BasketSnapshotFieldResolver; +use Icinga\Module\Director\DirectorObject\Automation\CompareBasketObject; +use Icinga\Module\Director\Forms\AddToBasketForm; +use Icinga\Module\Director\Forms\BasketCreateSnapshotForm; +use Icinga\Module\Director\Forms\BasketForm; +use Icinga\Module\Director\Forms\BasketUploadForm; +use Icinga\Module\Director\Forms\RestoreBasketForm; +use Icinga\Module\Director\Web\Controller\ActionController; +use ipl\Html\Html; +use Icinga\Module\Director\Web\Table\BasketSnapshotTable; + +class BasketController extends ActionController +{ + protected $isApified = true; + + protected function basketTabs() + { + $name = $this->params->get('name'); + return $this->tabs()->add('show', [ + 'label' => $this->translate('Basket'), + 'url' => 'director/basket', + 'urlParams' => ['name' => $name] + ])->add('snapshots', [ + 'label' => $this->translate('Snapshots'), + 'url' => 'director/basket/snapshots', + 'urlParams' => ['name' => $name] + ]); + } + + /** + * @throws \Icinga\Exception\NotFoundError + * @throws \Icinga\Exception\MissingParameterException + */ + public function indexAction() + { + $this->actions()->add( + Link::create( + $this->translate('Back'), + 'director/baskets', + null, + ['class' => 'icon-left-big'] + ) + ); + $basket = $this->requireBasket(); + $this->basketTabs()->activate('show'); + $this->addTitle($basket->get('basket_name')); + if ($basket->isEmpty()) { + $this->content()->add(Hint::info($this->translate('This basket is empty'))); + } + $this->content()->add( + (new BasketForm())->setObject($basket)->handleRequest() + ); + } + + /** + * @throws \Icinga\Exception\MissingParameterException + */ + public function addAction() + { + $this->actions()->add( + Link::create( + $this->translate('Baskets'), + 'director/baskets', + null, + ['class' => 'icon-tag'] + ) + ); + $this->addSingleTab($this->translate('Add to Basket')); + $this->addTitle($this->translate('Add chosen objects to a Configuration Basket')); + $form = new AddToBasketForm(); + $form->setDb($this->db()) + ->setType($this->params->getRequired('type')) + ->setNames($this->url()->getParams()->getValues('names')) + ->handleRequest(); + $this->content()->add($form); + } + + public function createAction() + { + $this->actions()->add( + Link::create( + $this->translate('back'), + 'director/baskets', + null, + ['class' => 'icon-left-big'] + ) + ); + $this->addSingleTab($this->translate('Create Basket')); + $this->addTitle($this->translate('Create a new Configuration Basket')); + $form = (new BasketForm()) + ->setDb($this->db()) + ->handleRequest(); + $this->content()->add($form); + } + + public function uploadAction() + { + $this->actions()->add( + Link::create( + $this->translate('back'), + 'director/baskets', + null, + ['class' => 'icon-left-big'] + ) + ); + $this->addSingleTab($this->translate('Upload a Basket')); + $this->addTitle($this->translate('Upload a Configuration Basket')); + $form = (new BasketUploadForm()) + ->setDb($this->db()) + ->handleRequest(); + $this->content()->add($form); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function snapshotsAction() + { + $name = $this->params->get('name'); + if ($name === null || $name === '') { + $basket = null; + } else { + $basket = Basket::load($name, $this->db()); + } + if ($basket === null) { + $this->addTitle($this->translate('Basket Snapshots')); + $this->addSingleTab($this->translate('Snapshots')); + } else { + $this->addTitle(sprintf( + $this->translate('%s: Snapshots'), + $basket->get('basket_name') + )); + $this->basketTabs()->activate('snapshots'); + } + if ($basket !== null) { + $this->content()->add( + (new BasketCreateSnapshotForm()) + ->setBasket($basket) + ->handleRequest() + ); + } + $table = new BasketSnapshotTable($this->db()); + if ($basket !== null) { + $table->setBasket($basket); + } + + $table->renderTo($this); + } + + /** + * @throws \Icinga\Exception\MissingParameterException + * @throws \Icinga\Exception\NotFoundError + */ + public function snapshotAction() + { + $basket = $this->requireBasket(); + $snapshot = BasketSnapshot::load([ + 'basket_uuid' => $basket->get('uuid'), + 'ts_create' => $this->params->getRequired('ts'), + ], $this->db()); + $snapSum = bin2hex($snapshot->get('content_checksum')); + + if ($this->params->get('action') === 'download') { + $this->getResponse()->setHeader('Content-Type', 'application/json', true); + $this->getResponse()->setHeader('Content-Disposition', sprintf( + 'attachment; filename=Director-Basket_%s_%s.json', + str_replace([' ', '"'], ['_', '_'], iconv( + 'UTF-8', + 'ISO-8859-1//IGNORE', + $basket->get('basket_name') + )), + substr($snapSum, 0, 7) + )); + echo $snapshot->getJsonDump(); + return; + } + + $this->addTitle( + $this->translate('%s: %s (Snapshot)'), + $basket->get('basket_name'), + substr($snapSum, 0, 7) + ); + + $this->actions()->add([ + Link::create( + $this->translate('Show Basket'), + 'director/basket', + ['name' => $basket->get('basket_name')], + ['data-base-target' => '_next'] + ), + Link::create( + $this->translate('Restore'), + $this->url()->with('action', 'restore'), + null, + ['class' => 'icon-rewind'] + ), + Link::create( + $this->translate('Download'), + $this->url() + ->with([ + 'action' => 'download', + 'dbResourceName' => $this->getDbResourceName() + ]), + null, + [ + 'class' => 'icon-download', + 'target' => '_blank' + ] + ), + ]); + + $properties = new NameValueTable(); + $properties->addNameValuePairs([ + $this->translate('Created') => DateFormatter::formatDateTime($snapshot->get('ts_create') / 1000), + $this->translate('Content Checksum') => bin2hex($snapshot->get('content_checksum')), + ]); + $this->content()->add($properties); + + if ($this->params->get('action') === 'restore') { + $form = new RestoreBasketForm(); + $form + ->setSnapshot($snapshot) + ->handleRequest(); + $this->content()->add($form); + $targetDbName = $form->getValue('target_db'); + $connection = $form->getDb(); + } else { + $targetDbName = null; + $connection = $this->db(); + } + + $json = $snapshot->getJsonDump(); + $this->addSingleTab($this->translate('Snapshot')); + $all = Json::decode($json); + $exporter = new Exporter($this->db()); + $fieldResolver = new BasketSnapshotFieldResolver($all, $connection); + foreach ($all as $type => $objects) { + if ($type === 'Datafield') { + // TODO: we should now be able to show all fields and link + // to a "diff" for the ones that should be created + // $this->content()->add(Html::tag('h2', sprintf('+%d Datafield(s)', count($objects)))); + continue; + } + $table = new NameValueTable(); + $table->addAttributes([ + 'class' => ['table-basket-changes', 'table-row-selectable'], + 'data-base-target' => '_next', + ]); + foreach ($objects as $key => $object) { + $linkParams = [ + 'name' => $basket->get('basket_name'), + 'checksum' => $this->params->get('checksum'), + 'ts' => $this->params->get('ts'), + 'type' => $type, + 'key' => $key, + ]; + if ($targetDbName !== null) { + $linkParams['target_db'] = $targetDbName; + } + try { + $current = BasketSnapshot::instanceByIdentifier($type, $key, $connection); + if ($current === null) { + $table->addNameValueRow( + $key, + Link::create( + Html::tag('strong', ['style' => 'color: green'], $this->translate('new')), + 'director/basket/snapshotobject', + $linkParams + ) + ); + continue; + } + $currentExport = $exporter->export($current); + $fieldResolver->tweakTargetIds($currentExport); + + // Ignore originalId + if (isset($currentExport->originalId)) { + unset($currentExport->originalId); + } + if (isset($object->originalId)) { + unset($object->originalId); + } + $hasChanged = ! CompareBasketObject::equals($currentExport, $object); + $table->addNameValueRow( + $key, + $hasChanged + ? Link::create( + Html::tag('strong', ['style' => 'color: orange'], $this->translate('modified')), + 'director/basket/snapshotobject', + $linkParams + ) + : Html::tag('span', ['style' => 'color: green'], $this->translate('unchanged')) + ); + } catch (Exception $e) { + $table->addNameValueRow( + $key, + Html::tag('a', sprintf( + '%s (%s:%d)', + $e->getMessage(), + basename($e->getFile()), + $e->getLine() + )) + ); + } + } + $this->content()->add(Html::tag('h2', $type)); + $this->content()->add($table); + } + $this->content()->add(Html::tag('div', ['style' => 'height: 5em'])); + } + + /** + * @throws \Icinga\Exception\MissingParameterException + * @throws \Icinga\Exception\NotFoundError + */ + public function snapshotobjectAction() + { + $basket = $this->requireBasket(); + $snapshot = BasketSnapshot::load([ + 'basket_uuid' => $basket->get('uuid'), + 'ts_create' => $this->params->getRequired('ts'), + ], $this->db()); + $snapshotUrl = $this->url()->without('type')->without('key')->setPath('director/basket/snapshot'); + $type = $this->params->get('type'); + $key = $this->params->get('key'); + + $this->addTitle($this->translate('Single Object Diff')); + $this->content()->add(Hint::info(Html::sprintf( + $this->translate('Comparing %s "%s" from Snapshot "%s" to current config'), + $type, + $key, + Link::create( + substr(bin2hex($snapshot->get('content_checksum')), 0, 7), + $snapshotUrl, + null, + ['data-base-target' => '_next'] + ) + ))); + $this->actions()->add([ + Link::create( + $this->translate('back'), + $snapshotUrl, + null, + ['class' => 'icon-left-big'] + ), + /* + Link::create( + $this->translate('Restore'), + $this->url()->with('action', 'restore'), + null, + ['class' => 'icon-rewind'] + ) + */ + ]); + $exporter = new Exporter($this->db()); + $json = $snapshot->getJsonDump(); + $this->addSingleTab($this->translate('Snapshot')); + $objects = Json::decode($json); + $targetDbName = $this->params->get('target_db'); + if ($targetDbName === null) { + $connection = $this->db(); + } else { + $connection = Db::fromResourceName($targetDbName); + } + $fieldResolver = new BasketSnapshotFieldResolver($objects, $connection); + $objectFromBasket = $objects->$type->$key; + unset($objectFromBasket->originalId); + CompareBasketObject::normalize($objectFromBasket); + $objectFromBasket = Json::encode($objectFromBasket, JSON_PRETTY_PRINT); + $current = BasketSnapshot::instanceByIdentifier($type, $key, $connection); + if ($current === null) { + $current = ''; + } else { + $exported = $exporter->export($current); + $fieldResolver->tweakTargetIds($exported); + unset($exported->originalId); + CompareBasketObject::normalize($exported); + $current = Json::encode($exported, JSON_PRETTY_PRINT); + } + + if ($current === $objectFromBasket) { + $this->content()->add([ + Hint::ok('Basket equals current object'), + Html::tag('pre', $current) + ]); + } else { + $this->content()->add(new InlineDiff(new PhpDiff($current, $objectFromBasket))); + } + } + + /** + * @return Basket + * @throws \Icinga\Exception\MissingParameterException + * @throws \Icinga\Exception\NotFoundError + */ + protected function requireBasket() + { + return Basket::load($this->params->getRequired('name'), $this->db()); + } +} diff --git a/application/controllers/BasketsController.php b/application/controllers/BasketsController.php new file mode 100644 index 0000000..6b50b62 --- /dev/null +++ b/application/controllers/BasketsController.php @@ -0,0 +1,53 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use ipl\Html\Html; +use gipfl\IcingaWeb2\Link; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Web\Table\BasketTable; + +class BasketsController extends ActionController +{ + protected $isApified = false; + + public function indexAction() + { + $this->setAutorefreshInterval(10); + $this->addSingleTab($this->translate('Baskets')); + $this->actions()->add([ + Link::create( + $this->translate('Create'), + 'director/basket/create', + null, + ['class' => 'icon-plus'] + ), + Link::create( + $this->translate('Upload'), + 'director/basket/upload', + null, + ['class' => 'icon-upload'] + ), + ]); + $this->addTitle($this->translate('Configuration Baskets')); + $this->content()->add(Html::tag('p', $this->translate( + 'A Configuration Basket references specific Configuration' + . ' Objects or all objects of a specific type. It has been' + . ' designed to share Templates, Import/Sync strategies and' + . ' other base Configuration Objects. It is not a tool to' + . ' operate with single Hosts or Services.' + ))); + $this->content()->add(Html::tag('p', $this->translate( + 'You can create Basket snapshots at any time, this will persist' + . ' a serialized representation of all involved objects at that' + . ' moment in time. Snapshots can be exported, imported, shared' + . ' and restored - to the very same or another Director instance.' + ))); + $table = (new BasketTable($this->db())) + ->setAttribute('data-base-target', '_self'); + // TODO: temporarily disabled, this was a thing in dipl + if (/*$table->hasSearch() || */count($table)) { + $table->renderTo($this); + } + } +} diff --git a/application/controllers/BranchController.php b/application/controllers/BranchController.php new file mode 100644 index 0000000..3b36e83 --- /dev/null +++ b/application/controllers/BranchController.php @@ -0,0 +1,138 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use gipfl\Diff\HtmlRenderer\SideBySideDiff; +use gipfl\Diff\PhpDiff; +use gipfl\IcingaWeb2\Widget\NameValueTable; +use Icinga\Module\Director\Data\Db\DbObjectStore; +use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry; +use Icinga\Module\Director\Db\Branch\BranchActivity; +use Icinga\Module\Director\Db\Branch\BranchStore; +use Icinga\Module\Director\IcingaConfig\IcingaConfig; +use Icinga\Module\Director\Objects\IcingaObject; +use Icinga\Module\Director\Objects\SyncRule; +use Icinga\Module\Director\PlainObjectRenderer; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Web\Controller\BranchHelper; +use Icinga\Module\Director\Web\Widget\IcingaConfigDiff; +use ipl\Html\Html; + +class BranchController extends ActionController +{ + use BranchHelper; + + public function init() + { + parent::init(); + IcingaObject::setDbObjectStore(new DbObjectStore($this->db(), $this->getBranch())); + SyncRule::setDbObjectStore(new DbObjectStore($this->db(), $this->getBranch())); + } + + protected function checkDirectorPermissions() + { + } + + public function activityAction() + { + $this->assertPermission('director/showconfig'); + $ts = $this->params->getRequired('ts'); + $activity = BranchActivity::load($ts, $this->db()); + $store = new BranchStore($this->db()); + $branch = $store->fetchBranchByUuid($activity->getBranchUuid()); + if ($branch->isSyncPreview()) { + $this->addSingleTab($this->translate('Sync Preview')); + $this->addTitle($this->translate('Expected Modification')); + } else { + $this->addSingleTab($this->translate('Activity')); + $this->addTitle($this->translate('Branch Activity')); + } + + $this->content()->add($this->prepareActivityInfo($activity)); + $this->showActivity($activity); + } + + protected function prepareActivityInfo(BranchActivity $activity) + { + $table = new NameValueTable(); + $table->addNameValuePairs([ + $this->translate('Author') => $activity->getAuthor(), + $this->translate('Date') => date('Y-m-d H:i:s', $activity->getTimestamp()), + $this->translate('Action') => $activity->getAction() + . ' ' . preg_replace('/^icinga_/', '', $activity->getObjectTable()) + . ' ' . $activity->getObjectName(), + // $this->translate('Actions') => ['Undo form'], + ]); + return $table; + } + + protected function leftFromActivity(BranchActivity $activity) + { + if ($activity->isActionCreate()) { + return null; + } + $object = DbObjectTypeRegistry::newObject($activity->getObjectTable(), [], $this->db()); + $properties = $this->objectTypeFirst($activity->getFormerProperties()->jsonSerialize()); + foreach ($properties as $key => $value) { + $object->set($key, $value); + } + + return $object; + } + + protected function rightFromActivity(BranchActivity $activity) + { + if ($activity->isActionDelete()) { + return null; + } + $object = DbObjectTypeRegistry::newObject($activity->getObjectTable(), [], $this->db()); + if (! $activity->isActionCreate()) { + foreach ($activity->getFormerProperties()->jsonSerialize() as $key => $value) { + $object->set($key, $value); + } + } + $properties = $this->objectTypeFirst($activity->getModifiedProperties()->jsonSerialize()); + foreach ($properties as $key => $value) { + $object->set($key, $value); + } + + return $object; + } + + protected function objectTypeFirst($properties) + { + $properties = (array) $properties; + if (isset($properties['object_type'])) { + $type = $properties['object_type']; + unset($properties['object_type']); + $properties = ['object_type' => $type] + $properties; + } + + return $properties; + } + + protected function showActivity(BranchActivity $activity) + { + $left = $this->leftFromActivity($activity); + $right = $this->rightFromActivity($activity); + if ($left instanceof IcingaObject || $right instanceof IcingaObject) { + $this->content()->add(new IcingaConfigDiff( + $left ? $left->toSingleIcingaConfig() : $this->createEmptyConfig(), + $right ? $right->toSingleIcingaConfig() : $this->createEmptyConfig() + )); + } else { + $this->content()->add([ + Html::tag('h3', $this->translate('Modification')), + new SideBySideDiff(new PhpDiff( + PlainObjectRenderer::render($left->getProperties()), + PlainObjectRenderer::render($right->getProperties()) + )) + ]); + } + } + + protected function createEmptyConfig() + { + return new IcingaConfig($this->db()); + } +} diff --git a/application/controllers/CommandController.php b/application/controllers/CommandController.php new file mode 100644 index 0000000..de0ba54 --- /dev/null +++ b/application/controllers/CommandController.php @@ -0,0 +1,126 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use gipfl\Web\Widget\Hint; +use Icinga\Module\Director\Objects\IcingaCommandArgument; +use Icinga\Module\Director\Web\Table\BranchedIcingaCommandArgumentTable; +use ipl\Html\Html; +use Icinga\Module\Director\Forms\IcingaCommandArgumentForm; +use Icinga\Module\Director\Objects\IcingaCommand; +use Icinga\Module\Director\Resolver\CommandUsage; +use Icinga\Module\Director\Web\Controller\ObjectController; +use Icinga\Module\Director\Web\Table\IcingaCommandArgumentTable; + +class CommandController extends ObjectController +{ + /** + * @throws \Icinga\Exception\AuthenticationException + * @throws \Icinga\Exception\NotFoundError + * @throws \Icinga\Security\SecurityException + */ + public function init() + { + parent::init(); + $o = $this->object; + if ($o && ! $o->isExternal()) { + if ($this->getBranch()->isBranch()) { + $urlParams = ['uuid' => $o->getUniqueId()->toString()]; + } else { + $urlParams = ['name' => $o->getObjectName()]; + } + $this->tabs()->add('arguments', [ + 'url' => 'director/command/arguments', + 'urlParams' => $urlParams, + 'label' => 'Arguments' + ]); + } + } + + /** + * @throws \Icinga\Exception\NotFoundError + * @throws \Zend_Db_Select_Exception + */ + public function indexAction() + { + if (! $this->getRequest()->isApiRequest()) { + $this->showUsage(); + } + parent::indexAction(); + } + + /** + * @throws \Icinga\Exception\NotFoundError + * @throws \Icinga\Security\SecurityException + * @throws \Zend_Db_Select_Exception + */ + public function renderAction() + { + if ($this->object->isExternal()) { + $this->showUsage(); + } + + parent::renderAction(); + } + + /** + * @throws \Zend_Db_Select_Exception + */ + protected function showUsage() + { + /** @var IcingaCommand $command */ + $command = $this->object; + if ($command->isInUse()) { + $usage = new CommandUsage($command); + $this->content()->add(Hint::info(Html::sprintf( + $this->translate('This Command is currently being used by %s'), + Html::tag('span', null, $usage->getLinks())->setSeparator(', ') + ))->addAttributes([ + 'data-base-target' => '_next' + ])); + } else { + $this->content()->add(Hint::warning($this->translate('This Command is currently not in use'))); + } + } + + public function argumentsAction() + { + $p = $this->params; + /** @var IcingaCommand $o */ + $o = $this->object; + $this->tabs()->activate('arguments'); + $this->addTitle($this->translate('Command arguments: %s'), $o->getObjectName()); + $form = (new IcingaCommandArgumentForm) + ->setBranch($this->getBranch()) + ->setCommandObject($o); + if ($argument = $p->shift('argument')) { + $this->addBackLink('director/command/arguments', [ + 'name' => $p->get('name') + ]); + if ($this->branch->isBranch()) { + $arguments = $o->arguments(); + $argument = $arguments->get($argument); + // IcingaCommandArgument::create((array) $arguments->get($argument)->toFullPlainObject()); + // $argument->setBeingLoadedFromDb(); + } else { + $argument = IcingaCommandArgument::load([ + 'command_id' => $o->get('id'), + 'argument_name' => $argument + ], $this->db()); + } + $form->setObject($argument); + } + $form->handleRequest(); + $this->content()->add([$form]); + if ($this->branch->isBranch()) { + (new BranchedIcingaCommandArgumentTable($o, $this->getBranch()))->renderTo($this); + } else { + (new IcingaCommandArgumentTable($o, $this->getBranch()))->renderTo($this); + } + } + + protected function hasBasketSupport() + { + return true; + } +} diff --git a/application/controllers/CommandsController.php b/application/controllers/CommandsController.php new file mode 100644 index 0000000..246028f --- /dev/null +++ b/application/controllers/CommandsController.php @@ -0,0 +1,20 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectsController; + +class CommandsController extends ObjectsController +{ + public function indexAction() + { + parent::indexAction(); + $validTypes = ['object', 'external_object']; + $type = $this->params->get('type', 'object'); + if (! in_array($type, $validTypes)) { + $type = 'object'; + } + + $this->table->setType($type); + } +} diff --git a/application/controllers/CommandtemplateController.php b/application/controllers/CommandtemplateController.php new file mode 100644 index 0000000..ca5f827 --- /dev/null +++ b/application/controllers/CommandtemplateController.php @@ -0,0 +1,16 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Objects\IcingaCommand; +use Icinga\Module\Director\Web\Controller\TemplateController; + +class CommandtemplateController extends TemplateController +{ + protected function requireTemplate() + { + return IcingaCommand::load([ + 'object_name' => $this->params->get('name') + ], $this->db()); + } +} diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php new file mode 100644 index 0000000..3f8a105 --- /dev/null +++ b/application/controllers/ConfigController.php @@ -0,0 +1,539 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use gipfl\Diff\HtmlRenderer\SideBySideDiff; +use gipfl\Diff\PhpDiff; +use gipfl\Web\Widget\Hint; +use Icinga\Data\Filter\Filter; +use Icinga\Exception\IcingaException; +use Icinga\Exception\NotFoundError; +use Icinga\Module\Director\Db\Branch\Branch; +use Icinga\Module\Director\Deployment\DeploymentStatus; +use Icinga\Module\Director\Forms\DeployConfigForm; +use Icinga\Module\Director\Forms\SettingsForm; +use Icinga\Module\Director\IcingaConfig\IcingaConfig; +use Icinga\Module\Director\Objects\DirectorDeploymentLog; +use Icinga\Module\Director\Settings; +use Icinga\Module\Director\Web\Controller\BranchHelper; +use Icinga\Module\Director\Web\Table\ActivityLogTable; +use Icinga\Module\Director\Web\Table\BranchActivityTable; +use Icinga\Module\Director\Web\Table\ConfigFileDiffTable; +use Icinga\Module\Director\Web\Table\DeploymentLogTable; +use Icinga\Module\Director\Web\Table\GeneratedConfigFileTable; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Web\Tabs\InfraTabs; +use Icinga\Module\Director\Web\Widget\ActivityLogInfo; +use Icinga\Module\Director\Web\Widget\DeployedConfigInfoHeader; +use Icinga\Module\Director\Web\Widget\ShowConfigFile; +use Icinga\Web\Notification; +use Exception; +use RuntimeException; +use ipl\Html\Html; +use ipl\Html\HtmlString; +use gipfl\IcingaWeb2\Icon; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Url; + +class ConfigController extends ActionController +{ + use BranchHelper; + + protected $isApified = true; + + protected function checkDirectorPermissions() + { + } + + /** + * @throws \Icinga\Security\SecurityException + */ + public function deploymentsAction() + { + if ($this->sendNotFoundForRestApi()) { + return; + } + $this->assertPermission('director/deploy'); + $this->addTitle($this->translate('Deployments')); + try { + if (DirectorDeploymentLog::hasUncollected($this->db())) { + $this->setAutorefreshInterval(2); + } else { + $this->setAutorefreshInterval(20); + } + } catch (Exception $e) { + $this->content()->prepend(Hint::warning($e->getMessage())); + // No problem, Icinga might be reloading + } + + if (! $this->getBranch()->isBranch()) { + // TODO: a form! + $this->actions()->add(Link::create( + $this->translate('Render config'), + 'director/config/store', + null, + ['class' => 'icon-wrench'] + )); + } + + $this->tabs(new InfraTabs($this->Auth()))->activate('deploymentlog'); + $table = new DeploymentLogTable($this->db()); + try { + // Move elsewhere + $table->setActiveStageName( + $this->api()->getActiveStageName() + ); + } catch (Exception $e) { + // Don't care + } + + $table->renderTo($this); + } + + /** + * @throws NotFoundError + * @throws \Icinga\Module\Director\Exception\DuplicateKeyException + * @throws \Icinga\Security\SecurityException + */ + public function deployAction() + { + $request = $this->getRequest(); + if (! $request->isApiRequest()) { + throw new NotFoundError('Not found'); + } + + if (! $request->isPost()) { + throw new RuntimeException(sprintf( + 'Unsupported method: %s', + $request->getMethod() + )); + } + $this->assertPermission('director/deploy'); + + // TODO: require POST + $checksum = $this->params->get('checksum'); + if ($checksum) { + $config = IcingaConfig::load(hex2bin($checksum), $this->db()); + } else { + $config = IcingaConfig::generate($this->db()); + $checksum = $config->getHexChecksum(); + } + + try { + $this->api()->wipeInactiveStages($this->db()); + } catch (Exception $e) { + $this->deploymentFailed($checksum, $e->getMessage()); + } + + if ($this->api()->dumpConfig($config, $this->db())) { + $this->deploymentSucceeded($checksum); + } else { + $this->deploymentFailed($checksum); + } + } + + public function deploymentStatusAction() + { + if ($this->sendNotFoundUnlessRestApi()) { + return; + } + $db = $this->db(); + $api = $this->api(); + $status = new DeploymentStatus($db, $api); + $result = $status->getDeploymentStatus($this->params->get('configs'), $this->params->get('activities')); + + $this->sendJson($this->getResponse(), (object) $result); + } + + /** + * @throws \Icinga\Security\SecurityException + */ + public function activitiesAction() + { + if ($this->sendNotFoundForRestApi()) { + return; + } + $this->assertPermission('director/audit'); + $this->showOptionalBranchActivity(); + $this->setAutorefreshInterval(10); + $this->tabs(new InfraTabs($this->Auth()))->activate('activitylog'); + $this->addTitle($this->translate('Activity Log')); + $lastDeployedId = $this->db()->getLastDeploymentActivityLogId(); + $table = new ActivityLogTable($this->db()); + $table->setLastDeployedId($lastDeployedId); + if ($idRangeEx = $this->url()->getParam('idRangeEx')) { + $table->applyFilter(Filter::fromQueryString($idRangeEx)); + } + $filter = Filter::fromQueryString( + $this->url()->without(['page', 'limit', 'q', 'idRangeEx'])->getQueryString() + ); + $table->applyFilter($filter); + if ($this->url()->hasParam('author')) { + $this->actions()->add(Link::create( + $this->translate('All changes'), + $this->url() + ->without(['author', 'page']), + null, + ['class' => 'icon-users', 'data-base-target' => '_self'] + )); + } else { + $this->actions()->add(Link::create( + $this->translate('My changes'), + $this->url() + ->with('author', $this->Auth()->getUser()->getUsername()) + ->without('page'), + null, + ['class' => 'icon-user', 'data-base-target' => '_self'] + )); + } + if ($this->hasPermission('director/deploy') && ! $this->getBranch()->isBranch()) { + if ($this->db()->hasDeploymentEndpoint()) { + $this->actions()->add(DeployConfigForm::load() + ->setDb($this->db()) + ->setApi($this->api()) + ->handleRequest()); + } + } + + $table->renderTo($this); + } + + /** + * @throws IcingaException + * @throws \Icinga\Exception\Http\HttpNotFoundException + * @throws \Icinga\Exception\ProgrammingError + */ + public function activityAction() + { + if ($this->sendNotFoundForRestApi()) { + return; + } + $this->assertPermission('director/showconfig'); + $p = $this->params; + $info = new ActivityLogInfo( + $this->db(), + $p->get('type'), + $p->get('name') + ); + + $info->setChecksum($p->get('checksum')) + ->setId($p->get('id')); + + $this->tabs($info->getTabs($this->url())); + $info->showTab($this->params->get('show')); + + $this->addTitle($info->getTitle()); + $this->controls()->prepend($info->getPagination($this->url())); + $this->content()->add($info); + } + + /** + * @throws \Icinga\Security\SecurityException + */ + public function settingsAction() + { + if ($this->sendNotFoundForRestApi()) { + return; + } + $this->assertPermission('director/admin'); + + $this->addSingleTab($this->translate('Settings')) + ->addTitle($this->translate('Global Director Settings')); + $this->content()->add( + SettingsForm::load() + ->setSettings(new Settings($this->db())) + ->handleRequest() + ); + } + + /** + * Show all files for a given config + * + * @throws \Icinga\Exception\MissingParameterException + * @throws \Icinga\Security\SecurityException + */ + public function filesAction() + { + if ($this->sendNotFoundForRestApi()) { + return; + } + $this->assertPermission('director/showconfig'); + $config = IcingaConfig::load( + hex2bin($this->params->getRequired('checksum')), + $this->db() + ); + $deploymentId = $this->params->get('deployment_id'); + + $tabs = $this->tabs(); + if ($deploymentId) { + $tabs->add('deployment', [ + 'label' => $this->translate('Deployment'), + 'url' => 'director/deployment', + 'urlParams' => ['id' => $deploymentId] + ]); + } + + $tabs->add('config', [ + 'label' => $this->translate('Config'), + 'url' => $this->url(), + ])->activate('config'); + + $this->addTitle($this->translate('Generated config')); + $this->content()->add(new DeployedConfigInfoHeader( + $config, + $this->db(), + $this->api(), + $this->getBranch(), + $deploymentId + )); + + GeneratedConfigFileTable::load($config, $this->db()) + ->setActiveFilename($this->params->get('active_file')) + ->setDeploymentId($deploymentId) + ->renderTo($this); + } + + /** + * Show a single file + * + * @throws \Icinga\Exception\MissingParameterException + * @throws \Icinga\Security\SecurityException + */ + public function fileAction() + { + if ($this->sendNotFoundForRestApi()) { + return; + } + $this->assertPermission('director/showconfig'); + $filename = $this->params->getRequired('file_path'); + $this->configTabs()->add('file', array( + 'label' => $this->translate('Rendered file'), + 'url' => $this->url(), + ))->activate('file'); + + $params = $this->getConfigTabParams(); + if ('deployment' === $this->params->get('backTo')) { + $this->addBackLink('director/deployment', ['id' => $params['deployment_id']]); + } else { + $params['active_file'] = $filename; + $this->addBackLink('director/config/files', $params); + } + + $config = IcingaConfig::load(hex2bin($this->params->get('config_checksum')), $this->db()); + $this->addTitle($this->translate('Config file "%s"'), $filename); + $this->content()->add(new ShowConfigFile( + $config->getFile($filename), + $this->params->get('highlight'), + $this->params->get('highlightSeverity') + )); + } + + /** + * TODO: Check if this can be removed + * + * @throws \Icinga\Security\SecurityException + */ + public function storeAction() + { + $this->assertPermission('director/deploy'); + try { + $config = IcingaConfig::generate($this->db()); + } catch (Exception $e) { + Notification::error($e->getMessage()); + $this->redirectNow('director/config/deployments'); + } + $this->redirectNow( + Url::fromPath( + 'director/config/files', + array('checksum' => $config->getHexChecksum()) + ) + ); + } + + /** + * @throws \Icinga\Security\SecurityException + */ + public function diffAction() + { + if ($this->sendNotFoundForRestApi()) { + return; + } + $this->assertPermission('director/showconfig'); + + $db = $this->db(); + $this->addTitle($this->translate('Config diff')); + $this->addSingleTab($this->translate('Config diff')); + + $leftSum = $this->params->get('left'); + $rightSum = $this->params->get('right'); + + $configs = $db->enumDeployedConfigs(); + foreach (array($leftSum, $rightSum) as $sum) { + if (! array_key_exists($sum, $configs)) { + $configs[$sum] = substr($sum, 0, 7); + } + } + + $baseUrl = $this->url()->without(['left', 'right']); + $this->content()->add(Html::tag('form', ['action' => (string) $baseUrl, 'method' => 'GET'], [ + new HtmlString($this->view->formSelect( + 'left', + $leftSum, + ['class' => 'autosubmit', 'style' => 'width: 37%'], + [null => $this->translate('- please choose -')] + $configs + )), + Link::create( + Icon::create('flapping'), + $baseUrl, + ['left' => $rightSum, 'right' => $leftSum] + ), + new HtmlString($this->view->formSelect( + 'right', + $rightSum, + ['class' => 'autosubmit', 'style' => 'width: 37%'], + [null => $this->translate('- please choose -')] + $configs + )), + ])); + + if ($rightSum === null || $leftSum === null || ! strlen($rightSum) || ! strlen($leftSum)) { + return; + } + ConfigFileDiffTable::load($leftSum, $rightSum, $this->db())->renderTo($this); + } + + /** + * @throws IcingaException + * @throws \Icinga\Exception\MissingParameterException + */ + public function filediffAction() + { + if ($this->sendNotFoundForRestApi()) { + return; + } + $this->assertPermission('director/showconfig'); + + $p = $this->params; + $db = $this->db(); + $leftSum = $p->getRequired('left'); + $rightSum = $p->getRequired('right'); + $filename = $p->getRequired('file_path'); + + $left = IcingaConfig::load(hex2bin($leftSum), $db); + $right = IcingaConfig::load(hex2bin($rightSum), $db); + + $this + ->addTitle($this->translate('Config file "%s"'), $filename) + ->addSingleTab($this->translate('Diff')) + ->content()->add(new SideBySideDiff(new PhpDiff( + $left->getFile($filename), + $right->getFile($filename) + ))); + } + + protected function showOptionalBranchActivity() + { + if ($this->url()->hasParam('idRangeEx')) { + return; + } + $branch = $this->getBranch(); + if ($branch->isBranch() && (int) $this->params->get('page', '1') === 1) { + $table = new BranchActivityTable($branch->getUuid(), $this->db()); + if (count($table) > 0) { + $this->content()->add(Hint::info(Html::sprintf($this->translate( + 'The following modifications are visible in this %s only...' + ), Branch::requireHook()->linkToBranch( + $branch, + $this->Auth(), + $this->translate('configuration branch') + )))); + $this->content()->add($table); + $this->content()->add(Html::tag('br')); + $this->content()->add(Hint::ok($this->translate( + '...and the modifications below are already in the main branch:' + ))); + $this->content()->add(Html::tag('br')); + } + } + } + + /** + * @param $checksum + */ + protected function deploymentSucceeded($checksum) + { + if ($this->getRequest()->isApiRequest()) { + $this->sendJson($this->getResponse(), (object) array('checksum' => $checksum)); + return; + } else { + $url = Url::fromPath('director/config/deployments'); + Notification::success( + $this->translate('Config has been submitted, validation is going on') + ); + $this->redirectNow($url); + } + } + + /** + * @param $checksum + * @param null $error + */ + protected function deploymentFailed($checksum, $error = null) + { + $extra = $error ? ': ' . $error: ''; + + if ($this->getRequest()->isApiRequest()) { + $this->sendJsonError($this->getResponse(), 'Config deployment failed' . $extra); + return; + } else { + $url = Url::fromPath('director/config/files', array('checksum' => $checksum)); + Notification::error( + $this->translate('Config deployment failed') . $extra + ); + $this->redirectNow($url); + } + } + + /** + * @return \gipfl\IcingaWeb2\Widget\Tabs + */ + protected function configTabs() + { + $tabs = $this->tabs(); + + if ($this->hasPermission('director/deploy') + && $deploymentId = $this->params->get('deployment_id') + ) { + $tabs->add('deployment', [ + 'label' => $this->translate('Deployment'), + 'url' => 'director/deployment', + 'urlParams' => ['id' => $deploymentId] + ]); + } + + if ($this->hasPermission('director/showconfig')) { + $tabs->add('config', [ + 'label' => $this->translate('Config'), + 'url' => 'director/config/files', + 'urlParams' => $this->getConfigTabParams() + ]); + } + + return $tabs; + } + + protected function getConfigTabParams() + { + $params = [ + 'checksum' => $this->params->get( + 'config_checksum', + $this->params->get('checksum') + ) + ]; + + if ($deploymentId = $this->params->get('deployment_id')) { + $params['deployment_id'] = $deploymentId; + } + + return $params; + } +} diff --git a/application/controllers/CustomvarController.php b/application/controllers/CustomvarController.php new file mode 100644 index 0000000..f0d4574 --- /dev/null +++ b/application/controllers/CustomvarController.php @@ -0,0 +1,17 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Web\Table\CustomvarVariantsTable; + +class CustomvarController extends ActionController +{ + public function variantsAction() + { + $varName = $this->params->getRequired('name'); + $this->addSingleTab($this->translate('Custom Variable')) + ->addTitle($this->translate('Custom Variable variants: %s'), $varName); + CustomvarVariantsTable::create($this->db(), $varName)->renderTo($this); + } +} diff --git a/application/controllers/DaemonController.php b/application/controllers/DaemonController.php new file mode 100644 index 0000000..ab0038f --- /dev/null +++ b/application/controllers/DaemonController.php @@ -0,0 +1,64 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use gipfl\Web\Widget\Hint; +use Icinga\Application\Icinga; +use Icinga\Module\Director\Daemon\RunningDaemonInfo; +use Icinga\Module\Director\Web\Tabs\MainTabs; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Web\Widget\BackgroundDaemonDetails; +use Icinga\Module\Director\Web\Widget\Documentation; +use ipl\Html\Html; + +class DaemonController extends ActionController +{ + public function indexAction() + { + $this->setAutorefreshInterval(10); + $this->tabs(new MainTabs($this->Auth(), $this->getDbResourceName()))->activate('daemon'); + $this->setTitle($this->translate('Director Background Daemon')); + // Avoiding layout issues: + $this->content()->add(Html::tag('h1', $this->translate('Director Background Daemon'))); + // TODO: move dashboard titles into controls. Or figure out whether 2.7 "broke" this + + $error = null; + try { + $db = $this->db()->getDbAdapter(); + $daemons = $db->fetchAll( + $db->select()->from('director_daemon_info')->order('fqdn')->order('username')->order('pid') + ); + } catch (\Exception $e) { + $daemons = []; + $error = $e->getMessage(); + } + + if (empty($daemons)) { + $documentation = new Documentation(Icinga::app(), $this->Auth()); + $message = Html::sprintf($this->translate( + 'The Icinga Director Background Daemon is not running.' + . ' Please check our %s in case you need step by step instructions' + . ' showing you how to fix this.' + ), $documentation->getModuleLink( + $this->translate('documentation'), + 'director', + '75-Background-Daemon', + $this->translate('Icinga Director Background Daemon') + )); + $this->content()->add(Hint::error([ + $message, + ($error ? [Html::tag('br'), Html::tag('strong', $error)] : null), + ])); + return; + } + + try { + foreach ($daemons as $daemon) { + $info = new RunningDaemonInfo($daemon); + $this->content()->add([new BackgroundDaemonDetails($info, $daemon) /*, $logWindow*/]); + } + } catch (\Exception $e) { + $this->content()->add(Hint::error($e->getMessage())); + } + } +} diff --git a/application/controllers/DashboardController.php b/application/controllers/DashboardController.php new file mode 100644 index 0000000..95c1cd0 --- /dev/null +++ b/application/controllers/DashboardController.php @@ -0,0 +1,78 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Tabs\MainTabs; +use Icinga\Module\Director\Dashboard\Dashboard; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Web\Form\DbSelectorForm; + +class DashboardController extends ActionController +{ + protected function checkDirectorPermissions() + { + // No special permissions required, override parent method + } + + protected function addDbSelection() + { + if ($this->isMultiDbSetup()) { + $form = new DbSelectorForm( + $this->getResponse(), + $this->Window(), + $this->listAllowedDbResourceNames() + ); + $this->content()->add($form); + $form->handleRequest($this->getServerRequest()); + } + } + + public function indexAction() + { + if ($this->getRequest()->isGet()) { + $this->setAutorefreshInterval(10); + } + + $mainDashboards = [ + 'Objects', + 'Alerts', + 'Branches', + 'Automation', + 'Deployment', + 'Director', + 'Data', + ]; + $this->setTitle($this->translate('Icinga Director - Main Dashboard')); + $names = $this->params->getValues('name', $mainDashboards); + if (! $this->params->has('name')) { + $this->addDbSelection(); + } + if (count($names) === 1) { + $name = $names[0]; + $dashboard = Dashboard::loadByName($name, $this->db()); + $this->tabs($dashboard->getTabs())->activate($name); + } else { + $this->tabs(new MainTabs($this->Auth(), $this->getDbResourceName()))->activate('main'); + } + + $cntDashboards = 0; + foreach ($names as $name) { + if ($name instanceof Dashboard) { + $dashboard = $name; + } else { + $dashboard = Dashboard::loadByName($name, $this->db()); + } + if ($dashboard->isAvailable()) { + $cntDashboards++; + $this->content()->add($dashboard); + } + } + + if ($cntDashboards === 0) { + $msg = $this->translate( + 'No dashboard available, you might have not enough permissions' + ); + $this->content()->add($msg); + } + } +} diff --git a/application/controllers/DataController.php b/application/controllers/DataController.php new file mode 100644 index 0000000..ae4bbcf --- /dev/null +++ b/application/controllers/DataController.php @@ -0,0 +1,406 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use gipfl\Web\Widget\Hint; +use Icinga\Exception\NotFoundError; +use Icinga\Module\Director\Forms\DirectorDatalistEntryForm; +use Icinga\Module\Director\Forms\DirectorDatalistForm; +use Icinga\Module\Director\Forms\IcingaServiceDictionaryMemberForm; +use Icinga\Module\Director\Objects\DirectorDatalist; +use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Objects\IcingaObject; +use Icinga\Module\Director\Objects\IcingaService; +use Icinga\Module\Director\PlainObjectRenderer; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Web\Form\IcingaObjectFieldLoader; +use Icinga\Module\Director\Web\Table\CustomvarTable; +use Icinga\Module\Director\Web\Table\DatafieldCategoryTable; +use Icinga\Module\Director\Web\Table\DatafieldTable; +use Icinga\Module\Director\Web\Table\DatalistEntryTable; +use Icinga\Module\Director\Web\Table\DatalistTable; +use Icinga\Module\Director\Web\Tabs\DataTabs; +use gipfl\IcingaWeb2\Link; +use InvalidArgumentException; +use ipl\Html\Html; +use ipl\Html\Table; + +class DataController extends ActionController +{ + public function listsAction() + { + $this->addTitle($this->translate('Data lists')); + $this->actions()->add( + Link::create($this->translate('Add'), 'director/data/list', null, [ + 'class' => 'icon-plus', + 'data-base-target' => '_next' + ]) + ); + + $this->tabs(new DataTabs())->activate('datalist'); + (new DatalistTable($this->db()))->renderTo($this); + } + + /** + * @throws \Icinga\Exception\MissingParameterException + * @throws \Icinga\Exception\NotFoundError + */ + public function listAction() + { + $form = DirectorDatalistForm::load() + ->setSuccessUrl('director/data/lists') + ->setDb($this->db()); + + if ($name = $this->params->get('name')) { + $list = $this->requireList('name'); + $form->setObject($list); + $this->addListActions($list); + $this->addTitle( + $this->translate('Data List: %s'), + $list->get('list_name') + )->addListTabs($name, 'list'); + } else { + $this + ->addTitle($this->translate('Add a new Data List')) + ->addSingleTab($this->translate('Data List')); + } + + $this->content()->add($form->handleRequest()); + } + + public function fieldsAction() + { + $this->setAutorefreshInterval(10); + $this->tabs(new DataTabs())->activate('datafield'); + $this->addTitle($this->translate('Data Fields')); + $this->actions()->add(Link::create( + $this->translate('Add'), + 'director/datafield/add', + null, + [ + 'class' => 'icon-plus', + 'data-base-target' => '_next', + ] + )); + + (new DatafieldTable($this->db()))->renderTo($this); + } + + public function fieldcategoriesAction() + { + $this->setAutorefreshInterval(10); + $this->tabs(new DataTabs())->activate('datafieldcategory'); + $this->addTitle($this->translate('Data Field Categories')); + $this->actions()->add(Link::create( + $this->translate('Add'), + 'director/datafieldcategory/add', + null, + [ + 'class' => 'icon-plus', + 'data-base-target' => '_next', + ] + )); + + (new DatafieldCategoryTable($this->db()))->renderTo($this); + } + + public function varsAction() + { + $this->tabs(new DataTabs())->activate('customvars'); + $this->addTitle($this->translate('Custom Vars - Overview')); + (new CustomvarTable($this->db()))->renderTo($this); + } + + /** + * @throws \Icinga\Exception\MissingParameterException + * @throws \Icinga\Exception\NotFoundError + */ + public function listentryAction() + { + $entryName = $this->params->get('entry_name'); + $list = $this->requireList('list'); + $this->addListActions($list); + $listId = $list->get('id'); + $listName = $list->get('list_name'); + $title = $title = $this->translate('List Entries') . ': ' . $listName; + $this->addTitle($title); + + $form = DirectorDatalistEntryForm::load() + ->setSuccessUrl('director/data/listentry', ['list' => $listName]) + ->setList($list); + + if (null !== $entryName) { + $form->loadObject([ + 'list_id' => $listId, + 'entry_name' => $entryName + ]); + $this->actions()->add(Link::create( + $this->translate('back'), + 'director/data/listentry', + ['list' => $listName], + ['class' => 'icon-left-big'] + )); + } + $form->handleRequest(); + + $this->addListTabs($listName, 'entries'); + + $table = new DatalistEntryTable($this->db()); + $table->getAttributes()->set('data-base-target', '_self'); + $table->setList($list); + $this->content()->add([$form, $table]); + } + + public function dictionaryAction() + { + $connection = $this->db(); + $this->addSingleTab('Nested Dictionary'); + $varName = $this->params->get('varname'); + $instance = $this->url()->getParam('instance'); + $action = $this->url()->getParam('action'); + $object = $this->requireObject(); + + if ($instance || $action) { + $this->actions()->add( + Link::create($this->translate('Back'), $this->url()->without(['action', 'instance']), null, [ + 'class' => 'icon-edit' + ]) + ); + } else { + $this->actions()->add( + Link::create($this->translate('Add'), $this->url(), [ + 'action' => 'add' + ], [ + 'class' => 'icon-edit' + ]) + ); + } + $subjects = $this->prepareSubjectsLabel($object, $varName); + $fieldLoader = new IcingaObjectFieldLoader($object); + $instances = $this->getCurrentInstances($object, $varName); + + if (empty($instances)) { + $this->content()->add(Hint::info(sprintf( + $this->translate('No %s have been created yet'), + $subjects + ))); + } else { + $this->content()->add($this->prepareInstancesTable($instances)); + } + + $field = $this->getFieldByName($fieldLoader, $varName); + $template = $object::load([ + 'object_name' => $field->getSetting('template_name') + ], $connection); + + $form = new IcingaServiceDictionaryMemberForm(); + $form->setDb($connection); + if ($instance) { + $instanceObject = $object::create([ + 'imports' => [$template], + 'object_name' => $instance, + 'vars' => $instances[$instance] + ], $connection); + $form->setObject($instanceObject); + } elseif ($action === 'add') { + $form->presetImports([$template->getObjectName()]); + } else { + return; + } + if ($instance) { + if (! isset($instances[$instance])) { + throw new NotFoundError("There is no such instance: $instance"); + } + $subTitle = sprintf($this->translate('Modify instance: %s'), $instance); + } else { + $subTitle = $this->translate('Add a new instance'); + } + + $this->content()->add(Html::tag('h2', ['style' => 'margin-top: 2em'], $subTitle)); + $form->handleRequest($this->getRequest()); + $this->content()->add($form); + if ($form->succeeded()) { + $virtualObject = $form->getObject(); + $name = $virtualObject->getObjectName(); + $params = $form->getObject()->getVars(); + $instances[$name] = $params; + if ($name !== $instance) { // Has been renamed + unset($instances[$instance]); + } + ksort($instances); + $object->set("vars.$varName", (object)$instances); + $object->store(); + $this->redirectNow($this->url()->without(['instance', 'action'])); + } elseif ($form->shouldBeDeleted()) { + unset($instances[$instance]); + if (empty($instances)) { + $object->set("vars.$varName", null)->store(); + } else { + $object->set("vars.$varName", (object)$instances)->store(); + } + $this->redirectNow($this->url()->without(['instance', 'action'])); + } + } + + protected function requireObject() + { + $connection = $this->db(); + $hostName = $this->params->getRequired('host'); + $serviceName = $this->params->get('service'); + if ($serviceName) { + $host = IcingaHost::load($hostName, $connection); + $object = IcingaService::load([ + 'host_id' => $host->get('id'), + 'object_name' => $serviceName, + ], $connection); + } else { + $object = IcingaHost::load($hostName, $connection); + } + + if (! $object->isObject()) { + throw new InvalidArgumentException(sprintf( + 'Only single objects allowed, %s is a %s', + $object->getObjectName(), + $object->get('object_type') + )); + } + return $object; + } + + protected function shorten($string, $maxLen) + { + if (strlen($string) <= $maxLen) { + return $string; + } + + return substr($string, 0, $maxLen) . '...'; + } + + protected function getFieldByName(IcingaObjectFieldLoader $loader, $name) + { + foreach ($loader->getFields() as $field) { + if ($field->get('varname') === $name) { + return $field; + } + } + + throw new InvalidArgumentException("Found no configured field for '$name'"); + } + + /** + * @param IcingaObject $object + * @param $varName + * @return array + */ + protected function getCurrentInstances(IcingaObject $object, $varName) + { + $currentVars = $object->getVars(); + if (isset($currentVars->$varName)) { + $currentValue = $currentVars->$varName; + } else { + $currentValue = (object)[]; + } + if (is_object($currentValue)) { + $currentValue = (array)$currentValue; + } else { + throw new InvalidArgumentException(sprintf( + '"%s" is not a valid Dictionary', + json_encode($currentValue) + )); + } + return $currentValue; + } + + /** + * @param array $currentValue + * @param $subjects + * @return Hint|Table + */ + protected function prepareInstancesTable(array $currentValue) + { + $table = new Table(); + $table->addAttributes([ + 'class' => 'common-table table-row-selectable' + ]); + $table->getHeader()->add( + Table::row([ + $this->translate('Key / Instance'), + $this->translate('Properties') + ], ['style' => 'text-align: left'], 'th') + ); + foreach ($currentValue as $key => $item) { + $table->add(Table::row([ + Link::create($key, $this->url()->with('instance', $key)), + str_replace("\n", ' ', $this->shorten(PlainObjectRenderer::render($item), 512)) + ])); + } + + return $table; + } + + /** + * @param IcingaObject $object + * @param $varName + * @return string + */ + protected function prepareSubjectsLabel(IcingaObject $object, $varName) + { + if ($object instanceof IcingaService) { + $hostName = $object->get('host'); + $subjects = $object->getObjectName() . " ($varName)"; + } else { + $hostName = $object->getObjectName(); + $subjects = sprintf( + $this->translate('%s instances'), + $varName + ); + } + $this->addTitle(sprintf( + $this->translate('%s on %s'), + $subjects, + $hostName + )); + return $subjects; + } + + protected function addListActions(DirectorDatalist $list) + { + $this->actions()->add( + Link::create( + $this->translate('Add to Basket'), + 'director/basket/add', + [ + 'type' => 'DataList', + 'names' => $list->getUniqueIdentifier() + ], + ['class' => 'icon-tag'] + ) + ); + } + + /** + * @param $paramName + * @return DirectorDatalist + * @throws \Icinga\Exception\MissingParameterException + * @throws \Icinga\Exception\NotFoundError + */ + protected function requireList($paramName) + { + return DirectorDatalist::load($this->params->getRequired($paramName), $this->db()); + } + + protected function addListTabs($name, $activate) + { + $this->tabs()->add('list', [ + 'url' => 'director/data/list', + 'urlParams' => ['name' => $name], + 'label' => $this->translate('Edit list'), + ])->add('entries', [ + 'url' => 'director/data/listentry', + 'urlParams' => ['list' => $name], + 'label' => $this->translate('List entries'), + ])->activate($activate); + + return $this; + } +} diff --git a/application/controllers/DatafieldController.php b/application/controllers/DatafieldController.php new file mode 100644 index 0000000..afad317 --- /dev/null +++ b/application/controllers/DatafieldController.php @@ -0,0 +1,40 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Forms\DirectorDatafieldForm; +use Icinga\Module\Director\Web\Controller\ActionController; + +class DatafieldController extends ActionController +{ + public function addAction() + { + $this->indexAction(); + } + + public function editAction() + { + $this->indexAction(); + } + + public function indexAction() + { + $form = DirectorDatafieldForm::load() + ->setDb($this->db()); + + if ($id = $this->params->get('id')) { + $form->loadObject((int) $id); + $this->addTitle( + $this->translate('Modify %s'), + $form->getObject()->varname + ); + $this->addSingleTab($this->translate('Edit a Field')); + } else { + $this->addTitle($this->translate('Add a new Data Field')); + $this->addSingleTab($this->translate('New Field')); + } + + $form->handleRequest(); + $this->content()->add($form); + } +} diff --git a/application/controllers/DatafieldcategoryController.php b/application/controllers/DatafieldcategoryController.php new file mode 100644 index 0000000..32c76ef --- /dev/null +++ b/application/controllers/DatafieldcategoryController.php @@ -0,0 +1,46 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Forms\DirectorDatafieldCategoryForm; +use Icinga\Module\Director\Web\Controller\ActionController; + +class DatafieldcategoryController extends ActionController +{ + public function addAction() + { + $this->indexAction(); + } + + public function editAction() + { + $this->indexAction(); + } + + public function indexAction() + { + $edit = false; + + if ($name = $this->params->get('name')) { + $edit = true; + } + + $form = DirectorDatafieldCategoryForm::load() + ->setDb($this->db()); + + if ($edit) { + $form->loadObject($name); + $this->addTitle( + $this->translate('Modify %s'), + $form->getObject()->category_name + ); + $this->addSingleTab($this->translate('Edit a Category')); + } else { + $this->addTitle($this->translate('Add a new Data Field Category')); + $this->addSingleTab($this->translate('New Category')); + } + + $form->handleRequest(); + $this->content()->add($form); + } +} diff --git a/application/controllers/DependenciesController.php b/application/controllers/DependenciesController.php new file mode 100644 index 0000000..276dd63 --- /dev/null +++ b/application/controllers/DependenciesController.php @@ -0,0 +1,15 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectsController; + +class DependenciesController extends ObjectsController +{ + protected function addObjectsTabs() + { + $res = parent::addObjectsTabs(); + $this->tabs()->remove('index'); + return $res; + } +} diff --git a/application/controllers/DependencyController.php b/application/controllers/DependencyController.php new file mode 100644 index 0000000..9d21cd5 --- /dev/null +++ b/application/controllers/DependencyController.php @@ -0,0 +1,63 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Forms\IcingaDependencyForm; +use Icinga\Module\Director\Web\Controller\ObjectController; +use Icinga\Module\Director\Objects\IcingaDependency; + +class DependencyController extends ObjectController +{ + protected $apply; + + /** + * @throws \Icinga\Exception\ConfigurationError + * @throws \Icinga\Exception\NotFoundError + */ + public function init() + { + parent::init(); + + if ($apply = $this->params->get('apply')) { + $this->apply = IcingaDependency::load( + array('object_name' => $apply, 'object_type' => 'template'), + $this->db() + ); + } + } + + /** + * @return \Icinga\Module\Director\Objects\IcingaObject + * @throws \Icinga\Exception\ConfigurationError + * @throws \Icinga\Exception\InvalidPropertyException + * @throws \Icinga\Exception\NotFoundError + */ + protected function loadObject() + { + if ($this->object === null) { + if ($name = $this->params->get('name')) { + $params = array('object_name' => $name); + $db = $this->db(); + + $this->object = IcingaDependency::load($params, $db); + } else { + parent::loadObject(); + } + } + + return $this->object; + } + + /** + * Hint: this is never being called. Why? + * + * @param $form + */ + protected function beforeHandlingAddRequest($form) + { + /** @var IcingaDependencyForm $form */ + if ($this->apply) { + $form->createApplyRuleFor($this->apply); + } + } +} diff --git a/application/controllers/DependencytemplateController.php b/application/controllers/DependencytemplateController.php new file mode 100644 index 0000000..e2bc49d --- /dev/null +++ b/application/controllers/DependencytemplateController.php @@ -0,0 +1,16 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Objects\IcingaDependency; +use Icinga\Module\Director\Web\Controller\TemplateController; + +class DependencytemplateController extends TemplateController +{ + protected function requireTemplate() + { + return IcingaDependency::load([ + 'object_name' => $this->params->get('name') + ], $this->db()); + } +} diff --git a/application/controllers/DeploymentController.php b/application/controllers/DeploymentController.php new file mode 100644 index 0000000..2d35f3c --- /dev/null +++ b/application/controllers/DeploymentController.php @@ -0,0 +1,28 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Objects\DirectorDeploymentLog; +use Icinga\Module\Director\Web\Widget\DeploymentInfo; + +class DeploymentController extends ActionController +{ + protected function checkDirectorPermissions() + { + $this->assertPermission('director/deploy'); + } + + public function indexAction() + { + $info = new DeploymentInfo(DirectorDeploymentLog::load( + $this->params->get('id'), + $this->db() + )); + $this->addTitle($this->translate('Deployment details')); + $this->tabs( + $info->getTabs($this->getAuth(), $this->getRequest()) + )->activate('deployment'); + $this->content()->add($info); + } +} diff --git a/application/controllers/EndpointController.php b/application/controllers/EndpointController.php new file mode 100644 index 0000000..e8a4fb0 --- /dev/null +++ b/application/controllers/EndpointController.php @@ -0,0 +1,9 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectController; + +class EndpointController extends ObjectController +{ +} diff --git a/application/controllers/EndpointsController.php b/application/controllers/EndpointsController.php new file mode 100644 index 0000000..40501a4 --- /dev/null +++ b/application/controllers/EndpointsController.php @@ -0,0 +1,9 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectsController; + +class EndpointsController extends ObjectsController +{ +} diff --git a/application/controllers/HealthController.php b/application/controllers/HealthController.php new file mode 100644 index 0000000..4fac4d2 --- /dev/null +++ b/application/controllers/HealthController.php @@ -0,0 +1,31 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Tabs\MainTabs; +use ipl\Html\Html; +use Icinga\Module\Director\Web\Widget\HealthCheckPluginOutput; +use Icinga\Module\Director\Health; +use Icinga\Module\Director\Web\Controller\ActionController; + +class HealthController extends ActionController +{ + public function indexAction() + { + $this->setAutorefreshInterval(10); + $this->tabs(new MainTabs($this->Auth(), $this->getDbResourceName()))->activate('health'); + $this->setTitle($this->translate('Director Health')); + $health = new Health(); + $health->setDbResourceName($this->getDbResourceName()); + $output = new HealthCheckPluginOutput($health); + $this->content()->add($output); + $this->content()->add([ + Html::tag('h1', ['class' => 'icon-pin'], $this->translate('Hint: Check Plugin')), + Html::tag('p', $this->translate( + 'Did you know that you can run this entire Health Check' + . ' (or just some sections) as an Icinga Check on a regular' + . ' base?' + )) + ]); + } +} diff --git a/application/controllers/HostController.php b/application/controllers/HostController.php new file mode 100644 index 0000000..e107d22 --- /dev/null +++ b/application/controllers/HostController.php @@ -0,0 +1,637 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use gipfl\Web\Widget\Hint; +use Icinga\Module\Director\Monitoring; +use Icinga\Module\Director\Web\Table\ObjectsTableService; +use ipl\Html\Html; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Url; +use gipfl\IcingaWeb2\Widget\Tabs; +use Exception; +use Icinga\Module\Director\CustomVariable\CustomVariableDictionary; +use Icinga\Module\Director\Db\AppliedServiceSetLoader; +use Icinga\Module\Director\DirectorObject\Lookup\ServiceFinder; +use Icinga\Module\Director\Forms\IcingaAddServiceForm; +use Icinga\Module\Director\Forms\IcingaServiceForm; +use Icinga\Module\Director\Forms\IcingaServiceSetForm; +use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Objects\IcingaService; +use Icinga\Module\Director\Objects\IcingaServiceSet; +use Icinga\Module\Director\Restriction\HostgroupRestriction; +use Icinga\Module\Director\Repository\IcingaTemplateRepository; +use Icinga\Module\Director\Web\Controller\ObjectController; +use Icinga\Module\Director\Web\SelfService; +use Icinga\Module\Director\Web\Table\IcingaHostAppliedForServiceTable; +use Icinga\Module\Director\Web\Table\IcingaHostAppliedServicesTable; +use Icinga\Module\Director\Web\Table\IcingaServiceSetServiceTable; + +class HostController extends ObjectController +{ + protected function checkDirectorPermissions() + { + if ($this->isServiceAction() && (new Monitoring())->authCanEditService( + $this->Auth(), + $this->getParam('name'), + $this->getParam('service') + )) { + return; + } + + if ($this->isServicesReadOnlyAction()) { + $this->assertPermission('director/monitoring/services-ro'); + return; + } + + if ($this->hasPermission('director/hosts')) { // faster + return; + } + + if ($this->canModifyHostViaMonitoringPermissions($this->getParam('name'))) { + return; + } + + $this->assertPermission('director/hosts'); // complain about default hosts permission + } + + protected function isServicesReadOnlyAction() + { + return in_array($this->getRequest()->getActionName(), [ + 'servicesro', + 'findservice', + 'invalidservice', + ]); + } + + protected function isServiceAction() + { + return in_array($this->getRequest()->getActionName(), [ + 'servicesro', + 'findservice', + 'invalidservice', + 'servicesetservice', + 'appliedservice', + 'inheritedservice', + ]); + } + + protected function canModifyHostViaMonitoringPermissions($hostname) + { + if ($this->hasPermission('director/monitoring/hosts')) { + $monitoring = new Monitoring(); + return $monitoring->authCanEditHost($this->Auth(), $hostname); + } + + return false; + } + + /** + * @return HostgroupRestriction + */ + protected function getHostgroupRestriction() + { + return new HostgroupRestriction($this->db(), $this->Auth()); + } + + public function editAction() + { + parent::editAction(); + $this->addOptionalMonitoringLink(); + } + + public function serviceAction() + { + $host = $this->getHostObject(); + $this->addServicesHeader(); + $this->addTitle($this->translate('Add Service to %s'), $host->getObjectName()); + $this->content()->add( + IcingaAddServiceForm::load() + ->setBranch($this->getBranch()) + ->setHost($host) + ->setDb($this->db()) + ->handleRequest() + ); + } + + public function servicesetAction() + { + $host = $this->getHostObject(); + $this->addServicesHeader(); + $this->addTitle($this->translate('Add Service Set to %s'), $host->getObjectName()); + + $this->content()->add( + IcingaServiceSetForm::load() + ->setBranch($this->getBranch()) + ->setHost($host) + ->setDb($this->db()) + ->handleRequest() + ); + } + + protected function addServicesHeader() + { + $host = $this->getHostObject(); + $hostname = $host->getObjectName(); + $this->tabs()->activate('services'); + + $this->actions()->add(Link::create( + $this->translate('Add service'), + 'director/host/service', + ['name' => $hostname], + ['class' => 'icon-plus'] + ))->add(Link::create( + $this->translate('Add service set'), + 'director/host/serviceset', + ['name' => $hostname], + ['class' => 'icon-plus'] + )); + } + + public function findserviceAction() + { + $host = $this->getHostObject(); + $this->redirectNow( + (new ServiceFinder($host, $this->getAuth())) + ->getRedirectionUrl($this->params->get('service')) + ); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function invalidserviceAction() + { + if (! $this->showInfoForNonDirectorService()) { + $this->content()->add(Hint::error(sprintf( + $this->translate('No such service: %s'), + $this->params->get('service') + ))); + } + + $this->servicesAction(); + } + + protected function showInfoForNonDirectorService() + { + try { + $api = $this->getApiIfAvailable(); + if ($api) { + $name = $this->params->get('name') . '!' . $this->params->get('service'); + $info = $api->getObject($name, 'Services'); + if (isset($info->attrs->source_location)) { + $source = $info->attrs->source_location; + $this->content()->add(Hint::info(Html::sprintf( + 'The configuration for this object has not been rendered by' + . ' Icinga Director. You can find it on line %s in %s.', + Html::tag('strong', null, $source->first_line), + Html::tag('strong', null, $source->path) + ))); + } + } + + return true; + } catch (Exception $e) { + return false; + } + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function servicesAction() + { + $this->addServicesHeader(); + $host = $this->getHostObject(); + $this->addTitle($this->translate('Services: %s'), $host->getObjectName()); + $branch = $this->getBranch(); + $hostHasBeenCreatedInBranch = $branch->isBranch() && $host->get('id'); + $content = $this->content(); + $table = (new ObjectsTableService($this->db())) + ->setAuth($this->Auth()) + ->setHost($host) + ->setBranch($branch) + ->setTitle($this->translate('Individual Service objects')) + ->removeQueryLimit(); + + if (count($table)) { + $content->add($table); + } + + /** @var IcingaHost[] $parents */ + $parents = IcingaTemplateRepository::instanceByObject($this->object) + ->getTemplatesFor($this->object, true); + foreach ($parents as $parent) { + $table = (new ObjectsTableService($this->db())) + ->setAuth($this->Auth()) + ->setBranch($branch) + ->setHost($parent) + ->setInheritedBy($host) + ->removeQueryLimit(); + + if (count($table)) { + $content->add( + $table->setTitle(sprintf( + $this->translate('Inherited from %s'), + $parent->getObjectName() + )) + ); + } + } + + if (! $hostHasBeenCreatedInBranch) { + $this->addHostServiceSetTables($host); + } + foreach ($parents as $parent) { + $this->addHostServiceSetTables($parent, $host); + } + + $appliedSets = AppliedServiceSetLoader::fetchForHost($host); + foreach ($appliedSets as $set) { + $title = sprintf($this->translate('%s (Applied Service set)'), $set->getObjectName()); + + $content->add( + IcingaServiceSetServiceTable::load($set) + // ->setHost($host) + ->setBranch($branch) + ->setAffectedHost($host) + ->setTitle($title) + ->removeQueryLimit() + ); + } + + $table = IcingaHostAppliedServicesTable::load($host) + ->setTitle($this->translate('Applied services')); + + if (count($table)) { + $content->add($table); + } + } + + /** + * Hint: this duplicates quite some logic from servicesAction. We might want + * to clean this up, but as soon as we store fully resolved Services this + * will be obsolete anyways + * + * @throws \Icinga\Exception\NotFoundError + * @throws \Icinga\Security\SecurityException + * @throws \Icinga\Exception\MissingParameterException + */ + public function servicesroAction() + { + $this->assertPermission('director/monitoring/services-ro'); + $host = $this->getHostObject(); + $service = $this->params->getRequired('service'); + $db = $this->db(); + $branch = $this->getBranch(); + $this->controls()->setTabs(new Tabs()); + $this->addSingleTab($this->translate('Configuration (read-only)')); + $this->addTitle($this->translate('Services on %s'), $host->getObjectName()); + $content = $this->content(); + + $table = (new ObjectsTableService($db)) + ->setAuth($this->Auth()) + ->setHost($host) + ->setBranch($branch) + ->setReadonly() + ->highlightService($service) + ->setTitle($this->translate('Individual Service objects')); + + if (count($table)) { + $content->add($table); + } + + /** @var IcingaHost[] $parents */ + $parents = IcingaTemplateRepository::instanceByObject($this->object) + ->getTemplatesFor($this->object, true); + foreach ($parents as $parent) { + $table = (new ObjectsTableService($db)) + ->setReadonly() + ->setBranch($branch) + ->setHost($parent) + ->highlightService($service) + ->setInheritedBy($host); + if (count($table)) { + $content->add( + $table->setTitle(sprintf( + 'Inherited from %s', + $parent->getObjectName() + )) + ); + } + } + + $this->addHostServiceSetTables($host); + foreach ($parents as $parent) { + $this->addHostServiceSetTables($parent, $host, $service); + } + + $appliedSets = AppliedServiceSetLoader::fetchForHost($host); + foreach ($appliedSets as $set) { + $title = sprintf($this->translate('%s (Applied Service set)'), $set->getObjectName()); + + $content->add( + IcingaServiceSetServiceTable::load($set) + // ->setHost($host) + ->setBranch($branch) + ->setAffectedHost($host) + ->setReadonly() + ->highlightService($service) + ->setTitle($title) + ); + } + + $table = IcingaHostAppliedServicesTable::load($host) + ->setReadonly() + ->highlightService($service) + ->setTitle($this->translate('Applied services')); + + if (count($table)) { + $content->add($table); + } + } + + /** + * @param IcingaHost $host + * @param IcingaHost|null $affectedHost + */ + protected function addHostServiceSetTables(IcingaHost $host, IcingaHost $affectedHost = null, $roService = null) + { + $db = $this->db(); + if ($affectedHost === null) { + $affectedHost = $host; + } + if ($host->get('id') === null) { + return; + } + + $query = $db->getDbAdapter()->select() + ->from( + array('ss' => 'icinga_service_set'), + 'ss.*' + )->join( + array('hsi' => 'icinga_service_set_inheritance'), + 'hsi.parent_service_set_id = ss.id', + array() + )->join( + array('hs' => 'icinga_service_set'), + 'hs.id = hsi.service_set_id', + array() + )->where('hs.host_id = ?', $host->get('id')); + + $sets = IcingaServiceSet::loadAll($db, $query, 'object_name'); + /** @var IcingaServiceSet $set*/ + foreach ($sets as $name => $set) { + $title = sprintf($this->translate('%s (Service set)'), $name); + $table = IcingaServiceSetServiceTable::load($set) + ->setHost($host) + ->setBranch($this->getBranch()) + ->setAffectedHost($affectedHost) + ->setTitle($title); + if ($roService) { + $table->setReadonly()->highlightService($roService); + } + $this->content()->add($table); + } + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function appliedserviceAction() + { + $db = $this->db(); + $host = $this->getHostObject(); + $serviceId = $this->params->get('service_id'); + $parent = IcingaService::loadWithAutoIncId($serviceId, $db); + $serviceName = $parent->getObjectName(); + + $service = IcingaService::create([ + 'imports' => $parent, + 'object_type' => 'apply', + 'object_name' => $serviceName, + 'host_id' => $host->get('id'), + 'vars' => $host->getOverriddenServiceVars($serviceName), + ], $db); + + $this->addTitle( + $this->translate('Applied service: %s'), + $serviceName + ); + + $this->content()->add( + IcingaServiceForm::load() + ->setDb($db) + ->setBranch($this->getBranch()) + ->setHost($host) + ->setApplyGenerated($parent) + ->setObject($service) + ->handleRequest() + ); + + $this->commonForServices(); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function inheritedserviceAction() + { + $db = $this->db(); + $host = $this->getHostObject(); + $serviceName = $this->params->get('service'); + $from = IcingaHost::load($this->params->get('inheritedFrom'), $this->db()); + + $parent = IcingaService::load([ + 'object_name' => $serviceName, + 'host_id' => $from->get('id') + ], $this->db()); + + // TODO: we want to eventually show the host template name, doesn't work + // as template resolution would break. + // $parent->object_name = $from->object_name; + + $service = IcingaService::create([ + 'object_type' => 'apply', + 'object_name' => $serviceName, + 'host_id' => $host->get('id'), + 'imports' => [$parent], + 'vars' => $host->getOverriddenServiceVars($serviceName), + ], $db); + + $this->addTitle($this->translate('Inherited service: %s'), $serviceName); + + $form = IcingaServiceForm::load() + ->setDb($db) + ->setBranch($this->getBranch()) + ->setHost($host) + ->setInheritedFrom($from->getObjectName()) + ->setObject($service) + ->handleRequest(); + $this->content()->add($form); + $this->commonForServices(); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function removesetAction() + { + // TODO: clean this up, use POST + $db = $this->db()->getDbAdapter(); + $query = $db->select()->from( + array('ss' => 'icinga_service_set'), + array('id' => 'ss.id') + )->join( + array('si' => 'icinga_service_set_inheritance'), + 'si.service_set_id = ss.id', + array() + )->where( + 'si.parent_service_set_id = ?', + $this->params->get('setId') + )->where('ss.host_id = ?', $this->object->get('id')); + + IcingaServiceSet::loadWithAutoIncId($db->fetchOne($query), $this->db())->delete(); + $this->redirectNow( + Url::fromPath('director/host/services', array( + 'name' => $this->object->getObjectName() + )) + ); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function servicesetserviceAction() + { + $db = $this->db(); + $host = $this->getHostObject(); + $serviceName = $this->params->get('service'); + $setParams = [ + 'object_name' => $this->params->get('set'), + 'host_id' => $host->get('id') + ]; + $setTemplate = IcingaServiceSet::load($this->params->get('set'), $db); + if (IcingaServiceSet::exists($setParams, $db)) { + $set = IcingaServiceSet::load($setParams, $db); + } else { + $set = $setTemplate; + } + + $service = IcingaService::load([ + 'object_name' => $serviceName, + 'service_set_id' => $setTemplate->get('id') + ], $this->db()); + $service = IcingaService::create([ + 'id' => $service->get('id'), + 'object_type' => 'apply', + 'object_name' => $serviceName, + 'host_id' => $host->get('id'), + 'imports' => $service->listImportNames(), + 'vars' => $host->getOverriddenServiceVars($serviceName), + ], $db); + + // $set->copyVarsToService($service); + $this->addTitle( + $this->translate('%s on %s (from set: %s)'), + $serviceName, + $host->getObjectName(), + $set->getObjectName() + ); + + $form = IcingaServiceForm::load() + ->setDb($db) + ->setBranch($this->getBranch()) + ->setHost($host) + ->setServiceSet($set) + ->setObject($service) + ->handleRequest(); + $this->tabs()->activate('services'); + $this->content()->add($form); + $this->commonForServices(); + } + + protected function commonForServices() + { + $host = $this->object; + $this->actions()->add(Link::create( + $this->translate('back'), + 'director/host/services', + ['name' => $host->getObjectName()], + ['class' => 'icon-left-big'] + )); + $this->tabs()->activate('services'); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function agentAction() + { + $selfService = new SelfService($this->getHostObject(), $this->api()); + if ($os = $this->params->get('download')) { + $selfService->handleLegacyAgentDownloads($os); + return; + } + + $selfService->renderTo($this); + $this->tabs()->activate('agent'); + } + + protected function addOptionalMonitoringLink() + { + $host = $this->object; + try { + $mon = $this->monitoring(); + if ($host->isObject() + && $mon->isAvailable() + && $mon->hasHost($host->getObjectName()) + ) { + $this->actions()->add(Link::create( + $this->translate('Show'), + 'monitoring/host/show', + ['host' => $host->getObjectName()], + [ + 'class' => 'icon-globe critical', + 'data-base-target' => '_next' + ] + )); + + // Intentionally placed here, show it only for deployed Hosts + $this->addOptionalInspectLink(); + } + } catch (Exception $e) { + // Silently ignore errors in the monitoring module + } + } + + protected function addOptionalInspectLink() + { + if (! $this->hasPermission('director/inspect')) { + return; + } + + $this->actions()->add(Link::create( + $this->translate('Inspect'), + 'director/inspect/object', + [ + 'type' => 'host', + 'plural' => 'hosts', + 'name' => $this->object->getObjectName() + ], + [ + 'class' => 'icon-zoom-in', + 'data-base-target' => '_next' + ] + )); + } + + /** + * @return IcingaHost + */ + protected function getHostObject() + { + assert($this->object instanceof IcingaHost); + return $this->object; + } +} diff --git a/application/controllers/HostgroupController.php b/application/controllers/HostgroupController.php new file mode 100644 index 0000000..aa4cc51 --- /dev/null +++ b/application/controllers/HostgroupController.php @@ -0,0 +1,9 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectController; + +class HostgroupController extends ObjectController +{ +} diff --git a/application/controllers/HostgroupsController.php b/application/controllers/HostgroupsController.php new file mode 100644 index 0000000..2b4b417 --- /dev/null +++ b/application/controllers/HostgroupsController.php @@ -0,0 +1,9 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectsController; + +class HostgroupsController extends ObjectsController +{ +} diff --git a/application/controllers/HostsController.php b/application/controllers/HostsController.php new file mode 100644 index 0000000..0332072 --- /dev/null +++ b/application/controllers/HostsController.php @@ -0,0 +1,138 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use gipfl\IcingaWeb2\Url; +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterChain; +use Icinga\Data\Filter\FilterExpression; +use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; +use Icinga\Module\Director\Forms\IcingaAddServiceForm; +use Icinga\Module\Director\Forms\IcingaAddServiceSetForm; +use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Web\Controller\ObjectsController; +use gipfl\IcingaWeb2\Link; + +class HostsController extends ObjectsController +{ + protected $multiEdit = array( + 'imports', + 'groups', + 'disabled' + ); + + protected function checkDirectorPermissions() + { + $this->assertPermission('director/hosts'); + } + + public function editAction() + { + $url = clone($this->getRequest()->getUrl()); + $url->setPath('director/hosts/addservice'); + + $urlSet = clone($url); + $urlSet->setPath('director/hosts/addserviceset'); + + parent::editAction(); + + $this->actions()->add(Link::create( + $this->translate('Add Service'), + $url, + null, + ['class' => 'icon-plus'] + ))->add(Link::create( + $this->translate('Add Service Set'), + $urlSet, + null, + ['class' => 'icon-plus'] + )); + } + + public function edittemplatesAction() + { + parent::editAction(); + + $objects = $this->loadMultiObjectsFromParams(); + $names = []; + /** @var ExportInterface $object */ + foreach ($objects as $object) { + $names[] = $object->getUniqueIdentifier(); + } + + $url = Url::fromPath('director/basket/add', [ + 'type' => 'HostTemplate', + ]); + + $url->getParams()->addValues('names', $names); + + $this->actions()->add(Link::create( + $this->translate('Add to Basket'), + $url, + null, + ['class' => 'icon-tag'] + )); + } + + public function addserviceAction() + { + $this->addSingleTab($this->translate('Add Service')); + $filter = Filter::fromQueryString($this->params->toString()); + + $objects = array(); + $db = $this->db(); + /** @var $filter FilterChain */ + foreach ($filter->filters() as $sub) { + /** @var $sub FilterChain */ + foreach ($sub->filters() as $ex) { + /** @var $ex FilterChain|FilterExpression */ + if ($ex->isExpression() && $ex->getColumn() === 'name') { + $name = $ex->getExpression(); + $objects[$name] = IcingaHost::load($name, $db); + } + } + } + $this->addTitle( + $this->translate('Add service to %d hosts'), + count($objects) + ); + + $this->content()->add( + IcingaAddServiceForm::load() + ->setHosts($objects) + ->setDb($this->db()) + ->handleRequest() + ); + } + + public function addservicesetAction() + { + $this->addSingleTab($this->translate('Add Service Set')); + $filter = Filter::fromQueryString($this->params->toString()); + + $objects = array(); + $db = $this->db(); + /** @var $filter FilterChain */ + foreach ($filter->filters() as $sub) { + /** @var $sub FilterChain */ + foreach ($sub->filters() as $ex) { + /** @var $ex FilterChain|FilterExpression */ + if ($ex->isExpression() && $ex->getColumn() === 'name') { + $name = $ex->getExpression(); + $objects[$name] = IcingaHost::load($name, $db); + } + } + } + $this->addTitle( + $this->translate('Add Service Set to %d hosts'), + count($objects) + ); + + $this->content()->add( + IcingaAddServiceSetForm::load() + ->setHosts($objects) + ->setDb($this->db()) + ->handleRequest() + ); + } +} diff --git a/application/controllers/HosttemplateController.php b/application/controllers/HosttemplateController.php new file mode 100644 index 0000000..a5bfc2b --- /dev/null +++ b/application/controllers/HosttemplateController.php @@ -0,0 +1,16 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Web\Controller\TemplateController; + +class HosttemplateController extends TemplateController +{ + protected function requireTemplate() + { + return IcingaHost::load([ + 'object_name' => $this->params->get('name') + ], $this->db()); + } +} diff --git a/application/controllers/ImportrunController.php b/application/controllers/ImportrunController.php new file mode 100644 index 0000000..d0e34e5 --- /dev/null +++ b/application/controllers/ImportrunController.php @@ -0,0 +1,24 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Objects\ImportRun; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Web\Table\ImportedrowsTable; + +class ImportrunController extends ActionController +{ + public function indexAction() + { + $importRun = ImportRun::load($this->params->getRequired('id'), $this->db()); + $this->addTitle($this->translate('Import run')); + $this->addSingleTab($this->translate('Import run')); + + $table = ImportedrowsTable::load($importRun); + if ($chosen = $this->params->get('chosenColumns')) { + $table->setColumns(preg_split('/,/', $chosen, -1, PREG_SPLIT_NO_EMPTY)); + } + + $table->renderTo($this); + } +} diff --git a/application/controllers/ImportsourceController.php b/application/controllers/ImportsourceController.php new file mode 100644 index 0000000..cbddb9e --- /dev/null +++ b/application/controllers/ImportsourceController.php @@ -0,0 +1,375 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Exception; +use gipfl\Web\Widget\Hint; +use Icinga\Module\Director\Data\Exporter; +use Icinga\Module\Director\Db\Branch\Branch; +use Icinga\Module\Director\Forms\ImportRowModifierForm; +use Icinga\Module\Director\Forms\ImportSourceForm; +use Icinga\Module\Director\Hook\ImportSourceHook; +use Icinga\Module\Director\Web\ActionBar\AutomationObjectActionBar; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Objects\ImportSource; +use Icinga\Module\Director\Web\Controller\BranchHelper; +use Icinga\Module\Director\Web\Form\CloneImportSourceForm; +use Icinga\Module\Director\Web\Table\ImportrunTable; +use Icinga\Module\Director\Web\Table\ImportsourceHookTable; +use Icinga\Module\Director\Web\Table\PropertymodifierTable; +use Icinga\Module\Director\Web\Tabs\ImportsourceTabs; +use Icinga\Module\Director\Web\Widget\ImportSourceDetails; +use InvalidArgumentException; +use gipfl\IcingaWeb2\Link; +use ipl\Html\Error; +use ipl\Html\Html; + +class ImportsourceController extends ActionController +{ + use BranchHelper; + + /** @var ImportSource|null */ + private $importSource; + + private $id; + + /** + * @throws \Icinga\Exception\AuthenticationException + * @throws \Icinga\Exception\NotFoundError + * @throws \Icinga\Security\SecurityException + */ + public function init() + { + parent::init(); + $id = $this->params->get('source_id', $this->params->get('id')); + if ($id !== null && is_numeric($id)) { + $this->id = (int) $id; + } + + $tabs = $this->tabs(new ImportsourceTabs($this->id)); + $action = $this->getRequest()->getActionName(); + if ($tabs->has($action)) { + $tabs->activate($action); + } + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + protected function addMainActions() + { + $this->actions(new AutomationObjectActionBar( + $this->getRequest() + )); + $source = $this->getImportSource(); + + $this->actions()->add(Link::create( + $this->translate('Add to Basket'), + 'director/basket/add', + [ + 'type' => 'ImportSource', + 'names' => $source->getUniqueIdentifier() + ], + [ + 'class' => 'icon-tag', + 'data-base-target' => '_next' + ] + )); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function indexAction() + { + $this->addMainActions(); + $source = $this->getImportSource(); + if ($this->params->get('format') === 'json') { + $this->sendJson($this->getResponse(), (new Exporter($this->db()))->export($source)); + return; + } + $this->addTitle( + $this->translate('Import source: %s'), + $source->get('source_name') + )->setAutorefreshInterval(10); + $branch = $this->getBranch(); + if ($this->getBranch()->isBranch()) { + $this->content()->add(Hint::info(Html::sprintf($this->translate( + 'Please note that importing data will take place in your main Branch.' + . ' Modifications to Import Sources are not allowed while being in a Configuration Branch.' + . ' To get the full functionality, please deactivate %s' + ), Branch::requireHook()->linkToBranch($branch, $this->getAuth(), $branch->getName())))); + } + $this->content()->add(new ImportSourceDetails($source)); + } + + public function addAction() + { + $this->addTitle($this->translate('Add import source')); + if ($this->showNotInBranch($this->translate('Creating Import Sources'))) { + return; + } + + $this->content()->add( + ImportSourceForm::load()->setDb($this->db()) + ->setSuccessUrl('director/importsources') + ->handleRequest() + ); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function editAction() + { + $this->addMainActions(); + $this->activateTabWithPostfix($this->translate('Modify')); + if ($this->showNotInBranch($this->translate('Modifying Import Sources'))) { + return; + } + $form = ImportSourceForm::load() + ->setObject($this->getImportSource()) + ->setListUrl('director/importsources') + ->handleRequest(); + $this->addTitle( + $this->translate('Import source: %s'), + $form->getObject()->get('source_name') + )->setAutorefreshInterval(10); + + $this->content()->add($form); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function cloneAction() + { + $this->addMainActions(); + $this->activateTabWithPostfix($this->translate('Clone')); + if ($this->showNotInBranch($this->translate('Cloning Import Sources'))) { + return; + } + $source = $this->getImportSource(); + $this->addTitle('Clone: %s', $source->get('source_name')); + $form = new CloneImportSourceForm($source); + $this->content()->add($form); + $form->on(CloneImportSourceForm::ON_SUCCESS, function (CloneImportSourceForm $form) { + $this->getResponse()->redirectAndExit($form->getSuccessUrl()); + }); + $form->handleRequest($this->getServerRequest()); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function previewAction() + { + $source = $this->getImportSource(); + + $this->addTitle( + $this->translate('Import source preview: %s'), + $source->get('source_name') + ); + $fetchUrl = clone($this->url()); + + $this->actions()->add(Link::create( + $this->translate('Download JSON'), + $fetchUrl->setPath('director/importsource/fetch'), + null, + [ + 'target' => '_blank', + 'class' => 'icon-download', + ] + ))->add(Link::create('[..]', '#', null, [ + 'onclick' => 'javascript:$("table.raw-data-table").toggleClass("collapsed");' + ])); + try { + (new ImportsourceHookTable())->setImportSource($source)->renderTo($this); + } catch (Exception $e) { + $this->content()->add(Error::show($e)); + } + } + + /** + * @throws \Icinga\Exception\ConfigurationError + * @throws \Icinga\Exception\NotFoundError + * @throws \Icinga\Module\Director\Exception\DuplicateKeyException + */ + public function fetchAction() + { + $response = $this->getResponse(); + try { + $source = $this->getImportSource(); + $source->checkForChanges(); + $hook = ImportSourceHook::forImportSource($source); + $data = $hook->fetchData(); + $source->applyModifiers($data); + + $filename = sprintf( + "director-importsource-%d_%s.json", + $this->getParam('id'), + date('YmdHis') + ); + $response->setHeader('Content-Type', 'application/json', true); + $response->setHeader('Content-disposition', "attachment; filename=$filename", true); + $response->sendHeaders(); + $this->sendJson($this->getResponse(), $data); + } catch (Exception $e) { + $this->sendJsonError($response, $e->getMessage()); + } + // TODO: this is not clean + if (\ob_get_level()) { + \ob_end_flush(); + } + exit; + } + + /** + * @return ImportSource + * @throws \Icinga\Exception\NotFoundError + */ + protected function requireImportSourceAndAddModifierTable() + { + $source = $this->getImportSource(); + $table = PropertymodifierTable::load($source, $this->url()); + if ($this->getBranch()->isBranch()) { + $table->setReadOnly(); + } else { + $table->handleSortPriorityActions($this->getRequest(), $this->getResponse()); + } + $table->renderTo($this); + + return $source; + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function modifierAction() + { + $source = $this->requireImportSourceAndAddModifierTable(); + $this->addTitle($this->translate('Property modifiers: %s'), $source->get('source_name')); + $this->addAddLink( + $this->translate('Add property modifier'), + 'director/importsource/addmodifier', + ['source_id' => $source->get('id')], + '_self' + ); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function historyAction() + { + $source = $this->getImportSource(); + $this->addTitle($this->translate('Import run history: %s'), $source->get('source_name')); + + // TODO: temporarily disabled, find a better place for stats: + // $this->view->stats = $this->db()->fetchImportStatistics(); + ImportrunTable::load($source)->renderTo($this); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function addmodifierAction() + { + $source = $this->requireImportSourceAndAddModifierTable(); + $this->addTitle( + $this->translate('%s: add Property Modifier'), + $source->get('source_name') + )->addBackToModifiersLink($source); + $this->tabs()->activate('modifier'); + + if ($this->showNotInBranch($this->translate('Modifying Import Sources'))) { + return; + } + + $this->content()->prepend( + ImportRowModifierForm::load()->setDb($this->db()) + ->setSource($source) + ->setSuccessUrl( + 'director/importsource/modifier', + ['source_id' => $source->get('id')] + )->handleRequest() + ); + } + + /** + * @throws \Icinga\Exception\MissingParameterException + * @throws \Icinga\Exception\NotFoundError + */ + public function editmodifierAction() + { + // We need to load the table AFTER adding the title, otherwise search + // will not be placed next to the title + $source = $this->getImportSource(); + + $this->addTitle( + $this->translate('%s: Property Modifier'), + $source->get('source_name') + )->addBackToModifiersLink($source); + $source = $this->requireImportSourceAndAddModifierTable(); + $this->tabs()->activate('modifier'); + if ($this->showNotInBranch($this->translate('Modifying Import Sources'))) { + return; + } + + $listUrl = 'director/importsource/modifier?source_id=' + . (int) $source->get('id'); + $this->content()->prepend( + ImportRowModifierForm::load()->setDb($this->db()) + ->loadObject((int) $this->params->getRequired('id')) + ->setListUrl($listUrl) + ->setSource($source) + ->handleRequest() + ); + } + + /** + * @return ImportSource + * @throws \Icinga\Exception\NotFoundError + */ + protected function getImportSource() + { + if ($this->importSource === null) { + if ($this->id === null) { + throw new InvalidArgumentException('Got no ImportSource id'); + } + $this->importSource = ImportSource::loadWithAutoIncId( + $this->id, + $this->db() + ); + } + + return $this->importSource; + } + + protected function activateTabWithPostfix($title) + { + /** @var ImportsourceTabs $tabs */ + $tabs = $this->tabs(); + $tabs->activateMainWithPostfix($title); + + return $this; + } + + /** + * @param ImportSource $source + * @return $this + */ + protected function addBackToModifiersLink(ImportSource $source) + { + $this->actions()->add( + Link::create( + $this->translate('back'), + 'director/importsource/modifier', + ['source_id' => $source->get('id')], + ['class' => 'icon-left-big'] + ) + ); + + return $this; + } +} diff --git a/application/controllers/ImportsourcesController.php b/application/controllers/ImportsourcesController.php new file mode 100644 index 0000000..4287292 --- /dev/null +++ b/application/controllers/ImportsourcesController.php @@ -0,0 +1,57 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\DirectorObject\Automation\ImportExport; +use Icinga\Module\Director\Web\Table\ImportsourceTable; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Web\Tabs\ImportTabs; + +class ImportsourcesController extends ActionController +{ + protected $isApified = true; + + public function indexAction() + { + if ($this->getRequest()->isApiRequest()) { + switch (strtolower($this->getRequest()->getMethod())) { + case 'get': + $this->sendExport(); + break; + case 'post': + $this->acceptImport($this->getRequest()->getRawBody()); + break; + // TODO: put / replace all? + default: + $this->sendUnsupportedMethod(); + } + + return; + } + + $this->addTitle($this->translate('Import source')) + ->setAutoRefreshInterval(10) + ->addAddLink( + $this->translate('Add a new Import Source'), + 'director/importsource/add' + )->tabs(new ImportTabs())->activate('importsource'); + + (new ImportsourceTable($this->db()))->renderTo($this); + } + + /** + * @param $raw + */ + protected function acceptImport($raw) + { + (new ImportExport($this->db()))->unserializeImportSources(json_decode($raw)); + } + + protected function sendExport() + { + $this->sendJson( + $this->getResponse(), + (new ImportExport($this->db()))->serializeAllImportSources() + ); + } +} diff --git a/application/controllers/IndexController.php b/application/controllers/IndexController.php new file mode 100644 index 0000000..3f6c62e --- /dev/null +++ b/application/controllers/IndexController.php @@ -0,0 +1,79 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Exception; +use gipfl\Web\Widget\Hint; +use Icinga\Module\Director\Db\Migrations; +use Icinga\Module\Director\Forms\ApplyMigrationsForm; +use Icinga\Module\Director\Forms\KickstartForm; +use ipl\Html\Html; + +class IndexController extends DashboardController +{ + protected $hasDeploymentEndpoint; + + public function indexAction() + { + if ($this->Config()->get('db', 'resource')) { + $migrations = new Migrations($this->db()); + + if ($migrations->hasSchema()) { + if (!$this->hasDeploymentEndpoint()) { + $this->showKickstartForm(); + } + } + + if ($migrations->hasPendingMigrations()) { + $this->content()->prepend( + ApplyMigrationsForm::load() + ->setMigrations($migrations) + ->handleRequest() + ); + } elseif ($migrations->hasBeenDowngraded()) { + $this->content()->add(Hint::warning(sprintf($this->translate( + 'Your DB schema (migration #%d) is newer than your code base.' + . ' Downgrading Icinga Director is not supported and might' + . ' lead to unexpected problems.' + ), $migrations->getLastMigrationNumber()))); + } + + if ($migrations->hasSchema()) { + parent::indexAction(); + } else { + $this->addTitle(sprintf( + $this->translate('Icinga Director Setup: %s'), + $this->translate('Create Schema') + )); + $this->addSingleTab('Setup'); + } + } else { + $this->addTitle(sprintf( + $this->translate('Icinga Director Setup: %s'), + $this->translate('Choose DB Resource') + )); + $this->addSingleTab('Setup'); + $this->showKickstartForm(); + } + } + + protected function showKickstartForm() + { + $form = KickstartForm::load(); + if ($name = $this->getPreferredDbResourceName()) { + $form->setDbResourceName($name); + } + $this->content()->prepend($form->handleRequest()); + } + + protected function hasDeploymentEndpoint() + { + try { + $this->hasDeploymentEndpoint = $this->db()->hasDeploymentEndpoint(); + } catch (Exception $e) { + return false; + } + + return $this->hasDeploymentEndpoint; + } +} diff --git a/application/controllers/InspectController.php b/application/controllers/InspectController.php new file mode 100644 index 0000000..d631652 --- /dev/null +++ b/application/controllers/InspectController.php @@ -0,0 +1,200 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use gipfl\IcingaWeb2\Link; +use Icinga\Module\Director\Objects\IcingaEndpoint; +use Icinga\Module\Director\PlainObjectRenderer; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Web\Table\CoreApiFieldsTable; +use Icinga\Module\Director\Web\Table\CoreApiObjectsTable; +use Icinga\Module\Director\Web\Table\CoreApiPrototypesTable; +use Icinga\Module\Director\Web\Tabs\ObjectTabs; +use Icinga\Module\Director\Web\Tree\InspectTreeRenderer; +use Icinga\Module\Director\Web\Widget\IcingaObjectInspection; +use Icinga\Module\Director\Web\Widget\InspectPackages; +use ipl\Html\Html; + +class InspectController extends ActionController +{ + private $endpoint; + + protected function checkDirectorPermissions() + { + $this->assertPermission('director/inspect'); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function typesAction() + { + $object = $this->endpoint(); + $name = $object->getObjectName(); + $this->tabs( + new ObjectTabs('endpoint', $this->Auth(), $object) + )->activate('inspect'); + + $this->addTitle($this->translate('Icinga 2 - Objects: %s'), $name); + + $this->actions()->add( + Link::create( + $this->translate('Status'), + 'director/inspect/status', + ['endpoint' => $name], + [ + 'class' => 'icon-eye', + 'data-base-target' => '_next' + ] + ) + ); + $this->content()->add( + new InspectTreeRenderer($object) + ); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function typeAction() + { + $api = $this->endpoint()->api(); + $typeName = $this->params->get('type'); + $this->addSingleTab($this->translate('Inspect - object list')); + $this->addTitle( + $this->translate('Object type "%s"'), + $typeName + ); + $c = $this->content(); + $type = $api->getType($typeName); + if ($type->abstract) { + $c->add($this->translate('This is an abstract object type.')); + } + + if (! $type->abstract) { + $objects = $api->listObjects($typeName, $type->plural_name); + $c->add(Html::tag('p', null, sprintf($this->translate('%d objects found'), count($objects)))); + $c->add(new CoreApiObjectsTable($objects, $this->endpoint(), $type)); + } + + if (count((array) $type->fields)) { + $c->add([ + Html::tag('h2', null, $this->translate('Type attributes')), + new CoreApiFieldsTable($type->fields, $this->url()) + ]); + } + + if (count($type->prototype_keys)) { + $c->add([ + Html::tag('h2', null, $this->translate('Prototypes (methods)')), + new CoreApiPrototypesTable($type->prototype_keys, $type->name) + ]); + } + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function objectAction() + { + $name = $this->params->get('name'); + $pType = $this->params->get('plural'); + $this->addSingleTab($this->translate('Object Inspection')); + $this->addTitle('%s "%s"', $pType, $name); + $this->showEndpointInformation($this->endpoint()); + $this->content()->add( + new IcingaObjectInspection( + $this->endpoint()->api()->getObject($name, $pType), + $this->db() + ) + ); + } + + /** + * @param IcingaEndpoint $endpoint + */ + protected function showEndpointInformation(IcingaEndpoint $endpoint) + { + $this->content()->add( + Html::tag('p', null, Html::sprintf( + 'Inspected via %s (%s)', + $this->linkToEndpoint($endpoint), + $endpoint->getDescriptiveUrl() + )) + ); + } + + /** + * @param IcingaEndpoint $endpoint + * @return Link + */ + protected function linkToEndpoint(IcingaEndpoint $endpoint) + { + return Link::create($endpoint->getObjectName(), 'director/endpoint', [ + 'name' => $endpoint->getObjectName() + ]); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function statusAction() + { + $this->addSingleTab($this->translate('Status')); + $this->addTitle($this->translate('Icinga 2 API - Status')); + $this->content()->add(Html::tag( + 'pre', + null, + PlainObjectRenderer::render($this->endpoint()->api()->getStatus()) + )); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function packagesAction() + { + $db = $this->db(); + $endpointName = $this->params->get('endpoint'); + $package = $this->params->get('package'); + $stage = $this->params->get('stage'); + $file = $this->params->get('file'); + if ($endpointName === null) { + $endpoint = null; + } else { + $endpoint = IcingaEndpoint::load($endpointName, $db); + } + if ($endpoint === null) { + $this->addSingleTab($this->translate('Inspect Packages')); + } elseif ($file !== null) { + $this->addSingleTab($this->translate('Inspect File Content')); + } else { + $this->tabs( + new ObjectTabs('endpoint', $this->Auth(), $endpoint) + )->activate('packages'); + } + $widget = new InspectPackages($this->db(), 'director/inspect/packages'); + $this->addTitle($widget->getTitle($endpoint, $package, $stage, $file)); + if ($file === null) { + $this->actions()->add($widget->getBreadCrumb($endpoint, $package, $stage)); + } + $this->content()->add($widget->getContent($endpoint, $package, $stage, $file)); + } + + /** + * @return IcingaEndpoint + * @throws \Icinga\Exception\NotFoundError + */ + protected function endpoint() + { + if ($this->endpoint === null) { + if ($name = $this->params->get('endpoint')) { + $this->endpoint = IcingaEndpoint::load($name, $this->db()); + } else { + $this->endpoint = $this->db()->getDeploymentEndpoint(); + } + } + + return $this->endpoint; + } +} diff --git a/application/controllers/JobController.php b/application/controllers/JobController.php new file mode 100644 index 0000000..278c96b --- /dev/null +++ b/application/controllers/JobController.php @@ -0,0 +1,117 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use gipfl\IcingaWeb2\Link; +use Icinga\Module\Director\Forms\DirectorJobForm; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Objects\DirectorJob; +use Icinga\Module\Director\Web\Controller\BranchHelper; +use Icinga\Module\Director\Web\Widget\JobDetails; + +class JobController extends ActionController +{ + use BranchHelper; + + /** + * @throws \Icinga\Exception\MissingParameterException + * @throws \Icinga\Exception\NotFoundError + */ + public function indexAction() + { + $this->setAutorefreshInterval(10); + $job = $this->requireJob(); + $this + ->addJobTabs($job, 'show') + ->addTitle($this->translate('Job: %s'), $job->get('job_name')) + ->addToBasketLink() + ->content()->add(new JobDetails($job)); + } + + public function addAction() + { + $this + ->addSingleTab($this->translate('New Job')) + ->addTitle($this->translate('Add a new Job')); + if ($this->showNotInBranch($this->translate('Creating Jobs'))) { + return; + } + + $this->content()->add( + DirectorJobForm::load() + ->setSuccessUrl('director/job') + ->setDb($this->db()) + ->handleRequest() + ); + } + + /** + * @throws \Icinga\Exception\MissingParameterException + * @throws \Icinga\Exception\NotFoundError + */ + public function editAction() + { + $job = $this->requireJob(); + $this + ->addJobTabs($job, 'edit') + ->addTitle($this->translate('Job: %s'), $job->get('job_name')) + ->addToBasketLink(); + if ($this->showNotInBranch($this->translate('Modifying Jobs'))) { + return; + } + + $form = DirectorJobForm::load() + ->setListUrl('director/jobs') + ->setObject($job) + ->handleRequest(); + $this->content()->add($form); + } + + /** + * @return DirectorJob + * @throws \Icinga\Exception\NotFoundError + * @throws \Icinga\Exception\MissingParameterException + */ + protected function requireJob() + { + return DirectorJob::loadWithAutoIncId((int) $this->params->getRequired('id'), $this->db()); + } + + /** + * @return $this + * @throws \Icinga\Exception\MissingParameterException + * @throws \Icinga\Exception\NotFoundError + */ + protected function addToBasketLink() + { + $job = $this->requireJob(); + $this->actions()->add(Link::create( + $this->translate('Add to Basket'), + 'director/basket/add', + [ + 'type' => 'DirectorJob', + 'names' => $job->getUniqueIdentifier() + ], + ['class' => 'icon-tag'] + )); + + return $this; + } + + protected function addJobTabs(DirectorJob $job, $active) + { + $id = $job->get('id'); + + $this->tabs()->add('show', [ + 'url' => 'director/job', + 'urlParams' => ['id' => $id], + 'label' => $this->translate('Job'), + ])->add('edit', [ + 'url' => 'director/job/edit', + 'urlParams' => ['id' => $id], + 'label' => $this->translate('Config'), + ])->activate($active); + + return $this; + } +} diff --git a/application/controllers/JobsController.php b/application/controllers/JobsController.php new file mode 100644 index 0000000..11e86ed --- /dev/null +++ b/application/controllers/JobsController.php @@ -0,0 +1,20 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Web\Table\JobTable; +use Icinga\Module\Director\Web\Tabs\ImportTabs; + +class JobsController extends ActionController +{ + public function indexAction() + { + $this->addTitle($this->translate('Jobs')) + ->setAutoRefreshInterval(10) + ->addAddLink($this->translate('Add a new Job'), 'director/job/add') + ->tabs(new ImportTabs())->activate('jobs'); + + (new JobTable($this->db()))->renderTo($this); + } +} diff --git a/application/controllers/KickstartController.php b/application/controllers/KickstartController.php new file mode 100644 index 0000000..99cde1b --- /dev/null +++ b/application/controllers/KickstartController.php @@ -0,0 +1,30 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Exception; +use Icinga\Module\Director\Forms\KickstartForm; +use Icinga\Module\Director\Web\Controller\BranchHelper; + +class KickstartController extends DashboardController +{ + use BranchHelper; + + public function indexAction() + { + $this->addSingleTab($this->translate('Kickstart')) + ->addTitle($this->translate('Director Kickstart Wizard')); + if ($this->showNotInBranch($this->translate('Kickstart'))) { + return; + } + $form = KickstartForm::load(); + try { + $form->setEndpoint($this->db()->getDeploymentEndpoint()); + } catch (Exception $e) { + // Silently ignore DB errors + } + + $form->handleRequest(); + $this->content()->add($form); + } +} diff --git a/application/controllers/NotificationController.php b/application/controllers/NotificationController.php new file mode 100644 index 0000000..97fa0f4 --- /dev/null +++ b/application/controllers/NotificationController.php @@ -0,0 +1,85 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectController; +use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Objects\IcingaNotification; +use Icinga\Module\Director\Objects\IcingaService; +use Icinga\Module\Director\Web\Form\DirectorObjectForm; + +class NotificationController extends ObjectController +{ + protected function checkDirectorPermissions() + { + $this->assertPermission('director/notifications'); + } + + // TODO: KILL IT + public function init() + { + parent::init(); + // TODO: Check if this is still needed, remove it otherwise + /** @var \Icinga\Web\Widget\Tab $tab */ + if ($this->object && $this->object->object_type === 'apply') { + if ($host = $this->params->get('host')) { + foreach ($this->getTabs()->getTabs() as $tab) { + $tab->getUrl()->setParam('host', $host); + } + } + + if ($service = $this->params->get('service')) { + foreach ($this->getTabs()->getTabs() as $tab) { + $tab->getUrl()->setParam('service', $service); + } + } + } + } + + /** + * @param DirectorObjectForm $form + */ + protected function onObjectFormLoaded(DirectorObjectForm $form) + { + if (! $this->object) { + return; + } + + if ($this->object->isTemplate()) { + $form->setListUrl('director/notifications/templates'); + } else { + $form->setListUrl('director/notifications/applyrules'); + } + } + + protected function hasBasketSupport() + { + return $this->object->isTemplate() || $this->object->isApplyRule(); + } + + protected function loadObject() + { + if ($this->object === null) { + if ($name = $this->params->get('name')) { + $params = array('object_name' => $name); + $db = $this->db(); + + if ($hostname = $this->params->get('host')) { + $this->view->host = IcingaHost::load($hostname, $db); + $params['host_id'] = $this->view->host->id; + } + + if ($service = $this->params->get('service')) { + $this->view->service = IcingaService::load($service, $db); + $params['service_id'] = $this->view->service->id; + } + + $this->object = IcingaNotification::load($params, $db); + } else { + parent::loadObject(); + } + } + + return $this->object; + } +} diff --git a/application/controllers/NotificationsController.php b/application/controllers/NotificationsController.php new file mode 100644 index 0000000..2ddb360 --- /dev/null +++ b/application/controllers/NotificationsController.php @@ -0,0 +1,31 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Exception\NotFoundError; +use Icinga\Module\Director\Web\Controller\ObjectsController; + +class NotificationsController extends ObjectsController +{ + protected function addObjectsTabs() + { + $res = parent::addObjectsTabs(); + $this->tabs()->remove('index'); + return $res; + } + + public function indexAction() + { + throw new NotFoundError('Not found'); + } + + protected function assertApplyRulePermission() + { + return $this->assertPermission('director/notifications'); + } + + protected function checkDirectorPermissions() + { + $this->assertPermission('director/notifications'); + } +} diff --git a/application/controllers/NotificationtemplateController.php b/application/controllers/NotificationtemplateController.php new file mode 100644 index 0000000..0b8602c --- /dev/null +++ b/application/controllers/NotificationtemplateController.php @@ -0,0 +1,16 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Objects\IcingaNotification; +use Icinga\Module\Director\Web\Controller\TemplateController; + +class NotificationtemplateController extends TemplateController +{ + protected function requireTemplate() + { + return IcingaNotification::load([ + 'object_name' => $this->params->get('name') + ], $this->db()); + } +} diff --git a/application/controllers/PhperrorController.php b/application/controllers/PhperrorController.php new file mode 100644 index 0000000..40a32c1 --- /dev/null +++ b/application/controllers/PhperrorController.php @@ -0,0 +1,43 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Application\Icinga; +use Icinga\Module\Director\Application\DependencyChecker; +use Icinga\Module\Director\Web\Table\Dependency\DependencyInfoTable; +use Icinga\Web\Controller; + +class PhperrorController extends Controller +{ + public function errorAction() + { + $this->getTabs()->add('error', array( + 'label' => $this->translate('Error'), + 'url' => $this->getRequest()->getUrl() + ))->activate('error'); + $msg = $this->translate( + "PHP version 5.4.x is required for Director >= 1.4.0, you're running %s." + . ' Please either upgrade PHP or downgrade Icinga Director' + ); + $this->view->title = $this->translate('Unsatisfied dependencies'); + $this->view->message = sprintf($msg, PHP_VERSION); + } + + public function dependenciesAction() + { + $checker = new DependencyChecker(Icinga::app()); + if ($checker->satisfiesDependencies($this->Module())) { + $this->redirectNow('director'); + } + $this->setAutorefreshInterval(15); + $this->getTabs()->add('error', [ + 'label' => $this->translate('Error'), + 'url' => $this->getRequest()->getUrl() + ])->activate('error'); + $this->view->title = $this->translate('Unsatisfied dependencies'); + $this->view->table = (new DependencyInfoTable($checker, $this->Module()))->render(); + $this->view->message = $this->translate( + "Icinga Director depends on the following modules, please install/upgrade as required" + ); + } +} diff --git a/application/controllers/ScheduledDowntimeController.php b/application/controllers/ScheduledDowntimeController.php new file mode 100644 index 0000000..e681a70 --- /dev/null +++ b/application/controllers/ScheduledDowntimeController.php @@ -0,0 +1,45 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Forms\IcingaScheduledDowntimeRangeForm; +use Icinga\Module\Director\Objects\IcingaScheduledDowntime; +use Icinga\Module\Director\Web\Controller\ObjectController; +use Icinga\Module\Director\Web\Table\IcingaScheduledDowntimeRangeTable; + +class ScheduledDowntimeController extends ObjectController +{ + protected $objectBaseUrl = 'director/scheduled-downtime'; + + protected function checkDirectorPermissions() + { + $this->assertPermission('director/scheduled-downtimes'); + } + + public function rangesAction() + { + /** @var IcingaScheduledDowntime $object */ + $object = $this->object; + $this->tabs()->activate('ranges'); + $this->addTitle($this->translate('Time period ranges')); + $form = IcingaScheduledDowntimeRangeForm::load() + ->setScheduledDowntime($object); + + if (null !== ($name = $this->params->get('range'))) { + $this->addBackLink($this->url()->without('range')); + $form->loadObject([ + 'scheduled_downtime_id' => $object->get('id'), + 'range_key' => $name, + 'range_type' => $this->params->get('range_type') + ]); + } + + $this->content()->add($form->handleRequest()); + IcingaScheduledDowntimeRangeTable::load($object)->renderTo($this); + } + + public function getType() + { + return 'scheduledDowntime'; + } +} diff --git a/application/controllers/ScheduledDowntimesController.php b/application/controllers/ScheduledDowntimesController.php new file mode 100644 index 0000000..b6d314c --- /dev/null +++ b/application/controllers/ScheduledDowntimesController.php @@ -0,0 +1,47 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectsController; + +class ScheduledDowntimesController extends ObjectsController +{ + protected function addObjectsTabs() + { + $res = parent::addObjectsTabs(); + $this->tabs()->remove('index'); + $this->tabs()->remove('templates'); + return $res; + } + + protected function getTable() + { + return parent::getTable() + ->setBaseObjectUrl('director/scheduled-downtime'); + } + + protected function getApplyRulesTable() + { + return parent::getApplyRulesTable()->createLinksWithNames(); + } + + public function getType() + { + return 'scheduledDowntime'; + } + + public function getBaseObjectUrl() + { + return 'scheduled-downtime'; + } + + protected function assertApplyRulePermission() + { + return $this->assertPermission('director/scheduled-downtimes'); + } + + protected function checkDirectorPermissions() + { + $this->assertPermission('director/scheduled-downtimes'); + } +} diff --git a/application/controllers/SchemaController.php b/application/controllers/SchemaController.php new file mode 100644 index 0000000..b0ca24e --- /dev/null +++ b/application/controllers/SchemaController.php @@ -0,0 +1,113 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ActionController; +use ipl\Html\Html; +use gipfl\IcingaWeb2\Link; + +class SchemaController extends ActionController +{ + protected $schemas; + + public function init() + { + $this->schemas = [ + 'mysql' => $this->translate('MySQL schema'), + 'pgsql' => $this->translate('PostgreSQL schema'), + ]; + } + + /** + * @throws \Icinga\Exception\IcingaException + */ + public function mysqlAction() + { + $this->serveSchema('mysql'); + } + + /** + * @throws \Icinga\Exception\IcingaException + */ + public function pgsqlAction() + { + $this->serveSchema('pgsql'); + } + + /** + * @param $type + * @throws \Icinga\Exception\IcingaException + */ + protected function serveSchema($type) + { + $schema = $this->loadSchema($type); + + if ($this->params->get('format') === 'sql') { + header('Content-type: application/octet-stream'); + header('Content-Disposition: attachment; filename=' . $type . '.sql'); + echo $schema; + exit; + // TODO: Shutdown + } + + $this + ->addSchemaTabs($type) + ->addTitle($this->schemas[$type]) + ->addDownloadAction() + ->content()->add(Html::tag('pre', null, $schema)); + } + + protected function loadSchema($type) + { + return file_get_contents( + sprintf( + '%s/schema/%s.sql', + $this->Module()->getBasedir(), + $type + ) + ); + } + + /** + * @return $this + * @throws \Icinga\Exception\IcingaException + * @throws \Icinga\Exception\ProgrammingError + */ + protected function addDownloadAction() + { + $this->actions()->add( + Link::create( + $this->translate('Download'), + $this->url()->with('format', 'sql'), + null, + [ + 'target' => '_blank', + 'class' => 'icon-download', + ] + ) + ); + + return $this; + } + + /** + * @param $active + * @return $this + * @throws \Icinga\Exception\Http\HttpNotFoundException + * @throws \Icinga\Exception\ProgrammingError + */ + protected function addSchemaTabs($active) + { + $tabs = $this->tabs(); + foreach ($this->schemas as $type => $title) { + $tabs->add($type, [ + 'url' => 'director/schema/' . $type, + 'label' => $title, + ]); + } + + $tabs->activate($active); + + return $this; + } +} diff --git a/application/controllers/SelfServiceController.php b/application/controllers/SelfServiceController.php new file mode 100644 index 0000000..0b3b642 --- /dev/null +++ b/application/controllers/SelfServiceController.php @@ -0,0 +1,435 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Exception; +use Icinga\Exception\NotFoundError; +use Icinga\Exception\ProgrammingError; +use Icinga\Module\Director\Forms\IcingaHostSelfServiceForm; +use Icinga\Module\Director\Objects\IcingaEndpoint; +use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Objects\IcingaZone; +use Icinga\Module\Director\Settings; +use Icinga\Module\Director\Util; +use Icinga\Module\Director\Web\Controller\ActionController; +use ipl\Html\Html; + +class SelfServiceController extends ActionController +{ + /** @var bool */ + protected $isApified = true; + + /** @var bool */ + protected $requiresAuthentication = false; + + /** @var Settings */ + protected $settings; + + protected function assertApiPermission() + { + // no permission required, we'll check the API key + } + + protected function checkDirectorPermissions() + { + } + + /** + * @throws NotFoundError + * @throws ProgrammingError + * @throws \Zend_Controller_Request_Exception + */ + public function apiVersionAction() + { + if ($this->getRequest()->isApiRequest()) { + $this->sendPowerShellResponse('1.4.0'); + } else { + throw new NotFoundError('Not found'); + } + } + + /** + * @throws \Icinga\Exception\ConfigurationError + * @throws \Icinga\Exception\IcingaException + * @throws \Zend_Controller_Exception + */ + public function registerHostAction() + { + $request = $this->getRequest(); + $form = IcingaHostSelfServiceForm::create($this->db()); + $form->setApiRequest($request->isApiRequest()); + try { + if ($key = $this->params->get('key')) { + $form->loadTemplateWithApiKey($key); + } + } catch (Exception $e) { + $this->sendPowerShellError($e->getMessage(), 404); + return; + } + if ($name = $this->params->get('name')) { + $form->setHostName($name); + } + + if ($request->isApiRequest()) { + $data = json_decode($request->getRawBody()); + $request->setPost((array) $data); + $form->handleRequest(); + if ($newKey = $form->getHostApiKey()) { + $this->sendPowerShellResponse($newKey); + } else { + $error = implode('; ', $form->getErrorMessages()); + if ($error === '') { + if ($form->isMissingRequiredFields()) { + $fields = $form->listMissingRequiredFields(); + if (count($fields) === 1) { + $this->sendPowerShellError( + sprintf("%s is required", $fields[0]), + 400 + ); + } else { + $this->sendPowerShellError( + sprintf("Missing parameters: %s", implode(', ', $fields)), + 400 + ); + } + return; + } else { + $this->sendPowerShellError('An unknown error ocurred', 500); + } + } else { + $this->sendPowerShellError($error, 400); + } + } + return; + } + + $form->handleRequest(); + $this->addSingleTab($this->translate('Self Service')) + ->addTitle($this->translate('Self Service - Host Registration')) + ->content()->add(Html::tag('p', null, $this->translate( + 'In case an Icinga Admin provided you with a self service API' + . ' token, this is where you can register new hosts' + ))) + ->add($form); + } + + /** + * @throws NotFoundError + * @throws \Zend_Controller_Request_Exception + * @throws \Zend_Controller_Response_Exception + */ + public function ticketAction() + { + if (!$this->getRequest()->isApiRequest()) { + throw new NotFoundError('Not found'); + } + + try { + $key = $this->params->getRequired('key'); + $host = IcingaHost::loadWithApiKey($key, $this->db()); + if ($host->isTemplate()) { + throw new NotFoundError('Got invalid API key "%s"', $key); + } + $name = $host->getObjectName(); + + if ($host->getResolvedProperty('has_agent') !== 'y') { + throw new NotFoundError('The host "%s" is not an agent', $name); + } + + $this->sendPowerShellResponse($this->api()->getTicket($name)); + } catch (Exception $e) { + if ($e instanceof NotFoundError) { + $this->sendPowerShellError($e->getMessage(), 404); + } else { + $this->sendPowerShellError($e->getMessage(), 500); + } + } + } + + /** + * @param $response + * @throws ProgrammingError + * @throws \Zend_Controller_Request_Exception + */ + protected function sendPowerShellResponse($response) + { + if ($this->getRequest()->getHeader('X-Director-Accept') === 'text/plain') { + if (is_array($response)) { + echo $this->makePlainTextPowerShellArray($response); + } else { + echo $response; + } + } else { + $this->sendJson($this->getResponse(), $response); + } + } + + /** + * @param $error + * @param $code + * @throws \Zend_Controller_Request_Exception + * @throws \Zend_Controller_Response_Exception + */ + protected function sendPowerShellError($error, $code) + { + if ($this->getRequest()->getHeader('X-Director-Accept') === 'text/plain') { + $this->getResponse()->setHttpResponseCode($code); + echo "ERROR: $error"; + } else { + $this->sendJsonError($this->getResponse(), $error, $code); + } + } + + /** + * @param $value + * @return string + * @throws ProgrammingError + */ + protected function makePowerShellBoolean($value) + { + if ($value === 'y' || $value === true) { + return 'true'; + } elseif ($value === 'n' || $value === false) { + return 'false'; + } else { + throw new ProgrammingError( + 'Expected boolean value, got %s', + var_export($value, 1) + ); + } + } + + /** + * @param array $params + * @return string + * @throws ProgrammingError + */ + protected function makePlainTextPowerShellArray(array $params) + { + $plain = ''; + + foreach ($params as $key => $value) { + if (is_bool($value)) { + $value = $this->makePowerShellBoolean($value); + } elseif (is_array($value)) { + $value = implode('!', $value); + } + $plain .= "$key: $value\r\n"; + } + + return $plain; + } + + /** + * @throws NotFoundError + * @throws \Zend_Controller_Request_Exception + * @throws \Zend_Controller_Response_Exception + */ + public function powershellParametersAction() + { + if (!$this->getRequest()->isApiRequest()) { + throw new NotFoundError('Not found'); + } + + try { + $this->shipPowershellParams(); + } catch (Exception $e) { + if ($e instanceof NotFoundError) { + $this->sendPowerShellError($e->getMessage(), 404); + } else { + $this->sendPowerShellError($e->getMessage(), 500); + } + } + } + + /** + * @throws NotFoundError + * @throws ProgrammingError + * @throws \Icinga\Exception\ConfigurationError + * @throws \Icinga\Exception\IcingaException + * @throws \Icinga\Exception\MissingParameterException + * @throws \Zend_Controller_Request_Exception + * @throws \Zend_Controller_Response_Exception + */ + protected function shipPowershellParams() + { + $db = $this->db(); + $key = $this->params->getRequired('key'); + $host = IcingaHost::loadWithApiKey($key, $db); + + $settings = $this->getSettings(); + $transform = $settings->get('self-service/transform_hostname'); + $params = [ + 'fetch_agent_name' => $settings->get('self-service/agent_name') === 'hostname', + 'fetch_agent_fqdn' => $settings->get('self-service/agent_name') === 'fqdn', + 'transform_hostname' => $transform, + 'flush_api_directory' => $settings->get('self-service/flush_api_dir') === 'y', + // ConvertEndpointIPConfig: + 'resolve_parent_host' => $settings->get('self-service/resolve_parent_host'), + // InstallFrameworkService: + 'install_framework_service' => '0', + // ServiceDirectory => framework_service_directory + // FrameworkServiceUrl => framework_service_url + // InstallFrameworkPlugins: + 'install_framework_plugins' => '0', + // PluginsUrl => framework_plugins_url + ]; + $username = $settings->get('self-service/icinga_service_user'); + if ($username !== null && strlen($username) > 0) { + $params['icinga_service_user'] = $username; + } + + if ($transform === '2') { + $transformMethod = '.upperCase'; + } elseif ($transform === '1') { + $transformMethod = '.lowerCase'; + } else { + $transformMethod = ''; + } + + $hostObject = (object) [ + 'address' => '&ipaddress&', + ]; + + switch ($settings->get('self-service/agent_name')) { + case 'hostname': + $hostObject->display_name = "&fqdn$transformMethod&"; + break; + case 'fqdn': + $hostObject->display_name = "&hostname$transformMethod&"; + break; + } + $params['director_host_object'] = json_encode($hostObject); + + if ($settings->get('self-service/download_type')) { + $params['download_url'] = $settings->get('self-service/download_url'); + $params['agent_version'] = $settings->get('self-service/agent_version'); + $params['allow_updates'] = $settings->get('self-service/allow_updates') === 'y'; + $params['agent_listen_port'] = $host->getAgentListenPort(); + if ($hashes = $settings->get('self-service/installer_hashes')) { + $params['installer_hashes'] = $hashes; + } + + if ($settings->get('self-service/install_nsclient') === 'y') { + $params['install_nsclient'] = true; + $this->addBooleanSettingsToParams($settings, [ + 'nsclient_add_defaults', + 'nsclient_firewall', + 'nsclient_service', + ], $params); + + + $this->addStringSettingsToParams($settings, [ + 'nsclient_directory', + 'nsclient_installer_path' + ], $params); + } + } + + $this->addHostToParams($host, $params); + + if ($this->getRequest()->getHeader('X-Director-Accept') === 'text/plain') { + echo $this->makePlainTextPowerShellArray($params); + } else { + $this->sendJson($this->getResponse(), $params); + } + } + + /** + * @param IcingaHost $host + * @param array $params + * @throws NotFoundError + * @throws ProgrammingError + * @throws \Icinga\Exception\ConfigurationError + * @throws \Icinga\Exception\IcingaException + * @throws \Zend_Controller_Request_Exception + * @throws \Zend_Controller_Response_Exception + */ + protected function addHostToParams(IcingaHost $host, array &$params) + { + if (! $host->isObject()) { + return; + } + + $db = $this->db(); + $settings = $this->getSettings(); + $name = $host->getObjectName(); + if ($host->getSingleResolvedProperty('has_agent') !== 'y') { + $this->sendPowerShellError(sprintf( + '%s is not configured for Icinga Agent usage', + $name + ), 403); + return; + } + + $zoneName = $host->getRenderingZone(); + if ($zoneName === IcingaHost::RESOLVE_ERROR) { + $this->sendPowerShellError(sprintf( + 'Could not resolve target Zone for %s', + $name + ), 404); + return; + } + + $masterConnectsToAgent = $host->getSingleResolvedProperty( + 'master_should_connect' + ) === 'y'; + $params['agent_add_firewall_rule'] = $masterConnectsToAgent; + + $params['global_zones'] = $settings->get('self-service/global_zones'); + + $zone = IcingaZone::load($zoneName, $db); + $endpointNames = $zone->listEndpoints(); + if (! $masterConnectsToAgent) { + $endpointsConfig = []; + foreach ($endpointNames as $endpointName) { + $endpoint = IcingaEndpoint::load($endpointName, $db); + $endpointsConfig[] = sprintf( + '%s;%s', + $endpoint->getSingleResolvedProperty('host'), + $endpoint->getResolvedPort() + ); + } + + $params['endpoints_config'] = $endpointsConfig; + } + $master = $db->getDeploymentEndpoint(); + $params['parent_zone'] = $zoneName; + $params['ca_server'] = $master->getObjectName(); + $params['parent_endpoints'] = $endpointNames; + $params['accept_config'] = $host->getSingleResolvedProperty('accept_config')=== 'y'; + } + + protected function addStringSettingsToParams(Settings $settings, array $keys, array &$params) + { + foreach ($keys as $key) { + $value = $settings->get("self-service/$key"); + if (strlen($value)) { + $params[$key] = $value; + } + } + } + + protected function addBooleanSettingsToParams(Settings $settings, array $keys, array &$params) + { + foreach ($keys as $key) { + $value = $settings->get("self-service/$key"); + if ($value !== null) { + $params[$key] = $value === 'y'; + } + } + } + + /** + * @return Settings + * @throws \Icinga\Exception\ConfigurationError + */ + protected function getSettings() + { + if ($this->settings === null) { + $this->settings = new Settings($this->db()); + } + + return $this->settings; + } +} diff --git a/application/controllers/ServiceController.php b/application/controllers/ServiceController.php new file mode 100644 index 0000000..3cd54d6 --- /dev/null +++ b/application/controllers/ServiceController.php @@ -0,0 +1,311 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Exception; +use Icinga\Exception\NotFoundError; +use Icinga\Module\Director\Data\Db\DbObjectStore; +use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry; +use Icinga\Module\Director\Db\Branch\UuidLookup; +use Icinga\Module\Director\Forms\IcingaServiceForm; +use Icinga\Module\Director\Monitoring; +use Icinga\Module\Director\Objects\IcingaObject; +use Icinga\Module\Director\Web\Controller\ObjectController; +use Icinga\Module\Director\Objects\IcingaService; +use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Web\Form\DirectorObjectForm; +use Icinga\Module\Director\Web\Table\IcingaAppliedServiceTable; +use Icinga\Web\Widget\Tab; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Widget\Tabs; + +class ServiceController extends ObjectController +{ + /** @var IcingaHost */ + protected $host; + + protected $set; + + protected $apply; + + protected function checkDirectorPermissions() + { + if ($this->hasPermission('director/monitoring/services')) { + $monitoring = new Monitoring(); + if ($monitoring->authCanEditService($this->Auth(), $this->getParam('host'), $this->getParam('name'))) { + return; + } + } + $this->assertPermission('director/hosts'); + } + + public function init() + { + // This happens in parent::init() too, but is required to take place before the next two lines + $this->enableStaticObjectLoader($this->getTableName()); + + // Hint: having Host and Set loaded first is important for UUID lookups with legacy URLs + $this->host = $this->getOptionalRelatedObjectFromParams('host', 'host'); + $this->set = $this->getOptionalRelatedObjectFromParams('service_set', 'set'); + parent::init(); + if ($this->object) { + if ($this->host === null) { + $this->host = $this->loadOptionalRelatedObject($this->object, 'host'); + } + if ($this->set === null) { + $this->set = $this->loadOptionalRelatedObject($this->object, 'service_set'); + } + } + $this->addOptionalHostTabs(); + $this->addOptionalSetTabs(); + } + + protected function getOptionalRelatedObjectFromParams($type, $parameter) + { + if ($id = $this->params->get("${parameter}_id")) { + $key = (int) $id; + } else { + $key = $this->params->get($parameter); + } + if ($key !== null) { + $table = DbObjectTypeRegistry::tableNameByType($type); + $key = UuidLookup::findUuidForKey($key, $table, $this->db(), $this->getBranch()); + return $this->loadSpecificObject($table, $key); + } + + return null; + } + + protected function loadOptionalRelatedObject(IcingaObject $object, $relation) + { + $key = $object->getUnresolvedRelated($relation); + if ($key === null) { + if ($key = $object->get("${relation}_id")) { + $key = (int) $key; + } else { + $key = $object->get($relation); + // We reach this when accessing Service Template Fields + } + } + + if ($key === null) { + return null; + } + + $table = DbObjectTypeRegistry::tableNameByType($relation); + $uuid = UuidLookup::findUuidForKey($key, $table, $this->db(), $this->getBranch()); + return $this->loadSpecificObject($table, $uuid); + } + + protected function addParamToTabs($name, $value) + { + foreach ($this->tabs()->getTabs() as $tab) { + /** @var Tab $tab */ + $tab->getUrl()->setParam($name, $value); + } + + return $this; + } + + public function addAction() + { + parent::addAction(); + if ($this->host) { + // TODO: use setTitle. And figure out, where we use this old route. + $this->view->title = $this->host->object_name . ': ' . $this->view->title; + } elseif ($this->set) { + $this->view->title = sprintf( + $this->translate('Add a service to "%s"'), + $this->set->object_name + ); + } elseif ($this->apply) { + $this->view->title = sprintf( + $this->translate('Apply "%s"'), + $this->apply->object_name + ); + } + } + + protected function onObjectFormLoaded(DirectorObjectForm $form) + { + if ($this->set) { + /** @var IcingaServiceForm$form */ + $form->setServiceSet($this->set); + } + if ($this->object === null && $this->apply) { + $form->createApplyRuleFor($this->apply); + } + } + + public function editAction() + { + $this->tabs()->activate('modify'); + + /** @var IcingaService $object */ + $object = $this->object; + $this->addTitle($object->getObjectName()); + if ($object->isTemplate() && $this->showNotInBranch($this->translate('Modifying Templates'))) { + return; + } + + $form = IcingaServiceForm::load()->setDb($this->db()); + $form->setBranch($this->getBranch()); + + if ($this->host) { + $this->actions()->add(Link::create( + $this->translate('back'), + 'director/host/services', + ['uuid' => $this->host->getUniqueId()->toString()], + ['class' => 'icon-left-big'] + )); + $form->setHost($this->host); + } + + if ($this->set) { + $form->setServiceSet($this->set); + } + if ($this->host && $object->usesVarOverrides()) { + $fake = IcingaService::create(array( + 'object_type' => 'object', + 'host_id' => $object->get('host_id'), + 'imports' => $object, + 'object_name' => $object->object_name, + 'use_var_overrides' => 'y', + 'vars' => $this->host->getOverriddenServiceVars($object->object_name), + ), $this->db()); + + $form->setObject($fake); + } else { + $form->setObject($object); + } + + $form->handleRequest(); + $this->addActionClone(); + + if ($this->host) { + $this->view->subtitle = sprintf( + $this->translate('(on %s)'), + $this->host->object_name + ); + } + + try { + if ($object->isTemplate() + && $object->getResolvedProperty('check_command_id') + ) { + $this->view->actionLinks .= ' ' . $this->view->qlink( + 'Create apply-rule', + 'director/service/add', + array('apply' => $object->object_name), + array('class' => 'icon-plus') + ); + } + } catch (Exception $e) { + // ignore the error, show no apply link + } + + $this->content()->add($form); + } + + public function assignAction() + { + // TODO: figure out whether and where we link to this + /** @var IcingaService $service */ + $service = $this->object; + $this->actions()->add(new Link( + $this->translate('back'), + $this->getRequest()->getUrl()->without('rule_id'), + null, + array('class' => 'icon-left-big') + )); + + $this->tabs()->activate('applied'); + $this->addTitle( + $this->translate('Apply: %s'), + $service->getObjectName() + ); + $table = (new IcingaAppliedServiceTable($this->db())) + ->setService($service); + $table->getAttributes()->set('data-base-target', '_self'); + + $this->content()->add($table); + } + + protected function getLegacyKey() + { + if ($key = $this->params->get('id')) { + $key = (int) $key; + } else { + $key = $this->params->get('name'); + } + + if ($key === null) { + throw new \InvalidArgumentException('uuid, name or id required'); + } + + return $key; + } + + protected function loadObject() + { + if ($this->params->has('uuid')) { + parent::loadObject(); + return; + } + + $key = $this->getLegacyKey(); + // Hint: not passing 'object' as type, we still have name-based links in previews and similar + $uuid = UuidLookup::findServiceUuid($this->db(), $this->getBranch(), null, $key, $this->host, $this->set); + if ($uuid === null) { + if (! $this->params->get('allowOverrides')) { + throw new NotFoundError('Not found'); + } + } else { + $this->params->set('uuid', $uuid->toString()); + parent::loadObject(); + } + } + + protected function addOptionalHostTabs() + { + if ($this->host === null) { + return; + } + $hostname = $this->host->getObjectName(); + $tabs = new Tabs(); + $urlParams = ['uuid' => $this->host->getUniqueId()->toString()]; + $tabs->add('host', [ + 'url' => 'director/host', + 'urlParams' => $urlParams, + 'label' => $this->translate('Host'), + ])->add('services', [ + 'url' => 'director/host/services', + 'urlParams' => $urlParams, + 'label' => $this->translate('Services'), + ]); + + $this->addParamToTabs('host', $hostname); + $this->controls()->prependTabs($tabs); + } + + protected function addOptionalSetTabs() + { + if ($this->set === null) { + return; + } + $setName = $this->set->getObjectName(); + $tabs = new Tabs(); + $tabs->add('set', [ + 'url' => 'director/serviceset', + 'urlParams' => ['name' => $setName], + 'label' => $this->translate('ServiceSet'), + ])->add('services', [ + 'url' => 'director/serviceset/services', + 'urlParams' => ['name' => $setName], + 'label' => $this->translate('Services'), + ]); + + $this->addParamToTabs('serviceset', $setName); + $this->controls()->prependTabs($tabs); + } +} diff --git a/application/controllers/ServiceapplyrulesController.php b/application/controllers/ServiceapplyrulesController.php new file mode 100644 index 0000000..c3a7f2b --- /dev/null +++ b/application/controllers/ServiceapplyrulesController.php @@ -0,0 +1,39 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Exception\NotFoundError; +use Icinga\Module\Director\Objects\IcingaService; +use Icinga\Module\Director\RestApi\IcingaObjectsHandler; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Web\Table\ApplyRulesTable; + +class ServiceapplyrulesController extends ActionController +{ + protected $isApified = true; + + public function indexAction() + { + $request = $this->getRequest(); + if (! $request->isApiRequest()) { + throw new NotFoundError('Not found'); + } + + $table = ApplyRulesTable::create('service', $this->db()); +/* + $query = $this->db()->getDbAdapter() + ->select() + ->from('icinga_service') + ->where('object_type = ?', 'apply'); + $rules = IcingaService::loadAll($this->db(), $query); +*/ + + $handler = (new IcingaObjectsHandler( + $request, + $this->getResponse(), + $this->db() + ))->setTable($table); + + $handler->dispatch(); + } +} diff --git a/application/controllers/ServicegroupController.php b/application/controllers/ServicegroupController.php new file mode 100644 index 0000000..b2fc50e --- /dev/null +++ b/application/controllers/ServicegroupController.php @@ -0,0 +1,9 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectController; + +class ServicegroupController extends ObjectController +{ +} diff --git a/application/controllers/ServicegroupsController.php b/application/controllers/ServicegroupsController.php new file mode 100644 index 0000000..d35e638 --- /dev/null +++ b/application/controllers/ServicegroupsController.php @@ -0,0 +1,9 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectsController; + +class ServicegroupsController extends ObjectsController +{ +} diff --git a/application/controllers/ServicesController.php b/application/controllers/ServicesController.php new file mode 100644 index 0000000..8d178c2 --- /dev/null +++ b/application/controllers/ServicesController.php @@ -0,0 +1,42 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Url; +use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; +use Icinga\Module\Director\Web\Controller\ObjectsController; + +class ServicesController extends ObjectsController +{ + protected $multiEdit = array( + 'imports', + 'groups', + 'disabled' + ); + + public function edittemplatesAction() + { + parent::editAction(); + + $objects = $this->loadMultiObjectsFromParams(); + $names = []; + /** @var ExportInterface $object */ + foreach ($objects as $object) { + $names[] = $object->getUniqueIdentifier(); + } + + $url = Url::fromPath('director/basket/add', [ + 'type' => 'ServiceTemplate', + ]); + + $url->getParams()->addValues('names', $names); + + $this->actions()->add(Link::create( + $this->translate('Add to Basket'), + $url, + null, + ['class' => 'icon-tag'] + )); + } +} diff --git a/application/controllers/ServicesetController.php b/application/controllers/ServicesetController.php new file mode 100644 index 0000000..684d2fc --- /dev/null +++ b/application/controllers/ServicesetController.php @@ -0,0 +1,141 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Data\Filter\Filter; +use Icinga\Module\Director\Forms\IcingaServiceSetForm; +use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Objects\IcingaServiceSet; +use Icinga\Module\Director\Web\Controller\ObjectController; +use Icinga\Module\Director\Web\Form\DirectorObjectForm; +use Icinga\Module\Director\Web\Table\IcingaHostsMatchingFilterTable; +use Icinga\Module\Director\Web\Table\IcingaServiceSetHostTable; +use Icinga\Module\Director\Web\Table\IcingaServiceSetServiceTable; +use gipfl\IcingaWeb2\Link; + +class ServicesetController extends ObjectController +{ + /** @var IcingaHost */ + protected $host; + + protected function checkDirectorPermissions() + { + $this->assertPermission('director/servicesets'); + } + + public function init() + { + if (null !== ($host = $this->params->get('host'))) { + $this->host = IcingaHost::load($host, $this->db()); + } + + parent::init(); + if ($this->object) { + $this->addServiceSetTabs(); + } + } + + protected function onObjectFormLoaded(DirectorObjectForm $form) + { + if ($this->host) { + /** @var IcingaServiceSetForm $form */ + $form->setHost($this->host); + } + } + + public function addAction() + { + parent::addAction(); + if ($this->host) { + $this->addTitle( + $this->translate('Add a service set to "%s"'), + $this->host->getObjectName() + ); + } + } + + public function servicesAction() + { + /** @var IcingaServiceSet $set */ + $set = $this->object; + $name = $set->getObjectName(); + $this->tabs()->activate('services'); + $this->addTitle( + $this->translate('Services in this set: %s'), + $name + ); + $this->actions()->add(Link::create( + $this->translate('Add service'), + 'director/service/add', + ['set' => $name], + ['class' => 'icon-plus'] + )); + + IcingaServiceSetServiceTable::load($set) + ->setBranch($this->getBranch()) + ->renderTo($this); + } + + public function hostsAction() + { + /** @var IcingaServiceSet $set */ + $set = $this->object; + $this->tabs()->activate('hosts'); + $this->addTitle( + $this->translate('Hosts using this set: %s'), + $set->getObjectName() + ); + + $table = IcingaServiceSetHostTable::load($set); + if ($table->count()) { + $table->renderTo($this); + } + $filter = $set->get('assign_filter'); + if ($filter !== null && \strlen($filter) > 0) { + $this->content()->add( + IcingaHostsMatchingFilterTable::load(Filter::fromQueryString($filter), $this->db()) + ); + } + } + + protected function addServiceSetTabs() + { + $hexUuid = $this->object->getUniqueId()->toString(); + $tabs = $this->tabs(); + $tabs->add('services', [ + 'url' => 'director/serviceset/services', + 'urlParams' => ['uuid' => $hexUuid], + 'label' => 'Services' + ]); + if ($this->branch->isBranch()) { + return $this; + } + $tabs->add('hosts', [ + 'url' => 'director/serviceset/hosts', + 'urlParams' => ['uuid' => $hexUuid], + 'label' => 'Hosts' + ]); + + return $this; + } + + protected function loadObject() + { + if ($this->object === null) { + if (null !== ($name = $this->params->get('name'))) { + $params = ['object_name' => $name]; + $db = $this->db(); + + if ($this->host) { + $params['host_id'] = $this->host->get('id'); + } + + $this->object = IcingaServiceSet::load($params, $db); + } else { + parent::loadObject(); + } + } + + return $this->object; + } +} diff --git a/application/controllers/ServicetemplateController.php b/application/controllers/ServicetemplateController.php new file mode 100644 index 0000000..25d0742 --- /dev/null +++ b/application/controllers/ServicetemplateController.php @@ -0,0 +1,16 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Objects\IcingaService; +use Icinga\Module\Director\Web\Controller\TemplateController; + +class ServicetemplateController extends TemplateController +{ + protected function requireTemplate() + { + return IcingaService::load([ + 'object_name' => $this->params->get('name') + ], $this->db()); + } +} diff --git a/application/controllers/SettingsController.php b/application/controllers/SettingsController.php new file mode 100644 index 0000000..c4709e6 --- /dev/null +++ b/application/controllers/SettingsController.php @@ -0,0 +1,48 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Forms\KickstartForm; +use Icinga\Module\Director\Forms\SelfServiceSettingsForm; +use Icinga\Module\Director\Settings; +use Icinga\Module\Director\Web\Controller\ActionController; +use ipl\Html\Html; + +class SettingsController extends ActionController +{ + /** + * @throws \Icinga\Exception\Http\HttpNotFoundException + */ + public function indexAction() + { + // Hint: this is for the module configuration tab, legacy code + $this->view->tabs = $this->Module() + ->getConfigTabs() + ->activate('config'); + + $this->view->form = KickstartForm::load() + ->setModuleConfig($this->Config()) + ->handleRequest(); + } + + /** + * @throws \Icinga\Exception\ConfigurationError + * @throws \Icinga\Exception\IcingaException + */ + public function selfServiceAction() + { + $form = SelfServiceSettingsForm::create($this->db(), new Settings($this->db())); + $form->handleRequest(); + + $hint = $this->translate( + 'The Icinga Director Self Service API allows your Hosts to register' + . ' themselves. This allows them to get their Icinga Agent configured,' + . ' installed and upgraded in an automated way.' + ); + + $this->addSingleTab($this->translate('Self Service')) + ->addTitle($this->translate('Self Service API - Global Settings')) + ->content()->add(Html::tag('p', null, $hint)) + ->add($form); + } +} diff --git a/application/controllers/SuggestController.php b/application/controllers/SuggestController.php new file mode 100644 index 0000000..659c48c --- /dev/null +++ b/application/controllers/SuggestController.php @@ -0,0 +1,415 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Restriction\HostgroupRestriction; +use ipl\Html\Html; +use Icinga\Exception\NotFoundError; +use Icinga\Module\Director\Hook\ImportSourceHook; +use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Objects\IcingaService; +use Icinga\Module\Director\Objects\ImportSource; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Data\Filter\Filter; +use Icinga\Module\Director\Objects\HostApplyMatches; + +class SuggestController extends ActionController +{ + protected function checkDirectorPermissions() + { + } + + public function indexAction() + { + // TODO: Using some temporarily hardcoded methods, should use DataViews later on + $context = $this->getRequest()->getPost('context'); + $key = null; + + if (strpos($context, '!') !== false) { + list($context, $key) = preg_split('~!~', $context, 2); + } + + $func = 'suggest' . ucfirst($context); + if (method_exists($this, $func)) { + if (! empty($key)) { + $all = $this->$func($key); + } else { + $all = $this->$func(); + } + } else { + $all = array(); + } + // TODO: also get cursor position and eventually add an asterisk in the middle + // tODO: filter also when fetching, eventually limit somehow + $search = $this->getRequest()->getPost('value'); + $begins = array(); + $matches = array(); + $begin = Filter::expression('value', '=', $search . '*'); + $middle = Filter::expression('value', '=', '*' . $search . '*')->setCaseSensitive(false); + $prefixes = array(); + foreach ($all as $str) { + if (false !== ($pos = strrpos($str, '.'))) { + $prefix = substr($str, 0, $pos) . '.'; + $prefixes[$prefix] = $prefix; + } + if (strlen($search)) { + $row = (object) array('value' => $str); + if ($begin->matches($row)) { + $begins[] = $this->highlight($str, $search); + } elseif ($middle->matches($row)) { + $matches[] = $this->highlight($str, $search); + } + } else { + $matches[] = Html::escape($str); + } + } + + $containing = array_slice(array_merge($begins, $matches), 0, 100); + $suggestions = $containing; + + if ($func === 'suggestHostFilterColumns' || $func === 'suggestHostaddresses') { + ksort($prefixes); + + if (count($suggestions) < 5) { + $suggestions = array_merge($suggestions, array_keys($prefixes)); + } + } + $this->view->suggestions = $suggestions; + } + + /** + * One more dummy helper for tests + * + * TODO: Should not remain here + * + * @return array + * @throws \Icinga\Exception\ConfigurationError + * @throws \Icinga\Security\SecurityException + */ + protected function suggestLocations() + { + $this->assertPermission('director/hosts'); + $db = $this->db()->getDbAdapter(); + $query = $db->select() + ->distinct() + ->from('icinga_host_var', 'varvalue') + ->where('varname = ?', 'location') + ->order('varvalue'); + return $db->fetchCol($query); + } + + protected function suggestHostnames($type = 'object') + { + $this->assertPermission('director/hosts'); + $db = $this->db()->getDbAdapter(); + $query = $db->select() + ->from('icinga_host', 'object_name') + ->order('object_name'); + + if ($type !== null) { + $query->where('object_type = ?', $type); + } + $restriction = new HostgroupRestriction($this->db(), $this->Auth()); + $restriction->filterHostsQuery($query); + + return $db->fetchCol($query); + } + + protected function suggestHostsAndTemplates() + { + return $this->suggestHostnames(null); + } + + protected function suggestServicenames() + { + $r=array(); + $this->assertPermission('director/services'); + $db = $this->db()->getDbAdapter(); + $for_host = $this->getRequest()->getPost('for_host'); + if (!empty($for_host)) { + $tmp_host = IcingaHost::load($for_host, $this->db()); + } + + $query = $db->select()->distinct() + ->from('icinga_service', 'object_name') + ->order('object_name') + ->where("object_type IN ('object','apply')"); + if (!empty($tmp_host)) { + $query->where('host_id = ?', $tmp_host->id); + } + $r = array_merge($r, $db->fetchCol($query)); + if (!empty($tmp_host)) { + $resolver = $tmp_host->templateResolver(); + foreach ($resolver->fetchResolvedParents() as $template_obj) { + $query = $db->select()->distinct() + ->from('icinga_service', 'object_name') + ->order('object_name') + ->where("object_type IN ('object','apply')") + ->where('host_id = ?', $template_obj->id); + $r = array_merge($r, $db->fetchCol($query)); + } + + $matcher = HostApplyMatches::prepare($tmp_host); + foreach ($this->getAllApplyRules() as $rule) { + if ($matcher->matchesFilter($rule->filter)) { //TODO + $r[]=$rule->name; + } + } + } + natcasesort($r); + return $r; + } + + protected function suggestHosttemplates() + { + $this->assertPermission('director/hosts'); + return $this->fetchTemplateNames('icinga_host', 'template_choice_id IS NULL'); + } + + protected function suggestServicetemplates() + { + $this->assertPermission('director/services'); + return $this->fetchTemplateNames('icinga_service', 'template_choice_id IS NULL'); + } + + protected function suggestNotificationtemplates() + { + $this->assertPermission('director/notifications'); + return $this->fetchTemplateNames('icinga_notification'); + } + + protected function suggestCommandtemplates() + { + $this->assertPermission('director/commands'); + $db = $this->db()->getDbAdapter(); + $query = $db->select() + ->from('icinga_command', 'object_name') + ->order('object_name'); + return $db->fetchCol($query); + } + + protected function suggestUsertemplates() + { + $this->assertPermission('director/users'); + return $this->fetchTemplateNames('icinga_user'); + } + + /** + * @return array + * @throws \Icinga\Security\SecurityException + * @codingStandardsIgnoreStart + */ + protected function suggestScheduled_downtimetemplates() + { + // @codingStandardsIgnoreEnd + $this->assertPermission('director/scheduled-downtimes'); + return $this->fetchTemplateNames('icinga_scheduled_downtime'); + } + + protected function suggestCheckcommandnames() + { + $db = $this->db()->getDbAdapter(); + $query = $db->select() + ->from('icinga_command', 'object_name') + ->where('object_type != ?', 'template') + ->order('object_name'); + + return $db->fetchCol($query); + } + + protected function fetchTemplateNames($table, $where = null) + { + $db = $this->db()->getDbAdapter(); + $query = $db->select() + ->from($table, 'object_name') + ->where('object_type = ?', 'template') + ->order('object_name'); + + if ($where !== null) { + $query->where('template_choice_id IS NULL'); + } + + return $db->fetchCol($query); + } + + protected function suggestHostgroupnames() + { + $db = $this->db()->getDbAdapter(); + $query = $db->select()->from('icinga_hostgroup', 'object_name')->order('object_name'); + return $db->fetchCol($query); + } + + protected function suggestHostaddresses() + { + $db = $this->db()->getDbAdapter(); + $query = $db->select()->from('icinga_host', 'address')->order('address'); + return $db->fetchCol($query); + } + + protected function suggestHostFilterColumns() + { + return $this->getFilterColumns('host.', [ + $this->translate('Host properties'), + $this->translate('Custom variables') + ]); + } + + protected function suggestServiceFilterColumns() + { + return $this->getFilterColumns('service.', [ + $this->translate('Service properties'), + $this->translate('Host properties'), + $this->translate('Host Custom variables'), + $this->translate('Custom variables') + ]); + } + + protected function suggestDataListValuesForListId($id) + { + $db = $this->db()->getDbAdapter(); + $select = $db->select() + ->from('director_datalist_entry', ['entry_name', 'entry_value']) + ->where('list_id = ?', $id) + ->order('entry_value ASC'); + + $result = $db->fetchPairs($select); + if ($result) { + return $result; + } else { + return []; + } + } + + protected function suggestDataListValues($field = null) + { + if ($field === null) { + // field is required! + return []; + } + + $datalistType = 'Icinga\\Module\\Director\\DataType\\DataTypeDatalist'; + $db = $this->db()->getDbAdapter(); + + $query = $db->select() + ->from(['f' =>'director_datafield'], []) + ->join( + ['sid' => 'director_datafield_setting'], + 'sid.datafield_id = f.id AND sid.setting_name = \'datalist_id\'', + [] + ) + ->join( + ['l' => 'director_datalist'], + 'l.id = sid.setting_value', + [] + ) + ->join( + ['e' => 'director_datalist_entry'], + 'e.list_id = l.id', + ['entry_name', 'entry_value'] + ) + ->where('datatype = ?', $datalistType) + ->where('varname = ?', $field) + ->order('entry_value'); + + + // TODO: respect allowed_roles + /* this implementation from DataTypeDatalist is broken + $roles = array_map('json_encode', Acl::instance()->listRoleNames()); + + if (empty($roles)) { + $query->where('allowed_roles IS NULL'); + } else { + $query->where('(allowed_roles IS NULL OR allowed_roles IN (?))', $roles); + } + */ + + $data = []; + foreach ($db->fetchPairs($query) as $key => $label) { + // TODO: find a better solution here + // $data[] = sprintf("%s [%s]", $label, $key); + $data[] = $key; + } + return $data; + } + + protected function getFilterColumns($prefix, $keys) + { + if ($prefix === 'host.') { + $all = IcingaHost::enumProperties($this->db(), $prefix); + } else { + $all = IcingaService::enumProperties($this->db(), $prefix); + } + $res = []; + foreach ($keys as $key) { + if (array_key_exists($key, $all)) { + $res = array_merge($res, array_keys($all[$key])); + } + } + + natsort($res); + return $res; + } + + protected function suggestDependencytemplates() + { + $this->assertPermission('director/hosts'); + return $this->fetchTemplateNames('icinga_dependency'); + } + + protected function highlight($val, $search) + { + $search = ($search); + $val = Html::escape($val); + return preg_replace( + '/(' . preg_quote($search, '/') . ')/i', + '<strong>\1</strong>', + $val + ); + } + + protected function getAllApplyRules() + { + $allApplyRules=$this->fetchAllApplyRules(); + foreach ($allApplyRules as $rule) { + $rule->filter = Filter::fromQueryString($rule->assign_filter); + } + + return $allApplyRules; + } + + protected function fetchAllApplyRules() + { + $db = $this->db()->getDbAdapter(); + $query = $db->select()->from( + array('s' => 'icinga_service'), + array( + 'id' => 's.id', + 'name' => 's.object_name', + 'assign_filter' => 's.assign_filter', + ) + )->where('object_type = ? AND assign_filter IS NOT NULL', 'apply'); + + return $db->fetchAll($query); + } + + protected function suggestImportsourceproperties($sourceId = null) + { + if ($sourceId === null) { + return []; + } + + try { + $importSource = ImportSource::loadWithAutoIncId($sourceId, $this->db()); + $source = ImportSourceHook::loadByName($importSource->get('source_name'), $this->db()); + + $columns = array_merge( + $source->listColumns(), + $importSource->listProperties() + ); + + return array_combine($columns, $columns); + } catch (NotFoundError $e) { + return []; + } + } +} diff --git a/application/controllers/SyncruleController.php b/application/controllers/SyncruleController.php new file mode 100644 index 0000000..928cf2c --- /dev/null +++ b/application/controllers/SyncruleController.php @@ -0,0 +1,696 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use gipfl\IcingaWeb2\Link; +use gipfl\Web\Widget\Hint; +use Icinga\Date\DateFormatter; +use Icinga\Module\Director\Data\Db\DbObjectStore; +use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry; +use Icinga\Module\Director\Db\Branch\Branch; +use Icinga\Module\Director\Db\Branch\BranchStore; +use Icinga\Module\Director\Db\Branch\BranchSupport; +use Icinga\Module\Director\Web\Controller\BranchHelper; +use Icinga\Module\Director\Web\Form\ClickHereForm; +use Icinga\Module\Director\Web\Table\BranchActivityTable; +use Icinga\Module\Director\Web\Widget\IcingaConfigDiff; +use Icinga\Module\Director\Web\Widget\UnorderedList; +use Icinga\Module\Director\Db\Cache\PrefetchCache; +use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; +use Icinga\Module\Director\Forms\SyncCheckForm; +use Icinga\Module\Director\Forms\SyncPropertyForm; +use Icinga\Module\Director\Forms\SyncRuleForm; +use Icinga\Module\Director\Forms\SyncRunForm; +use Icinga\Module\Director\IcingaConfig\IcingaConfig; +use Icinga\Module\Director\Import\Sync; +use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Objects\IcingaObject; +use Icinga\Module\Director\Objects\IcingaService; +use Icinga\Module\Director\Web\ActionBar\AutomationObjectActionBar; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Objects\SyncRule; +use Icinga\Module\Director\Objects\SyncRun; +use Icinga\Module\Director\Web\Form\CloneSyncRuleForm; +use Icinga\Module\Director\Web\Table\SyncpropertyTable; +use Icinga\Module\Director\Web\Table\SyncRunTable; +use Icinga\Module\Director\Web\Tabs\SyncRuleTabs; +use Icinga\Module\Director\Web\Widget\SyncRunDetails; +use Icinga\Web\Notification; +use ipl\Html\Form; +use ipl\Html\Html; + +class SyncruleController extends ActionController +{ + use BranchHelper; + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function indexAction() + { + $this->setAutoRefreshInterval(10); + $rule = $this->requireSyncRule(); + $this->tabs(new SyncRuleTabs($rule))->activate('show'); + $ruleName = $rule->get('rule_name'); + $this->addTitle($this->translate('Sync rule: %s'), $ruleName); + + $checkForm = SyncCheckForm::load()->setSyncRule($rule)->handleRequest(); + $store = new DbObjectStore($this->db(), $this->getBranch()); + $runForm = new SyncRunForm($rule, $store); + $runForm->on(SyncRunForm::ON_SUCCESS, function (SyncRunForm $form) { + $message = $form->getSuccessMessage(); + if ($message === null) { + Notification::error($this->translate('Synchronization failed')); + } else { + Notification::success($message); + } + $this->redirectNow($this->url()); + }); + $runForm->handleRequest($this->getServerRequest()); + + if ($lastRunId = $rule->getLastSyncRunId()) { + $run = SyncRun::load($lastRunId, $this->db()); + } else { + $run = null; + } + + $c = $this->content(); + $c->add(Html::tag('p', null, $rule->get('description'))); + if (! $rule->hasSyncProperties()) { + $this->addPropertyHint($rule); + return; + } + $this->addMainActions(); + if (! $run) { + $c->add(Hint::warning($this->translate('This Sync Rule has never been run before.'))); + } + + switch ($rule->get('sync_state')) { + case 'unknown': + $c->add(Html::tag('p', null, $this->translate( + "It's currently unknown whether we are in sync with this rule." + . ' You should either check for changes or trigger a new Sync Run.' + ))); + break; + case 'in-sync': + $c->add(Html::tag('p', null, sprintf( + $this->translate('This Sync Rule was last found to by in Sync at %s.'), + $rule->get('last_attempt') + ))); + /* + TODO: check whether... + - there have been imports since then, differing from former ones + - there have been activities since then + */ + break; + case 'pending-changes': + $c->add(Hint::warning($this->translate( + 'There are pending changes for this Sync Rule. You should trigger a new' + . ' Sync Run.' + ))); + break; + case 'failing': + $c->add(Hint::error(sprintf( + $this->translate( + 'This Sync Rule failed when last checked at %s: %s' + ), + $rule->get('last_attempt'), + $rule->get('last_error_message') + ))); + break; + } + + $c->add($checkForm); + if ($this->hasBranch()) { + $objectType = $rule->get('object_type'); + $table = DbObjectTypeRegistry::tableNameByType($objectType); + if (! BranchSupport::existsForTableName($table)) { + $this->showNotInBranch(sprintf($this->translate("Synchronizing '%s'"), $objectType)); + return; + } + } + + $c->add($runForm); + + if ($run) { + $c->add(Html::tag('h3', null, $this->translate('Last sync run details'))); + $c->add(new SyncRunDetails($run)); + if ($run->get('rule_name') !== $ruleName) { + $c->add(Html::tag('p', null, sprintf( + $this->translate("It has been renamed since then, its former name was %s"), + $run->get('rule_name') + ))); + } + } + } + + /** + * @param SyncRule $rule + */ + protected function addPropertyHint(SyncRule $rule) + { + $this->content()->add(Hint::warning(Html::sprintf( + $this->translate('You must define some %s before you can run this Sync Rule'), + new Link( + $this->translate('Sync Properties'), + 'director/syncrule/property', + ['rule_id' => $rule->get('id')] + ) + ))); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function addAction() + { + $this->editAction(); + } + + /** + * @throws \Icinga\Exception\NotFoundError + * @throws \Exception + */ + public function previewAction() + { + $rule = $this->requireSyncRule(); + $branchSupport = BranchSupport::existsForSyncRule($rule); + $branchStore = new BranchStore($this->db()); + $owner = $this->getAuth()->getUser()->getUsername(); + if ($branchSupport) { + if ($this->getBranch()->isBranch()) { + $tmpBranchName = sprintf( + '%s/%s-%s', + Branch::PREFIX_SYNC_PREVIEW, + $this->getBranch()->getUuid()->toString(), + $rule->get('id') + ); + // We could keep changes for preview on branch too + $branchStore->deleteByName($tmpBranchName); + $tmpBranch = $branchStore->cloneBranchForSync($this->getBranch(), $tmpBranchName, $owner); + $after = 1600000000; // a date in 2020, minus 10000000 + } else { + $tmpBranchName = Branch::PREFIX_SYNC_PREVIEW . '/' . $rule->get('id'); + $tmpBranch = $branchStore->fetchOrCreateByName($tmpBranchName, $owner); + $after = null; + } + $store = new DbObjectStore($this->db(), $tmpBranch); + } else { + $tmpBranch = $store = null; + } + + $this->tabs(new SyncRuleTabs($rule))->activate('preview'); + $this->addTitle($this->translate('Sync Preview')); + $sync = new Sync($rule, $store); + $keepBranchPreview = false; + if ($tmpBranch) { + if ($lastTime = $branchStore->getLastActivityTime($tmpBranch, $after)) { + if ((time() - $lastTime) > 100) { + $branchStore->wipeBranch($tmpBranch, $after); + } else { + $here = (new ClickHereForm())->handleRequest($this->getServerRequest()); + if ($here->hasBeenClicked()) { + $branchStore->wipeBranch($tmpBranch, $after); + $this->redirectNow($this->url()); + } else { + $keepBranchPreview = true; + } + $this->content()->add(Hint::info(Html::sprintf( + $this->translate('This preview has been generated %s, please click %s to regenerate it'), + DateFormatter::timeAgo($lastTime), + $here + ))); + } + } + } + if (!$keepBranchPreview) { + $modifications = $sync->getExpectedModifications(); + } + + if ($tmpBranch) { + try { + if (!$keepBranchPreview) { + $sync->apply(); + } + } catch (\Exception $e) { + $this->content()->add(Hint::error($e->getMessage())); + return; + } + + $changes = new BranchActivityTable($tmpBranch->getUuid(), $this->db()); + $changes->disableObjectLink(); + if (count($changes) === 0) { + $this->showInSync(); + } + $changes->renderTo($this); + } else { + if (empty($modifications)) { + $this->showInSync(); + return; + } + $this->showExpectedModificationSummary($modifications); + } + } + + protected function showInSync() + { + $this->content()->add(Hint::ok($this->translate( + 'This Sync Rule is in sync and would currently not apply any changes' + ))); + } + + protected function showExpectedModificationSummary($modifications) + { + $create = []; + $modify = []; + $delete = []; + $modifiedProperties = []; + /** @var IcingaObject $object */ + foreach ($modifications as $object) { + if ($object->hasBeenLoadedFromDb()) { + if ($object->shouldBeRemoved()) { + $delete[] = $object; + } else { + $modify[] = $object; + foreach ($object->getModifiedProperties() as $property => $value) { + if (isset($modifiedProperties[$property])) { + $modifiedProperties[$property]++; + } else { + $modifiedProperties[$property] = 1; + } + } + if (! $object instanceof IcingaObject) { + continue; + } + if ($object->supportsGroups()) { + if ($object->hasModifiedGroups()) { + if (isset($modifiedProperties['groups'])) { + $modifiedProperties['groups']++; + } else { + $modifiedProperties['groups'] = 1; + } + } + } + + if ($object->supportsImports()) { + if ($object->imports()->hasBeenModified()) { + if (isset($modifiedProperties['imports'])) { + $modifiedProperties['imports']++; + } else { + $modifiedProperties['imports'] = 1; + } + } + } + if ($object->supportsCustomVars()) { + if ($object->vars()->hasBeenModified()) { + foreach ($object->vars() as $var) { + if ($var->isNew()) { + $varName = 'add vars.' . $var->getKey(); + } elseif ($var->hasBeenDeleted()) { + $varName = 'remove vars.' . $var->getKey(); + } elseif ($var->hasBeenModified()) { + $varName = 'vars.' . $var->getKey(); + } else { + continue; + } + if (isset($modifiedProperties[$varName])) { + $modifiedProperties[$varName]++; + } else { + $modifiedProperties[$varName] = 1; + } + } + } + } + } + } else { + $create[] = $object; + } + } + + $content = $this->content(); + if (! empty($delete)) { + $content->add([ + Html::tag('h2', ['class' => 'icon-cancel action-delete'], sprintf( + $this->translate('%d object(s) will be deleted'), + count($delete) + )), + $this->objectList($delete) + ]); + } + if (! empty($modify)) { + $content->add([ + Html::tag('h2', ['class' => 'icon-wrench action-modify'], sprintf( + $this->translate('%d object(s) will be modified'), + count($modify) + )), + $this->listModifiedProperties($modifiedProperties), + $this->objectList($modify), + ]); + } + if (! empty($create)) { + $content->add([ + Html::tag('h2', ['class' => 'icon-plus action-create'], sprintf( + $this->translate('%d object(s) will be created'), + count($create) + )), + $this->objectList($create) + ]); + } + } + + /** + * @param IcingaObject[] $objects + * @return \ipl\Html\HtmlElement + * @throws \Icinga\Exception\NotFoundError + */ + protected function objectList($objects) + { + return Html::tag('p', $this->firstNames($objects)); + } + + /** + * Lots of duplicated code, this whole diff logic should be mouved to a + * dedicated class + * + * @param IcingaObject[] $objects + * @param int $max + * @return string + * @throws \Icinga\Exception\NotFoundError + */ + protected function firstNames($objects, $max = 50) + { + $names = []; + $list = new UnorderedList(); + $list->addAttributes([ + 'style' => 'list-style-type: none; marign: 0; padding: 0', + ]); + $total = count($objects); + $i = 0; + PrefetchCache::forget(); + IcingaHost::clearAllPrefetchCaches(); // why?? + IcingaService::clearAllPrefetchCaches(); + foreach ($objects as $object) { + $i++; + $name = $this->getObjectNameString($object); + if ($object->hasBeenLoadedFromDb()) { + if ($object instanceof IcingaHost) { + $names[$name] = Link::create( + $name, + 'director/host', + ['name' => $name], + ['data-base-target' => '_next'] + ); + $oldObject = IcingaHost::load($object->getObjectName(), $this->db()); + $cfgNew = new IcingaConfig($this->db()); + $cfgOld = new IcingaConfig($this->db()); + $oldObject->renderToConfig($cfgOld); + $object->renderToConfig($cfgNew); + foreach (IcingaConfigDiff::getDiffs($cfgOld, $cfgNew) as $file => $diff) { + $names[$name . '___PRETITLE___' . $file] = Html::tag('h3', $file); + $names[$name . '___PREVIEW___' . $file] = $diff; + } + } elseif ($object instanceof IcingaService && $object->isObject()) { + $host = $object->getRelated('host'); + + $names[$name] = Link::create( + $name, + 'director/service/edit', + [ + 'name' => $object->getObjectName(), + 'host' => $host->getObjectName() + ], + ['data-base-target' => '_next'] + ); + $oldObject = IcingaService::load([ + 'host_id' => $host->get('id'), + 'object_name' => $object->getObjectName() + ], $this->db()); + + $cfgNew = new IcingaConfig($this->db()); + $cfgOld = new IcingaConfig($this->db()); + $oldObject->renderToConfig($cfgOld); + $object->renderToConfig($cfgNew); + foreach (IcingaConfigDiff::getDiffs($cfgOld, $cfgNew) as $file => $diff) { + $names[$name . '___PRETITLE___' . $file] = Html::tag('h3', $file); + $names[$name . '___PREVIEW___' . $file] = $diff; + } + } else { + $names[$name] = $name; + } + } else { + $names[$name] = $name; + } + if ($i === $max) { + break; + } + } + ksort($names); + + foreach ($names as $name) { + $list->addItem($name); + } + + if ($total > $max) { + $list->add(sprintf( + $this->translate('...and %d more'), + $total - $max + )); + } + + return $list; + } + + protected function listModifiedProperties($properties) + { + $list = new UnorderedList(); + foreach ($properties as $property => $cnt) { + $list->addItem("${cnt}x $property"); + } + + return $list; + } + + protected function getObjectNameString($object) + { + if ($object instanceof IcingaService) { + if ($object->isObject()) { + return $object->getRelated('host')->getObjectName() + . ': ' . $object->getObjectName(); + } else { + return $object->getObjectName(); + } + } elseif ($object instanceof IcingaHost) { + return $object->getObjectName(); + } elseif ($object instanceof ExportInterface) { + return $object->getUniqueIdentifier(); + } elseif ($object instanceof IcingaObject) { + return $object->getObjectName(); + } else { + /** @var \Icinga\Module\Director\Data\Db\DbObject $object */ + return json_encode($object->getKeyParams()); + } + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function editAction() + { + $form = SyncRuleForm::load() + ->setListUrl('director/syncrules') + ->setDb($this->db()); + + if ($id = $this->params->get('id')) { + $form->loadObject((int) $id); + /** @var SyncRule $rule */ + $rule = $form->getObject(); + $this->tabs(new SyncRuleTabs($rule))->activate('edit'); + $this->addTitle(sprintf( + $this->translate('Sync rule: %s'), + $rule->get('rule_name') + )); + $this->addMainActions(); + + if (! $rule->hasSyncProperties()) { + $this->addPropertyHint($rule); + } + if ($this->showNotInBranch($this->translate('Modifying Sync Rules'))) { + return; + } + + } else { + $this->addTitle($this->translate('Add sync rule')); + $this->tabs(new SyncRuleTabs())->activate('add'); + if ($this->showNotInBranch($this->translate('Creating Sync Rules'))) { + return; + } + } + + $form->handleRequest(); + $this->content()->add($form); + } + + /** + * @throws \Icinga\Exception\MissingParameterException + * @throws \Icinga\Exception\NotFoundError + */ + public function cloneAction() + { + $id = $this->params->getRequired('id'); + $rule = SyncRule::loadWithAutoIncId((int) $id, $this->db()); + $this->tabs()->add('show', [ + 'url' => 'director/syncrule', + 'urlParams' => ['id' => $id], + 'label' => $this->translate('Sync rule'), + ])->add('clone', [ + 'url' => 'director/syncrule/clone', + 'urlParams' => ['id' => $id], + 'label' => $this->translate('Clone'), + ])->activate('clone'); + $this->addTitle('Clone: %s', $rule->get('rule_name')); + $this->actions()->add( + Link::create( + $this->translate('Modify'), + 'director/syncrule/edit', + ['id' => $rule->get('id')], + ['class' => 'icon-paste'] + ) + ); + if ($this->showNotInBranch($this->translate('Cloning Sync Rules'))) { + return; + } + + $form = new CloneSyncRuleForm($rule); + $this->content()->add($form); + $form->on(Form::ON_SUCCESS, function (CloneSyncRuleForm $form) { + $this->getResponse()->redirectAndExit($form->getSuccessUrl()); + }); + $form->handleRequest($this->getServerRequest()); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function propertyAction() + { + $rule = $this->requireSyncRule('rule_id'); + $this->tabs(new SyncRuleTabs($rule))->activate('property'); + + $this->actions()->add(Link::create( + $this->translate('Add sync property rule'), + 'director/syncrule/addproperty', + ['rule_id' => $rule->get('id')], + ['class' => 'icon-plus'] + )); + $this->addTitle($this->translate('Sync properties') . ': ' . $rule->get('rule_name')); + + SyncpropertyTable::create($rule) + ->handleSortPriorityActions($this->getRequest(), $this->getResponse()) + ->renderTo($this); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function editpropertyAction() + { + $this->addpropertyAction(); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function addpropertyAction() + { + $db = $this->db(); + $rule = $this->requireSyncRule('rule_id'); + $ruleId = (int) $rule->get('id'); + + $form = SyncPropertyForm::load()->setDb($db); + $this->tabs(new SyncRuleTabs($rule))->activate('property'); + $this->actions()->add(new Link( + $this->translate('back'), + 'director/syncrule/property', + ['rule_id' => $ruleId], + ['class' => 'icon-left-big'] + )); + + if ($id = $this->params->get('id')) { + $form->loadObject((int) $id); + $this->addTitle( + $this->translate('Sync "%s": %s'), + $form->getObject()->get('destination_field'), + $rule->get('rule_name') + ); + if ($this->showNotInBranch($this->translate('Modifying Sync Rules'))) { + return; + } + } else { + $this->addTitle( + $this->translate('Add sync property: %s'), + $rule->get('rule_name') + ); + if ($this->showNotInBranch($this->translate('Modifying Sync Rules'))) { + return; + } + } + $form->setRule($rule); + $form->setSuccessUrl('director/syncrule/property', ['rule_id' => $ruleId]); + $this->content()->add($form->handleRequest()); + SyncpropertyTable::create($rule) + ->handleSortPriorityActions($this->getRequest(), $this->getResponse()) + ->renderTo($this); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function historyAction() + { + $this->setAutoRefreshInterval(30); + $rule = $this->requireSyncRule(); + $this->tabs(new SyncRuleTabs($rule))->activate('history'); + $this->addTitle($this->translate('Sync history') . ': ' . $rule->get('rule_name')); + + if ($runId = $this->params->get('run_id')) { + $run = SyncRun::load($runId, $this->db()); + $this->content()->add(new SyncRunDetails($run)); + } + (new SyncRunTable($rule))->renderTo($this); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + protected function addMainActions() + { + $this->actions(new AutomationObjectActionBar( + $this->getRequest() + )); + $source = $this->requireSyncRule(); + + $this->actions()->add(Link::create( + $this->translate('Add to Basket'), + 'director/basket/add', + [ + 'type' => 'SyncRule', + 'names' => $source->getUniqueIdentifier() + ], + [ + 'class' => 'icon-tag', + 'data-base-target' => '_next', + ] + )); + } + + /** + * @param string $key + * @return SyncRule + * @throws \Icinga\Exception\NotFoundError + */ + protected function requireSyncRule($key = 'id') + { + $id = $this->params->get($key); + return SyncRule::loadWithAutoIncId($id, $this->db()); + } +} diff --git a/application/controllers/SyncrulesController.php b/application/controllers/SyncrulesController.php new file mode 100644 index 0000000..1829ebe --- /dev/null +++ b/application/controllers/SyncrulesController.php @@ -0,0 +1,45 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\DirectorObject\Automation\ImportExport; +use Icinga\Module\Director\Web\Table\SyncruleTable; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Web\Tabs\ImportTabs; + +class SyncrulesController extends ActionController +{ + protected $isApified = true; + + /** + * @throws \Icinga\Exception\ConfigurationError + * @throws \Icinga\Exception\Http\HttpNotFoundException + */ + public function indexAction() + { + if ($this->getRequest()->isApiRequest()) { + $this->sendExport(); + return; + } + + $this->addTitle($this->translate('Sync rule')) + ->setAutoRefreshInterval(10) + ->addAddLink( + $this->translate('Add a new Sync Rule'), + 'director/syncrule/add' + )->tabs(new ImportTabs())->activate('syncrule'); + + (new SyncruleTable($this->db()))->renderTo($this); + } + + /** + * @throws \Icinga\Exception\ConfigurationError + */ + protected function sendExport() + { + $this->sendJson( + $this->getResponse(), + (new ImportExport($this->db()))->serializeAllSyncRules() + ); + } +} diff --git a/application/controllers/TemplatechoiceController.php b/application/controllers/TemplatechoiceController.php new file mode 100644 index 0000000..faf3dfe --- /dev/null +++ b/application/controllers/TemplatechoiceController.php @@ -0,0 +1,41 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Forms\IcingaTemplateChoiceForm; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Web\Controller\BranchHelper; + +class TemplatechoiceController extends ActionController +{ + use BranchHelper; + + protected function checkDirectorPermissions() + { + $this->assertPermission('director/admin'); + } + + public function hostAction() + { + $this->prepare('host', $this->translate('Host template choice')); + } + + public function serviceAction() + { + $this->prepare('service', $this->translate('Service template choice')); + } + + protected function prepare($type, $title) + { + $this->addSingleTab('Choice') + ->addTitle($title); + $form = IcingaTemplateChoiceForm::create($type, $this->db()) + ->optionallyLoad($this->params->get('name')) + ->setListUrl("director/templatechoices/$type") + ->handleRequest(); + if ($this->showNotInBranch($this->translate('Modifying Template Choices'))) { + return; + } + $this->content()->add($form); + } +} diff --git a/application/controllers/TemplatechoicesController.php b/application/controllers/TemplatechoicesController.php new file mode 100644 index 0000000..753591a --- /dev/null +++ b/application/controllers/TemplatechoicesController.php @@ -0,0 +1,39 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\ActionBar\ChoicesActionBar; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Web\Table\ChoicesTable; +use Icinga\Module\Director\Web\Tabs\ObjectsTabs; + +class TemplatechoicesController extends ActionController +{ + protected function checkDirectorPermissions() + { + $this->assertPermission('director/admin'); + } + + public function hostAction() + { + $this->prepare('host', $this->translate('Host template choices')); + } + + public function serviceAction() + { + $this->prepare('service', $this->translate('Service template choices')); + } + + public function notificationAction() + { + $this->prepare('notification', $this->translate('Notification template choices')); + } + + protected function prepare($type, $title) + { + $this->tabs(new ObjectsTabs($type, $this->Auth(), $type))->activate('choices'); + $this->setAutorefreshInterval(10)->addTitle($title); + $this->actions(new ChoicesActionBar($type, $this->url())); + ChoicesTable::create($type, $this->db())->renderTo($this); + } +} diff --git a/application/controllers/TimeperiodController.php b/application/controllers/TimeperiodController.php new file mode 100644 index 0000000..82c7749 --- /dev/null +++ b/application/controllers/TimeperiodController.php @@ -0,0 +1,33 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Forms\IcingaTimePeriodRangeForm; +use Icinga\Module\Director\Objects\IcingaTimePeriod; +use Icinga\Module\Director\Web\Controller\ObjectController; +use Icinga\Module\Director\Web\Table\IcingaTimePeriodRangeTable; + +class TimeperiodController extends ObjectController +{ + public function rangesAction() + { + /** @var IcingaTimePeriod $object */ + $object = $this->object; + $this->tabs()->activate('ranges'); + $this->addTitle($this->translate('Time period ranges')); + $form = IcingaTimePeriodRangeForm::load() + ->setTimePeriod($object); + + if (null !== ($name = $this->params->get('range'))) { + $this->addBackLink($this->url()->without('range')); + $form->loadObject([ + 'timeperiod_id' => $object->get('id'), + 'range_key' => $name, + 'range_type' => $this->params->get('range_type') + ]); + } + + $this->content()->add($form->handleRequest()); + IcingaTimePeriodRangeTable::load($object)->renderTo($this); + } +} diff --git a/application/controllers/TimeperiodsController.php b/application/controllers/TimeperiodsController.php new file mode 100644 index 0000000..e5adb19 --- /dev/null +++ b/application/controllers/TimeperiodsController.php @@ -0,0 +1,9 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectsController; + +class TimeperiodsController extends ObjectsController +{ +} diff --git a/application/controllers/TimeperiodtemplateController.php b/application/controllers/TimeperiodtemplateController.php new file mode 100644 index 0000000..a7b26a8 --- /dev/null +++ b/application/controllers/TimeperiodtemplateController.php @@ -0,0 +1,16 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Objects\IcingaTimePeriod; +use Icinga\Module\Director\Web\Controller\TemplateController; + +class TimeperiodtemplateController extends TemplateController +{ + protected function requireTemplate() + { + return IcingaTimePeriod::load([ + 'object_name' => $this->params->get('name') + ], $this->db()); + } +} diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php new file mode 100644 index 0000000..b021be9 --- /dev/null +++ b/application/controllers/UserController.php @@ -0,0 +1,18 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectController; + +class UserController extends ObjectController +{ + protected function checkDirectorPermissions() + { + $this->assertPermission('director/users'); + } + + protected function hasBasketSupport() + { + return true; + } +} diff --git a/application/controllers/UsergroupController.php b/application/controllers/UsergroupController.php new file mode 100644 index 0000000..e58fd7e --- /dev/null +++ b/application/controllers/UsergroupController.php @@ -0,0 +1,9 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectController; + +class UsergroupController extends ObjectController +{ +} diff --git a/application/controllers/UsergroupsController.php b/application/controllers/UsergroupsController.php new file mode 100644 index 0000000..057890f --- /dev/null +++ b/application/controllers/UsergroupsController.php @@ -0,0 +1,9 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectsController; + +class UsergroupsController extends ObjectsController +{ +} diff --git a/application/controllers/UsersController.php b/application/controllers/UsersController.php new file mode 100644 index 0000000..ee6d93d --- /dev/null +++ b/application/controllers/UsersController.php @@ -0,0 +1,13 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectsController; + +class UsersController extends ObjectsController +{ + protected function checkDirectorPermissions() + { + $this->assertPermission('director/users'); + } +} diff --git a/application/controllers/UsertemplateController.php b/application/controllers/UsertemplateController.php new file mode 100644 index 0000000..41fce86 --- /dev/null +++ b/application/controllers/UsertemplateController.php @@ -0,0 +1,16 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Objects\IcingaUser; +use Icinga\Module\Director\Web\Controller\TemplateController; + +class UsertemplateController extends TemplateController +{ + protected function requireTemplate() + { + return IcingaUser::load([ + 'object_name' => $this->params->get('name') + ], $this->db()); + } +} diff --git a/application/controllers/ZoneController.php b/application/controllers/ZoneController.php new file mode 100644 index 0000000..a4125bb --- /dev/null +++ b/application/controllers/ZoneController.php @@ -0,0 +1,9 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectController; + +class ZoneController extends ObjectController +{ +} diff --git a/application/controllers/ZonesController.php b/application/controllers/ZonesController.php new file mode 100644 index 0000000..2dcaf58 --- /dev/null +++ b/application/controllers/ZonesController.php @@ -0,0 +1,9 @@ +<?php + +namespace Icinga\Module\Director\Controllers; + +use Icinga\Module\Director\Web\Controller\ObjectsController; + +class ZonesController extends ObjectsController +{ +} |