summaryrefslogtreecommitdiffstats
path: root/application/controllers
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-14 13:17:31 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-14 13:17:31 +0000
commitf66ab8dae2f3d0418759f81a3a64dc9517a62449 (patch)
treefbff2135e7013f196b891bbde54618eb050e4aaf /application/controllers
parentInitial commit. (diff)
downloadicingaweb2-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 '')
-rw-r--r--application/controllers/ApiuserController.php9
-rw-r--r--application/controllers/ApiusersController.php9
-rw-r--r--application/controllers/BasketController.php416
-rw-r--r--application/controllers/BasketsController.php53
-rw-r--r--application/controllers/BranchController.php138
-rw-r--r--application/controllers/CommandController.php126
-rw-r--r--application/controllers/CommandsController.php20
-rw-r--r--application/controllers/CommandtemplateController.php16
-rw-r--r--application/controllers/ConfigController.php539
-rw-r--r--application/controllers/CustomvarController.php17
-rw-r--r--application/controllers/DaemonController.php64
-rw-r--r--application/controllers/DashboardController.php78
-rw-r--r--application/controllers/DataController.php406
-rw-r--r--application/controllers/DatafieldController.php40
-rw-r--r--application/controllers/DatafieldcategoryController.php46
-rw-r--r--application/controllers/DependenciesController.php15
-rw-r--r--application/controllers/DependencyController.php63
-rw-r--r--application/controllers/DependencytemplateController.php16
-rw-r--r--application/controllers/DeploymentController.php28
-rw-r--r--application/controllers/EndpointController.php9
-rw-r--r--application/controllers/EndpointsController.php9
-rw-r--r--application/controllers/HealthController.php31
-rw-r--r--application/controllers/HostController.php637
-rw-r--r--application/controllers/HostgroupController.php9
-rw-r--r--application/controllers/HostgroupsController.php9
-rw-r--r--application/controllers/HostsController.php138
-rw-r--r--application/controllers/HosttemplateController.php16
-rw-r--r--application/controllers/ImportrunController.php24
-rw-r--r--application/controllers/ImportsourceController.php375
-rw-r--r--application/controllers/ImportsourcesController.php57
-rw-r--r--application/controllers/IndexController.php79
-rw-r--r--application/controllers/InspectController.php200
-rw-r--r--application/controllers/JobController.php117
-rw-r--r--application/controllers/JobsController.php20
-rw-r--r--application/controllers/KickstartController.php30
-rw-r--r--application/controllers/NotificationController.php85
-rw-r--r--application/controllers/NotificationsController.php31
-rw-r--r--application/controllers/NotificationtemplateController.php16
-rw-r--r--application/controllers/PhperrorController.php43
-rw-r--r--application/controllers/ScheduledDowntimeController.php45
-rw-r--r--application/controllers/ScheduledDowntimesController.php47
-rw-r--r--application/controllers/SchemaController.php113
-rw-r--r--application/controllers/SelfServiceController.php435
-rw-r--r--application/controllers/ServiceController.php311
-rw-r--r--application/controllers/ServiceapplyrulesController.php39
-rw-r--r--application/controllers/ServicegroupController.php9
-rw-r--r--application/controllers/ServicegroupsController.php9
-rw-r--r--application/controllers/ServicesController.php42
-rw-r--r--application/controllers/ServicesetController.php141
-rw-r--r--application/controllers/ServicetemplateController.php16
-rw-r--r--application/controllers/SettingsController.php48
-rw-r--r--application/controllers/SuggestController.php415
-rw-r--r--application/controllers/SyncruleController.php696
-rw-r--r--application/controllers/SyncrulesController.php45
-rw-r--r--application/controllers/TemplatechoiceController.php41
-rw-r--r--application/controllers/TemplatechoicesController.php39
-rw-r--r--application/controllers/TimeperiodController.php33
-rw-r--r--application/controllers/TimeperiodsController.php9
-rw-r--r--application/controllers/TimeperiodtemplateController.php16
-rw-r--r--application/controllers/UserController.php18
-rw-r--r--application/controllers/UsergroupController.php9
-rw-r--r--application/controllers/UsergroupsController.php9
-rw-r--r--application/controllers/UsersController.php13
-rw-r--r--application/controllers/UsertemplateController.php16
-rw-r--r--application/controllers/ZoneController.php9
-rw-r--r--application/controllers/ZonesController.php9
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
+{
+}