diff --git a/application/clicommands/ConfigCommand.php b/application/clicommands/ConfigCommand.php
index ca44d853..85408e1c 100644
--- a/application/clicommands/ConfigCommand.php
+++ b/application/clicommands/ConfigCommand.php
@@ -5,7 +5,7 @@ namespace Icinga\Module\Director\Clicommands;
use Icinga\Application\Benchmark;
use Icinga\Module\Director\Cli\Command;
use Icinga\Module\Director\IcingaConfig\IcingaConfig;
-use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Util;
/**
* Generate, show and deploy Icinga 2 configuration
diff --git a/application/controllers/CommandController.php b/application/controllers/CommandController.php
index 0327b40a..b546ced1 100644
--- a/application/controllers/CommandController.php
+++ b/application/controllers/CommandController.php
@@ -10,7 +10,7 @@ class CommandController extends ObjectController
public function init()
{
parent::init();
- if ($this->object) {
+ if ($this->object && ! $this->object->isExternal()) {
$this->getTabs()->add('arguments', array(
'url' => 'director/command/arguments',
'urlParams' => array('name' => $this->object->object_name),
@@ -22,7 +22,10 @@ class CommandController extends ObjectController
public function argumentsAction()
{
$this->getTabs()->activate('arguments');
- $this->view->title = $this->translate('Command arguments');
+ $this->view->title = sprintf(
+ $this->translate('Command arguments: %s'),
+ $this->object->object_name
+ );
$this->view->table = $this
->loadTable('icingaCommandArgument')
diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php
index 7ee73a0d..f55f9294 100644
--- a/application/controllers/ConfigController.php
+++ b/application/controllers/ConfigController.php
@@ -2,6 +2,7 @@
namespace Icinga\Module\Director\Controllers;
+use Icinga\Module\Director\ConfigDiff;
use Icinga\Module\Director\IcingaConfig\IcingaConfig;
use Icinga\Module\Director\Util;
use Icinga\Module\Director\Web\Controller\ActionController;
@@ -173,6 +174,56 @@ class ConfigController extends ActionController
);
}
+ public function diffAction()
+ {
+ $db = $this->db();
+ $this->view->title = $this->translate('Config diff');
+
+ $tabs = $this->getTabs()->add('diff', array(
+ 'label' => $this->translate('Config diff'),
+ 'url' => $this->getRequest()->getUrl()
+ ))->activate('diff');
+
+ $leftSum = $this->view->leftSum = $this->params->get('left');
+ $rightSum = $this->view->rightSum = $this->params->get('right');
+ $left = IcingaConfig::load(Util::hex2binary($leftSum), $db);
+
+ $this->view->configs = $db->enumDeployedConfigs();
+ if ($rightSum === null) {
+ return;
+ }
+
+ $right = IcingaConfig::load(Util::hex2binary($rightSum), $db);
+ $this->view->table = $this
+ ->loadTable('ConfigFileDiff')
+ ->setConnection($this->db())
+ ->setLeftChecksum($leftSum)
+ ->setRightChecksum($rightSum);
+ }
+
+ public function filediffAction()
+ {
+ $db = $this->db();
+ $leftSum = $this->params->get('left');
+ $rightSum = $this->params->get('right');
+ $filename = $this->view->filename = $this->params->get('file_path');
+
+ $left = IcingaConfig::load(Util::hex2binary($leftSum), $db);
+ $right = IcingaConfig::load(Util::hex2binary($rightSum), $db);
+
+ $leftFile = $left->getFile($filename);
+ $rightFile = $right->getFile($filename);
+
+ $d = ConfigDiff::create($leftFile, $rightFile);
+
+ $this->view->title = sprintf(
+ $this->translate('Config file "%s"'),
+ $filename
+ );
+
+ $this->view->output = $d->renderHtml();
+ }
+
protected function overviewTabs()
{
$this->view->tabs = $this->getTabs()->add(
diff --git a/application/controllers/DataController.php b/application/controllers/DataController.php
index 3223c951..8886305c 100644
--- a/application/controllers/DataController.php
+++ b/application/controllers/DataController.php
@@ -137,7 +137,7 @@ class DataController extends ActionController
$listId = $list->id;
$form = $this->view->form = $this->loadForm('directorDatalistentry')
- ->setSuccessUrl('director/data/listentry')
+ ->setSuccessUrl('director/data/listentry?list_id=' . $listId)
->setList($list)
->setDb($this->db());
diff --git a/application/controllers/DatalistentryController.php b/application/controllers/DatalistentryController.php
deleted file mode 100644
index 2cf6c2be..00000000
--- a/application/controllers/DatalistentryController.php
+++ /dev/null
@@ -1,66 +0,0 @@
-indexAction();
- }
-
- public function editAction()
- {
- $this->indexAction(true);
- }
-
- public function indexAction($edit = false)
- {
- $request = $this->getRequest();
-
- $listId = $this->params->get('list_id');
- $this->view->lastId = $listId;
-
- if ($this->params->get('list_id') && $entryName = $this->params->get('entry_name')) {
- $edit = true;
- }
-
- if ($edit) {
- $this->view->title = $this->translate('Edit entry');
- $this->getTabs()->add('editentry', array(
- 'url' => 'director/datalistentry/edit' . '?list_id=' . $listId . '&entry_name=' . $entryName,
- 'label' => $this->view->title,
- ))->activate('editentry');
- } else {
- $this->view->title = $this->translate('Add entry');
- $this->getTabs()->add('addlistentry', array(
- 'url' => 'director/datalistentry/add' . '?list_id=' . $listId,
- 'label' => $this->view->title,
- ))->activate('addlistentry');
- }
-
- $form = $this->view->form = $this->loadForm('directorDatalistentry')
- ->setListId($listId)
- ->setSuccessUrl('director/datalistentry' . '?list_id=' . $listId)
- ->setDb($this->db());
-
- if ($request->isPost()) {
- $listId = $request->getParam('list_id');
- $entryName = $request->getParam('entry_name');
- }
-
- if ($edit) {
- $form->loadObject(array('list_id' => $listId, 'entry_name' => $entryName));
- if ($el = $form->getElement('entry_name')) {
- // TODO: Doesn't work without setup
- $el->setAttribs(array('readonly' => true));
- }
- }
-
- $form->handleRequest();
-
- $this->render('object/form', null, true);
- }
-}
diff --git a/application/controllers/HostController.php b/application/controllers/HostController.php
index 8219676f..fb193551 100644
--- a/application/controllers/HostController.php
+++ b/application/controllers/HostController.php
@@ -2,6 +2,7 @@
namespace Icinga\Module\Director\Controllers;
+use Exception;
use Icinga\Exception\NotFoundError;
use Icinga\Module\Director\Objects\IcingaEndpoint;
use Icinga\Module\Director\Objects\IcingaZone;
@@ -78,7 +79,24 @@ class HostController extends ObjectController
$this->view->title = 'Agent deployment instructions';
// TODO: Fail when no ticket
$this->view->certname = $this->object->object_name;
- $this->view->ticket = Util::getIcingaTicket($this->view->certname, $this->api()->getTicketSalt());
+
+ try {
+ $this->view->ticket = Util::getIcingaTicket(
+ $this->view->certname,
+ $this->api()->getTicketSalt()
+ );
+
+ } catch (Exception $e) {
+ $this->view->ticket = 'ERROR';
+ $this->view->error = sprintf(
+ $this->translate(
+ 'A ticket for this agent could not have been requested from'
+ . ' your deployment endpoint: %s'
+ ),
+ $e->getMessage()
+ );
+ }
+
$this->view->master = $this->db()->getDeploymentEndpointName();
$this->view->masterzone = $this->db()->getMasterZoneName();
$this->view->globalzone = $this->db()->getDefaultGlobalZoneName();
@@ -95,48 +113,11 @@ class HostController extends ObjectController
throw new NotFoundError('The host "%s" is not an agent', $host->object_name);
}
- return $this->sendJson(Util::getIcingaTicket($host->object_name, $this->api()->getTicketSalt()));
- }
-
- public function renderAction()
- {
- $this->renderAgentExtras();
- return parent::renderAction();
- }
-
- protected function renderAgentExtras()
- {
- $host = $this->object;
- $db = $this->db();
- if ($host->object_type !== 'object') {
- return;
- }
-
- if ($host->getResolvedProperty('has_agent') !== 'y') {
- return;
- }
-
- $name = $host->object_name;
- if (IcingaEndpoint::exists($name, $db)) {
- return;
- }
-
- $props = array(
- 'object_name' => $name,
- 'object_type' => 'object',
- 'log_duration' => 0
- );
- if ($host->getResolvedProperty('master_should_connect') === 'y') {
- $props['host'] = $host->getResolvedProperty('address');
- $props['zone_id'] = $host->getResolvedProperty('zone_id');
- }
-
- $this->view->extraObjects = array(
- IcingaEndpoint::create($props),
- IcingaZone::create(array(
- 'object_name' => $name,
- 'parent' => $db->getMasterZoneName()
- ), $db)->setEndpointList(array($name))
+ return $this->sendJson(
+ Util::getIcingaTicket(
+ $host->object_name,
+ $this->api()->getTicketSalt()
+ )
);
}
}
diff --git a/application/controllers/IndexController.php b/application/controllers/IndexController.php
index 391b26eb..34f06792 100644
--- a/application/controllers/IndexController.php
+++ b/application/controllers/IndexController.php
@@ -3,6 +3,10 @@
namespace Icinga\Module\Director\Controllers;
use Exception;
+use Icinga\Module\Director\Db\Migrations;
+use Icinga\Module\Director\Objects\DirectorJob;
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\Objects\SyncRule;
use Icinga\Module\Director\Web\Controller\ActionController;
class IndexController extends ActionController
@@ -29,9 +33,98 @@ class IndexController extends ActionController
'url' => $this->getRequest()->getUrl(),
'label' => $this->translate('Overview')
))->activate('overview');
+
+ $migrations = new Migrations($this->db());
+
+ if ($migrations->hasPendingMigrations()) {
+ $this->view->migrationsForm = $this
+ ->loadForm('applyMigrations')
+ ->setMigrations($migrations)
+ ->handleRequest();
+ }
+
+ try {
+ $this->fetchSyncState()
+ ->fetchImportState()
+ ->fetchJobState();
+ } catch (Exception $e) {
+ }
}
}
+ protected function fetchSyncState()
+ {
+ $syncs = SyncRule::loadAll($this->db());
+ if (count($syncs) > 0) {
+ $state = 'ok';
+ } else {
+ $state = null;
+ }
+
+ foreach ($syncs as $sync) {
+ if ($sync->sync_state !== 'in-sync') {
+ if ($sync->sync_state === 'failing') {
+ $state = 'critical';
+ break;
+ } else {
+ $state = 'warning';
+ }
+ }
+ }
+
+ $this->view->syncState = $state;
+
+ return $this;
+ }
+
+ protected function fetchImportState()
+ {
+ $srcs = ImportSource::loadAll($this->db());
+ if (count($srcs) > 0) {
+ $state = 'ok';
+ } else {
+ $state = null;
+ }
+
+ foreach ($srcs as $src) {
+ if ($src->import_state !== 'in-sync') {
+ if ($src->import_state === 'failing') {
+ $state = 'critical';
+ break;
+ } else {
+ $state = 'warning';
+ }
+ }
+ }
+
+ $this->view->importState = $state;
+
+ return $this;
+ }
+
+ protected function fetchJobState()
+ {
+ $jobs = DirectorJob::loadAll($this->db());
+ if (count($jobs) > 0) {
+ $state = 'ok';
+ } else {
+ $state = null;
+ }
+
+ foreach ($jobs as $job) {
+ if ($job->isPending()) {
+ $state = 'pending';
+ } elseif (! $job->lastAttemptSucceeded()) {
+ $state = 'critical';
+ break;
+ }
+ }
+
+ $this->view->jobState = $state;
+
+ return $this;
+ }
+
protected function hasDeploymentEndpoint()
{
try {
diff --git a/application/controllers/JobController.php b/application/controllers/JobController.php
new file mode 100644
index 00000000..0ac1ca55
--- /dev/null
+++ b/application/controllers/JobController.php
@@ -0,0 +1,78 @@
+indexAction();
+ }
+
+ public function editAction()
+ {
+ $this->indexAction();
+ }
+
+ public function runAction()
+ {
+ // TODO: Form, POST
+ $id = $this->params->get('id');
+ $job = Job::load($id, $this->db());
+ if ($job->run()) {
+ Notification::success('Job has successfully been completed');
+ $this->redirectNow(
+ Url::fromPath(
+ 'director/job',
+ array('id' => $id)
+ )
+ );
+ } else {
+ Notification::success('Job run failed');
+ }
+ }
+
+ public function indexAction()
+ {
+ $form = $this->view->form = $this->loadForm('directorJob')
+ ->setSuccessUrl('director/job')
+ ->setDb($this->db());
+
+ if ($id = $this->params->get('id')) {
+ $this->prepareTabs($id)->activate('edit');
+ $form->loadObject($id);
+ $this->view->title = sprintf(
+ $this->translate('Job %s'),
+ $form->getObject()->job_name
+ );
+ } else {
+ $this->view->title = $this->translate('Add job');
+ $this->prepareTabs()->activate('add');
+ }
+
+ $form->handleRequest();
+ $this->setViewScript('object/form');
+ }
+
+ protected function prepareTabs($id = null)
+ {
+ if ($id) {
+ return $this->getTabs()->add('edit', array(
+ 'url' => 'director/job/edit',
+ 'urlParams' => array('id' => $id),
+ 'label' => $this->translate('Job'),
+ ));
+ } else {
+ return $this->getTabs()->add('add', array(
+ 'url' => 'director/job/add',
+ 'label' => $this->translate('Job'),
+ ));
+ }
+ }
+}
diff --git a/application/controllers/JobsController.php b/application/controllers/JobsController.php
new file mode 100644
index 00000000..23ddd9c9
--- /dev/null
+++ b/application/controllers/JobsController.php
@@ -0,0 +1,33 @@
+setAutoRefreshInterval(10);
+ $this->view->title = $this->translate('Jobs');
+
+ $this->getTabs()->add('jobs', array(
+ 'url' => 'director/jobs',
+ 'label' => $this->translate('Jobs'),
+ ))->activate('jobs');
+
+ $this->view->addLink = $this->view->qlink(
+ $this->translate('Add'),
+ 'director/job',
+ null,
+ array('class' => 'icon-plus')
+ );
+
+ $this->view->table = $this->applyPaginationLimits(
+ $this->loadTable('job')
+ ->setConnection($this->db())
+ );
+ $this->setViewScript('list/table');
+
+ }
+}
diff --git a/application/controllers/SyncruleController.php b/application/controllers/SyncruleController.php
index b233d87a..9e1405c5 100644
--- a/application/controllers/SyncruleController.php
+++ b/application/controllers/SyncruleController.php
@@ -4,9 +4,11 @@ namespace Icinga\Module\Director\Controllers;
use Icinga\Module\Director\Web\Controller\ActionController;
use Icinga\Module\Director\Objects\SyncRule;
+use Icinga\Module\Director\Objects\SyncRun;
use Icinga\Module\Director\Import\Sync;
use Icinga\Data\Filter\Filter;
use Icinga\Web\Notification;
+use Icinga\Web\Url;
class SyncruleController extends ActionController
{
@@ -22,81 +24,67 @@ class SyncruleController extends ActionController
public function runAction()
{
- $sync = new Sync(SyncRule::load($this->params->get('id'), $this->db()));
+ $id = $this->params->get('id');
+ $sync = new Sync(SyncRule::load($id, $this->db()));
if ($runId = $sync->apply()) {
Notification::success('Source has successfully been synchronized');
- $this->redirectNow('director/list/syncrule');
+ $this->redirectNow(
+ Url::fromPath(
+ 'director/syncrule/history',
+ array(
+ 'id' => $id,
+ 'run_id' => $runId
+ )
+ )
+ );
} else {
}
}
public function indexAction()
{
- $edit = false;
-
- if ($id = $this->params->get('id')) {
- $edit = true;
- }
-
- if ($edit) {
- $this->view->title = $this->translate('Edit sync rule');
- $this->getTabs()->add('edit', array(
- 'url' => 'director/syncrule/edit',
- 'urlParams' => array('id' => $id),
- 'label' => $this->view->title,
- ))->add('property', array(
- 'label' => $this->translate('Properties'),
- 'url' => 'director/syncrule/property',
- 'urlParams' => array('rule_id' => $id)
- ))->activate('edit');
- } else {
- $this->view->title = $this->translate('Add sync rule');
- $this->getTabs()->add('add', array(
- 'url' => 'director/syncrule/add',
- 'label' => $this->view->title,
- ))->activate('add');
- }
-
$form = $this->view->form = $this->loadForm('syncRule')
->setSuccessUrl('director/list/syncrule')
->setDb($this->db());
- if ($edit) {
+ if ($id = $this->params->get('id')) {
+ $this->prepareRuleTabs($id)->activate('edit');
$form->loadObject($id);
+ $this->view->title = sprintf(
+ $this->translate('Sync rule: %s'),
+ $form->getObject()->rule_name
+ );
+ } else {
+ $this->view->title = $this->translate('Add sync rule');
+ $this->prepareRuleTabs()->activate('add');
}
$form->handleRequest();
-
- $this->render('object/form', null, true);
+ $this->setViewScript('object/form');
}
public function propertyAction()
{
$this->view->stayHere = true;
+
+ $db = $this->db();
$id = $this->params->get('rule_id');
+ $rule = SyncRule::load($id, $db);
- $this->view->addLink = $this->view->icon('plus')
- . ' '
- . $this->view->qlink(
- $this->translate('Add sync property rule'),
- 'director/syncrule/addproperty',
- array('rule_id' => $id)
- );
- $this->getTabs()->add('edit', array(
- 'url' => 'director/syncrule/edit',
- 'urlParams' => array('id' => $id),
- 'label' => $this->translate('Edit sync rule'),
- ))->add('property', array(
- 'label' => $this->translate('Properties'),
- 'url' => 'director/syncrule/property',
- 'urlParams' => array('rule_id' => $id)
- ))->activate('property');
+ $this->prepareRuleTabs($id)->activate('property');
- $this->view->title = $this->translate('Sync properties: ');
+ $this->view->addLink = $this->view->qlink(
+ $this->translate('Add sync property rule'),
+ 'director/syncrule/addproperty',
+ array('rule_id' => $id),
+ array('class' => 'icon-plus')
+ );
+
+ $this->view->title = $this->translate('Sync properties') . ': ' . $rule->rule_name;
$this->view->table = $this->loadTable('syncproperty')
->enforceFilter(Filter::where('rule_id', $id))
->setConnection($this->db());
- $this->render('list/table', null, true);
+ $this->setViewScript('list/table');
}
public function editpropertyAction()
@@ -109,49 +97,102 @@ class SyncruleController extends ActionController
$this->view->stayHere = true;
$edit = false;
+ $db = $this->db();
+ $ruleId = $this->params->get('rule_id');
+ $rule = SyncRule::load($ruleId, $db);
+
if ($id = $this->params->get('id')) {
$edit = true;
}
- $form = $this->view->form = $this->loadForm('syncProperty')->setDb($this->db());
+ $this->view->addLink = $this->view->qlink(
+ $this->translate('back'),
+ 'director/syncrule/property',
+ array('rule_id' => $ruleId),
+ array('class' => 'icon-left-big')
+ );
+
+ $form = $this->view->form = $this->loadForm('syncProperty')->setDb($db);
if ($edit) {
$form->loadObject($id);
$rule_id = $form->getObject()->rule_id;
- $form->setRule(SyncRule::load($rule_id, $this->db()));
+ $form->setRule(SyncRule::load($rule_id, $db));
} elseif ($rule_id = $this->params->get('rule_id')) {
- $form->setRule(SyncRule::load($rule_id, $this->db()));
+ $form->setRule(SyncRule::load($rule_id, $db));
}
- $form->setSuccessUrl('director/syncrule/property', array('rule_id' => $rule_id));
+ $form->setSuccessUrl('director/syncrule/property', array('rule_id' => $rule_id));
$form->handleRequest();
- $tabs = $this->getTabs()->add('edit', array(
- 'url' => 'director/syncrule/edit',
- 'urlParams' => array('id' => $rule_id),
- 'label' => $this->translate('Edit sync rule'),
- ));
+ $this->prepareRuleTabs($rule_id)->activate('property');
if ($edit) {
- $tabs->add('property', array(
- 'label' => $this->translate('Properties'),
- 'url' => 'director/syncrule/property',
- 'urlParams' => array('rule_id' => $rule_id)
- ));
+ $this->view->title = sprintf(
+ $this->translate('Sync "%s": %s'),
+ $form->getObject()->destination_field,
+ $rule->rule_name
+ );
} else {
- $tabs->add('property', array(
- 'label' => $this->translate('Properties'),
- 'url' => 'director/syncrule/property',
- 'urlParams' => array('rule_id' => $rule_id)
- ));
+ $this->view->title = sprintf(
+ $this->translate('Add sync property: %s'),
+ $rule->rule_name
+ );
}
- $tabs->activate('property');
-
- $this->view->title = $this->translate('Sync property'); // add/edit
$this->view->table = $this->loadTable('syncproperty')
->enforceFilter(Filter::where('rule_id', $rule_id))
->setConnection($this->db());
- $this->render('list/table', null, true);
+ $this->setViewScript('list/table');
+ }
+
+ public function historyAction()
+ {
+ $this->view->stayHere = true;
+
+ $db = $this->db();
+ $id = $this->params->get('id');
+ $rule = SyncRule::load($id, $db);
+
+ $this->prepareRuleTabs($id)->activate('history');
+ $this->view->title = $this->translate('Sync history') . ': ' . $rule->rule_name;
+ $this->view->table = $this->loadTable('syncRun')
+ ->enforceFilter(Filter::where('rule_id', $id))
+ ->setConnection($this->db());
+
+ if ($runId = $this->params->get('run_id')) {
+ $this->view->run = SyncRun::load($runId, $db);
+ $this->view->formerId = $db->fetchActivityLogIdByChecksum(
+ $this->view->run->last_former_activity
+ );
+
+ $this->view->lastId = $db->fetchActivityLogIdByChecksum(
+ $this->view->run->last_related_activity
+ );
+ }
+ }
+
+ protected function prepareRuleTabs($ruleId = null)
+ {
+ if ($ruleId) {
+ return $this->getTabs()->add('edit', array(
+ 'url' => 'director/syncrule/edit',
+ 'urlParams' => array('id' => $ruleId),
+ 'label' => $this->translate('Sync rule'),
+ ))->add('property', array(
+ 'label' => $this->translate('Properties'),
+ 'url' => 'director/syncrule/property',
+ 'urlParams' => array('rule_id' => $ruleId)
+ ))->add('history', array(
+ 'label' => $this->translate('History'),
+ 'url' => 'director/syncrule/history',
+ 'urlParams' => array('id' => $ruleId)
+ ));
+ } else {
+ return $this->getTabs()->add('add', array(
+ 'url' => 'director/syncrule/add',
+ 'label' => $this->translate('Sync rule'),
+ ));
+ }
}
}
diff --git a/application/forms/ApplyMigrationsForm.php b/application/forms/ApplyMigrationsForm.php
new file mode 100644
index 00000000..c1d954e4
--- /dev/null
+++ b/application/forms/ApplyMigrationsForm.php
@@ -0,0 +1,37 @@
+setSubmitLabel($this->translate('Apply schema migrations'));
+ }
+
+ public function onSuccess()
+ {
+ try {
+ $this->setSuccessMessage($this->translate(
+ 'Pending database schema migrations have successfully been applied'
+ ));
+
+ $this->migrations->applyPendingMigrations();
+ parent::onSuccess();
+ } catch (Exception $e) {
+ $this->addError($e->getMessage());
+ }
+ }
+
+ public function setMigrations(Migrations $migrations)
+ {
+ $this->migrations = $migrations;
+ return $this;
+ }
+}
diff --git a/application/forms/DirectorDatafieldForm.php b/application/forms/DirectorDatafieldForm.php
index e9cd485f..f813a719 100644
--- a/application/forms/DirectorDatafieldForm.php
+++ b/application/forms/DirectorDatafieldForm.php
@@ -74,6 +74,8 @@ class DirectorDatafieldForm extends DirectorObjectForm
$el->setValue($val);
}
}
+
+ $this->setButtons();
}
protected function addSettings($class = null)
diff --git a/application/forms/DirectorJobForm.php b/application/forms/DirectorJobForm.php
new file mode 100644
index 00000000..51bf36ef
--- /dev/null
+++ b/application/forms/DirectorJobForm.php
@@ -0,0 +1,117 @@
+addElement('select', 'job_class', array(
+ 'label' => $this->translate('Job Type'),
+ 'required' => true,
+ 'multiOptions' => $this->optionalEnum($this->enumJobTypes()),
+ 'description' => $this->translate(
+ 'These are different available job types'
+ ),
+ 'class' => 'autosubmit'
+ ));
+
+ if (! $jobClass = $this->getJobClass()) {
+ return;
+ }
+
+ if ($desc = $jobClass::getDescription($this)) {
+ $this->addHtmlHint($desc);
+ }
+
+ $this->addBoolean(
+ 'disabled',
+ array(
+ 'label' => $this->translate('Disabled'),
+ 'description' => $this->translate(
+ 'This allows to temporarily disable this job'
+ )
+ ),
+ 'n'
+ );
+
+ $this->addElement('text', 'run_interval', array(
+ 'label' => $this->translate('Run interval'),
+ 'description' => $this->translate(
+ 'Execution interval for this job, in seconds'
+ ),
+ 'value' => $jobClass::getSuggestedRunInterval($this)
+ ));
+
+ $this->addElement('text', 'job_name', array(
+ 'label' => $this->translate('Job name'),
+ 'description' => $this->translate(
+ 'A short name identifying this job. Use something meaningful,'
+ . ' like "Import Puppet Hosts"'
+ ),
+ 'required' => true,
+ ));
+
+ $this->addSettings();
+ $this->setButtons();
+ }
+
+ public function getSentOrObjectSetting($name, $default = null)
+ {
+ if ($this->hasObject()) {
+ $value = $this->getSentValue($name);
+ if ($value === null) {
+ $object = $this->getObject();
+
+ return $object->getSetting($name, $default);
+ } else {
+ return $value;
+ }
+ } else {
+ return $this->getSentValue($name, $default);
+ }
+ }
+
+ protected function getJobClass($class = null)
+ {
+ if ($class === null) {
+ $class = $this->getSentOrObjectValue('job_class');
+ }
+
+ if (array_key_exists($class, $this->enumJobTypes())) {
+ return $class;
+ }
+
+ return null;
+ }
+
+ protected function addSettings($class = null)
+ {
+ if (! $class = $this->getJobClass($class)) {
+ return;
+ }
+
+ $class::addSettingsFormFields($this);
+ foreach ($this->object()->getSettings() as $key => $val) {
+ if ($el = $this->getElement($key)) {
+ $el->setValue($val);
+ }
+ }
+ }
+
+ protected function enumJobTypes()
+ {
+ $hooks = Hook::all('Director\\Job');
+
+ $enum = array();
+ foreach ($hooks as $hook) {
+ $enum[get_class($hook)] = $hook->getName();
+ }
+ asort($enum);
+
+ return $enum;
+ }
+}
diff --git a/application/forms/IcingaAssignServiceToHostForm.php b/application/forms/IcingaAssignServiceToHostForm.php
deleted file mode 100644
index 3c63a499..00000000
--- a/application/forms/IcingaAssignServiceToHostForm.php
+++ /dev/null
@@ -1,140 +0,0 @@
-db = $db;
- return $this;
- }
-
- public function setIcingaObject($object)
- {
- $this->icingaObject = $object;
-// $this->className = get_class($object) . 'Field';
- return $this;
- }
-
- public function setup()
- {
- $this->addHidden('service_id', $this->icingaObject->id);
-
- if ($this->icingaObject->isTemplate()) {
- $this->addHtmlHint(
- 'Assign all services importing this service template to one or'
- . ' more hosts'
- );
- } else {
- $this->addHtmlHint(
- 'Assign this service to one or more hosts'
- );
- }
-
- $this->addElement('select', 'object_type', array(
- 'label' => 'Assign',
- 'required' => true,
- 'multiOptions' => $this->optionalEnum(
- array(
- 'host_group' => $this->translate('to a host group'),
- 'host_property' => $this->translate('by host property'),
- 'host_group_property' => $this->translate('by host group property'),
- )
- ),
- 'class' => 'autosubmit'
-
- ));
-
- switch ($this->getSentValue('object_type')) {
- case 'host_group':
- $this->addHostGroupElements();
- break;
- case 'host_property':
- $this->addHostPropertyElements();
- break;
- case 'host_property':
- $this->addHostFilterElements();
- break;
- }
-
- $this->setSubmitLabel(
- $this->translate('Assign')
- );
- }
-
- protected function addHostGroupElements()
- {
- $this->addElement('select', 'host_id', array(
- 'label' => 'Hostgroup',
- 'required' => true,
- 'multiOptions' => $this->optionalEnum($this->db->enumHostgroups())
- ));
- }
-
- protected function addHostPropertyElements()
- {
- $this->addElement('select', 'host_property', array(
- 'label' => 'Host property',
- 'required' => true,
- 'multiOptions' => $this->optionalEnum(IcingaHost::enumProperties($this->db))
- ));
- $this->addElement('text', 'filter_expression', array(
- 'label' => 'Filter expression',
- 'required' => true,
- ));
- }
-
- protected function addHostFilterElements()
- {
- $this->addElement('text', 'host_filter', array(
- 'label' => 'Host filter string',
- 'required' => true,
- ));
- }
-
- public function onSuccess()
- {
- switch ($this->getValue('object_type')) {
- case 'host_group':
- $this->db->insert('icinga_service_assignment', array(
- 'service_id' => $this->getValue('service_id'),
- // TODO: in?
- 'filter_string' => 'groups=' . $this->getValue('host_group'),
- ));
- break;
- case 'host_property':
- $this->db->insert('icinga_service_assignment', array(
- 'service_id' => $this->getValue('service_id'),
- 'filter_string' => sprintf(
- 'host.%s=%s',
- $this->getValue('host_property'),
- c::renderString($this->getValue('filter_expression'))
- )
- ));
- break;
- case 'host_filter':
- $this->db->insert('icinga_service_assignment', array(
- 'service_id' => $this->getValue('service_id'),
- 'filter_string' => $this->getValue('filter_string'),
- ));
- break;
- }
- }
-}
diff --git a/application/forms/KickstartForm.php b/application/forms/KickstartForm.php
index b9b8d4df..b1da43c5 100644
--- a/application/forms/KickstartForm.php
+++ b/application/forms/KickstartForm.php
@@ -27,9 +27,10 @@ class KickstartForm extends QuickForm
$this->migrateDbLabel = $this->translate('Apply schema migrations');
$this->addResourceConfigElements();
+ $this->addResourceDisplayGroup();
+
if (!$this->config()->get('db', 'resource')
|| ($this->config()->get('db', 'resource') !== $this->getResourceName())) {
- $this->addResourceDisplayGroup();
return;
}
@@ -66,6 +67,9 @@ class KickstartForm extends QuickForm
));
$this->addHtmlHint($hint, array('name' => 'HINT_ready'));
+ $this->getDisplayGroup('config')->addElements(
+ array($this->getElement('HINT_ready'))
+ );
return;
}
@@ -168,7 +172,6 @@ class KickstartForm extends QuickForm
$this->addHtmlHint($hint, array('name' => 'HINT_db_perms'));
}
-
}
}
@@ -214,10 +217,11 @@ class KickstartForm extends QuickForm
{
$elements = array(
'HINT_no_resource',
- 'HINT_ready',
'resource',
+ 'HINT_ready',
'HINT_schema',
- 'HINT_db_perms'
+ 'HINT_db_perms',
+ 'HINT_config_store'
);
$this->addDisplayGroup($elements, 'config', array(
@@ -258,26 +262,41 @@ class KickstartForm extends QuickForm
try {
$config->saveIni();
$this->setSuccessMessage($this->translate('Configuration has been stored'));
+
+ return true;
} catch (Exception $e) {
$this->getElement('resource')->addError(
sprintf(
- $this->translate('Unable to store the configuration to "%s"'),
+ $this->translate(
+ 'Unable to store the configuration to "%s". Please check'
+ . ' file permissions or manually store the content shown below'
+ ),
$config->getConfigFile()
)
- )->removeDecorator('description');
- $this->addHtmlHint(
- '
' . $config . '
'
);
- }
+ $this->addHtmlHint(
+ '' . $config . '
',
+ array('name' => 'HINT_config_store')
+ );
+ $this->getDisplayGroup('config')->addElements(
+ array($this->getElement('HINT_config_store'))
+ );
+ $this->removeElement('HINT_ready');
+
+ return false;
+ }
}
public function onSuccess()
{
try {
if ($this->getSubmitLabel() === $this->storeConfigLabel) {
- $this->storeResourceConfig();
- return parent::onSuccess();
+ if ($this->storeResourceConfig()) {
+ return parent::onSuccess();
+ } else {
+ return;
+ }
}
if ($this->getSubmitLabel() === $this->createDbLabel
diff --git a/application/forms/SyncPropertyForm.php b/application/forms/SyncPropertyForm.php
index 77d40d89..7ffa7b23 100644
--- a/application/forms/SyncPropertyForm.php
+++ b/application/forms/SyncPropertyForm.php
@@ -25,7 +25,6 @@ class SyncPropertyForm extends DirectorObjectForm
public function setup()
{
- $this->addHtml(sprintf('%s
', $this->getView()->escape($this->rule->rule_name)));
$this->addHidden('rule_id', $this->rule_id);
$this->addElement('select', 'source_id', array(
diff --git a/application/tables/ConfigFileDiffTable.php b/application/tables/ConfigFileDiffTable.php
new file mode 100644
index 00000000..347c9967
--- /dev/null
+++ b/application/tables/ConfigFileDiffTable.php
@@ -0,0 +1,151 @@
+file_action;
+ }
+
+ protected function getActionUrl($row)
+ {
+ $params = array('file_path' => $row->file_path);
+
+ if ($row->file_checksum_left === $row->file_checksum_right) {
+ $params['config_checksum'] = $row->config_checksum_right;
+ } elseif ($row->file_checksum_left === null) {
+ $params['config_checksum'] = $row->config_checksum_right;
+ } elseif ($row->file_checksum_right === null) {
+ $params['config_checksum'] = $row->config_checksum_left;
+ } else {
+ $params['left'] = $row->config_checksum_left;
+ $params['right'] = $row->config_checksum_right;
+ return $this->url('director/config/filediff', $params);
+ }
+
+ return $this->url('director/config/file', $params);
+ }
+
+ public function setLeftChecksum($checksum)
+ {
+ $this->leftChecksum = $checksum;
+ return $this;
+ }
+
+ public function setRightChecksum($checksum)
+ {
+ $this->rightChecksum = $checksum;
+ return $this;
+ }
+
+ public function getTitles()
+ {
+ $view = $this->view();
+ return array(
+ 'file_action' => $view->translate('Action'),
+ 'file_path' => $view->translate('File'),
+ );
+ }
+
+ public function count()
+ {
+ $db = $this->connection()->getConnection();
+ $query = clone($this->getBaseQuery());
+ $query->reset('order');
+ $this->applyFiltersToQuery($query);
+ return $db->fetchOne($db->select()->from(
+ array('cntsub' => $query),
+ array('cnt' => 'COUNT(*)')
+ ));
+ }
+
+ public function fetchData()
+ {
+ $db = $this->connection()->getConnection();
+ $query = $this->getBaseQuery();
+
+ if ($this->hasLimit() || $this->hasOffset()) {
+ $query->limit($this->getLimit(), $this->getOffset());
+ }
+
+ $this->applyFiltersToQuery($query);
+
+ return $db->fetchAll($query);
+ }
+
+ public function getBaseQuery()
+ {
+ $conn = $this->connection();
+ $db = $conn->getConnection();
+
+ $left = $db->select()
+ ->from(
+ array('cfl' => 'director_generated_config_file'),
+ array(
+ 'file_path' => 'COALESCE(cfl.file_path, cfr.file_path)',
+ 'config_checksum_left' => $conn->dbHexFunc('cfl.config_checksum'),
+ 'config_checksum_right' => $conn->dbHexFunc('cfr.config_checksum'),
+ 'file_checksum_left' => $conn->dbHexFunc('cfl.file_checksum'),
+ 'file_checksum_right' => $conn->dbHexFunc('cfr.file_checksum'),
+ 'file_action' => '(CASE WHEN cfr.config_checksum IS NULL'
+ . " THEN 'removed' WHEN cfl.file_checksum = cfr.file_checksum"
+ . " THEN 'unmodified' ELSE 'modified' END)",
+ )
+ )->joinLeft(
+ array('cfr' => 'director_generated_config_file'),
+ $db->quoteInto(
+ 'cfl.file_path = cfr.file_path AND cfr.config_checksum = ?',
+ $conn->quoteBinary(Util::hex2binary($this->rightChecksum))
+ ),
+ array()
+ )->where(
+ 'cfl.config_checksum = ?',
+ $conn->quoteBinary(Util::hex2binary($this->leftChecksum))
+ );
+
+ $right = $db->select()
+ ->from(
+ array('cfl' => 'director_generated_config_file'),
+ array(
+ 'file_path' => 'COALESCE(cfr.file_path, cfl.file_path)',
+ 'config_checksum_left' => $conn->dbHexFunc('cfl.config_checksum'),
+ 'config_checksum_right' => $conn->dbHexFunc('cfr.config_checksum'),
+ 'file_checksum_left' => $conn->dbHexFunc('cfl.file_checksum'),
+ 'file_checksum_right' => $conn->dbHexFunc('cfr.file_checksum'),
+ 'file_action' => "('created')",
+ )
+ )->joinRight(
+ array('cfr' => 'director_generated_config_file'),
+ $db->quoteInto(
+ 'cfl.file_path = cfr.file_path AND cfl.config_checksum = ?',
+ $conn->quoteBinary(Util::hex2binary($this->leftChecksum))
+ ),
+ array()
+ )->where(
+ 'cfr.config_checksum = ?',
+ $conn->quoteBinary(Util::hex2binary($this->rightChecksum))
+ )->where('cfl.file_checksum IS NULL');
+
+ return $db->select()->union(array($left, $right))->order('file_path');
+ }
+}
diff --git a/application/tables/DeploymentLogTable.php b/application/tables/DeploymentLogTable.php
index 1f56715c..79375add 100644
--- a/application/tables/DeploymentLogTable.php
+++ b/application/tables/DeploymentLogTable.php
@@ -45,22 +45,29 @@ class DeploymentLogTable extends QuickTable
public function getColumns()
{
+ $db = $this->connection();
+
$columns = array(
'id' => 'l.id',
'peer_identity' => 'l.peer_identity',
+ 'identifier' => "l.peer_identity || ' (' || SUBSTRING(",
'start_time' => 'l.start_time',
'stage_collected' => 'l.stage_collected',
'dump_succeeded' => 'l.dump_succeeded',
'stage_name' => 'l.stage_name',
'startup_succeeded' => 'l.startup_succeeded',
- 'checksum' => 'LOWER(HEX(c.checksum))',
+ 'checksum' => $db->dbHexFunc('c.checksum'),
'duration' => "l.duration_dump || 'ms'",
);
if ($this->connection->isPgsql()) {
- $columns['checksum'] = "LOWER(ENCODE(c.checksum, 'hex'))";
+ $columns['identifier'] .= $columns['checksum'] . ' FROM 1 FOR 7)';
+ } else {
+ $columns['identifier'] .= $columns['checksum'] . ', 1, 7)';
}
+ $columns['identifier'] .= " || ')'";
+
return $columns;
}
@@ -73,8 +80,8 @@ class DeploymentLogTable extends QuickTable
{
$view = $this->view();
return array(
- 'peer_identity' => $view->translate('Icinga Node'),
- 'start_time' => $view->translate('Time'),
+ 'identifier' => $view->translate('Icinga Node'),
+ 'start_time' => $view->translate('Time'),
);
}
diff --git a/application/tables/IcingaHostTable.php b/application/tables/IcingaHostTable.php
index 59cfb520..799d4ddf 100644
--- a/application/tables/IcingaHostTable.php
+++ b/application/tables/IcingaHostTable.php
@@ -73,6 +73,7 @@ class IcingaHostTable extends IcingaObjectTable
$db = $this->connection()->getConnection();
$sub = clone($this->getBaseQuery());
$sub->columns($this->getColumns());
+ $this->applyFiltersToQuery($sub);
$query = $db->select()->from(
array('sub' => $sub),
'COUNT(*)'
diff --git a/application/tables/ImportsourceTable.php b/application/tables/ImportsourceTable.php
index 32e50bf2..54525111 100644
--- a/application/tables/ImportsourceTable.php
+++ b/application/tables/ImportsourceTable.php
@@ -9,8 +9,6 @@ use Exception;
class ImportsourceTable extends QuickTable
{
- protected $revalidate = false;
-
protected $searchColumns = array(
'source_name',
);
@@ -18,9 +16,11 @@ class ImportsourceTable extends QuickTable
public function getColumns()
{
return array(
- 'id' => 's.id',
- 'source_name' => 's.source_name',
- 'provider_class' => 's.provider_class',
+ 'id' => 's.id',
+ 'source_name' => 's.source_name',
+ 'provider_class' => 's.provider_class',
+ 'import_state' => 's.import_state',
+ 'last_error_message' => 's.last_error_message',
);
}
@@ -44,25 +44,11 @@ class ImportsourceTable extends QuickTable
protected function getRowClasses($row)
{
- if (! $this->revalidate) {
- return array();
- }
- try {
- $import = new Import(ImportSource::load($row->id, $this->connection()));
- if ($import->providesChanges()) {
- $row->source_name = sprintf(
- '%s (%s)',
- $row->source_name,
- $this->view()->translate('has changes')
- );
- return 'pending-changes';
- } else {
- return 'in-sync';
- }
- } catch (Exception $e) {
- $row->source_name = $row->source_name . ' (' . $e->getMessage() . ')';
- return 'failing';
+ if ($row->import_state === 'failing' && $row->last_error_message) {
+ $row->source_name .= ' (' . $row->last_error_message . ')';
}
+
+ return $row->import_state;
}
public function getBaseQuery()
diff --git a/application/tables/JobTable.php b/application/tables/JobTable.php
new file mode 100644
index 00000000..55f5014b
--- /dev/null
+++ b/application/tables/JobTable.php
@@ -0,0 +1,74 @@
+ 'j.id',
+ 'job_name' => 'j.job_name',
+ 'job_class' => 'j.job_class',
+ 'disabled' => 'j.disabled',
+ 'run_interval' => 'j.run_interval',
+ 'last_attempt_succeeded' => 'j.last_attempt_succeeded',
+ 'ts_last_attempt' => 'j.ts_last_attempt',
+ 'unixts_last_attempt' => 'UNIX_TIMESTAMP(j.ts_last_attempt)',
+ 'ts_last_error' => 'j.ts_last_error',
+ 'last_error_message' => 'j.last_error_message',
+ );
+ }
+
+ protected function getActionUrl($row)
+ {
+ return $this->url('director/job', array('id' => $row->id));
+ }
+
+ protected function listTableClasses()
+ {
+ return array_merge(array('jobs'), parent::listTableClasses());
+ }
+
+ protected function getRowClasses($row)
+ {
+ if ($row->unixts_last_attempt === null) {
+ return 'pending';
+ }
+ if ($row->unixts_last_attempt + $row->run_interval < time()) {
+ return 'pending';
+ }
+
+ if ($row->last_attempt_succeeded === 'y') {
+ return 'ok';
+ } elseif ($row->last_attempt_succeeded === 'n') {
+ return 'critical';
+ } else {
+ return 'unknown';
+ }
+ }
+
+ public function getTitles()
+ {
+ $view = $this->view();
+ return array(
+ 'job_name' => $view->translate('Job name'),
+ );
+ }
+
+ public function getBaseQuery()
+ {
+ $db = $this->connection()->getConnection();
+
+ $query = $db->select()->from(
+ array('j' => 'director_job'),
+ array()
+ )->order('job_name');
+
+ return $query;
+ }
+}
diff --git a/application/tables/SyncRunTable.php b/application/tables/SyncRunTable.php
new file mode 100644
index 00000000..73530323
--- /dev/null
+++ b/application/tables/SyncRunTable.php
@@ -0,0 +1,78 @@
+ 'sr.id',
+ 'rule_id' => 'sr.rule_id',
+ 'rule_name' => 'sr.rule_name',
+ 'start_time' => 'sr.start_time',
+ 'duration_ms' => 'sr.duration_ms',
+ 'objects_deleted' => 'sr.objects_deleted',
+ 'objects_created' => 'sr.objects_created',
+ 'objects_modified' => 'sr.objects_modified',
+ 'last_former_activity' => 'sr.last_former_activity',
+ 'last_related_activity' => 'sr.last_related_activity',
+ );
+ }
+
+ protected function getActionUrl($row)
+ {
+ return $this->url(
+ 'director/syncrule/history',
+ array(
+ 'id' => $row->rule_id,
+ 'run_id' => $row->id,
+ )
+ );
+ }
+
+ public function getTitles()
+ {
+ $singleRule = false;
+
+ foreach ($this->enforcedFilters as $filter) {
+ if (in_array('rule_id', $filter->listFilteredColumns())) {
+ $singleRule = true;
+ break;
+ }
+ }
+
+ $view = $this->view();
+
+ if ($singleRule) {
+ return array(
+ 'start_time' => $view->translate('Start time'),
+ 'objects_created' => $view->translate('Created'),
+ 'objects_modified' => $view->translate('Modified'),
+ 'objects_deleted' => $view->translate('Deleted'),
+ );
+ } else {
+ return array(
+ 'rule_name' => $view->translate('Rule name'),
+ 'start_time' => $view->translate('Start time'),
+ );
+ }
+ }
+
+ public function getBaseQuery()
+ {
+ $db = $this->connection()->getConnection();
+
+ $query = $db->select()->from(
+ array('sr' => 'sync_run'),
+ array()
+ )->order('start_time DESC');
+
+ return $query;
+ }
+}
diff --git a/application/tables/SyncruleTable.php b/application/tables/SyncruleTable.php
index a47c4d0e..ac2f6cdb 100644
--- a/application/tables/SyncruleTable.php
+++ b/application/tables/SyncruleTable.php
@@ -9,13 +9,12 @@ use Exception;
class SyncruleTable extends QuickTable
{
- protected $revalidate = false;
-
public function getColumns()
{
return array(
'id' => 's.id',
'rule_name' => 's.rule_name',
+ 'sync_state' => 's.sync_state',
'object_type' => 's.object_type',
'update_policy' => 's.update_policy',
'purge_existing' => 's.purge_existing',
@@ -25,7 +24,7 @@ class SyncruleTable extends QuickTable
protected function getActionUrl($row)
{
- return $this->url('director/syncrule/edit', array('id' => $row->id));
+ return $this->url('director/syncrule', array('id' => $row->id));
}
protected function listTableClasses()
@@ -45,25 +44,7 @@ class SyncruleTable extends QuickTable
protected function getRowClasses($row)
{
- if (! $this->revalidate) {
- return array();
- }
-
- try {
- // $mod = Sync::hasModifications(
- $sync = new Sync(SyncRule::load($row->id, $this->connection()));
- $mod = $sync->getExpectedModifications();
-
- if (count($mod) > 0) {
- $row->rule_name = $row->rule_name . ' (' . count($mod) . ')';
- return 'pending-changes';
- } else {
- return 'in-sync';
- }
- } catch (Exception $e) {
- $row->rule_name = $row->rule_name . ' (' . $e->getMessage() . ')';
- return 'failing';
- }
+ return $row->sync_state;
}
public function getTitles()
diff --git a/application/views/scripts/command/arguments.phtml b/application/views/scripts/command/arguments.phtml
index e5a04589..693053d6 100644
--- a/application/views/scripts/command/arguments.phtml
+++ b/application/views/scripts/command/arguments.phtml
@@ -1,7 +1,7 @@
= $this->tabs ?>
= $this->escape($this->title) ?>
-
+
= $this->addLink ?>
diff --git a/application/views/scripts/config/diff.phtml b/application/views/scripts/config/diff.phtml
new file mode 100644
index 00000000..d8bf64c5
--- /dev/null
+++ b/application/views/scripts/config/diff.phtml
@@ -0,0 +1,31 @@
+
+= $this->tabs ?>
+
= $this->escape($this->title) ?>
+
+= $this->addLink ?>
+
+
+
+
+
+table)): ?>
+
+= $this->table->render() ?>
+
+
+
diff --git a/application/views/scripts/config/file.phtml b/application/views/scripts/config/file.phtml
index 468bf95c..586ba879 100644
--- a/application/views/scripts/config/file.phtml
+++ b/application/views/scripts/config/file.phtml
@@ -1,7 +1,7 @@
= $this->tabs ?>
= $title ?>
-
+
= $this->addLink ?>
diff --git a/application/views/scripts/config/filediff.phtml b/application/views/scripts/config/filediff.phtml
new file mode 100644
index 00000000..14e52d73
--- /dev/null
+++ b/application/views/scripts/config/filediff.phtml
@@ -0,0 +1,11 @@
+
+= $this->tabs ?>
+
= $this->escape($this->title) ?>
+
+= $this->addLink ?>
+
+
+
+
+= $this->output ?>
+
diff --git a/application/views/scripts/config/files.phtml b/application/views/scripts/config/files.phtml
index d9663b8a..7f0b22ef 100644
--- a/application/views/scripts/config/files.phtml
+++ b/application/views/scripts/config/files.phtml
@@ -1,7 +1,7 @@
= $this->tabs ?>
= $this->escape($this->title) ?>
-
+
= $this->addLink ?>
filterEditor->getFilter()->isEmpty()): ?>
@@ -24,6 +24,11 @@
'director/show/activitylog',
array('checksum' => $this->config->getLastActivityHexChecksum()),
array('class' => 'icon-clock', 'data-base-target' => '_next')
+ ) ?>
= $this->qlink(
+ $this->translate('Diff with other config'),
+ 'director/config/diff',
+ array('left' => $this->config->getHexChecksum()),
+ array('class' => 'icon-flapping', 'data-base-target' => '_self')
) ?>
diff --git a/application/views/scripts/host/agent.phtml b/application/views/scripts/host/agent.phtml
index e7998347..e6149a42 100644
--- a/application/views/scripts/host/agent.phtml
+++ b/application/views/scripts/host/agent.phtml
@@ -8,7 +8,11 @@
$cert = $this->escape($this->certname);
$master = $this->escape($this->master);
?>
-Please check the Icinga 2 Client documentation for more related information. The Director-assisted setup corresponds to configuring the Client as Command Execution Bridge.
+Please check the Icinga 2 Client documentation for more related information. The Director-assisted setup corresponds to configuring the Client as Command Execution Bridge.
+
+error): ?>
+= $this->escape($this->error) ?>
+
When using the node wizard
Ticket : = $this->escape($ticket) ?>
@@ -49,7 +53,7 @@ icinga2 pki request --host = $master ?> \
include "constants.conf"
include <itl>
include <plugins>
-include <plugins-contrib>
+// include <plugins-contrib>
object FileLogger "main-log" {
severity = "information"
diff --git a/application/views/scripts/host/services.phtml b/application/views/scripts/host/services.phtml
index 83e1115c..2109b7b5 100644
--- a/application/views/scripts/host/services.phtml
+++ b/application/views/scripts/host/services.phtml
@@ -1,7 +1,7 @@
= $this->tabs ?>
= $this->escape($this->title) ?>
-
+
= $this->addLink ?>
filterEditor && ! $this->filterEditor->getFilter()->isEmpty())): ?>
diff --git a/application/views/scripts/index/index.phtml b/application/views/scripts/index/index.phtml
index 7cf930fb..30423a7e 100644
--- a/application/views/scripts/index/index.phtml
+++ b/application/views/scripts/index/index.phtml
@@ -84,6 +84,11 @@ if (!$this->hasDeploymentEndpoint) {
echo $this->form;
}
+if ($this->migrationsForm) {
+ echo '' . $this->translate('There are pending database schema migrations') . "\n";
+ echo $this->migrationsForm;
+}
+
$all = array(
$this->translate('Define whatever you want to be monitored') => array(
array('host', $this->translate('Host objects'), 'director/hosts', statSummary($this, 'host')),
@@ -101,8 +106,9 @@ $all = array(
array('globe', $this->translate('Zones'), 'director/zones', statSummary($this, 'zone')),
),
$this->translate('Do more with your data') => array(
- array('database', $this->translate('Import data sources'), 'director/list/importsource', $this->translate('Define and manage imports from various data sources')),
- array('flapping', $this->translate('Synchronize'), 'director/list/importsource', $this->translate('Define how imported data should be synchronized with Icinga')),
+ array('database', $this->translate('Import data sources'), 'director/list/importsource', $this->translate('Define and manage imports from various data sources'), $this->importState),
+ array('flapping', $this->translate('Synchronize'), 'director/list/syncrule', $this->translate('Define how imported data should be synchronized with Icinga'), $this->syncState),
+ array('clock', $this->translate('Jobs'), 'director/jobs', $this->translate('Schedule and automate Import, Syncronization, Config Deployment, Housekeeping and more'), $this->jobState),
array('sort-name-up', $this->translate('Provide data lists'), 'director/data/lists', $this->translate('Provide data lists to make life easier for your users')),
array('edit', $this->translate('Define data fields'), 'director/data/fields', $this->translate('Data fields make sure that configuration fits your rules')),
)
diff --git a/application/views/scripts/list/importrun.phtml b/application/views/scripts/list/importrun.phtml
index b58bdd81..ce1db762 100644
--- a/application/views/scripts/list/importrun.phtml
+++ b/application/views/scripts/list/importrun.phtml
@@ -27,7 +27,7 @@ $pt = $loc['thousands_sep'];
-
+
= $this->addLink ?>
= $this->table->getPaginator() ?>
diff --git a/application/views/scripts/list/table.phtml b/application/views/scripts/list/table.phtml
index e444b7ab..518c1a78 100644
--- a/application/views/scripts/list/table.phtml
+++ b/application/views/scripts/list/table.phtml
@@ -1,7 +1,7 @@
= $this->tabs ?>
= $this->escape($this->title) ?>
-
stayHere): ?> data-base-target="_next">
+stayHere): ?> data-base-target="_next">
= $this->addLink ?>
= $this->filterEditor ?>
diff --git a/application/views/scripts/object/fields.phtml b/application/views/scripts/object/fields.phtml
index 64bd111b..22ae8a72 100644
--- a/application/views/scripts/object/fields.phtml
+++ b/application/views/scripts/object/fields.phtml
@@ -1,7 +1,7 @@
= $this->tabs ?>
= $this->escape($this->title) ?>
-
+
= $this->actionLinks ?>
diff --git a/application/views/scripts/object/form.phtml b/application/views/scripts/object/form.phtml
index e697c04e..004f2dac 100644
--- a/application/views/scripts/object/form.phtml
+++ b/application/views/scripts/object/form.phtml
@@ -1,7 +1,7 @@
= $this->tabs ?>
= $this->escape($this->title) ?>
-
+
= $this->actionLinks ?>
= $this->render('object/deploymentLink.phtml') ?>
diff --git a/application/views/scripts/object/show.phtml b/application/views/scripts/object/show.phtml
index 66fac28c..bc20c9f4 100644
--- a/application/views/scripts/object/show.phtml
+++ b/application/views/scripts/object/show.phtml
@@ -1,7 +1,7 @@
= $this->tabs ?>
= $this->escape($this->title) ?>
-
+
= $this->actionLinks ?>
@@ -10,7 +10,7 @@
disabled === 'y'): ?>
= $this->translate('This object will not be deployed as it has been disabled') ?>
-isExternal()): ?>
+isExternal): ?>
= $this->translate(
'This is an external object. It has been imported from Icinga 2 throught the'
. ' Core API and cannot be managed with the Icinga Director. It is however'
@@ -19,7 +19,11 @@
. ' object more enjoyable'
) ?>
-disabled === 'y'): ?> class="disabled">= $this->escape($object) ?>extraObjects): ?>
-= implode('', $this->extraObjects) ?>
-
+config->getFiles() as $filename => $file): ?>
+isExternal): ?>= $this->escape($filename) ?>
+isDisabled): ?> class="disabled"isExternal): ?> class="logfile">
+= $this->escape($file->getContent()) ?>
+
+
+
diff --git a/application/views/scripts/objects/table.phtml b/application/views/scripts/objects/table.phtml
index 549f4605..e29bb64c 100644
--- a/application/views/scripts/objects/table.phtml
+++ b/application/views/scripts/objects/table.phtml
@@ -3,7 +3,7 @@
= $this->tabs ?>
= $this->escape($this->title) ?>= $this->quickSearch ?>
-stayHere): ?> data-base-target="_next">
+stayHere): ?> data-base-target="_next">
= $this->addLink ?>
filterEditor): ?>
diff --git a/application/views/scripts/syncrule/history.phtml b/application/views/scripts/syncrule/history.phtml
new file mode 100644
index 00000000..9994dd04
--- /dev/null
+++ b/application/views/scripts/syncrule/history.phtml
@@ -0,0 +1,84 @@
+
+= $this->tabs ?>
+
= $this->escape($this->title) ?>
+stayHere): ?> data-base-target="_next">
+= $this->addLink ?>
+
+= $this->filterEditor ?>
+= $this->table->getPaginator() ?>
+
+
+stayHere): ?> data-base-target="_next">
+run): ?>
+
= $this->translate('Sync run details') ?>
+
+
+ = $this->translate('Start time') ?> |
+ = $this->escape($run->start_time) ?> |
+
+
+ = $this->translate('Duration') ?> |
+ = sprintf('%.2fs', $run->duration_ms / 1000) ?> |
+
+
+ = $this->translate('Activity') ?> |
+ objects_deleted + $run->objects_created + $run->objects_modified;
+ if ($total === 0) {
+ echo $this->translate('No changes have been made');
+ } else {
+ if ($total === 1) {
+ echo $this->translate('One object has been modified');
+ } else {
+ printf(
+ $this->translate('%s objects have been modified'),
+ $total
+ );
+ }
+
+ $activityUrl = sprintf(
+ 'director/config/activities?id>%d&id<=%d',
+ $formerId,
+ $lastId
+ );
+
+ $links = array();
+ if ($run->objects_created > 0) {
+ $links[] = $this->qlink(
+ sprintf('%d created', $run->objects_created),
+ $activityUrl,
+ array('action_name' => 'create')
+ );
+ }
+ if ($run->objects_modified > 0) {
+ $links[] = $this->qlink(
+ sprintf('%d modified', $run->objects_modified),
+ $activityUrl,
+ array('action_name' => 'modify')
+ );
+ }
+ if ($run->objects_deleted > 0) {
+ $links[] = $this->qlink(
+ sprintf('%d deleted', $run->objects_deleted),
+ $activityUrl,
+ array('action_name' => 'delete')
+ );
+ }
+
+ if (count($links) > 1) {
+ $links[] = $this->qlink(
+ 'Show all actions',
+ $activityUrl
+ );
+ }
+
+ if (! empty($links)) {
+ echo ': ' . implode(', ', $links);
+ }
+ }
+ ?> |
+
+
+
+= $this->table->render() ?>
+
diff --git a/configuration.php b/configuration.php
index 65134dcf..711d5412 100644
--- a/configuration.php
+++ b/configuration.php
@@ -37,10 +37,10 @@ $section->add($this->translate('Hosts'))->setUrl('director/hosts')->setPriority(
$section->add($this->translate('Services'))->setUrl('director/services')->setPriority(40);
$section->add($this->translate('Commands'))->setUrl('director/commands')->setPriority(50);
$section->add($this->translate('Users'))->setUrl('director/users')->setPriority(70);
-$section->add($this->translate('Import / Sync'))
+$section->add($this->translate('Automation'))
->setUrl('director/list/importsource')
->setPriority(901);
-$section->add($this->translate('Deployments / History'))
+$section->add($this->translate('Config history'))
->setUrl('director/config/deployments')
->setPriority(902)
->setRenderer('ConfigHealthItemRenderer');
diff --git a/doc/02-Installation.md b/doc/02-Installation.md
index be4502d4..78d58878 100644
--- a/doc/02-Installation.md
+++ b/doc/02-Installation.md
@@ -42,7 +42,7 @@ Web-based Configuration
The following steps should guide you through the web-based Kickstart wizard.
In case you prefer automated configuration, you should check the dedicated
-[documentation section](doc/03-Automation.md).
+[documentation section](03-Automation.md).
### Create a Database resource
diff --git a/doc/03-Automation.md b/doc/03-Automation.md
index f6579598..d212246f 100644
--- a/doc/03-Automation.md
+++ b/doc/03-Automation.md
@@ -1,4 +1,4 @@
-Automation - Configuration management
+Automation - Configuration management
=====================================
Director has been designed to work in distributed environments. In case
diff --git a/doc/04-Getting-started.md b/doc/04-Getting-started.md
index 20b7ba3a..6293421e 100644
--- a/doc/04-Getting-started.md
+++ b/doc/04-Getting-started.md
@@ -1,5 +1,13 @@
-Preparing your Icinga 2 environment for the Director
-====================================================
+Getting started
+===============
+
+When new to the Director please make your first steps with a naked Icinga
+environment. Director is not allowed to modify existing configuration in
+`/etc/icinga2`. And while importing existing config is possible (happens for
+example automagically at kickstart time), it is a pretty advanced task you
+should not tackle at the early beginning.
+
+
Create an API user
------------------
@@ -21,14 +29,22 @@ checking your clients, you will have to create them.
The easiest way to set up Icinga 2 with a `zone` and `endpoint` is by
running the [Icinga 2 Setup Wizard](http://docs.icinga.org/icinga2/latest/doc/module/icinga2/chapter/icinga2-client#icinga2-client-installation-master-setup).
-Start with a new, empty Icinga setup. Director is not allowed to modify
-existing configuration in `/etc/icinga2`, and while importing existing
-config is possible (happens for example automagically at kickstart time)
-this is an advanced task you should not tackle at the early beginning.
-
Take some time to really understand how to work with Icinga Director first.
-Working with Agents and Config Zones
-====================================
-Hint: Large: max packet size
+Other topics that might interest you
+------------------------------------
+
+* [Working with agents](24-Working-with-agents.md)
+* [Undstanding how Icinga Director works](10-How-it-works.md)
+
+What you should not try to start with
+-------------------------------------
+
+Director has not been built to help you with managing existing hand-crafted
+configuration in /etc/icinga2. There are cases where it absolutely would
+make sense to combine the Director with manual configuration. You can also
+use multiple tools owning separare config packages. But these are pretty
+advanced topics.
+
+
diff --git a/doc/70-REST-API.md b/doc/70-REST-API.md
index d63166d7..97b22ef5 100644
--- a/doc/70-REST-API.md
+++ b/doc/70-REST-API.md
@@ -79,12 +79,12 @@ PASSWORD="***"
test -z "$PASSWORD" || USERNAME="$USERNAME:$PASSWORD"
test -z "$BODY" && curl -u "$USERNAME" \
- -i http://icingaweb/icingaweb/$URL \
+ -i https://icingaweb/icingaweb/$URL \
-H 'Accept: application/json' \
-X $METHOD
test -z "$BODY" || curl -u "$USERNAME" \
- -i http://icingaweb/icingaweb/$URL \
+ -i https://icingaweb/icingaweb/$URL \
-H 'Accept: application/json' \
-X $METHOD \
-d "$BODY"
diff --git a/library/Director/Data/Db/DbObject.php b/library/Director/Data/Db/DbObject.php
index 480e04b2..4406af26 100644
--- a/library/Director/Data/Db/DbObject.php
+++ b/library/Director/Data/Db/DbObject.php
@@ -1062,6 +1062,18 @@ abstract class DbObject
return self::$prefetched[$class];
}
+ public static function clearPrefetchCache()
+ {
+ $class = get_called_class();
+ if (! array_key_exists($class, self::$prefetched)) {
+ return false;
+ }
+
+ unset(self::$prefetched[$class]);
+ unset(self::$prefetchedNames[$class]);
+ unset(self::$prefetchStats[$class]);
+ }
+
public static function exists($id, DbConnection $connection)
{
if (static::getPrefetched($id)) {
diff --git a/library/Director/Db.php b/library/Director/Db.php
index 4d099d02..5217d467 100644
--- a/library/Director/Db.php
+++ b/library/Director/Db.php
@@ -789,6 +789,32 @@ class Db extends DbConnection
return $binary;
}
+ public function enumDeployedConfigs()
+ {
+ $db = $this->db();
+
+ $columns = array(
+ 'checksum' => $this->dbHexFunc('c.checksum'),
+ );
+
+ if ($this->isPgsql()) {
+ $columns['caption'] = 'SUBSTRING(' . $columns['checksum'] . ' FROM 1 FOR 7)';
+ } else {
+ $columns['caption'] = 'SUBSTRING(' . $columns['checksum'] . ', 1, 7)';
+ }
+
+ $query = $db->select()->from(
+ array('l' => 'director_deployment_log'),
+ $columns
+ )->joinLeft(
+ array('c' => 'director_generated_config'),
+ 'c.checksum = l.config_checksum',
+ array()
+ )->order('l.start_time DESC');
+
+ return $db->fetchPairs($query);
+ }
+
public function getUncollectedDeployments()
{
$db = $this->db();
diff --git a/library/Director/Db/Housekeeping.php b/library/Director/Db/Housekeeping.php
index bdbc9ac2..366fe976 100644
--- a/library/Director/Db/Housekeeping.php
+++ b/library/Director/Db/Housekeeping.php
@@ -64,6 +64,11 @@ class Housekeeping
);
}
+ public function hasPendingTasks()
+ {
+ return count($this->getPendingTaskSummary()) > 0;
+ }
+
public function runAllTasks()
{
$result = array();
diff --git a/library/Director/Hook/JobHook.php b/library/Director/Hook/JobHook.php
new file mode 100644
index 00000000..f2366cfa
--- /dev/null
+++ b/library/Director/Hook/JobHook.php
@@ -0,0 +1,64 @@
+db = $db;
+ return $this;
+ }
+
+ protected function db()
+ {
+ return $this->db;
+ }
+
+
+}
diff --git a/library/Director/IcingaConfig/IcingaConfig.php b/library/Director/IcingaConfig/IcingaConfig.php
index f4c126b7..48ae55a1 100644
--- a/library/Director/IcingaConfig/IcingaConfig.php
+++ b/library/Director/IcingaConfig/IcingaConfig.php
@@ -233,8 +233,14 @@ class IcingaConfig
return $checksums;
}
- protected function getZoneName($id)
+ // TODO: prepare lookup cache if empty?
+ public function getZoneName($id)
{
+ if (! array_key_exists($id, $this->zoneMap)) {
+ $zone = IcingaZone::loadWithAutoIncId($id, $this->connection);
+ $this->zoneMap[$id] = $zone->object_name;
+ }
+
return $this->zoneMap[$id];
}
@@ -546,7 +552,7 @@ class IcingaConfig
return in_array($type, $types);
}
- protected function configFile($name, $suffix = '.conf')
+ public function configFile($name, $suffix = '.conf')
{
$filename = $name . $suffix;
if (! array_key_exists($filename, $this->files)) {
diff --git a/library/Director/Import/Sync.php b/library/Director/Import/Sync.php
index 9eeebf14..575429e3 100644
--- a/library/Director/Import/Sync.php
+++ b/library/Director/Import/Sync.php
@@ -105,6 +105,8 @@ class Sync
foreach ($objects as $object) {
if ($object->hasBeenModified()) {
$modified[] = $object;
+ } elseif ($object instanceof IcingaObject && $object->shouldBeRemoved()) {
+ $modified[] = $object;
}
}
@@ -671,9 +673,6 @@ class Sync
/**
* Runs a SyncRule and applies all resulting changes
*
- * TODO: Should return the id of the related sync_history table entry.
- * Such a table does not yet exist, so 42 is the answer right now.
- *
* @return int
*/
public function apply()
@@ -739,7 +738,6 @@ class Sync
(microtime(true) - $this->runStartTime) * 1000
))->store();
-
return $this->run->id;
}
}
diff --git a/library/Director/Job/ConfigJob.php b/library/Director/Job/ConfigJob.php
new file mode 100644
index 00000000..da80a985
--- /dev/null
+++ b/library/Director/Job/ConfigJob.php
@@ -0,0 +1,107 @@
+housekeeping()->runAllTasks();
+ }
+
+ public function isPending()
+ {
+ return $this->housekeeping()->hasPendingTasks();
+ }
+
+ public static function getDescription(QuickForm $form)
+ {
+ return $form->translate(
+ 'The Housekeeping job provides various task that keep your Director'
+ . ' database fast and clean'
+ );
+ }
+
+ protected function housekeeping()
+ {
+ if ($this->housekeeping === null) {
+ $this->housekeeping = new Housekeeping($this->db());
+ }
+
+ return $this->housekeeping;
+ }
+
+ /**
+ * Re-render the current configuration
+ */
+ public function renderAction()
+ {
+ $config = new IcingaConfig($this->db());
+ Benchmark::measure('Rendering config');
+ if ($config->hasBeenModified()) {
+ Benchmark::measure('Config rendered, storing to db');
+ $config->store();
+ Benchmark::measure('All done');
+ $checksum = $config->getHexChecksum();
+ $this->printf(
+ "New config with checksum %s has been generated\n",
+ $checksum
+ );
+ } else {
+ $checksum = $config->getHexChecksum();
+ $this->printf(
+ "Config with checksum %s already exists\n",
+ $checksum
+ );
+ }
+ }
+
+ /**
+ * Deploy the current configuration
+ *
+ * Does nothing if config didn't change unless you provide
+ * the --force parameter
+ */
+ public function deployAction()
+ {
+ $api = $this->api();
+ $db = $this->db();
+
+ $checksum = $this->params->get('checksum');
+ if ($checksum) {
+ $config = IcingaConfig::load(Util::hex2binary($checksum), $db);
+ } else {
+ $config = IcingaConfig::generate($db);
+ $checksum = $config->getHexChecksum();
+ }
+
+ $api->wipeInactiveStages($db);
+ $current = $api->getActiveChecksum($db);
+ if ($current === $checksum) {
+ if ($this->params->get('force')) {
+ echo "Config matches active stage, deploying anyway\n";
+ } else {
+ echo "Config matches active stage, nothing to do\n";
+
+ return;
+ }
+
+ } else {
+ if ($api->dumpConfig($config, $db)) {
+ $this->printf("Config '%s' has been deployed\n", $checksum);
+ } else {
+ $this->fail(
+ sprintf("Failed to deploy config '%s'\n", $checksum)
+ );
+ }
+ }
+ }
+}
diff --git a/library/Director/Job/HousekeepingJob.php b/library/Director/Job/HousekeepingJob.php
new file mode 100644
index 00000000..9f3f596e
--- /dev/null
+++ b/library/Director/Job/HousekeepingJob.php
@@ -0,0 +1,39 @@
+housekeeping()->runAllTasks();
+ }
+
+ public static function getDescription(QuickForm $form)
+ {
+ return $form->translate(
+ 'The Housekeeping job provides various task that keep your Director'
+ . ' database fast and clean'
+ );
+ }
+
+ public function isPending()
+ {
+ return $this->housekeeping()->hasPendingTasks();
+ }
+
+ protected function housekeeping()
+ {
+ if ($this->housekeeping === null) {
+ $this->housekeeping = new Housekeeping($this->db());
+ }
+
+ return $this->housekeeping;
+ }
+}
diff --git a/library/Director/Job/ImportJob.php b/library/Director/Job/ImportJob.php
new file mode 100644
index 00000000..2e480a54
--- /dev/null
+++ b/library/Director/Job/ImportJob.php
@@ -0,0 +1,24 @@
+translate(
+ 'The "Import" job allows to run import actions at regular intervals'
+ );
+ }
+
+ public function isPending()
+ {
+ }
+}
diff --git a/library/Director/Job/JobRunner.php b/library/Director/Job/JobRunner.php
new file mode 100644
index 00000000..f3ea7bd9
--- /dev/null
+++ b/library/Director/Job/JobRunner.php
@@ -0,0 +1,46 @@
+db = $db;
+ }
+
+ public function runPendingJobs()
+ {
+ foreach ($this->getConfiguredJobs() as $job) {
+ if ($job->isPending()) {
+ $this->run($job);
+ }
+ }
+ }
+
+ protected function run(Job $job)
+ {
+ if ($this->shouldFork()) {
+ $this->fork($job);
+ } else {
+ $job->run();
+ }
+ }
+
+ protected function fork(Job $job)
+ {
+ $cmd = 'icingacli director job run ' . $job->id;
+ $output = `$cmd`;
+ }
+
+ protected function shouldFork()
+ {
+ return true;
+ }
+
+ protected function getRegisteredJobs()
+ {
+ }
+}
diff --git a/library/Director/Job/SyncJob.php b/library/Director/Job/SyncJob.php
new file mode 100644
index 00000000..0f2c4b26
--- /dev/null
+++ b/library/Director/Job/SyncJob.php
@@ -0,0 +1,83 @@
+getSetting('apply_changes') === 'y') {
+ $this->syncRule()->applyChanges();
+ } else{
+ $this->syncRule()->checkForChanges();
+ }
+ }
+
+ public static function getDescription(QuickForm $form)
+ {
+ return $form->translate(
+ 'The "Sync" job allows to run sync actions at regular intervals'
+ );
+ }
+
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $rules = self::enumSyncRules($form);
+
+ $form->addElement('select', 'rule_id', array(
+ 'label' => $form->translate('Synchronization rule'),
+ 'description' => $form->translate(
+ 'Please choose your synchronization rule that should be executed.'
+ . ' You could create different schedules for different rules or also'
+ . ' opt for running all of them at once.'
+ ),
+ 'required' => true,
+ 'class' => 'autosubmit',
+ 'multiOptions' => $rules
+ ));
+
+ $form->addElement('select', 'apply_changes', array(
+ 'label' => $form->translate('Apply changes'),
+ 'description' => $form->translate(
+ 'You could immediately apply eventual changes or just learn about them.'
+ . ' In case you do not want them to be applied immediately, defining a'
+ . ' job still makes sense. You will be made aware of available changes'
+ . ' in your Director GUI.'
+ ),
+ 'value' => 'n',
+ 'multiOptions' => array(
+ 'y' => $form->translate('Yes'),
+ 'n' => $form->translate('No'),
+ )
+ ));
+
+ if (! strlen($form->getSentOrObjectValue('job_name'))) {
+ if (($ruleId = $form->getSentValue('rule_id')) && array_key_exists($ruleId, $rules)) {
+ $name = sprintf('Sync job: %s', $rules[$ruleId]);
+ $form->getElement('job_name')->setValue($name);
+ ///$form->getObject()->set('job_name', $name);
+ }
+ }
+
+ return $form;
+ }
+
+ protected static function enumSyncRules(QuickForm $form)
+ {
+ $db = $form->getDb();
+ $query = $db->select()->from('sync_rule', array('id', 'rule_name'))->order('rule_name');
+ $res = $db->fetchPairs($query);
+ return array(
+ null => $form->translate('- please choose -'),
+ '__ALL__' => $form->translate('Run all rules at once')
+ ) + $res;
+ }
+
+ public function isPending()
+ {
+ }
+}
diff --git a/library/Director/Objects/DirectorJob.php b/library/Director/Objects/DirectorJob.php
new file mode 100644
index 00000000..18c0373f
--- /dev/null
+++ b/library/Director/Objects/DirectorJob.php
@@ -0,0 +1,48 @@
+ null,
+ 'job_name' => null,
+ 'job_class' => null,
+ 'disabled' => null,
+ 'run_interval' => null,
+ 'last_attempt_succeeded' => null,
+ 'ts_last_attempt' => null,
+ 'ts_last_error' => null,
+ 'last_error_message' => null,
+ );
+
+ protected $settingsTable = 'director_job_setting';
+
+ protected $settingsRemoteId = 'job_id';
+
+ public function isPending()
+ {
+ if ($this->ts_last_attempt === null) {
+ return true;
+ }
+
+ if (strtotime($this->unixts_last_attempt) + $this->run_interval < time()) {
+ return true;
+ }
+
+ return false;
+ }
+
+ public function lastAttemptSucceeded()
+ {
+ return $this->last_attempt_succeeded === 'y';
+ }
+}
diff --git a/library/Director/Objects/IcingaArguments.php b/library/Director/Objects/IcingaArguments.php
index ecc543cc..4d39ed98 100644
--- a/library/Director/Objects/IcingaArguments.php
+++ b/library/Director/Objects/IcingaArguments.php
@@ -82,7 +82,9 @@ class IcingaArguments implements Iterator, Countable, IcingaConfigRenderer
public function set($key, $value)
{
- $argument = IcingaCommandArgument::create($this->mungeCommandArgument($key, $value));
+ $argument = IcingaCommandArgument::create(
+ $this->mungeCommandArgument($key, $value)
+ )->set('command_id', $this->object->id);
$key = $argument->argument_name;
if (array_key_exists($key, $this->arguments)) {
$this->arguments[$key]->replaceWith($argument);
diff --git a/library/Director/Objects/IcingaCommand.php b/library/Director/Objects/IcingaCommand.php
index 6020205d..1e48a3ab 100644
--- a/library/Director/Objects/IcingaCommand.php
+++ b/library/Director/Objects/IcingaCommand.php
@@ -2,6 +2,7 @@
namespace Icinga\Module\Director\Objects;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
class IcingaCommand extends IcingaObject
@@ -74,6 +75,11 @@ class IcingaCommand extends IcingaObject
return $value;
}
+ public function getRenderingZone(IcingaConfig $config = null)
+ {
+ return $this->connection->getDefaultGlobalZoneName();
+ }
+
protected function renderCommand()
{
$command = $this->command;
diff --git a/library/Director/Objects/IcingaObject.php b/library/Director/Objects/IcingaObject.php
index c375936a..b6d5e10e 100644
--- a/library/Director/Objects/IcingaObject.php
+++ b/library/Director/Objects/IcingaObject.php
@@ -122,7 +122,6 @@ abstract class IcingaObject extends DbObject implements IcingaConfigRenderer
return $this;
}
-
private function loadMultiRelation($property)
{
if ($this->hasBeenLoadedFromDb()) {
@@ -155,6 +154,7 @@ abstract class IcingaObject extends DbObject implements IcingaConfigRenderer
}
}
+ ksort($this->loadedMultiRelations);
return $this->loadedMultiRelations;
}
@@ -1128,15 +1128,15 @@ abstract class IcingaObject extends DbObject implements IcingaConfigRenderer
}
$config->configFile(
- 'zones.d/' . $this->getRenderingZone($config)
+ 'zones.d/' . $this->getRenderingZone($config) . '/' . $filename
)->addObject($this);
}
public function getRenderingZone(IcingaConfig $config = null)
{
- if ($this->zone_id) {
+ if ($zoneId = $this->getResolvedProperty('zone_id')) {
// Config has a lookup cache, is faster:
- return $config->getZoneName($this->zone_id);
+ return $config->getZoneName($zoneId);
}
if ($this->isTemplate() || $this->isApplyRule()) {
diff --git a/library/Director/Objects/IcingaObjectGroup.php b/library/Director/Objects/IcingaObjectGroup.php
index cb667b51..ff840876 100644
--- a/library/Director/Objects/IcingaObjectGroup.php
+++ b/library/Director/Objects/IcingaObjectGroup.php
@@ -2,6 +2,8 @@
namespace Icinga\Module\Director\Objects;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+
abstract class IcingaObjectGroup extends IcingaObject
{
protected $supportsImports = true;
@@ -13,4 +15,9 @@ abstract class IcingaObjectGroup extends IcingaObject
'disabled' => 'n',
'display_name' => null,
);
+
+ public function getRenderingZone(IcingaConfig $config = null)
+ {
+ return $this->connection->getDefaultGlobalZoneName();
+ }
}
diff --git a/library/Director/Objects/IcingaService.php b/library/Director/Objects/IcingaService.php
index 27168d1f..02834aca 100644
--- a/library/Director/Objects/IcingaService.php
+++ b/library/Director/Objects/IcingaService.php
@@ -162,13 +162,18 @@ class IcingaService extends IcingaObject
protected function renderCustomExtensions()
{
- if ($this->command_endpoint_id !== null
- || $this->object_type !== 'object'
- || $this->getResolvedProperty('use_agent') !== 'y') {
+ // A hand-crafted command endpoint overrides use_agent
+ if ($this->command_endpoint_id !== null) {
return '';
}
- if ($this->hasBeenAssignedToHostTemplate()) {
+ // In case use_agent isn't defined, do nothing
+ // TODO: what if we inherit use_agent and override it with 'n'?
+ if ($this->use_agent !== 'y') {
+ return '';
+ }
+
+ if ($this->hasBeenAssignedToHostTemplate() || $this->object_type !== 'object') {
return c::renderKeyValue('command_endpoint', 'host.name');
} else {
return $this->renderRelationProperty('host', $this->host_id, 'command_endpoint');
diff --git a/library/Director/Objects/IcingaUser.php b/library/Director/Objects/IcingaUser.php
index c50a2312..a344093e 100644
--- a/library/Director/Objects/IcingaUser.php
+++ b/library/Director/Objects/IcingaUser.php
@@ -2,6 +2,8 @@
namespace Icinga\Module\Director\Objects;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+
class IcingaUser extends IcingaObject
{
protected $table = 'icinga_user';
@@ -40,4 +42,9 @@ class IcingaUser extends IcingaObject
'period' => 'IcingaTimePeriod',
'zone' => 'IcingaZone',
);
+
+ public function getRenderingZone(IcingaConfig $config = null)
+ {
+ return $this->connection->getMasterZoneName();
+ }
}
diff --git a/library/Director/Objects/IcingaUserGroup.php b/library/Director/Objects/IcingaUserGroup.php
index b5386149..b85c312c 100644
--- a/library/Director/Objects/IcingaUserGroup.php
+++ b/library/Director/Objects/IcingaUserGroup.php
@@ -2,7 +2,14 @@
namespace Icinga\Module\Director\Objects;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+
class IcingaUserGroup extends IcingaObjectGroup
{
protected $table = 'icinga_usergroup';
+
+ public function getRenderingZone(IcingaConfig $config = null)
+ {
+ return $this->connection->getMasterZoneName();
+ }
}
diff --git a/library/Director/Objects/IcingaZone.php b/library/Director/Objects/IcingaZone.php
index ea05b8db..4fe0688b 100644
--- a/library/Director/Objects/IcingaZone.php
+++ b/library/Director/Objects/IcingaZone.php
@@ -2,6 +2,7 @@
namespace Icinga\Module\Director\Objects;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
class IcingaZone extends IcingaObject
@@ -40,6 +41,21 @@ class IcingaZone extends IcingaObject
return c::renderKeyValue('endpoints', c::renderArray($endpoints));
}
+ public function getRenderingZone(IcingaConfig $config = null)
+ {
+ // If the zone has a parent zone...
+ if ($this->get('parent_id')) {
+ // ...we render the zone object to the parent zone
+ return $this->parent;
+ } elseif ($this->is_global === 'y') {
+ // ...additional global zones are rendered to our global zone...
+ return $this->connection->getDefaultGlobalZoneName();
+ } else {
+ // ...and all the other zones are rendered to our master zone
+ return $this->connection->getMasterZoneName();
+ }
+ }
+
public function setEndpointList($list)
{
$this->endpointList = $list;
diff --git a/library/Director/Objects/ImportSource.php b/library/Director/Objects/ImportSource.php
index 8cdc12ca..82e99f78 100644
--- a/library/Director/Objects/ImportSource.php
+++ b/library/Director/Objects/ImportSource.php
@@ -2,7 +2,10 @@
namespace Icinga\Module\Director\Objects;
+use Icinga\Application\Benchmark;
use Icinga\Module\Director\Data\Db\DbObjectWithSettings;
+use Icinga\Module\Director\Import\Import;
+use Exception;
class ImportSource extends DbObjectWithSettings
{
@@ -13,10 +16,13 @@ class ImportSource extends DbObjectWithSettings
protected $autoincKeyName = 'id';
protected $defaultProperties = array(
- 'id' => null,
- 'source_name' => null,
- 'provider_class' => null,
- 'key_column' => null
+ 'id' => null,
+ 'source_name' => null,
+ 'provider_class' => null,
+ 'key_column' => null,
+ 'import_state' => 'unknown',
+ 'last_error_message' => null,
+ 'last_attempt' => null,
);
protected $settingsTable = 'import_source_setting';
@@ -34,4 +40,44 @@ class ImportSource extends DbObjectWithSettings
->order('priority DESC')
);
}
+
+ public function checkForChanges($runImport = false)
+ {
+ $hadChanges = false;
+
+ Benchmark::measure('Starting with import ' . $this->source_name);
+ try {
+ $import = new Import($this);
+ if ($import->providesChanges()) {
+ Benchmark::measure('Found changes for ' . $this->source_name);
+ $this->hadChanges = true;
+ $this->import_state = 'pending-changes';
+
+ if ($runImport && $import->run()) {
+ Benchmark::measure('Import succeeded for ' . $this->source_name);
+ $this->import_state = 'in-sync';
+ }
+ } else {
+ $this->import_state = 'in-sync';
+ }
+
+ $this->last_error_message = null;
+
+ } catch (Exception $e) {
+ $this->import_state = 'failing';
+ Benchmark::measure('Import failed for ' . $this->source_name);
+ $this->last_error_message = 'ERR: ' . $e->getMessage();
+ }
+
+ if ($this->hasBeenModified()) {
+ $this->store();
+ }
+
+ return $hadChanges;
+ }
+
+ public function runImport()
+ {
+ return $this->checkForChanges(true);
+ }
}
diff --git a/library/Director/Objects/SyncRule.php b/library/Director/Objects/SyncRule.php
index f26bd043..d5b7d72d 100644
--- a/library/Director/Objects/SyncRule.php
+++ b/library/Director/Objects/SyncRule.php
@@ -2,8 +2,11 @@
namespace Icinga\Module\Director\Objects;
+use Icinga\Application\Benchmark;
use Icinga\Data\Filter\Filter;
use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Import\Sync;
+use Exception;
class SyncRule extends DbObject
{
@@ -14,14 +17,19 @@ class SyncRule extends DbObject
protected $autoincKeyName = 'id';
protected $defaultProperties = array(
- 'id' => null,
- 'rule_name' => null,
- 'object_type' => null,
- 'update_policy' => null,
- 'purge_existing' => null,
- 'filter_expression' => null,
+ 'id' => null,
+ 'rule_name' => null,
+ 'object_type' => null,
+ 'update_policy' => null,
+ 'purge_existing' => null,
+ 'filter_expression' => null,
+ 'sync_state' => 'unknown',
+ 'last_error_message' => null,
+ 'last_attempt' => null,
);
+ private $sync;
+
private $filter;
public function listInvolvedSourceIds()
@@ -67,6 +75,55 @@ class SyncRule extends DbObject
return $this->filter()->matches($row);
}
+ public function checkForChanges($apply = false)
+ {
+ $hadChanges = false;
+
+ Benchmark::measure('Checking sync rule ' . $this->rule_name);
+ try {
+ $sync = $this->sync();
+ if ($sync->hasModifications()) {
+ Benchmark::measure('Got modifications for sync rule ' . $this->rule_name);
+ $this->sync_state = 'pending-changes';
+ if ($apply && $sync->apply()) {
+ Benchmark::measure('Successfully synced rule ' . $rule->rule_name);
+ $this->sync_state = 'in-sync';
+ }
+
+ $hadChanges = true;
+
+ } else {
+ Benchmark::measure('No modifications for sync rule ' . $this->rule_name);
+ $this->sync_state = 'in-sync';
+ }
+
+ $this->last_error_message = null;
+ } catch (Exception $e) {
+ $this->sync_state = 'failing';
+ $this->last_error_message = $e->getMessage();
+ }
+
+ if ($this->hasBeenModified()) {
+ $this->store();
+ }
+
+ return $hadChanges;
+ }
+
+ public function applyChanges()
+ {
+ return $this->checkForChanges(true);
+ }
+
+ protected function sync()
+ {
+ if ($this->sync === null) {
+ $this->sync = new Sync($this);
+ }
+
+ return $this->sync;
+ }
+
protected function filter()
{
if ($this->filter === null) {
diff --git a/library/Director/Web/Controller/ActionController.php b/library/Director/Web/Controller/ActionController.php
index 1e7d9556..3e9b2c5a 100644
--- a/library/Director/Web/Controller/ActionController.php
+++ b/library/Director/Web/Controller/ActionController.php
@@ -152,6 +152,12 @@ abstract class ActionController extends Controller
'label' => $this->translate('Sync rule'),
'url' => 'director/list/syncrule'
)
+ )->add(
+ 'jobs',
+ array(
+ 'label' => $this->translate('Jobs'),
+ 'url' => 'director/jobs'
+ )
);
return $this->view->tabs;
}
diff --git a/library/Director/Web/Controller/ObjectController.php b/library/Director/Web/Controller/ObjectController.php
index 47922514..c435efaf 100644
--- a/library/Director/Web/Controller/ObjectController.php
+++ b/library/Director/Web/Controller/ObjectController.php
@@ -6,6 +6,7 @@ use Exception;
use Icinga\Exception\IcingaException;
use Icinga\Exception\InvalidPropertyException;
use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
use Icinga\Module\Director\Objects\IcingaObject;
use Icinga\Web\Url;
@@ -103,23 +104,22 @@ abstract class ObjectController extends ActionController
$type = $this->getType();
$this->getTabs()->activate('render');
$object = $this->object;
+ $this->view->isDisabled = $object->disabled === 'y';
+ $this->view->isExternal = $object->isExternal();
if ($this->params->shift('resolved')) {
- $this->view->object = $object::fromPlainObject(
+ $object = $object::fromPlainObject(
$object->toPlainObject(true),
$object->getConnection()
);
- if ($object->imports()->count() > 0) {
- $this->view->actionLinks = $this->view->qlink(
- $this->translate('Show normal'),
- $this->getRequest()->getUrl()->without('resolved'),
- null,
- array('class' => 'icon-resize-small state-warning')
- );
- }
+ $this->view->actionLinks = $this->view->qlink(
+ $this->translate('Show normal'),
+ $this->getRequest()->getUrl()->without('resolved'),
+ null,
+ array('class' => 'icon-resize-small state-warning')
+ );
} else {
- $this->view->object = $object;
if ($object->supportsImports() && $object->imports()->count() > 0) {
$this->view->actionLinks = $this->view->qlink(
@@ -131,6 +131,18 @@ abstract class ObjectController extends ActionController
}
}
+ if ($this->view->isExternal) {
+ $object->object_type = 'object';
+ }
+
+ if ($this->view->isDisabledd) {
+ $object->disabled = 'n';
+ }
+
+ $this->view->object = $object;
+ $this->view->config = new IcingaConfig($this->db());
+ $object->renderToConfig($this->view->config);
+
$this->view->title = sprintf(
$this->translate('Config preview: %s'),
$object->object_name
@@ -227,10 +239,9 @@ abstract class ObjectController extends ActionController
$type = $this->getType();
$this->getTabs()->activate('fields');
- $title = $this->translate('%s template "%s": custom fields');
+
$this->view->title = sprintf(
- $title,
- $this->translate(ucfirst($type)),
+ $this->translate('Custom fields: %s'),
$object->object_name
);
diff --git a/public/css/module.less b/public/css/module.less
index f696646d..ce53dcb2 100644
--- a/public/css/module.less
+++ b/public/css/module.less
@@ -34,9 +34,11 @@ span.disabled {
color: @gray-light;
}
-.controls span a {
- color: @icinga-blue;
- margin-right: 1em;
+.controls span.action-links {
+ a {
+ color: @icinga-blue;
+ margin-right: 1em;
+ }
}
pre.disabled {
@@ -404,9 +406,15 @@ a:hover::before {
text-decoration: none;
}
+h1 {
+ min-width: 27em;
+}
+
ul.main-actions {
margin: 0;
padding: 0;
+ min-width: 36em;
+
li {
list-style-type: none;
@@ -415,7 +423,6 @@ ul.main-actions {
padding: 0;
clear: both;
width: 19em;
- min-width: 16em;
vertical-align: top;
a {
@@ -450,7 +457,6 @@ ul.main-actions {
}
padding: 1em;
- font-size: 1.1em;
color: #666;
font-weight: bold;
display: block;
@@ -477,8 +483,9 @@ ul.main-actions {
#layout.poor-layout ul.main-actions {
li {
a { height: 12em; }
+ width: 18em;
> a > i {
- font-size: 2.4em;
+ font-size: 2em;
}
}
}
@@ -487,7 +494,7 @@ ul.main-actions {
#layout.twocols ul.main-actions {
li {
a { height: 12em; }
- width: 16em;
+ width: 17em;
> a > i {
font-size: 1.8em;
}
@@ -722,7 +729,6 @@ table.tinystats {
}
/* Simple table, test */
-
table.syncstate {
tr td:first-child {
padding-left: 2em;
@@ -753,6 +759,41 @@ table.syncstate {
}
}
+table.jobs {
+ tr td:first-child {
+ padding-left: 2em;
+ &::before {
+ font-family: 'ifont';
+ // icon-help:
+ content: '\e85b';
+ float: left;
+ font-weight: bold;
+ margin-left: -1.5em;
+ line-height: 1.5em;
+ }
+ }
+
+ tr.ok td:first-child::before {
+ content: '\e803';
+ color: @color-ok;
+ }
+
+ tr.warning td:first-child::before {
+ content: '\e864';
+ color: @color-warning;
+ }
+
+ tr.pending td:first-child::before {
+ content: '\e864';
+ color: @color-pending;
+ }
+
+ tr.critical td:first-child::before {
+ content: '\e804';
+ color: @color-critical;
+ }
+}
+
table.icinga-objects {
tr td:first-child {
padding-left: 2em;
@@ -926,7 +967,8 @@ table.activity-log {
}
tr.undeployed td, tr.undeployed a {
- font-weight: bold;
+ color: @gray;
+ background-color: @gray-lightest;
}
tr.undeployed td:first-child::before {
@@ -934,6 +976,50 @@ table.activity-log {
}
}
+table.config-diff {
+
+ tr th:first-child {
+ padding-left: 2em;
+ }
+
+ tr td:first-child {
+ padding-left: 2em;
+ &::before {
+ font-family: 'ifont';
+ // icon-help:
+ content: '\e85b';
+ float: left;
+ font-weight: bold;
+ margin-left: -1.5em;
+ line-height: 1.5em;
+ }
+ }
+
+ tr.file-unmodified td:first-child::before {
+ // icon-ok
+ color: @color-ok;
+ content: '\e803';
+ }
+
+ tr.file-created td:first-child::before {
+ // icon-plus
+ color: @color-pending;
+ content: '\e805';
+ }
+
+ tr.file-removed td:first-child::before {
+ // icon-cancel
+ color: @color-critical;
+ content: '\e804';
+ }
+
+ tr.file-modified td:first-child::before {
+ // icon-flapping
+ color: @color-warning;
+ content: '\e85d';
+ }
+}
+
.tree li a {
display: inline-block;
padding-left: 2.4em;
diff --git a/run.php b/run.php
index c9814721..01170726 100644
--- a/run.php
+++ b/run.php
@@ -32,6 +32,11 @@ $this->provideHook('director/PropertyModifier', $prefix . 'PropertyModifier\\Pro
$this->provideHook('director/PropertyModifier', $prefix . 'PropertyModifier\\PropertyModifierFromAdSid');
$this->provideHook('director/PropertyModifier', $prefix . 'PropertyModifier\\PropertyModifierFromLatin1');
+$this->provideHook('director/Job', $prefix . 'Job\\HousekeepingJob');
+$this->provideHook('director/Job', $prefix . 'Job\\ConfigJob');
+$this->provideHook('director/Job', $prefix . 'Job\\ImportJob');
+$this->provideHook('director/Job', $prefix . 'Job\\SyncJob');
+
if (Icinga::app()->isCli()) {
return;
}
diff --git a/schema/mysql-migrations/upgrade_93.sql b/schema/mysql-migrations/upgrade_93.sql
new file mode 100644
index 00000000..845d4bfd
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_93.sql
@@ -0,0 +1,22 @@
+ALTER TABLE sync_rule
+ ADD COLUMN sync_state ENUM(
+ 'unknown',
+ 'in-sync',
+ 'pending-changes',
+ 'failing'
+ ) NOT NULL DEFAULT 'unknown',
+ ADD COLUMN last_error_message VARCHAR(255) DEFAULT NULL,
+ ADD COLUMN last_attempt DATETIME DEFAULT NULL
+;
+
+UPDATE sync_rule r
+ JOIN (
+ SELECT rule_id, MAX(start_time) AS start_time
+ FROM sync_run
+ GROUP BY rule_id
+ ) lr ON r.id = lr.rule_id
+ SET r.last_attempt = lr.start_time;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (93, NOW());
diff --git a/schema/mysql-migrations/upgrade_94.sql b/schema/mysql-migrations/upgrade_94.sql
new file mode 100644
index 00000000..5b55b37b
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_94.sql
@@ -0,0 +1,29 @@
+CREATE TABLE director_job (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ job_name VARCHAR(64) NOT NULL,
+ job_class VARCHAR(72) NOT NULL,
+ disabled ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ run_interval INT(10) UNSIGNED NOT NULL, -- seconds
+ last_attempt_succeeded ENUM('y', 'n') DEFAULT NULL,
+ ts_last_attempt DATETIME DEFAULT NULL,
+ ts_last_error DATETIME DEFAULT NULL,
+ last_error_message TEXT,
+ PRIMARY KEY (id),
+ UNIQUE KEY (job_name)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE director_job_setting (
+ job_id INT UNSIGNED NOT NULL,
+ setting_name VARCHAR(64) NOT NULL,
+ setting_value TEXT DEFAULT NULL,
+ PRIMARY KEY (job_id, setting_name),
+ CONSTRAINT job_settings
+ FOREIGN KEY director_job (job_id)
+ REFERENCES director_job (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (94, NOW());
diff --git a/schema/mysql-migrations/upgrade_95.sql b/schema/mysql-migrations/upgrade_95.sql
new file mode 100644
index 00000000..aa49c5b2
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_95.sql
@@ -0,0 +1,22 @@
+ALTER TABLE import_source
+ ADD COLUMN import_state ENUM(
+ 'unknown',
+ 'in-sync',
+ 'pending-changes',
+ 'failing'
+ ) NOT NULL DEFAULT 'unknown',
+ ADD COLUMN last_error_message TEXT DEFAULT NULL,
+ ADD COLUMN last_attempt DATETIME DEFAULT NULL
+;
+
+UPDATE import_source s
+ JOIN (
+ SELECT source_id, MAX(start_time) AS start_time
+ FROM import_run
+ GROUP BY source_id
+ ) ir ON s.id = ir.source_id
+ SET s.last_attempt = ir.start_time;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (95, NOW());
diff --git a/schema/mysql.sql b/schema/mysql.sql
index 8f06b0eb..69cf9efc 100644
--- a/schema/mysql.sql
+++ b/schema/mysql.sql
@@ -140,6 +140,32 @@ CREATE TABLE director_datafield_setting (
ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+CREATE TABLE director_job (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ job_name VARCHAR(64) NOT NULL,
+ job_class VARCHAR(72) NOT NULL,
+ disabled ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ run_interval INT(10) UNSIGNED NOT NULL, -- seconds
+ last_attempt_succeeded ENUM('y', 'n') DEFAULT NULL,
+ ts_last_attempt DATETIME DEFAULT NULL,
+ ts_last_error DATETIME DEFAULT NULL,
+ last_error_message TEXT,
+ PRIMARY KEY (id),
+ UNIQUE KEY (job_name)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE director_job_setting (
+ job_id INT UNSIGNED NOT NULL,
+ setting_name VARCHAR(64) NOT NULL,
+ setting_value TEXT DEFAULT NULL,
+ PRIMARY KEY (job_id, setting_name),
+ CONSTRAINT job_settings
+ FOREIGN KEY director_job (job_id)
+ REFERENCES director_job (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
CREATE TABLE director_schema_migration (
schema_version SMALLINT UNSIGNED NOT NULL,
migration_time DATETIME NOT NULL,
@@ -1081,6 +1107,14 @@ CREATE TABLE import_source (
source_name VARCHAR(64) NOT NULL,
key_column VARCHAR(64) NOT NULL,
provider_class VARCHAR(72) NOT NULL,
+ import_state ENUM(
+ 'unknown',
+ 'in-sync',
+ 'pending-changes',
+ 'failing'
+ ) NOT NULL DEFAULT 'unknown',
+ last_error_message TEXT DEFAULT NULL,
+ last_attempt DATETIME DEFAULT NULL,
PRIMARY KEY (id),
INDEX search_idx (key_column)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
@@ -1214,6 +1248,14 @@ CREATE TABLE sync_rule (
update_policy ENUM('merge', 'override', 'ignore') NOT NULL,
purge_existing ENUM('y', 'n') NOT NULL DEFAULT 'n',
filter_expression TEXT DEFAULT NULL,
+ sync_state ENUM(
+ 'unknown',
+ 'in-sync',
+ 'pending-changes',
+ 'failing'
+ ) NOT NULL DEFAULT 'unknown',
+ last_error_message VARCHAR(255) DEFAULT NULL,
+ last_attempt DATETIME DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
@@ -1260,4 +1302,4 @@ CREATE TABLE sync_run (
INSERT INTO director_schema_migration
SET migration_time = NOW(),
- schema_version = 92;
+ schema_version = 95;
diff --git a/test/php/library/Director/Objects/IcingaHostTest.php b/test/php/library/Director/Objects/IcingaHostTest.php
index c7ad2e36..c487bee6 100644
--- a/test/php/library/Director/Objects/IcingaHostTest.php
+++ b/test/php/library/Director/Objects/IcingaHostTest.php
@@ -284,7 +284,7 @@ class IcingaHostTest extends BaseTestCase
$config = new IcingaConfig($db);
$host->renderToConfig($config);
$this->assertEquals(
- array('zones.d/master.conf'),
+ array('zones.d/master/hosts.conf'),
$config->getFileNames()
);
@@ -295,7 +295,7 @@ class IcingaHostTest extends BaseTestCase
$host->zone = '___TEST___zone';
$host->renderToConfig($config);
$this->assertEquals(
- array('zones.d/___TEST___zone.conf'),
+ array('zones.d/___TEST___zone/hosts.conf'),
$config->getFileNames()
);
@@ -306,7 +306,7 @@ class IcingaHostTest extends BaseTestCase
$config = new IcingaConfig($db);
$host->renderToConfig($config);
$this->assertEquals(
- array('zones.d/___TEST___zone.conf'),
+ array('zones.d/___TEST___zone/hosts.conf'),
$config->getFileNames()
);
@@ -316,7 +316,7 @@ class IcingaHostTest extends BaseTestCase
$config = new IcingaConfig($db);
$host->renderToConfig($config);
$this->assertEquals(
- array('zones.d/director-global.conf'),
+ array('zones.d/director-global/host_templates.conf'),
$config->getFileNames()
);
diff --git a/test/php/library/Director/Objects/IcingaServiceTest.php b/test/php/library/Director/Objects/IcingaServiceTest.php
index d4aa8151..fdfcb3df 100644
--- a/test/php/library/Director/Objects/IcingaServiceTest.php
+++ b/test/php/library/Director/Objects/IcingaServiceTest.php
@@ -204,7 +204,7 @@ class IcingaServiceTest extends BaseTestCase
$config = new IcingaConfig($db);
$service->renderToConfig($config);
$this->assertEquals(
- array('zones.d/master.conf'),
+ array('zones.d/master/services.conf'),
$config->getFileNames()
);
}