Merge branch 'feature/job-scheduling-11627'

This commit is contained in:
Thomas Gelf 2016-05-13 13:43:39 +02:00
commit 7d06c23d48
75 changed files with 1993 additions and 472 deletions

View File

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

View File

@ -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')

View File

@ -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(

View File

@ -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());

View File

@ -1,66 +0,0 @@
<?php
namespace Icinga\Module\Director\Controllers;
use Icinga\Module\Director\Web\Controller\ActionController;
class DatalistentryController extends ActionController
{
public function addAction()
{
$this->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);
}
}

View File

@ -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()
)
);
}
}

View File

@ -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 {

View File

@ -0,0 +1,78 @@
<?php
namespace Icinga\Module\Director\Controllers;
use Icinga\Module\Director\Web\Controller\ActionController;
use Icinga\Module\Director\Objects\Job;
use Icinga\Data\Filter\Filter;
use Icinga\Web\Notification;
use Icinga\Web\Url;
class JobController extends ActionController
{
public function addAction()
{
$this->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'),
));
}
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace Icinga\Module\Director\Controllers;
use Icinga\Module\Director\Web\Controller\ActionController;
class JobsController extends ActionController
{
public function indexAction()
{
$this->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');
}
}

View File

@ -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'),
));
}
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Icinga\Module\Director\Forms;
use Exception;
use Icinga\Module\Director\Db\Migrations;
use Icinga\Module\Director\Web\Form\QuickForm;
class ApplyMigrationsForm extends QuickForm
{
protected $migrations;
public function setup()
{
$this->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;
}
}

View File

@ -74,6 +74,8 @@ class DirectorDatafieldForm extends DirectorObjectForm
$el->setValue($val);
}
}
$this->setButtons();
}
protected function addSettings($class = null)

View File

@ -0,0 +1,117 @@
<?php
namespace Icinga\Module\Director\Forms;
use Icinga\Module\Director\Web\Form\DirectorObjectForm;
use Icinga\Web\Hook;
class DirectorJobForm extends DirectorObjectForm
{
public function setup()
{
$this->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;
}
}

View File

@ -1,140 +0,0 @@
<?php
namespace Icinga\Module\Director\Forms;
use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
use Icinga\Module\Director\Objects\IcingaObject;
use Icinga\Module\Director\Objects\IcingaHost;
use Icinga\Module\Director\Web\Form\QuickForm;
class IcingaAssignServiceToHostForm extends QuickForm
{
// NOT TOUCHED
/**
*
* Please note that $object would conflict with logic in parent class
*/
protected $icingaObject;
protected $db;
public function setDb($db)
{
$this->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;
}
}
}

View File

@ -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(
'<pre>' . $config . '</pre>'
);
}
$this->addHtmlHint(
'<pre>' . $config . '</pre>',
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

View File

@ -25,7 +25,6 @@ class SyncPropertyForm extends DirectorObjectForm
public function setup()
{
$this->addHtml(sprintf('<h3>%s</h3>', $this->getView()->escape($this->rule->rule_name)));
$this->addHidden('rule_id', $this->rule_id);
$this->addElement('select', 'source_id', array(

View File

@ -0,0 +1,151 @@
<?php
namespace Icinga\Module\Director\Tables;
use Icinga\Exception\ProgrammingError;
use Icinga\Module\Director\Web\Table\QuickTable;
use Icinga\Module\Director\Util;
class ConfigFileDiffTable extends QuickTable
{
protected $leftChecksum;
protected $rightChecksum;
public function getColumns()
{
throw new ProgrammingError('Accessing getColumns() is not supported');
}
protected function listTableClasses()
{
return array_merge(array('config-diff'), parent::listTableClasses());
}
protected function getRowClasses($row)
{
return 'file-' . $row->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');
}
}

View File

@ -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'),
);
}

View File

@ -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(*)'

View File

@ -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()

View File

@ -0,0 +1,74 @@
<?php
namespace Icinga\Module\Director\Tables;
use Icinga\Module\Director\Web\Table\QuickTable;
use Icinga\Module\Director\Objects\Job;
use Exception;
class JobTable extends QuickTable
{
public function getColumns()
{
return array(
'id' => '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;
}
}

View File

@ -0,0 +1,78 @@
<?php
namespace Icinga\Module\Director\Tables;
use Icinga\Module\Director\Web\Table\QuickTable;
use Exception;
class SyncRunTable extends QuickTable
{
protected $revalidate = false;
public function getColumns()
{
return array(
'id' => '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;
}
}

View File

@ -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()

View File

@ -1,7 +1,7 @@
<div class="controls">
<?= $this->tabs ?>
<h1><?= $this->escape($this->title) ?></h1>
<span data-base-target="_next">
<span class="action-links" data-base-target="_next">
<?= $this->addLink ?>
</div>

View File

@ -0,0 +1,31 @@
<div class="controls">
<?= $this->tabs ?>
<h1><?= $this->escape($this->title) ?></h1>
<span class="action-links" data-base-target="_next">
<?= $this->addLink ?>
</span>
<form action="<?= $this->url ?>" method="GET">
<?= $this->formSelect(
'left',
$this->leftSum,
array('class' => 'autosubmit'),
array(null => $this->translate('- please choose -')) + $this->configs
)
?>
<?= $this->formSelect(
'right',
$this->rightSum,
array('class' => 'autosubmit'),
array(null => $this->translate('- please choose -')) + $this->configs
)
?>
</form>
</div>
<div class="content" data-base-target="_next">
<?php if (count($this->table)): ?>
<div>
<?= $this->table->render() ?>
</div>
<?php endif ?>
</div>

View File

@ -1,7 +1,7 @@
<div class="controls">
<?= $this->tabs ?>
<h1><?= $title ?></h1>
<span data-base-target="_self">
<span class="action-links" data-base-target="_self">
<?= $this->addLink ?>
</span>
</div>

View File

@ -0,0 +1,11 @@
<div class="controls">
<?= $this->tabs ?>
<h1><?= $this->escape($this->title) ?></h1>
<span class="action-links" data-base-target="_next">
<?= $this->addLink ?>
</span>
</div>
<div class="content" data-base-target="_next">
<?= $this->output ?>
</div>

View File

@ -1,7 +1,7 @@
<div class="controls">
<?= $this->tabs ?>
<h1><?= $this->escape($this->title) ?></h1>
<span data-base-target="_next">
<span class="action-links" data-base-target="_next">
<?= $this->addLink ?>
</span>
<?php if (count($table) || ! $this->filterEditor->getFilter()->isEmpty()): ?>
@ -24,6 +24,11 @@
'director/show/activitylog',
array('checksum' => $this->config->getLastActivityHexChecksum()),
array('class' => 'icon-clock', 'data-base-target' => '_next')
) ?><br /><?= $this->qlink(
$this->translate('Diff with other config'),
'director/config/diff',
array('left' => $this->config->getHexChecksum()),
array('class' => 'icon-flapping', 'data-base-target' => '_self')
) ?></td>
</tr>
<tr>

View File

@ -8,7 +8,11 @@
$cert = $this->escape($this->certname);
$master = $this->escape($this->master);
?>
Please check the <a href="http://docs.icinga.org/icinga2/latest/doc/module/icinga2/chapter/icinga2-client">Icinga 2 Client documentation</a> for more related information. The Director-assisted setup corresponds to configuring the <a href="http://docs.icinga.org/icinga2/latest/doc/module/icinga2/chapter/icinga2-client#icinga2-client-configuration-command-bridge">Client as Command Execution Bridge</a>.
<p>Please check the <a href="http://docs.icinga.org/icinga2/latest/doc/module/icinga2/chapter/icinga2-client">Icinga 2 Client documentation</a> for more related information. The Director-assisted setup corresponds to configuring the <a href="http://docs.icinga.org/icinga2/latest/doc/module/icinga2/chapter/icinga2-client#icinga2-client-configuration-command-bridge">Client as Command Execution Bridge</a>.</p>
<?php if ($this->error): ?>
<p class="error"><?= $this->escape($this->error) ?></p>
<?php endif ?>
<h2>When using the node wizard</h2>
<p>Ticket : <code><?= $this->escape($ticket) ?></code></p>
@ -49,7 +53,7 @@ icinga2 pki request --host <?= $master ?> \
include "constants.conf"
include &lt;itl&gt;
include &lt;plugins&gt;
include &lt;plugins-contrib&gt;
// include &lt;plugins-contrib&gt;
object FileLogger "main-log" {
severity = "information"

View File

@ -1,7 +1,7 @@
<div class="controls">
<?= $this->tabs ?>
<h1><?= $this->escape($this->title) ?></h1>
<span>
<span class="action-links">
<?= $this->addLink ?>
</span>
<?php if (count($table) || ($this->filterEditor && ! $this->filterEditor->getFilter()->isEmpty())): ?>

View File

@ -84,6 +84,11 @@ if (!$this->hasDeploymentEndpoint) {
echo $this->form;
}
if ($this->migrationsForm) {
echo '<h1>' . $this->translate('There are pending database schema migrations') . "</h2>\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')),
)

View File

@ -27,7 +27,7 @@ $pt = $loc['thousands_sep'];
</tr>
</tbody>
</table>
<span data-base-target="_next">
<span class="action-links" data-base-target="_next">
<?= $this->addLink ?>
</span><br />
<?= $this->table->getPaginator() ?>

View File

@ -1,7 +1,7 @@
<div class="controls">
<?= $this->tabs ?>
<h1><?= $this->escape($this->title) ?></h1>
<span<?php if (! $this->stayHere): ?> data-base-target="_next"<?php endif ?>>
<span class="action-links"<?php if (! $this->stayHere): ?> data-base-target="_next"<?php endif ?>>
<?= $this->addLink ?>
</span>
<?= $this->filterEditor ?>

View File

@ -1,7 +1,7 @@
<div class="controls">
<?= $this->tabs ?>
<h1><?= $this->escape($this->title) ?></h1>
<span>
<span class="action-links">
<?= $this->actionLinks ?>
</span>
</div>

View File

@ -1,7 +1,7 @@
<div class="controls">
<?= $this->tabs ?>
<h1><?= $this->escape($this->title) ?></h1>
<span>
<span class="action-links">
<?= $this->actionLinks ?>
<?= $this->render('object/deploymentLink.phtml') ?>
</span>

View File

@ -1,7 +1,7 @@
<div class="controls">
<?= $this->tabs ?>
<h1><?= $this->escape($this->title) ?></h1>
<span>
<span class="action-links">
<?= $this->actionLinks ?>
</span>
</div>
@ -10,7 +10,7 @@
<?php if ($object->disabled === 'y'): ?>
<p class="error"><?= $this->translate('This object will not be deployed as it has been disabled') ?></p>
<?php endif ?>
<?php if ($object->isExternal()): ?>
<?php if ($this->isExternal): ?>
<p><?= $this->translate(
'This is an external object. It has been imported from Icinga 2 throught the'
. ' Core API and cannot be managed with the Icinga Director. It is however'
@ -19,7 +19,11 @@
. ' object more enjoyable'
) ?></p>
<?php endif ?>
<pre<?php if ($object->disabled === 'y'): ?> class="disabled"<?php endif ?>><?= $this->escape($object) ?><?php if ($this->extraObjects): ?>
<?= implode('', $this->extraObjects) ?>
<?php endif ?></pre>
<?php foreach ($this->config->getFiles() as $filename => $file): ?>
<?php if (! $this->isExternal): ?><h2><?= $this->escape($filename) ?></h2><?php endif ?>
<pre<?php if ($this->isDisabled): ?> class="disabled"<?php elseif ($this->isExternal): ?> class="logfile"<?php endif ?>>
<?= $this->escape($file->getContent()) ?>
</pre>
<?php endforeach ?>
</div>

View File

@ -3,7 +3,7 @@
<?= $this->tabs ?>
<h1><?= $this->escape($this->title) ?><?= $this->quickSearch ?></h1>
<span<?php if (! $this->stayHere): ?> data-base-target="_next"<?php endif ?>>
<span class="action-links"<?php if (! $this->stayHere): ?> data-base-target="_next"<?php endif ?>>
<?= $this->addLink ?>
</span>
<?php if ($this->filterEditor): ?>

View File

@ -0,0 +1,84 @@
<div class="controls">
<?= $this->tabs ?>
<h1><?= $this->escape($this->title) ?></h1>
<span class="action-links"<?php if (! $this->stayHere): ?> data-base-target="_next"<?php endif ?>>
<?= $this->addLink ?>
</span>
<?= $this->filterEditor ?>
<?= $this->table->getPaginator() ?>
</div>
<div class="content"<?php if (! $this->stayHere): ?> data-base-target="_next"<?php endif ?>>
<?php if ($this->run): ?>
<h3><?= $this->translate('Sync run details') ?></h3>
<table class="key-value-table">
<tr>
<th><?= $this->translate('Start time') ?></th>
<td><?= $this->escape($run->start_time) ?></td>
</tr>
<tr>
<th><?= $this->translate('Duration') ?></th>
<td><?= sprintf('%.2fs', $run->duration_ms / 1000) ?></td>
</tr>
<tr>
<th><?= $this->translate('Activity') ?></th>
<td data-base-target="_next"><?php
$total = $run->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);
}
}
?></td>
</tr>
</table>
<?php endif ?>
<?= $this->table->render() ?>
</div>

View File

@ -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');

View File

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

View File

@ -1,4 +1,4 @@
Automation - Configuration management
<a id="Automation"></a>Automation - Configuration management
=====================================
Director has been designed to work in distributed environments. In case

View File

@ -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.

View File

@ -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"

View File

@ -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)) {

View File

@ -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();

View File

@ -64,6 +64,11 @@ class Housekeeping
);
}
public function hasPendingTasks()
{
return count($this->getPendingTaskSummary()) > 0;
}
public function runAllTasks()
{
$result = array();

View File

@ -0,0 +1,64 @@
<?php
namespace Icinga\Module\Director\Hook;
use Icinga\Module\Director\Db;
use Icinga\Module\Director\Web\Form\QuickForm;
abstract class JobHook
{
private $db;
public static function getDescription(QuickForm $form)
{
return false;
}
abstract public function run();
abstract public function isPending();
public function getName()
{
$parts = explode('\\', get_class($this));
$class = preg_replace('/Job$/', '', array_pop($parts));
if (array_shift($parts) === 'Icinga' && array_shift($parts) === 'Module') {
$module = array_shift($parts);
if ($module !== 'Director') {
return sprintf('%s (%s)', $class, $module);
}
}
return $class;
}
public static function getSuggestedRunInterval(QuickForm $form)
{
return 900;
}
/**
* Override this method if you want to extend the settings form
*
* @param QuickForm $form QuickForm that should be extended
* @return QuickForm
*/
public static function addSettingsFormFields(QuickForm $form)
{
return $form;
}
public function setDb(Db $db)
{
$this->db = $db;
return $this;
}
protected function db()
{
return $this->db;
}
}

View File

@ -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)) {

View File

@ -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;
}
}

View File

@ -0,0 +1,107 @@
<?php
namespace Icinga\Module\Director\Job;
use Icinga\Application\Benchmark;
use Icinga\Module\Director\IcingaConfig\IcingaConfig;
use Icinga\Module\Director\Hook\JobHook;
use Icinga\Module\Director\Web\Form\QuickForm;
use Icinga\Module\Director\Util;
class ConfigJob extends JobHook
{
protected $housekeeping;
public function run()
{
$this->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)
);
}
}
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace Icinga\Module\Director\Job;
use Icinga\Module\Director\Db\Housekeeping;
use Icinga\Module\Director\Hook\JobHook;
use Icinga\Module\Director\Web\Form\QuickForm;
class HousekeepingJob extends JobHook
{
protected $housekeeping;
public function run()
{
$this->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;
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Icinga\Module\Director\Job;
use Icinga\Module\Director\Hook\JobHook;
use Icinga\Module\Director\Web\Form\QuickForm;
class ImportJob extends JobHook
{
public function run()
{
}
public static function getDescription(QuickForm $form)
{
return $form->translate(
'The "Import" job allows to run import actions at regular intervals'
);
}
public function isPending()
{
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace Icinga\Module\Director\Job;
use Icinga\Module\Director\Db;
class JobRunner
{
public function __construct(Db $db)
{
$this->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()
{
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace Icinga\Module\Director\Job;
use Icinga\Module\Director\Db;
use Icinga\Module\Director\Hook\JobHook;
use Icinga\Module\Director\Web\Form\QuickForm;
class SyncJob extends JobHook
{
public function run()
{
if ($this->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()
{
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace Icinga\Module\Director\Objects;
use Icinga\Module\Director\Data\Db\DbObjectWithSettings;
class DirectorJob extends DbObjectWithSettings
{
protected $table = 'director_job';
protected $keyName = 'id';
protected $autoincKeyName = 'id';
protected $defaultProperties = array(
'id' => 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';
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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()) {

View File

@ -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();
}
}

View File

@ -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');

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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) {

View File

@ -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;
}

View File

@ -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
);

View File

@ -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;

View File

@ -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;
}

View File

@ -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());

View File

@ -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());

View File

@ -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());

View File

@ -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;

View File

@ -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()
);

View File

@ -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()
);
}