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 @@
tabs ?>

escape($this->title) ?>

- + 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 @@ +
+tabs ?> +

escape($this->title) ?>

+ +addLink ?> + +
+formSelect( + 'left', + $this->leftSum, + array('class' => 'autosubmit'), + array(null => $this->translate('- please choose -')) + $this->configs +) +?> +formSelect( + 'right', + $this->rightSum, + array('class' => 'autosubmit'), + array(null => $this->translate('- please choose -')) + $this->configs +) +?> +
+
+ +
+table)): ?> +
+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 @@
tabs ?>

- + 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 @@ +
+tabs ?> +

escape($this->title) ?>

+ +addLink ?> + +
+ +
+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 @@
tabs ?>

escape($this->title) ?>

- + addLink ?> filterEditor->getFilter()->isEmpty()): ?> @@ -24,6 +24,11 @@ 'director/show/activitylog', array('checksum' => $this->config->getLastActivityHexChecksum()), array('class' => 'icon-clock', 'data-base-target' => '_next') + ) ?>
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): ?> +

escape($this->error) ?>

+

When using the node wizard

Ticket : escape($ticket) ?>

@@ -49,7 +53,7 @@ icinga2 pki request --host \ 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 @@
tabs ?>

escape($this->title) ?>

- + 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']; - + addLink ?>
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 @@
tabs ?>

escape($this->title) ?>

-stayHere): ?> data-base-target="_next"> +stayHere): ?> data-base-target="_next"> addLink ?> 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 @@
tabs ?>

escape($this->title) ?>

- + 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 @@
tabs ?>

escape($this->title) ?>

- + actionLinks ?> 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 @@
tabs ?>

escape($this->title) ?>

- + actionLinks ?>
@@ -10,7 +10,7 @@ disabled === 'y'): ?>

translate('This object will not be deployed as it has been disabled') ?>

-isExternal()): ?> +isExternal): ?>

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">escape($object) ?>extraObjects): ?> -extraObjects) ?> - +config->getFiles() as $filename => $file): ?> +isExternal): ?>

escape($filename) ?>

+isDisabled): ?> class="disabled"isExternal): ?> class="logfile"> +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 @@ tabs ?>

escape($this->title) ?>quickSearch ?>

-stayHere): ?> data-base-target="_next"> +stayHere): ?> data-base-target="_next"> 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 @@ +
+tabs ?> +

escape($this->title) ?>

+stayHere): ?> data-base-target="_next"> +addLink ?> + +filterEditor ?> +table->getPaginator() ?> +
+ +
stayHere): ?> data-base-target="_next"> +run): ?> +

translate('Sync run details') ?>

+ + + + + + + + + + + + + +
translate('Start time') ?>escape($run->start_time) ?>
translate('Duration') ?>duration_ms / 1000) ?>
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); + } + } + ?>
+ +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() ); }