Merge branch 'feature/basket-1630'

This commit is contained in:
Thomas Gelf 2018-10-15 15:10:38 +02:00
commit 5295165386
57 changed files with 3285 additions and 177 deletions

View File

@ -0,0 +1,96 @@
<?php
namespace Icinga\Module\Director\Clicommands;
use Icinga\Date\DateFormatter;
use Icinga\Module\Director\Cli\Command;
use Icinga\Module\Director\DirectorObject\Automation\Basket;
use Icinga\Module\Director\DirectorObject\Automation\BasketSnapshot;
/**
* Export Director Config Objects
*/
class BasketCommand extends Command
{
/**
* List configured Baskets
*
* USAGE
*
* icingacli director basket list
*
* OPTIONS
*/
public function listAction()
{
$db = $this->db()->getDbAdapter();
$query = $db->select()
->from('director_basket', 'basket_name')
->order('basket_name');
foreach ($db->fetchCol($query) as $name) {
echo "$name\n";
}
}
/**
* JSON-dump for objects related to the given Basket
*
* USAGE
*
* icingacli director basket dump --name <basket>
*
* OPTIONS
*/
public function dumpAction()
{
$basket = $this->requireBasket();
$snapshot = BasketSnapshot::createForBasket($basket, $this->db());
echo $snapshot->getJsonDump() . "\n";
}
/**
* Take a snapshot for the given Basket
*
* USAGE
*
* icingacli director basket snapshot --name <basket>
*
* OPTIONS
*/
public function snapshotAction()
{
$basket = $this->requireBasket();
$snapshot = BasketSnapshot::createForBasket($basket, $this->db());
$snapshot->store();
$hexSum = bin2hex($snapshot->get('content_checksum'));
printf(
"Snapshot '%s' taken for Basket '%s' at %s\n",
substr($hexSum, 0, 7),
$basket->get('basket_name'),
DateFormatter::formatDateTime($snapshot->get('ts_create') / 1000)
);
}
/**
* Restore a Basket from JSON dump provided on STDIN
*
* USAGE
*
* icingacli director basket restore < basket-dump.json
*
* OPTIONS
*/
public function restoreAction()
{
$json = file_get_contents('php://stdin');
BasketSnapshot::restoreJson($json, $this->db());
echo "Objects from Basket Snapshot have been restored\n";
}
/**
*/
protected function requireBasket()
{
return Basket::load($this->params->getRequired('name'), $this->db());
}
}

View File

@ -125,7 +125,10 @@ class ImportsourceCommand extends Command
*/
protected function getImportSource()
{
return ImportSource::load($this->params->getRequired('id'), $this->db());
return ImportSource::loadWithAutoIncId(
(int) $this->params->getRequired('id'),
$this->db()
);
}
/**

View File

@ -95,7 +95,10 @@ class SyncruleCommand extends Command
*/
protected function getSyncRule()
{
return SyncRule::load($this->params->getRequired('id'), $this->db());
return SyncRule::loadWithAutoIncId(
(int) $this->params->getRequired('id'),
$this->db()
);
}
/**

View File

@ -0,0 +1,347 @@
<?php
namespace Icinga\Module\Director\Controllers;
use dipl\Html\Link;
use dipl\Web\Widget\NameValueTable;
use Exception;
use Icinga\Date\DateFormatter;
use Icinga\Module\Director\ConfigDiff;
use Icinga\Module\Director\Core\Json;
use Icinga\Module\Director\Db;
use Icinga\Module\Director\DirectorObject\Automation\Basket;
use Icinga\Module\Director\DirectorObject\Automation\BasketSnapshot;
use Icinga\Module\Director\DirectorObject\Automation\BasketSnapshotFieldResolver;
use Icinga\Module\Director\Forms\AddToBasketForm;
use Icinga\Module\Director\Forms\BasketCreateSnapshotForm;
use Icinga\Module\Director\Forms\BasketForm;
use Icinga\Module\Director\Forms\RestoreBasketForm;
use Icinga\Module\Director\Web\Controller\ActionController;
use dipl\Html\Html;
use Icinga\Module\Director\Web\Table\BasketSnapshotTable;
class BasketController extends ActionController
{
protected $isApified = true;
protected function basketTabs()
{
$name = $this->params->get('name');
return $this->tabs()->add('show', [
'label' => $this->translate('Basket'),
'url' => 'director/basket',
'urlParams' => ['name' => $name]
])->add('snapshots', [
'label' => $this->translate('Snapshots'),
'url' => 'director/basket/snapshots',
'urlParams' => ['name' => $name]
]);
}
/**
* @throws \Icinga\Exception\NotFoundError
* @throws \Icinga\Exception\MissingParameterException
*/
public function indexAction()
{
$this->actions()->add(
Link::create(
$this->translate('Back'),
'director/baskets',
null,
['class' => 'icon-left-big']
)
);
$basket = $this->requireBasket();
$this->basketTabs()->activate('show');
$this->addTitle($basket->get('basket_name'));
if ($basket->isEmpty()) {
$this->content()->add(Html::tag('p', [
'class' => 'information'
], $this->translate('This basket is empty')));
}
$this->content()->add(
(new BasketForm())->setObject($basket)->handleRequest()
);
}
public function addAction()
{
$this->addSingleTab($this->translate('Add to Basket'));
$this->addTitle($this->translate('Add chosen objects to a Configuration Basket'));
$form = new AddToBasketForm();
$form->setDb($this->db())
->setType($this->params->getRequired('type'))
->setNames($this->url()->getParams()->getValues('names'))
->handleRequest();
$this->content()->add($form);
}
public function createAction()
{
$this->actions()->add(
Link::create(
$this->translate('back'),
'director/baskets',
null,
['class' => 'icon-left-big']
)
);
$this->addSingleTab($this->translate('Create Basket'));
$this->addTitle($this->translate('Create a new Configuration Basket'));
$form = (new BasketForm())
->setDb($this->db())
->handleRequest();
$this->content()->add($form);
}
/**
* @throws \Icinga\Exception\NotFoundError
*/
public function snapshotsAction()
{
$name = $this->params->get('name');
if ($name === null || $name === '') {
$basket = null;
} else {
$basket = Basket::load($name, $this->db());
}
if ($basket === null) {
$this->addTitle($this->translate('Basket Snapshots'));
$this->addSingleTab($this->translate('Snapshots'));
} else {
$this->addTitle(sprintf(
$this->translate('%: Snapshots'),
$basket->get('basket_name')
));
$this->basketTabs()->activate('snapshots');
}
if ($basket !== null) {
$this->content()->add(
(new BasketCreateSnapshotForm())
->setBasket($basket)
->handleRequest()
);
}
$table = new BasketSnapshotTable($this->db());
if ($basket !== null) {
$table->setBasket($basket);
}
$table->renderTo($this);
}
/**
* @throws \Icinga\Exception\MissingParameterException
* @throws \Icinga\Exception\NotFoundError
*/
public function snapshotAction()
{
$basket = $this->requireBasket();
$snapshot = BasketSnapshot::load([
'basket_uuid' => $basket->get('uuid'),
'ts_create' => $this->params->getRequired('ts'),
], $this->db());
$snapSum = bin2hex($snapshot->get('content_checksum'));
if ($this->params->get('action') === 'download') {
$this->getResponse()->setHeader('Content-Type', 'application/json', true);
echo $snapshot->getJsonDump();
return;
}
$this->addTitle(
$this->translate('%s: %s (Snapshot)'),
$basket->get('basket_name'),
substr($snapSum, 0, 7)
);
$this->actions()->add([
Link::create(
$this->translate('Show Basket'),
'director/basket',
['name' => $basket->get('basket_name')],
['data-base-target' => '_next']
),
Link::create(
$this->translate('Restore'),
$this->url()->with('action', 'restore'),
null,
['class' => 'icon-rewind']
),
Link::create(
$this->translate('Download'),
$this->url()->with('action', 'download'),
null,
[
'class' => 'icon-download',
'target' => '_blank'
]
),
]);
$properties = new NameValueTable();
$properties->addNameValuePairs([
$this->translate('Created') => DateFormatter::formatDateTime($snapshot->get('ts_create') / 1000),
$this->translate('Content Checksum') => bin2hex($snapshot->get('content_checksum')),
]);
$this->content()->add($properties);
if ($this->params->get('action') === 'restore') {
$form = new RestoreBasketForm();
$form
->setSnapshot($snapshot)
->handleRequest();
$this->content()->add($form);
$targetDbName = $form->getValue('target_db');
$connection = $form->getDb();
} else {
$targetDbName = null;
$connection = $this->db();
}
$json = $snapshot->getJsonDump();
$this->addSingleTab($this->translate('Snapshot'));
$all = Json::decode($json);
$fieldResolver = new BasketSnapshotFieldResolver($all, $connection);
foreach ($all as $type => $objects) {
if ($type === 'Datafield') {
// TODO: we should now be able to show all fields and link
// to a "diff" for the ones that should be created
// $this->content()->add(Html::tag('h2', sprintf('+%d Datafield(s)', count($objects))));
continue;
}
$table = new NameValueTable();
$table->setAttribute('data-base-target', '_next');
foreach ($objects as $key => $object) {
$linkParams = [
'name' => $basket->get('basket_name'),
'checksum' => $this->params->get('checksum'),
'ts' => $this->params->get('ts'),
'type' => $type,
'key' => $key,
];
if ($targetDbName !== null) {
$linkParams['target_db'] = $targetDbName;
}
try {
$current = BasketSnapshot::instanceByIdentifier($type, $key, $connection);
if ($current === null) {
$table->addNameValueRow(
$key,
Link::create(
Html::tag('strong', ['style' => 'color: green'], $this->translate('new')),
'director/basket/snapshotobject',
$linkParams
)
);
continue;
}
$currentExport = $current->export();
$fieldResolver->tweakTargetIds($currentExport);
$hasChanged = Json::encode($currentExport) !== Json::encode($object);
$table->addNameValueRow(
$key,
$hasChanged
? Link::create(
Html::tag('strong', ['style' => 'color: orange'], $this->translate('modified')),
'director/basket/snapshotobject',
$linkParams
)
: Html::tag('span', ['style' => 'color: green'], $this->translate('unchanged'))
);
} catch (Exception $e) {
$table->addNameValueRow(
$key,
$e->getMessage()
);
}
}
$this->content()->add(Html::tag('h2', $type));
$this->content()->add($table);
}
$this->content()->add(Html::tag('div', ['style' => 'height: 5em']));
}
/**
* @throws \Icinga\Exception\MissingParameterException
* @throws \Icinga\Exception\NotFoundError
*/
public function snapshotobjectAction()
{
$basket = $this->requireBasket();
$snapshot = BasketSnapshot::load([
'basket_uuid' => $basket->get('uuid'),
'ts_create' => $this->params->getRequired('ts'),
], $this->db());
$snapshotUrl = $this->url()->without('type')->without('key')->setPath('director/basket/snapshot');
$type = $this->params->get('type');
$key = $this->params->get('key');
$this->addTitle($this->translate('Single Object Diff'));
$this->content()->add(Html::tag('p', [
'class' => 'information'
], Html::sprintf(
$this->translate('Comparing %s "%s" from Snapshot "%s" to current config'),
$type,
$key,
Link::create(
substr(bin2hex($snapshot->get('content_checksum')), 0, 7),
$snapshotUrl,
null,
['data-base-target' => '_next']
)
)));
$this->actions()->add([
Link::create(
$this->translate('back'),
$snapshotUrl,
null,
['class' => 'icon-left-big']
),
Link::create(
$this->translate('Restore'),
$this->url()->with('action', 'restore'),
null,
['class' => 'icon-rewind']
)
]);
$json = $snapshot->getJsonDump();
$this->addSingleTab($this->translate('Snapshot'));
$objects = Json::decode($json);
$targetDbName = $this->params->get('target_db');
if ($targetDbName === null) {
$connection = $this->db();
} else {
$connection = Db::fromResourceName($targetDbName);
}
$fieldResolver = new BasketSnapshotFieldResolver($objects, $connection);
$objectFromBasket = $objects->$type->$key;
$current = BasketSnapshot::instanceByIdentifier($type, $key, $connection);
if ($current === null) {
$current = '';
} else {
$exported = $current->export();
$fieldResolver->tweakTargetIds($exported);
$current = Json::encode($exported, JSON_PRETTY_PRINT);
}
$this->content()->add(
ConfigDiff::create(
$current,
Json::encode($objectFromBasket, JSON_PRETTY_PRINT)
)->setHtmlRenderer('Inline')
);
}
/**
* @return Basket
* @throws \Icinga\Exception\MissingParameterException
* @throws \Icinga\Exception\NotFoundError
*/
protected function requireBasket()
{
return Basket::load($this->params->getRequired('name'), $this->db());
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace Icinga\Module\Director\Controllers;
use dipl\Html\Html;
use dipl\Html\Link;
use Icinga\Module\Director\Web\Controller\ActionController;
use Icinga\Module\Director\Web\Table\BasketTable;
class BasketsController extends ActionController
{
protected $isApified = true;
public function indexAction()
{
$this->setAutorefreshInterval(10);
$this->addSingleTab($this->translate('Baskets'));
$this->actions()->add([
Link::create(
$this->translate('Create'),
'director/basket/create',
null,
['class' => 'icon-plus']
)
]);
$this->addTitle($this->translate('Configuration Baskets'));
$this->content()->add(Html::tag('p', $this->translate(
'A Configuration Basket references a specific Configuration'
. ' Objects or all objects of a specific type. It has been'
. ' designed to share Templates, Import/Sync strategies and'
. ' other base Configuration Objects. It is not a tool to'
. ' operate with single Hosts or Services.'
)));
$this->content()->add(Html::tag('p', $this->translate(
'You can create Basket snapshots at any time, this will persist'
. ' a serialized representation of all involved objects at that'
. ' moment in time. Snapshots can be exported, imported, shared'
. ' and restored - to the very same or another Director instance.'
)));
$table = (new BasketTable($this->db()))
->setAttribute('data-base-target', '_self');
if ($table->hasSearch() || count($table)) {
$table->renderTo($this);
}
}
}

View File

@ -31,7 +31,7 @@ class DashboardController extends ActionController
$this->setAutorefreshInterval(10);
}
$mainDashboards = ['Objects', 'Alerts', 'Automation', 'Deployment', 'Data'];
$mainDashboards = ['Objects', 'Alerts', 'Automation', 'Deployment', 'Director', 'Data'];
$this->setTitle($this->translate('Icinga Director - Main Dashboard'));
$names = $this->params->getValues('name', $mainDashboards);
if (! $this->params->has('name')) {

View File

@ -2,9 +2,11 @@
namespace Icinga\Module\Director\Controllers;
use dipl\Web\Url;
use Icinga\Data\Filter\Filter;
use Icinga\Data\Filter\FilterChain;
use Icinga\Data\Filter\FilterExpression;
use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
use Icinga\Module\Director\Forms\IcingaAddServiceForm;
use Icinga\Module\Director\Forms\IcingaAddServiceSetForm;
use Icinga\Module\Director\Objects\IcingaHost;
@ -47,6 +49,31 @@ class HostsController extends ObjectsController
));
}
public function edittemplatesAction()
{
parent::editAction();
$objects = $this->loadMultiObjectsFromParams();
$names = [];
/** @var ExportInterface $object */
foreach ($objects as $object) {
$names[] = $object->getUniqueIdentifier();
}
$url = Url::fromPath('director/basket/add', [
'type' => 'HostTemplate',
]);
$url->getParams()->addValues('names', $names);
$this->actions()->add(Link::create(
$this->translate('Add to Basket'),
$url,
null,
['class' => 'icon-tag']
));
}
public function addserviceAction()
{
$this->addSingleTab($this->translate('Add Service'));

View File

@ -2,6 +2,7 @@
namespace Icinga\Module\Director\Controllers;
use Icinga\Exception\NotFoundError;
use Icinga\Module\Director\Forms\ImportRowModifierForm;
use Icinga\Module\Director\Forms\ImportSourceForm;
use Icinga\Module\Director\Web\ActionBar\AutomationObjectActionBar;
@ -13,22 +14,30 @@ use Icinga\Module\Director\Web\Table\ImportsourceHookTable;
use Icinga\Module\Director\Web\Table\PropertymodifierTable;
use Icinga\Module\Director\Web\Tabs\ImportsourceTabs;
use Icinga\Module\Director\Web\Widget\ImportSourceDetails;
use InvalidArgumentException;
use dipl\Html\Link;
class ImportsourceController extends ActionController
{
/** @var ImportSource|null */
private $importSource;
private $id;
/**
* @throws \Icinga\Exception\AuthenticationException
* @throws \Icinga\Exception\Http\HttpNotFoundException
* @throws \Icinga\Exception\NotFoundError
* @throws \Icinga\Security\SecurityException
*/
public function init()
{
parent::init();
$id = $this->params->get('source_id', $this->params->get('id'));
$tabs = $this->tabs(new ImportsourceTabs($id));
if ($id !== null && is_numeric($id)) {
$this->id = (int) $id;
}
$tabs = $this->tabs(new ImportsourceTabs($this->id));
$action = $this->getRequest()->getActionName();
if ($tabs->has($action)) {
$tabs->activate($action);
@ -40,18 +49,27 @@ class ImportsourceController extends ActionController
$this->actions(new AutomationObjectActionBar(
$this->getRequest()
));
$source = $this->getImportSource();
$this->actions()->add(Link::create(
$this->translate('Add to Basket'),
'director/basket/add',
[
'type' => 'ImportSource',
'names' => $source->getUniqueIdentifier()
],
['class' => 'icon-tag']
));
}
/**
* @throws \Icinga\Exception\ConfigurationError
* @throws \Icinga\Exception\IcingaException
* @throws \Icinga\Exception\MissingParameterException
* @throws \Icinga\Exception\NotFoundError
*/
public function indexAction()
{
$this->addMainActions();
$source = ImportSource::load($this->params->getRequired('id'), $this->db());
$source = $this->getImportSource();
if ($this->params->get('format') === 'json') {
$this->sendJson($this->getResponse(), $source->export());
return;
@ -63,9 +81,6 @@ class ImportsourceController extends ActionController
$this->content()->add(new ImportSourceDetails($source));
}
/**
* @throws \Icinga\Exception\ConfigurationError
*/
public function addAction()
{
$this->addTitle($this->translate('Add import source'))
@ -77,16 +92,14 @@ class ImportsourceController extends ActionController
}
/**
* @throws \Icinga\Exception\ConfigurationError
* @throws \Icinga\Exception\MissingParameterException
* @throws NotFoundError
*/
public function editAction()
{
$this->addMainActions();
$this->tabs()->activateMainWithPostfix($this->translate('Modify'));
$id = $this->params->getRequired('id');
$form = ImportSourceForm::load()->setDb($this->db())
->loadObject($id)
$this->activateTabWithPostfix($this->translate('Modify'));
$form = ImportSourceForm::load()
->setObject($this->getImportSource())
->setListUrl('director/importsources')
->handleRequest();
$this->addTitle(
@ -98,16 +111,13 @@ class ImportsourceController extends ActionController
}
/**
* @throws \Icinga\Exception\ConfigurationError
* @throws \Icinga\Exception\MissingParameterException
* @throws \Icinga\Exception\NotFoundError
*/
public function cloneAction()
{
$this->addMainActions();
$this->tabs()->activateMainWithPostfix($this->translate('Clone'));
$id = $this->params->getRequired('id');
$source = ImportSource::load($id, $this->db());
$this->activateTabWithPostfix($this->translate('Clone'));
$source = $this->getImportSource();
$this->addTitle('Clone: %s', $source->get('source_name'));
$form = new CloneImportSourceForm($source);
$this->content()->add($form);
@ -115,13 +125,11 @@ class ImportsourceController extends ActionController
}
/**
* @throws \Icinga\Exception\ConfigurationError
* @throws \Icinga\Exception\MissingParameterException
* @throws \Icinga\Exception\NotFoundError
*/
public function previewAction()
{
$source = ImportSource::load($this->params->getRequired('id'), $this->db());
$source = $this->getImportSource();
$this->addTitle(
$this->translate('Import source preview: %s'),
@ -136,13 +144,11 @@ class ImportsourceController extends ActionController
/**
* @return ImportSource
* @throws \Icinga\Exception\ConfigurationError
* @throws \Icinga\Exception\MissingParameterException
* @throws \Icinga\Exception\NotFoundError
*/
protected function requireImportSourceAndAddModifierTable()
{
$source = ImportSource::load($this->params->getRequired('source_id'), $this->db());
$source = $this->getImportSource();
PropertymodifierTable::load($source, $this->url())
->handleSortPriorityActions($this->getRequest(), $this->getResponse())
->renderTo($this);
@ -151,8 +157,6 @@ class ImportsourceController extends ActionController
}
/**
* @throws \Icinga\Exception\ConfigurationError
* @throws \Icinga\Exception\MissingParameterException
* @throws \Icinga\Exception\NotFoundError
*/
public function modifierAction()
@ -162,19 +166,17 @@ class ImportsourceController extends ActionController
$this->addAddLink(
$this->translate('Add property modifier'),
'director/importsource/addmodifier',
['source_id' => $source->getId()],
['source_id' => $source->get('id')],
'_self'
);
}
/**
* @throws \Icinga\Exception\ConfigurationError
* @throws \Icinga\Exception\MissingParameterException
* @throws \Icinga\Exception\NotFoundError
*/
public function historyAction()
{
$source = ImportSource::load($this->params->getRequired('id'), $this->db());
$source = $this->getImportSource();
$this->addTitle($this->translate('Import run history: %s'), $source->get('source_name'));
// TODO: temporarily disabled, find a better place for stats:
@ -183,9 +185,6 @@ class ImportsourceController extends ActionController
}
/**
* @throws \Icinga\Exception\ConfigurationError
* @throws \Icinga\Exception\Http\HttpNotFoundException
* @throws \Icinga\Exception\MissingParameterException
* @throws \Icinga\Exception\NotFoundError
*/
public function addmodifierAction()
@ -202,14 +201,12 @@ class ImportsourceController extends ActionController
->setSource($source)
->setSuccessUrl(
'director/importsource/modifier',
['source_id' => $source->getId()]
['source_id' => $source->get('id')]
)->handleRequest()
);
}
/**
* @throws \Icinga\Exception\ConfigurationError
* @throws \Icinga\Exception\Http\HttpNotFoundException
* @throws \Icinga\Exception\MissingParameterException
* @throws \Icinga\Exception\NotFoundError
*/
@ -217,7 +214,7 @@ class ImportsourceController extends ActionController
{
// We need to load the table AFTER adding the title, otherwise search
// will not be placed next to the title
$source = ImportSource::load($this->params->getRequired('source_id'), $this->db());
$source = $this->getImportSource();
$this->addTitle(
$this->translate('%s: Property Modifier'),
@ -237,6 +234,34 @@ class ImportsourceController extends ActionController
);
}
/**
* @return ImportSource
* @throws NotFoundError
*/
protected function getImportSource()
{
if ($this->importSource === null) {
if ($this->id === null) {
throw new InvalidArgumentException('Got no ImportSource id');
}
$this->importSource = ImportSource::loadWithAutoIncId(
$this->id,
$this->db()
);
}
return $this->importSource;
}
protected function activateTabWithPostfix($title)
{
/** @var ImportsourceTabs $tabs */
$tabs = $this->tabs();
$tabs->activateMainWithPostfix($title);
return $this;
}
/**
* @param ImportSource $source
* @return $this
@ -247,7 +272,7 @@ class ImportsourceController extends ActionController
Link::create(
$this->translate('back'),
'director/importsource/modifier',
['source_id' => $source->getId()],
['source_id' => $source->get('id')],
['class' => 'icon-left-big']
)
);

View File

@ -2,6 +2,7 @@
namespace Icinga\Module\Director\Controllers;
use dipl\Html\Link;
use Icinga\Module\Director\Forms\DirectorJobForm;
use Icinga\Module\Director\Web\Controller\ActionController;
use Icinga\Module\Director\Objects\DirectorJob;
@ -19,6 +20,7 @@ class JobController extends ActionController
$this
->addJobTabs($job, 'show')
->addTitle($this->translate('Job: %s'), $job->get('job_name'))
->addToBasketLink()
->content()->add(new JobDetails($job));
}
@ -45,12 +47,12 @@ class JobController extends ActionController
$form = DirectorJobForm::load()
->setListUrl('director/jobs')
->setObject($job)
->loadObject($this->params->getRequired('id'))
->handleRequest();
$this
->addJobTabs($job, 'edit')
->addTitle($this->translate('Job: %s'), $job->get('job_name'))
->addToBasketLink()
->content()->add($form);
}
@ -61,7 +63,28 @@ class JobController extends ActionController
*/
protected function requireJob()
{
return DirectorJob::load($this->params->getRequired('id'), $this->db());
return DirectorJob::loadWithAutoIncId((int) $this->params->getRequired('id'), $this->db());
}
/**
* @return $this
* @throws \Icinga\Exception\MissingParameterException
* @throws \Icinga\Exception\NotFoundError
*/
protected function addToBasketLink()
{
$job = $this->requireJob();
$this->actions()->add(Link::create(
$this->translate('Add to Basket'),
'director/basket/add',
[
'type' => 'DirectorJob',
'names' => $job->getUniqueIdentifier()
],
['class' => 'icon-tag']
));
return $this;
}
protected function addJobTabs(DirectorJob $job, $active)

View File

@ -6,7 +6,6 @@ use Icinga\Module\Director\Forms\SyncCheckForm;
use Icinga\Module\Director\Forms\SyncPropertyForm;
use Icinga\Module\Director\Forms\SyncRuleForm;
use Icinga\Module\Director\Forms\SyncRunForm;
use Icinga\Module\Director\Objects\SyncProperty;
use Icinga\Module\Director\Web\Controller\ActionController;
use Icinga\Module\Director\Objects\SyncRule;
use Icinga\Module\Director\Objects\SyncRun;
@ -21,9 +20,6 @@ use dipl\Html\Link;
class SyncruleController extends ActionController
{
/**
* @throws \Icinga\Exception\ConfigurationError
* @throws \Icinga\Exception\Http\HttpNotFoundException
* @throws \Icinga\Exception\IcingaException
* @throws \Icinga\Exception\NotFoundError
*/
public function indexAction()
@ -49,6 +45,15 @@ class SyncruleController extends ActionController
$this->addPropertyHint($rule);
return;
}
$this->actions()->add(Link::create(
$this->translate('Add to Basket'),
'director/basket/add',
[
'type' => 'SyncRule',
'names' => $rule->getUniqueIdentifier()
],
['class' => 'icon-tag']
));
if (! $run) {
$this->warning($this->translate('This Sync Rule has never been run before.'));
@ -106,7 +111,6 @@ class SyncruleController extends ActionController
/**
* @param SyncRule $rule
* @throws \Icinga\Exception\IcingaException
*/
protected function addPropertyHint(SyncRule $rule)
{
@ -122,7 +126,6 @@ class SyncruleController extends ActionController
/**
* @param $msg
* @throws \Icinga\Exception\IcingaException
*/
protected function warning($msg)
{
@ -131,28 +134,17 @@ class SyncruleController extends ActionController
/**
* @param $msg
* @throws \Icinga\Exception\IcingaException
*/
protected function error($msg)
{
$this->content()->add(Html::tag('p', ['class' => 'error'], $msg));
}
/**
* @throws \Icinga\Exception\ConfigurationError
* @throws \Icinga\Exception\Http\HttpNotFoundException
* @throws \Icinga\Exception\IcingaException
*/
public function addAction()
{
$this->editAction();
}
/**
* @throws \Icinga\Exception\ConfigurationError
* @throws \Icinga\Exception\Http\HttpNotFoundException
* @throws \Icinga\Exception\IcingaException
*/
public function editAction()
{
$form = SyncRuleForm::load()
@ -160,7 +152,7 @@ class SyncruleController extends ActionController
->setDb($this->db());
if ($id = $this->params->get('id')) {
$form->loadObject($id);
$form->loadObject((int) $id);
/** @var SyncRule $rule */
$rule = $form->getObject();
$this->tabs(new SyncRuleTabs($rule))->activate('edit');
@ -176,6 +168,15 @@ class SyncruleController extends ActionController
['class' => 'icon-paste']
)
);
$this->actions()->add(Link::create(
$this->translate('Add to Basket'),
'director/basket/add',
[
'type' => 'SyncRule',
'names' => $rule->getUniqueIdentifier()
],
['class' => 'icon-tag']
));
if (! $rule->hasSyncProperties()) {
$this->addPropertyHint($rule);
@ -190,16 +191,13 @@ class SyncruleController extends ActionController
}
/**
* @throws \Icinga\Exception\ConfigurationError
* @throws \Icinga\Exception\Http\HttpNotFoundException
* @throws \Icinga\Exception\MissingParameterException
* @throws \Icinga\Exception\NotFoundError
* @throws \Icinga\Exception\ProgrammingError
*/
public function cloneAction()
{
$id = $this->params->getRequired('id');
$rule = SyncRule::load($id, $this->db());
$rule = SyncRule::loadWithAutoIncId((int) $id, $this->db());
$this->tabs()->add('show', [
'url' => 'director/syncrule',
'urlParams' => ['id' => $id],
@ -225,8 +223,7 @@ class SyncruleController extends ActionController
}
/**
* @throws \Icinga\Exception\Http\HttpNotFoundException
* @throws \Icinga\Exception\IcingaException
* @throws \Icinga\Exception\NotFoundError
*/
public function propertyAction()
{
@ -247,9 +244,7 @@ class SyncruleController extends ActionController
}
/**
* @throws \Icinga\Exception\ConfigurationError
* @throws \Icinga\Exception\Http\HttpNotFoundException
* @throws \Icinga\Exception\IcingaException
* @throws \Icinga\Exception\NotFoundError
*/
public function editpropertyAction()
{
@ -257,9 +252,7 @@ class SyncruleController extends ActionController
}
/**
* @throws \Icinga\Exception\ConfigurationError
* @throws \Icinga\Exception\Http\HttpNotFoundException
* @throws \Icinga\Exception\IcingaException
* @throws \Icinga\Exception\NotFoundError
*/
public function addpropertyAction()
{
@ -269,7 +262,7 @@ class SyncruleController extends ActionController
$form = SyncPropertyForm::load()->setDb($db);
if ($id = $this->params->get('id')) {
$form->loadObject($id);
$form->loadObject((int) $id);
$this->addTitle(
$this->translate('Sync "%s": %s'),
$form->getObject()->get('destination_field'),
@ -299,9 +292,6 @@ class SyncruleController extends ActionController
}
/**
* @throws \Icinga\Exception\ConfigurationError
* @throws \Icinga\Exception\Http\HttpNotFoundException
* @throws \Icinga\Exception\IcingaException
* @throws \Icinga\Exception\NotFoundError
*/
public function historyAction()
@ -321,13 +311,11 @@ class SyncruleController extends ActionController
/**
* @param string $key
* @return SyncRule
* @throws \Icinga\Exception\ConfigurationError
* @throws \Icinga\Exception\IcingaException
* @throws \Icinga\Exception\NotFoundError
*/
protected function requireSyncRule($key = 'id')
{
$id = $this->params->get($key);
return SyncRule::load($id, $this->db());
return SyncRule::loadWithAutoIncId($id, $this->db());
}
}

View File

@ -0,0 +1,131 @@
<?php
namespace Icinga\Module\Director\Forms;
use dipl\Html\Html;
use dipl\Html\HtmlDocument;
use dipl\Html\Link;
use Icinga\Exception\NotFoundError;
use Icinga\Module\Director\DirectorObject\Automation\Basket;
use Icinga\Module\Director\Web\Form\DirectorForm;
class AddToBasketForm extends DirectorForm
{
/** @var Basket */
private $basket;
private $type = '(has not been set)';
private $names = [];
/**
* @throws \Zend_Form_Exception
* @throws \Icinga\Exception\NotFoundError
*/
public function setup()
{
$baskets = Basket::loadAll($this->getDb());
$enum = [];
foreach ($baskets as $basket) {
$enum[$basket->get('basket_name')] = $basket->get('basket_name');
}
$names = [];
$basket = null;
if ($this->hasBeenSent()) {
$basketName = $this->getSentValue('basket');
if ($basketName) {
$basket = Basket::load($basketName, $this->getDb());
}
}
$count = 0;
$type = $this->type;
foreach ($this->names as $name) {
if (! empty($names)) {
$names[] = ', ';
}
if ($basket && $basket->hasObject($type, $name)) {
$names[] = Html::tag('span', [
'style' => 'text-decoration: line-through'
], $name);
} else {
$count++;
$names[] = $name;
}
}
$this->addHtmlHint((new HtmlDocument())->add([
'The following objects will be added: ',
$names
]));
$this->addElement('select', 'basket', [
'label' => $this->translate('Basket'),
'multiOptions' => $this->optionalEnum($enum),
'required' => true,
'class' => 'autosubmit',
]);
if ($count > 0) {
$this->setSubmitLabel(sprintf(
$this->translate('Add %s objects'),
$count
));
} else {
$this->setSubmitLabel($this->translate('Add'));
$this->addSubmitButtonIfSet();
$this->getElement($this->submitButtonName)->setAttrib('disabled', true);
}
}
public function setType($type)
{
$this->type = $type;
return $this;
}
public function setNames($names)
{
$this->names = $names;
return $this;
}
/**
* @throws \Icinga\Exception\NotFoundError
* @throws \Icinga\Module\Director\Exception\DuplicateKeyException
*/
public function onSuccess()
{
$type = $this->type;
$basket = Basket::load($this->getValue('basket'), $this->getDb());
$basketName = $basket->get('basket_name');
if (empty($this->names)) {
$this->getElement('basket')->addErrorMessage($this->translate(
'No object has been chosen'
));
}
if ($basket->supportsCustomSelectionFor($type)) {
$basket->addObjects($type, $this->names);
$basket->store();
$this->setSuccessMessage(sprintf($this->translate(
'Configuration objects have been added to the chosen basket "%s"'
), $basketName));
return parent::onSuccess();
} else {
$this->addHtmlHint(Html::tag('p', [
'class' => 'error'
], Html::sprintf($this->translate(
'Please check your Basket configuration, %s does not support'
. ' single "%s" configuration objects'
), Link::create(
$basketName,
'director/basket',
['name' => $basketName],
['data-base-target' => '_next']
), $type)));
return false;
}
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Icinga\Module\Director\Forms;
use Icinga\Module\Director\DirectorObject\Automation\Basket;
use Icinga\Module\Director\DirectorObject\Automation\BasketSnapshot;
use Icinga\Module\Director\Web\Form\DirectorForm;
class BasketCreateSnapshotForm extends DirectorForm
{
/** @var Basket */
private $basket;
public function setBasket(Basket $basket)
{
$this->basket = $basket;
return $this;
}
public function setup()
{
$this->setSubmitLabel($this->translate('Create Snapshot'));
}
/**
* @throws \Icinga\Module\Director\Exception\DuplicateKeyException
*/
public function onSuccess()
{
/** @var \Icinga\Module\Director\Db $connection */
$connection = $this->basket->getConnection();
$snapshot = BasketSnapshot::createForBasket($this->basket, $connection);
$snapshot->store();
parent::onSuccess();
}
}

View File

@ -0,0 +1,139 @@
<?php
namespace Icinga\Module\Director\Forms;
use Icinga\Module\Director\Data\Db\DbObject;
use Icinga\Module\Director\DirectorObject\Automation\Basket;
use Icinga\Module\Director\Web\Form\DirectorObjectForm;
use Zend_Form_SubForm as ZfSubForm;
class BasketForm extends DirectorObjectForm
{
protected $listUrl = 'director/baskets';
protected function getAvailableTypes()
{
return [
'Command' => $this->translate('Command Definitions'),
'HostGroup' => $this->translate('Host Group'),
'IcingaTemplateChoiceHost' => $this->translate('Host Template Choice'),
'HostTemplate' => $this->translate('Host Templates'),
'ServiceGroup' => $this->translate('Service Groups'),
'IcingaTemplateChoiceService' => $this->translate('Service Template Choice'),
'ServiceTemplate' => $this->translate('Service Templates'),
'ServiceSet' => $this->translate('Service Sets'),
'Notification' => $this->translate('Notifications'),
'Dependency' => $this->translate('Dependencies'),
'ImportSource' => $this->translate('Import Sources'),
'SyncRule' => $this->translate('Sync Rules'),
'DirectorJob' => $this->translate('Job Definitions'),
'Basket' => $this->translate('Basket Definitions'),
];
}
/**
* @throws \Zend_Form_Exception
*/
public function setup()
{
$this->addElement('text', 'basket_name', [
'label' => $this->translate('Basket Name'),
'required' => true,
]);
$types = $this->getAvailableTypes();
$options = [
'IGNORE' => $this->translate('Ignore'),
'ALL' => $this->translate('All of them'),
'[]' => $this->translate('Custom Selection'),
];
$this->addHtmlHint($this->translate(
'What should we place into this Basket every time we create'
. ' new snapshot?'
));
$sub = new ZfSubForm();
$sub->setDecorators([
['HtmlTag', ['tag' => 'dl']],
'FormElements'
]);
foreach ($types as $name => $label) {
$sub->addElement('select', $name, [
'label' => $label,
'multiOptions' => $options,
]);
}
$this->addSubForm($sub, 'objects');
$this->addDeleteButton();
$this->addHtmlHint($this->translate(
'Choose "All" to always add all of them,'
. ' "Ignore" to not care about a specific Type at all and'
. ' opt for "Custom Selection" in case you want to choose'
. ' just some specific Objects.'
));
}
protected function setDefaultsFromObject(DbObject $object)
{
parent::setDefaultsFromObject($object);
/** @var Basket $object */
$values = [];
foreach ($this->getAvailableTypes() as $type => $label) {
$values[$type] = 'IGNORE';
}
foreach ($object->getChosenObjects() as $type => $selection) {
if ($selection === true) {
$values[$type] = 'ALL';
} elseif (is_array($selection)) {
$values[$type] = '[]';
}
}
$this->populate([
'objects' => $values
]);
}
protected function onRequest()
{
parent::onRequest(); // TODO: Change the autogenerated stub
}
protected function getObjectClassname()
{
return '\\Icinga\\Module\\Director\\DirectorObject\\Automation\\Basket';
}
public function onSuccess()
{
/** @var Basket $basket */
$basket = $this->object();
if ($basket->isEmpty()) {
$this->addError($this->translate("It's not allowed to store an empty basket"));
return;
}
if (! $basket->hasBeenLoadedFromDb()) {
$basket->set('owner_type', 'user');
$basket->set('owner_value', $this->getAuth()->getUser()->getUsername());
}
parent::onSuccess();
}
protected function setObjectSuccessUrl()
{
/** @var Basket $basket */
$basket = $this->object();
$this->setSuccessUrl(
'director/basket',
['name' => $basket->get('basket_name')]
);
}
}

View File

@ -0,0 +1,79 @@
<?php
namespace Icinga\Module\Director\Forms;
use Icinga\Application\Config;
use Icinga\Authentication\Auth;
use Icinga\Module\Director\Db;
use Icinga\Module\Director\DirectorObject\Automation\BasketSnapshot;
use Icinga\Module\Director\Web\Controller\Extension\DirectorDb;
use Icinga\Module\Director\Web\Form\QuickForm;
class RestoreBasketForm extends QuickForm
{
use DirectorDb;
/** @var BasketSnapshot */
private $snapshot;
public function setSnapshot(BasketSnapshot $snapshot)
{
$this->snapshot = $snapshot;
return $this;
}
/**
* @codingStandardsIgnoreStart
* @return Auth
*/
protected function Auth()
{
return Auth::getInstance();
}
/**
* @return Config
*/
protected function Config()
{
// @codingStandardsIgnoreEnd
return Config::module('director');
}
/**
* @throws \Zend_Form_Exception
*/
public function setup()
{
$allowedDbs = $this->listAllowedDbResourceNames();
if (count($allowedDbs) > 1) {
$this->addElement('select', 'target_db', [
'label' => $this->translate('Target DB'),
'description' => $this->translate('Restore to this target Director DB'),
'multiOptions' => $allowedDbs,
'value' => $this->getRequest()->getParam('target_db', $this->getFirstDbResourceName()),
'class' => 'autosubmit',
]);
}
$this->setSubmitLabel($this->translate('Restore'));
}
public function getDb()
{
return Db::fromResourceName($this->getValue('target_db'));
}
/**
* @throws \Icinga\Exception\NotFoundError
*/
public function onSuccess()
{
$this->snapshot->restoreTo($this->getDb());
$this->setSuccessUrl($this->getSuccessUrl()->with('target_db', $this->getValue('target_db')));
$this->setSuccessMessage(sprintf('Restored to %s', $this->getValue('target_db')));
parent::onSuccess();
}
}

View File

@ -368,10 +368,11 @@ class SyncPropertyForm extends DirectorObjectForm
{
if ($this->importSource === null) {
if ($this->hasObject()) {
$this->importSource = ImportSource::load($this->object->get('source_id'), $this->db);
$id = (int) $this->object->get('source_id');
} else {
$this->importSource = ImportSource::load($this->getSentValue('source_id'), $this->db);
$id = (int) $this->getSentValue('source_id');
}
$this->importSource = ImportSource::loadWithAutoIncId($id, $this->db);
}
return $this->importSource;

View File

@ -342,6 +342,34 @@ specific type:
This feature is available since v1.5.0.
Director Configuration Basket
-----------------------------
A basket contains a set of Director Configuration objects (like Templates,
Commands, Import/Sync definitions - but not single Hosts or Services). This
CLI command allows you to integrate them into your very own workflows
## Available Actions
| Action | Description |
|------------|---------------------------------------------------|
| `dump` | JSON-dump for objects related to the given Basket |
| `list` | List configured Baskets |
| `restore` | Restore a Basket from JSON dump provided on STDIN |
| `snapshot` | Take a snapshot for the given Basket |
### Options
| Option | Description |
|----------|------------------------------------------------------|
| `--name` | `dump` and `snapshot` require a specific object name |
Use `icingacli director basket restore < exported-basket.json` to restore objects
from a specific basket. Take a snapshot or a backup first to be on the safe side.
This feature is available since v1.6.0.
Health Check Plugin
-------------------

View File

@ -0,0 +1,30 @@
<?php
namespace Icinga\Module\Director\Dashboard\Dashlet;
class BasketDashlet extends Dashlet
{
protected $icon = 'tag';
public function getTitle()
{
return $this->translate('Configuration Baskets');
}
public function getSummary()
{
return $this->translate(
'Preserve specific configuration objects in a specific state'
);
}
public function getUrl()
{
return 'director/baskets';
}
public function listRequiredPermissions()
{
return array('director/admin');
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace Icinga\Module\Director\Dashboard;
class DirectorDashboard extends Dashboard
{
protected $dashletNames = array(
'Settings',
'Basket',
'SelfService',
);
public function getTitle()
{
return $this->translate('Icinga Director Configuration');
}
}

View File

@ -8,11 +8,9 @@ class InfrastructureDashboard extends Dashboard
{
protected $dashletNames = array(
'Kickstart',
'SelfService',
'ApiUserObject',
'EndpointObject',
'ZoneObject',
'Settings',
);
public function getTitle()

View File

@ -69,6 +69,9 @@ abstract class DbObject
*/
protected $autoincKeyName;
/** @var bool forbid updates to autoinc values */
protected $protectAutoinc = true;
/**
* Filled with object instances when prefetchAll is used
*/
@ -448,7 +451,11 @@ abstract class DbObject
$props = array();
foreach (array_keys($this->modifiedProperties) as $key) {
if ($key === $this->autoincKeyName) {
continue;
if ($this->protectAutoinc) {
continue;
} elseif ($this->properties[$key] === null) {
continue;
}
}
$props[$key] = $this->properties[$key];
@ -728,7 +735,9 @@ abstract class DbObject
{
$properties = $this->getPropertiesForDb();
if ($this->autoincKeyName !== null) {
unset($properties[$this->autoincKeyName]);
if ($this->protectAutoinc || $properties[$this->autoincKeyName] === null) {
unset($properties[$this->autoincKeyName]);
}
}
// TODO: Remove this!
if ($this->connection->isPgsql()) {
@ -785,15 +794,20 @@ abstract class DbObject
}
} else {
if ($id && $this->existsInDb()) {
$logId = '"' . $this->getLogId() . '"';
if ($autoId = $this->getAutoincId()) {
$logId .= sprintf(', %s=%s', $this->autoincKeyName, $autoId);
}
throw new DuplicateKeyException(
'Trying to recreate %s (%s)',
$table,
$this->getLogId()
$logId
);
}
if ($this->insertIntoDb()) {
if ($this->autoincKeyName) {
if ($this->autoincKeyName && $this->getProperty($this->autoincKeyName) === null) {
if ($this->connection->isPgsql()) {
$this->properties[$this->autoincKeyName] = $this->db->lastInsertId(
$table,
@ -885,6 +899,12 @@ abstract class DbObject
public function createWhere()
{
if ($id = $this->getAutoincId()) {
if ($originalId = $this->getOriginalProperty($this->autoincKeyName)) {
return $this->db->quoteInto(
sprintf('%s = ?', $this->autoincKeyName),
$originalId
);
}
return $this->db->quoteInto(
sprintf('%s = ?', $this->autoincKeyName),
$id

View File

@ -51,6 +51,8 @@ abstract class DbObjectWithSettings extends DbObject
public function getSettings()
{
// Sort them, important only for new objects
ksort($this->settings);
return $this->settings;
}

View File

@ -3,8 +3,8 @@
namespace Icinga\Module\Director\Db;
use Exception;
use Icinga\Exception\IcingaException;
use Icinga\Module\Director\Data\Db\DbConnection;
use RuntimeException;
class Migration
{
@ -27,21 +27,25 @@ class Migration
/**
* @param DbConnection $connection
* @return $this
* @throws IcingaException
*/
public function apply(DbConnection $connection)
{
/** @var \Zend_Db_Adapter_Pdo_Abstract $db */
$db = $connection->getDbAdapter();
// TODO: this is fagile and depends on accordingly written schema files:
$queries = preg_split('/[\n\s\t]*\;[\n\s\t]+/s', $this->sql, -1, PREG_SPLIT_NO_EMPTY);
// TODO: this is fragile and depends on accordingly written schema files:
$queries = preg_split(
'/[\n\s\t]*\;[\n\s\t]+/s',
$this->sql,
-1,
PREG_SPLIT_NO_EMPTY
);
if (empty($queries)) {
throw new IcingaException(
throw new RuntimeException(sprintf(
'Migration %d has no queries',
$this->version
);
));
}
try {
@ -53,12 +57,12 @@ class Migration
}
}
} catch (Exception $e) {
throw new IcingaException(
throw new RuntimeException(sprintf(
'Migration %d failed (%s) while running %s',
$this->version,
$e->getMessage(),
$query
);
));
}
return $this;

View File

@ -86,7 +86,6 @@ class Migrations
/**
* @return $this
* @throws \Icinga\Exception\IcingaException
*/
public function applyPendingMigrations()
{

View File

@ -0,0 +1,185 @@
<?php
namespace Icinga\Module\Director\DirectorObject\Automation;
use Icinga\Module\Director\Core\Json;
use Icinga\Module\Director\Data\Db\DbObject;
use Icinga\Module\Director\Db;
/**
* Class Basket
*
* TODO
* - create a UUID like in RFC4122
*/
class Basket extends DbObject
{
const SELECTION_ALL = true;
const SELECTION_NONE = false;
protected $validTypes = [
'host_template',
'host_object',
'service_template',
'service_object',
'service_apply',
'import_source',
'sync_rule'
];
protected $table = 'director_basket';
protected $keyName = 'basket_name';
protected $chosenObjects = [];
protected $defaultProperties = [
'uuid' => null,
'basket_name' => null,
'objects' => null,
'owner_type' => null,
'owner_value' => null,
];
public function getHexUuid()
{
return bin2hex($this->get('uuid'));
}
public function listObjectTypes()
{
return array_keys($this->objects);
}
public function getChosenObjects()
{
return $this->chosenObjects;
}
public function isEmpty()
{
return count($this->getChosenObjects()) === 0;
}
protected function onLoadFromDb()
{
$this->chosenObjects = (array) Json::decode($this->get('objects'));
}
public function supportsCustomSelectionFor($type)
{
if (! array_key_exists($type, $this->chosenObjects)) {
return false;
}
return is_array($this->chosenObjects[$type]);
}
public function setObjects($objects)
{
if (empty($objects)) {
$this->chosenObjects = [];
} else {
$this->chosenObjects = [];
foreach ((array) $objects as $type => $object) {
$this->addObjects($type, $object);
}
}
return $this;
}
/**
* This is a weird method, as it is required to deal with raw form data
*
* @param $type
* @param ExportInterface[]|bool $objects
*/
public function addObjects($type, $objects = true)
{
BasketSnapshot::assertValidType($type);
// '1' -> from Form!
if ($objects === 'ALL') {
$objects = true;
} elseif ($objects === null || $objects === 'IGNORE') {
return;
} elseif ($objects === '[]') {
if (isset($this->chosenObjects[$type])) {
if (! is_array($this->chosenObjects[$type])) {
$this->chosenObjects[$type] = [];
}
} else {
$this->chosenObjects[$type] = [];
}
$objects = [];
}
if ($objects === true) {
$this->chosenObjects[$type] = true;
} elseif ($objects === '0') {
// nothing
} else {
foreach ($objects as $object) {
$this->addObject($type, $object);
}
if (array_key_exists($type, $this->chosenObjects)) {
ksort($this->chosenObjects[$type]);
}
}
$this->reallySet('objects', Json::encode($this->chosenObjects));
}
public function hasObject($type, $object)
{
if (! $this->hasType($type)) {
return false;
}
if ($this->chosenObjects[$type] === true) {
return true;
}
if ($object instanceof ExportInterface) {
$object = $object->getUniqueIdentifier();
}
if (is_array($this->chosenObjects[$type])) {
return in_array($object, $this->chosenObjects[$type]);
} else {
return false;
}
}
/**
* @param $type
* @param string $object
*/
public function addObject($type, $object)
{
if (is_array($this->chosenObjects[$type])) {
$this->chosenObjects[$type][] = $object;
} else {
throw new \InvalidArgumentException(sprintf(
'The Basket "%s" has not been configured for single objects of type "%s"',
$this->get('basket_name'),
$type
));
}
}
public function hasType($type)
{
return isset($this->chosenObjects[$type]);
}
protected function beforeStore()
{
if (! $this->hasBeenLoadedFromDb()) {
// TODO: This is BS, use a real UUID
$this->set('uuid', hex2bin(substr(sha1(microtime(true) . rand(1, 100000)), 0, 32)));
}
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace Icinga\Module\Director\DirectorObject\Automation;
use Icinga\Module\Director\Data\Db\DbObject;
class BasketContent extends DbObject
{
protected $objects;
protected $table = 'director_basket_content';
protected $keyName = 'checksum';
protected $defaultProperties = [
'checksum' => null,
'summary' => null,
'content' => null,
];
}

View File

@ -0,0 +1,396 @@
<?php
namespace Icinga\Module\Director\DirectorObject\Automation;
use Icinga\Module\Director\Core\Json;
use Icinga\Module\Director\Db;
use Icinga\Module\Director\Data\Db\DbObject;
use Icinga\Module\Director\Objects\DirectorDatafield;
use Icinga\Module\Director\Objects\IcingaCommand;
use Icinga\Module\Director\Objects\IcingaObject;
use InvalidArgumentException;
use RuntimeException;
class BasketSnapshot extends DbObject
{
protected static $typeClasses = [
'Datafield' => '\\Icinga\\Module\\Director\\Objects\\DirectorDatafield',
'Command' => '\\Icinga\\Module\\Director\\Objects\\IcingaCommand',
'HostGroup' => '\\Icinga\\Module\\Director\\Objects\\IcingaHostGroup',
'IcingaTemplateChoiceHost' => '\\Icinga\\Module\\Director\\Objects\\IcingaTemplateChoiceHost',
'HostTemplate' => '\\Icinga\\Module\\Director\\Objects\\IcingaHost',
'ServiceGroup' => '\\Icinga\\Module\\Director\\Objects\\IcingaServiceGroup',
'IcingaTemplateChoiceService' => '\\Icinga\\Module\\Director\\Objects\\IcingaTemplateChoiceService',
'ServiceTemplate' => '\\Icinga\\Module\\Director\\Objects\\IcingaService',
'ServiceSet' => '\\Icinga\\Module\\Director\\Objects\\IcingaServiceSet',
'Notification' => '\\Icinga\\Module\\Director\\Objects\\IcingaNotification',
'Dependency' => '\\Icinga\\Module\\Director\\Objects\\IcingaDependency',
'ImportSource' => '\\Icinga\\Module\\Director\\Objects\\ImportSource',
'SyncRule' => '\\Icinga\\Module\\Director\\Objects\\SyncRule',
'DirectorJob' => '\\Icinga\\Module\\Director\\Objects\\DirectorJob',
'Basket' => '\\Icinga\\Module\\Director\\DirectorObject\\Automation\\Automation',
];
protected $objects = [];
protected $content;
protected $table = 'director_basket_snapshot';
protected $keyName = [
'basket_uuid',
'ts_create',
];
protected $restoreOrder = [
'Command',
'HostGroup',
'IcingaTemplateChoiceHost',
'HostTemplate',
'ServiceGroup',
'IcingaTemplateChoiceService',
'ServiceTemplate',
'ServiceSet',
'Notification',
'Dependency',
'ImportSource',
'SyncRule',
'DirectorJob',
'Basket',
];
protected $defaultProperties = [
'basket_uuid' => null,
'content_checksum' => null,
'ts_create' => null,
];
public static function supports($type)
{
return isset(self::$typeClasses[$type]);
}
public static function assertValidType($type)
{
if (! static::supports($type)) {
throw new InvalidArgumentException("Basket does not support '$type'");
}
}
public static function getClassForType($type)
{
static::assertValidType($type);
return self::$typeClasses[$type];
}
/**
* @param Basket $basket
* @param Db $db
* @return BasketSnapshot
* @throws \Icinga\Exception\NotFoundError
*/
public static function createForBasket(Basket $basket, Db $db)
{
$snapshot = static::create([
'basket_uuid' => $basket->get('uuid')
], $db);
$snapshot->addObjectsChosenByBasket($basket);
$snapshot->resolveRequiredFields();
return $snapshot;
}
/**
* @throws \Icinga\Exception\NotFoundError
*/
protected function resolveRequiredFields()
{
/** @var Db $db */
$db = $this->getConnection();
$fieldResolver = new BasketSnapshotFieldResolver($this->objects, $db);
/** @var DirectorDatafield[] $fields */
$fields = $fieldResolver->loadCurrentFields($db);
if (! empty($fields)) {
$plain = [];
foreach ($fields as $id => $field) {
$plain[$id] = $field->export();
}
$this->objects['Datafield'] = $plain;
}
}
protected function addObjectsChosenByBasket(Basket $basket)
{
foreach ($basket->getChosenObjects() as $typeName => $selection) {
if ($selection === true) {
$this->addAll($typeName);
} elseif (! empty($selection)) {
$this->addByIdentifiers($typeName, $selection);
}
}
}
/**
* @throws \Icinga\Module\Director\Exception\DuplicateKeyException
* @throws \Icinga\Exception\NotFoundError
*/
protected function beforeStore()
{
if ($this->hasBeenLoadedFromDb()) {
throw new RuntimeException('A basket snapshot cannot be modified');
}
$json = $this->getJsonDump();
$checksum = sha1($json, true);
if (! BasketContent::exists($checksum, $this->getConnection())) {
BasketContent::create([
'checksum' => $checksum,
'summary' => $this->getJsonSummary(),
'content' => $json,
], $this->getConnection())->store();
}
$this->set('content_checksum', $checksum);
$this->set('ts_create', round(microtime(true) * 1000));
}
/**
* @param Db $connection
* @param bool $replace
* @throws \Icinga\Exception\NotFoundError
*/
public function restoreTo(Db $connection, $replace = true)
{
static::restoreJson(
$this->getJsonDump(),
$connection,
$replace
);
}
public static function restoreJson($string, Db $connection, $replace = true)
{
$snapshot = new static();
$snapshot->restoreObjects(
Json::decode($string),
$connection,
$replace
);
}
/**
* @param $all
* @param Db $connection
* @param bool $replace
* @throws \Icinga\Module\Director\Exception\DuplicateKeyException
* @throws \Zend_Db_Adapter_Exception
* @throws \Icinga\Exception\NotFoundError
*/
protected function restoreObjects($all, Db $connection, $replace = true)
{
$db = $connection->getDbAdapter();
$db->beginTransaction();
$fieldResolver = new BasketSnapshotFieldResolver($all, $connection);
$fieldResolver->storeNewFields();
foreach ($this->restoreOrder as $typeName) {
if (isset($all->$typeName)) {
$objects = $all->$typeName;
$class = static::getClassForType($typeName);
$changed = [];
foreach ($objects as $key => $object) {
/** @var DbObject $new */
$new = $class::import($object, $connection, $replace);
if ($new->hasBeenModified()) {
if ($new instanceof IcingaObject && $new->supportsImports()) {
/** @var ExportInterface $new */
$changed[$new->getUniqueIdentifier()] = $new;
} else {
$new->store();
// Linking fields right now, as we're not in $changed
if ($new instanceof IcingaObject) {
$fieldResolver->relinkObjectFields($new, $object);
}
}
} else {
// No modification on the object, still, fields might have
// been changed
if ($new instanceof IcingaObject) {
$fieldResolver->relinkObjectFields($new, $object);
}
}
$allObjects[spl_object_hash($new)] = $object;
}
/** @var IcingaObject $object */
foreach ($changed as $object) {
$this->recursivelyStore($object, $changed);
}
foreach ($changed as $key => $new) {
// Store related fields. As objects might have formerly been
// unstored, let's to it right here
if ($new instanceof IcingaObject) {
$fieldResolver->relinkObjectFields($new, $objects[$key]);
}
}
}
}
$db->commit();
}
/**
* @param IcingaObject $object
* @param $list
* @throws \Icinga\Module\Director\Exception\DuplicateKeyException
*/
protected function recursivelyStore(IcingaObject $object, & $list)
{
foreach ($object->listImportNames() as $parent) {
if (array_key_exists($parent, $list)) {
$this->recursivelyStore($list[$parent], $list);
}
}
$object->store();
}
/**
* @return BasketContent
* @throws \Icinga\Exception\NotFoundError
*/
protected function getContent()
{
if ($this->content === null) {
$this->content = BasketContent::load($this->get('content_checksum'), $this->getConnection());
}
return $this->content;
}
protected function onDelete()
{
$db = $this->getDb();
$db->delete(
['bc' => 'director_basket_content'],
'NOT EXISTS (SELECT director_basket_checksum WHERE content_checksum = bc.checksum)'
);
}
/**
* @return string
* @throws \Icinga\Exception\NotFoundError
*/
public function getJsonSummary()
{
if ($this->hasBeenLoadedFromDb()) {
return $this->getContent()->get('summary');
} else {
return Json::encode($this->getSummary(), JSON_PRETTY_PRINT);
}
}
/**
* @return array|mixed
* @throws \Icinga\Exception\NotFoundError
*/
public function getSummary()
{
if ($this->hasBeenLoadedFromDb()) {
return Json::decode($this->getContent()->get('summary'));
} else {
$summary = [];
foreach (array_keys($this->objects) as $key) {
$summary[$key] = count($this->objects[$key]);
}
return $summary;
}
}
/**
* @return string
* @throws \Icinga\Exception\NotFoundError
*/
public function getJsonDump()
{
if ($this->hasBeenLoadedFromDb()) {
return $this->getContent()->get('content');
} else {
return Json::encode($this->objects, JSON_PRETTY_PRINT);
}
}
protected function addAll($typeName)
{
$class = static::getClassForType($typeName);
/** @var IcingaObject $dummy */
$dummy = $class::create();
/** @var ExportInterface $object */
if ($dummy instanceof IcingaObject && $dummy->supportsImports()) {
$db = $this->getDb();
if ($dummy instanceof IcingaCommand) {
$select = $db->select()->from($dummy->getTableName())
->where('object_type != ?', 'external_object');
} elseif (! $dummy->isGroup()) {
$select = $db->select()->from($dummy->getTableName())
->where('object_type = ?', 'template');
} else {
$select = $db->select()->from($dummy->getTableName());
}
$all = $class::loadAll($this->getConnection(), $select);
} else {
$all = $class::loadAll($this->getConnection());
}
foreach ($all as $object) {
$this->objects[$typeName][$object->getUniqueIdentifier()] = $object->export();
}
}
protected function addByIdentifiers($typeName, $identifiers)
{
foreach ($identifiers as $identifier) {
$this->addByIdentifier($typeName, $identifier);
}
}
/**
* @param $typeName
* @param $identifier
* @param Db $connection
* @return ExportInterface|null
*/
public static function instanceByIdentifier($typeName, $identifier, Db $connection)
{
$class = static::getClassForType($typeName);
if (substr($class, -13) === 'IcingaService') {
$identifier = [
'object_type' => 'template',
'object_name' => $identifier,
];
}
/** @var ExportInterface $object */
if ($class::exists($identifier, $connection)) {
$object = $class::load($identifier, $connection);
} else {
$object = null;
}
return $object;
}
/**
* @param $typeName
* @param $identifier
*/
protected function addByIdentifier($typeName, $identifier)
{
/** @var Db $connection */
$connection = $this->getConnection();
$object = static::instanceByIdentifier(
$typeName,
$identifier,
$connection
);
$this->objects[$typeName][$identifier] = $object->export();
}
}

View File

@ -0,0 +1,224 @@
<?php
namespace Icinga\Module\Director\DirectorObject\Automation;
use Icinga\Module\Director\Db;
use Icinga\Module\Director\Objects\DirectorDatafield;
use Icinga\Module\Director\Objects\IcingaObject;
class BasketSnapshotFieldResolver
{
/** @var BasketSnapshot */
protected $snapshot;
/** @var \Icinga\Module\Director\Data\Db\DbConnection */
protected $targetDb;
/** @var array|null */
protected $requiredIds;
protected $objects;
/** @var int */
protected $nextNewId = 1;
/** @var array|null */
protected $idMap;
/** @var DirectorDatafield[]|null */
protected $targetFields;
public function __construct($objects, Db $targetDb)
{
$this->objects = $objects;
$this->targetDb = $targetDb;
}
/**
* @param Db $db
* @return DirectorDatafield[]
* @throws \Icinga\Exception\NotFoundError
*/
public function loadCurrentFields(Db $db)
{
$fields = [];
foreach ($this->getRequiredIds() as $id) {
$fields[$id] = DirectorDatafield::loadWithAutoIncId((int) $id, $db);
}
return $fields;
}
/**
* @throws \Icinga\Exception\NotFoundError
* @throws \Icinga\Module\Director\Exception\DuplicateKeyException
*/
public function storeNewFields()
{
foreach ($this->getTargetFields() as $id => $field) {
if ($field->hasBeenModified()) {
$field->store();
$this->idMap[$id] = $field->get('id');
}
}
}
/**
* @param IcingaObject $new
* @param $object
* @throws \Icinga\Exception\NotFoundError
* @throws \Zend_Db_Adapter_Exception
*/
public function relinkObjectFields(IcingaObject $new, $object)
{
if (! $new->supportsFields() || ! isset($object->fields)) {
return;
}
$fieldMap = $this->getIdMap();
$objectId = (int) $new->get('id');
$table = $new->getTableName() . '_field';
$objectKey = $new->getShortTableName() . '_id';
$existingFields = [];
$db = $this->targetDb->getDbAdapter();
foreach ($db->fetchAll(
$db->select()->from($table)->where("$objectKey = ?", $objectId)
) as $mapping) {
$existingFields[(int) $mapping->datafield_id] = $mapping;
}
foreach ($object->fields as $field) {
$id = $fieldMap[(int) $field->datafield_id];
if (isset($existingFields[$id])) {
unset($existingFields[$id]);
} else {
$db->insert($table, [
$objectKey => $objectId,
'datafield_id' => $id,
'is_required' => $field->is_required,
'var_filter' => $field->var_filter,
]);
}
}
if (! empty($existingFields)) {
$db->delete(
$table,
$db->quoteInto(
"$objectKey = $objectId AND datafield_id IN (?)",
array_keys($existingFields)
)
);
}
}
/**
* @param object $object
* @throws \Icinga\Exception\NotFoundError
*/
public function tweakTargetIds($object)
{
$forward = $this->getIdMap();
$map = array_flip($forward);
if (isset($object->fields)) {
foreach ($object->fields as $field) {
$id = $field->datafield_id;
if (isset($map[$id])) {
$field->datafield_id = $map[$id];
} else {
$field->datafield_id = "(NEW)";
}
}
}
}
/**
* @return int
*/
protected function getNextNewId()
{
return $this->nextNewId++;
}
protected function getRequiredIds()
{
if ($this->requiredIds === null) {
if (isset($this->objects['Datafield'])) {
$this->requiredIds = array_keys($this->objects['Datafield']);
} else {
$ids = [];
foreach ($this->objects as $typeName => $objects) {
foreach ($objects as $key => $object) {
if (isset($object->fields)) {
foreach ($object->fields as $field) {
$ids[$field->datafield_id] = true;
}
}
}
}
$this->requiredIds = array_keys($ids);
}
}
return $this->requiredIds;
}
/**
* @param $type
* @return object[]
*/
protected function getObjectsByType($type)
{
if (isset($this->objects->$type)) {
return $this->objects->$type;
} else {
return [];
}
}
/**
* @return DirectorDatafield[]
* @throws \Icinga\Exception\NotFoundError
*/
protected function getTargetFields()
{
if ($this->targetFields === null) {
$this->calculateIdMap();
}
return $this->targetFields;
}
/**
* @throws \Icinga\Exception\NotFoundError
*/
protected function getIdMap()
{
if ($this->idMap === null) {
$this->calculateIdMap();
}
return $this->idMap;
}
/**
* @throws \Icinga\Exception\NotFoundError
*/
protected function calculateIdMap()
{
$this->idMap = [];
$this->targetFields = [];
foreach ($this->getObjectsByType('Datafield') as $id => $object) {
// Hint: import() doesn't store!
$new = DirectorDatafield::import($object, $this->targetDb);
if ($new->hasBeenLoadedFromDb()) {
$newId = (int) $new->get('id');
} else {
$newId = sprintf('NEW(%s)', $this->getNextNewId());
}
$this->idMap[$id] = $newId;
$this->targetFields[$id] = $new;
}
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace Icinga\Module\Director\DirectorObject\Automation;
interface ExportInterface
{
/**
* @return \stdClass
*/
public function export();
// TODO:
// public function getXyzChecksum();
public function getUniqueIdentifier();
}

View File

@ -8,6 +8,7 @@ use Icinga\Module\Director\Objects\DirectorDatalist;
use Icinga\Module\Director\Objects\DirectorJob;
use Icinga\Module\Director\Objects\IcingaHostGroup;
use Icinga\Module\Director\Objects\IcingaServiceGroup;
use Icinga\Module\Director\Objects\IcingaServiceSet;
use Icinga\Module\Director\Objects\IcingaTemplateChoiceHost;
use Icinga\Module\Director\Objects\ImportSource;
use Icinga\Module\Director\Objects\SyncRule;
@ -21,6 +22,21 @@ class ImportExport
$this->connection = $connection;
}
public function serializeAllServiceSets()
{
// TODO: Export host templates in Inheritance order
$res = [];
$related = [];
foreach (IcingaServiceSet::loadAll($this->connection) as $object) {
$res[] = $object->export();
foreach ($object->exportRelated() as $key => $relatedObject) {
$related[$key] = $relatedObject;
}
}
return $res;
}
public function serializeAllHostTemplateChoices()
{
$res = [];

View File

@ -210,7 +210,10 @@ class Sync
foreach ($this->syncProperties as $p) {
$id = $p->source_id;
if (! array_key_exists($id, $this->sources)) {
$this->sources[$id] = ImportSource::load($id, $this->db);
$this->sources[$id] = ImportSource::loadWithAutoIncId(
(int) $id,
$this->db
);
}
}

View File

@ -9,6 +9,10 @@ use Icinga\Module\Director\Web\Form\QuickForm;
class ImportJob extends JobHook
{
/**
* @throws \Icinga\Exception\NotFoundError
* @throws \Icinga\Module\Director\Exception\DuplicateKeyException
*/
public function run()
{
$db = $this->db();
@ -18,10 +22,14 @@ class ImportJob extends JobHook
$this->runForSource($source);
}
} else {
$this->runForSource(ImportSource::load($id, $db));
$this->runForSource(ImportSource::loadWithAutoIncId($id, $db));
}
}
/**
* @return array
* @throws \Icinga\Exception\NotFoundError
*/
public function exportSettings()
{
$settings = parent::exportSettings();
@ -40,6 +48,10 @@ class ImportJob extends JobHook
return $settings;
}
/**
* @param ImportSource $source
* @throws \Icinga\Module\Director\Exception\DuplicateKeyException
*/
protected function runForSource(ImportSource $source)
{
if ($this->getSetting('run_import') === 'y') {
@ -56,6 +68,10 @@ class ImportJob extends JobHook
);
}
/**
* @param QuickForm $form
* @throws \Zend_Form_Exception
*/
public static function addSettingsFormFields(QuickForm $form)
{
$rules = self::enumImportSources($form);

View File

@ -11,6 +11,10 @@ class SyncJob extends JobHook
{
protected $rule;
/**
* @throws \Icinga\Exception\NotFoundError
* @throws \Icinga\Module\Director\Exception\DuplicateKeyException
*/
public function run()
{
$db = $this->db();
@ -20,10 +24,14 @@ class SyncJob extends JobHook
$this->runForRule($rule);
}
} else {
$this->runForRule(SyncRule::load($id, $db));
$this->runForRule(SyncRule::loadWithAutoIncId((int) $id, $db));
}
}
/**
* @return array
* @throws \Icinga\Exception\NotFoundError
*/
public function exportSettings()
{
$settings = [
@ -31,13 +39,17 @@ class SyncJob extends JobHook
];
$id = $this->getSetting('rule_id');
if ($id !== '__ALL__') {
$settings['rule'] = SyncRule::load((int) $id, $this->db())
$settings['rule'] = SyncRule::loadWithAutoIncId((int) $id, $this->db())
->get('rule_name');
}
return $settings;
}
/**
* @param SyncRule $rule
* @throws \Icinga\Module\Director\Exception\DuplicateKeyException
*/
protected function runForRule(SyncRule $rule)
{
if ($this->getSetting('apply_changes') === 'y') {
@ -54,6 +66,11 @@ class SyncJob extends JobHook
);
}
/**
* @param QuickForm $form
* @return DirectorObjectForm|QuickForm
* @throws \Zend_Form_Exception
*/
public static function addSettingsFormFields(QuickForm $form)
{
/** @var DirectorObjectForm $form */

View File

@ -2,10 +2,13 @@
namespace Icinga\Module\Director\Objects;
use Icinga\Module\Director\Core\Json;
use Icinga\Module\Director\Data\Db\DbObjectWithSettings;
use Icinga\Module\Director\Db;
use Icinga\Module\Director\Exception\DuplicateKeyException;
use Icinga\Module\Director\Hook\DataTypeHook;
use Icinga\Module\Director\Web\Form\DirectorObjectForm;
use InvalidArgumentException;
use Zend_Form_Element as ZfElement;
class DirectorDatafield extends DbObjectWithSettings
@ -50,6 +53,10 @@ class DirectorDatafield extends DbObjectWithSettings
return $obj;
}
/**
* @return object
* @throws \Icinga\Exception\NotFoundError
*/
public function export()
{
$plain = (object) $this->getProperties();
@ -68,6 +75,61 @@ class DirectorDatafield extends DbObjectWithSettings
return $plain;
}
/**
* @param $plain
* @param Db $db
* @param bool $replace
* @return DirectorDatafield
* @throws \Icinga\Exception\NotFoundError
*/
public static function import($plain, Db $db, $replace = false)
{
$properties = (array) $plain;
if (isset($properties['originalId'])) {
$id = $properties['originalId'];
unset($properties['originalId']);
} else {
$id = null;
}
if (isset($properties['settings']->datalist)) {
$list = DirectorDatalist::load(
$properties['settings']->datalist,
$db
);
$properties['settings']->datalist_id = $list->get('id');
unset($properties['settings']->datalist);
}
$encoded = Json::encode($properties);
if ($id) {
if (static::exists($id, $db)) {
$existing = static::loadWithAutoIncId($id, $db);
$existingProperties = (array) $existing->export();
unset($existingProperties['originalId']);
if ($encoded === Json::encode($existingProperties)) {
return $existing;
}
}
}
$dba = $db->getDbAdapter();
$query = $dba->select()
->from('director_datafield')
->where('varname = ?', $plain->varname);
$candidates = DirectorDatafield::loadAll($db, $query);
foreach ($candidates as $candidate) {
$export = $candidate->export();
unset($export->originalId);
if (Json::encode($export) === $encoded) {
return $candidate;
}
}
return static::create($properties, $db);
}
protected function setObject(IcingaObject $object)
{
$this->object = $object;

View File

@ -2,22 +2,28 @@
namespace Icinga\Module\Director\Objects;
use Icinga\Exception\NotFoundError;
use Icinga\Module\Director\Data\Db\DbObjectWithSettings;
use Icinga\Module\Director\Db;
use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
use Icinga\Module\Director\Exception\DuplicateKeyException;
use Icinga\Module\Director\Hook\JobHook;
use Exception;
use InvalidArgumentException;
class DirectorJob extends DbObjectWithSettings
class DirectorJob extends DbObjectWithSettings implements ExportInterface
{
/** @var JobHook */
protected $job;
protected $table = 'director_job';
protected $keyName = 'id';
protected $keyName = 'job_name';
protected $autoincKeyName = 'id';
protected $protectAutoinc = false;
protected $defaultProperties = [
'id' => null,
'job_name' => null,
@ -42,6 +48,11 @@ class DirectorJob extends DbObjectWithSettings
protected $settingsRemoteId = 'job_id';
public function getUniqueIdentifier()
{
return $this->get('job_name');
}
/**
* @return JobHook
*/
@ -188,6 +199,71 @@ class DirectorJob extends DbObjectWithSettings
return $plain;
}
/**
* @param $plain
* @param Db $db
* @param bool $replace
* @return DirectorJob
* @throws DuplicateKeyException
* @throws NotFoundError
*/
public static function import($plain, Db $db, $replace = false)
{
$dummy = new static;
$idCol = $dummy->autoincKeyName;
$keyCol = $dummy->keyName;
$properties = (array) $plain;
if (isset($properties['originalId'])) {
$id = $properties['originalId'];
unset($properties['originalId']);
} else {
$id = null;
}
$name = $properties[$keyCol];
if ($replace && static::existsWithNameAndId($name, $id, $db)) {
$object = static::loadWithAutoIncId($id, $db);
} elseif ($replace && static::exists($name, $db)) {
$object = static::load($name, $db);
} elseif (static::exists($name, $db)) {
throw new DuplicateKeyException(
'Director Job "%s" already exists',
$name
);
} else {
$object = static::create([], $db);
}
$object->setProperties($properties);
if ($id !== null) {
$object->reallySet($idCol, $id);
}
return $object;
}
/**
* @param string $name
* @param int $id
* @param Db $connection
* @api internal
* @return bool
*/
protected static function existsWithNameAndId($name, $id, Db $connection)
{
$db = $connection->getDbAdapter();
$dummy = new static;
$idCol = $dummy->autoincKeyName;
$keyCol = $dummy->keyName;
return (string) $id === (string) $db->fetchOne(
$db->select()
->from($dummy->table, $idCol)
->where("$idCol = ?", $id)
->where("$keyCol = ?", $name)
);
}
/**
* @return IcingaTimePeriod
* @throws \Icinga\Exception\NotFoundError

View File

@ -2,12 +2,15 @@
namespace Icinga\Module\Director\Objects;
use Icinga\Module\Director\Db;
use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
use Icinga\Module\Director\Exception\DuplicateKeyException;
use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1;
use Icinga\Module\Director\Objects\Extension\Arguments;
use Zend_Db_Select as DbSelect;
class IcingaCommand extends IcingaObject implements ObjectWithArguments
class IcingaCommand extends IcingaObject implements ObjectWithArguments, ExportInterface
{
use Arguments;
@ -197,6 +200,59 @@ class IcingaCommand extends IcingaObject implements ObjectWithArguments
return $this->countDirectUses() > 0;
}
public function getUniqueIdentifier()
{
return $this->getObjectName();
}
/**
* @return object
* @throws \Icinga\Exception\NotFoundError
*/
public function export()
{
$object = $this->toPlainObject();
if (property_exists($object, 'arguments')) {
foreach ($object->arguments as $key => $argument) {
if (property_exists($argument, 'command_id')) {
unset($argument->command_id);
}
}
}
return $object;
}
/**
* @param $plain
* @param Db $db
* @param bool $replace
* @return IcingaCommand
* @throws DuplicateKeyException
* @throws \Icinga\Exception\NotFoundError
*/
public static function import($plain, Db $db, $replace = false)
{
$properties = (array) $plain;
$name = $properties['object_name'];
$key = $name;
if ($replace && static::exists($key, $db)) {
$object = static::load($key, $db);
} elseif (static::exists($key, $db)) {
throw new DuplicateKeyException(
'Command "%s" already exists',
$name
);
} else {
$object = static::create([], $db);
}
$object->setProperties($properties);
return $object;
}
protected function renderCommand()
{
$command = $this->get('command');

View File

@ -6,11 +6,15 @@ use Icinga\Data\Db\DbConnection;
use Icinga\Exception\NotFoundError;
use Icinga\Module\Director\Data\PropertiesFilter;
use Icinga\Module\Director\Db;
use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
use Icinga\Module\Director\Exception\DuplicateKeyException;
use Icinga\Module\Director\IcingaConfig\IcingaConfig;
use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1;
use Icinga\Module\Director\Objects\Extension\FlappingSupport;
use InvalidArgumentException;
use RuntimeException;
class IcingaHost extends IcingaObject
class IcingaHost extends IcingaObject implements ExportInterface
{
use FlappingSupport;
@ -253,6 +257,96 @@ class IcingaHost extends IcingaObject
}
}
public function getUniqueIdentifier()
{
if ($this->isTemplate()) {
return $this->getObjectName();
} else {
throw new RuntimeException(
'getUniqueIdentifier() is supported by Host Templates only'
);
}
}
/**
* @return object
* @throws \Icinga\Exception\NotFoundError
*/
public function export()
{
// TODO: ksort in toPlainObject?
$props = (array) $this->toPlainObject();
$props['fields'] = $this->loadFieldReferences();
ksort($props);
return (object) $props;
}
/**
* @param $plain
* @param Db $db
* @param bool $replace
* @return IcingaHost
* @throws DuplicateKeyException
* @throws \Icinga\Exception\NotFoundError
*/
public static function import($plain, Db $db, $replace = false)
{
$properties = (array) $plain;
$name = $properties['object_name'];
if ($properties['object_type'] !== 'template') {
throw new InvalidArgumentException(sprintf(
'Can import only Templates, got "%s" for "%s"',
$properties['object_type'],
$name
));
}
$key = $name;
if ($replace && static::exists($key, $db)) {
$object = static::load($key, $db);
} elseif (static::exists($key, $db)) {
throw new DuplicateKeyException(
'Service Template "%s" already exists',
$name
);
} else {
$object = static::create([], $db);
}
// $object->newFields = $properties['fields'];
unset($properties['fields']);
$object->setProperties($properties);
return $object;
}
protected function loadFieldReferences()
{
$db = $this->getDb();
$res = $db->fetchAll(
$db->select()->from([
'hf' => 'icinga_host_field'
], [
'hf.datafield_id',
'hf.is_required',
'hf.var_filter',
])->join(['df' => 'director_datafield'], 'df.id = hf.datafield_id', [])
->where('host_id = ?', $this->get('id'))
->order('varname ASC')
);
if (empty($res)) {
return [];
} else {
foreach ($res as $field) {
$field->datafield_id = (int) $field->datafield_id;
}
return $res;
}
}
public function hasAnyOverridenServiceVars()
{
$varname = $this->getServiceOverrivesVarname();

View File

@ -2,7 +2,11 @@
namespace Icinga\Module\Director\Objects;
abstract class IcingaObjectGroup extends IcingaObject
use Icinga\Module\Director\Db;
use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
use Icinga\Module\Director\Exception\DuplicateKeyException;
abstract class IcingaObjectGroup extends IcingaObject implements ExportInterface
{
protected $supportsImports = true;
@ -17,6 +21,50 @@ abstract class IcingaObjectGroup extends IcingaObject
'assign_filter' => null,
];
public function getUniqueIdentifier()
{
return $this->getObjectName();
}
/**
* @return object
* @throws \Icinga\Exception\NotFoundError
*/
public function export()
{
return $this->toPlainObject();
}
/**
* @param $plain
* @param Db $db
* @param bool $replace
* @return IcingaObjectGroup
* @throws DuplicateKeyException
* @throws \Icinga\Exception\NotFoundError
*/
public static function import($plain, Db $db, $replace = false)
{
$properties = (array) $plain;
$name = $properties['object_name'];
$key = $name;
if ($replace && static::exists($key, $db)) {
$object = static::load($key, $db);
} elseif (static::exists($key, $db)) {
throw new DuplicateKeyException(
'Group "%s" already exists',
$name
);
} else {
$object = static::create([], $db);
}
$object->setProperties($properties);
return $object;
}
protected function prefersGlobalZone()
{
return true;

View File

@ -7,6 +7,8 @@ use Icinga\Exception\IcingaException;
use Icinga\Module\Director\Data\PropertiesFilter;
use Icinga\Module\Director\Db;
use Icinga\Module\Director\Db\Cache\PrefetchCache;
use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
use Icinga\Module\Director\Exception\DuplicateKeyException;
use Icinga\Module\Director\IcingaConfig\IcingaConfig;
use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1;
@ -15,7 +17,7 @@ use Icinga\Module\Director\Resolver\HostServiceBlacklist;
use InvalidArgumentException;
use RuntimeException;
class IcingaService extends IcingaObject
class IcingaService extends IcingaObject implements ExportInterface
{
use FlappingSupport;
@ -150,6 +152,100 @@ class IcingaService extends IcingaObject
return $this->get('use_var_overrides') === 'y';
}
public function getUniqueIdentifier()
{
if ($this->isTemplate()) {
return $this->getObjectName();
} else {
throw new RuntimeException(
'getUniqueIdentifier() is supported by Service Templates only'
);
}
}
/**
* @return object
* @throws \Icinga\Exception\NotFoundError
*/
public function export()
{
// TODO: ksort in toPlainObject?
$props = (array) $this->toPlainObject();
$props['fields'] = $this->loadFieldReferences();
ksort($props);
return (object) $props;
}
/**
* @param $plain
* @param Db $db
* @param bool $replace
* @return IcingaService
* @throws DuplicateKeyException
* @throws \Icinga\Exception\NotFoundError
*/
public static function import($plain, Db $db, $replace = false)
{
$properties = (array) $plain;
$name = $properties['object_name'];
if ($properties['object_type'] !== 'template') {
throw new InvalidArgumentException(sprintf(
'Can import only Templates, got "%s" for "%s"',
$properties['object_type'],
$name
));
}
$key = [
'object_type' => 'template',
'object_name' => $name
];
if ($replace && static::exists($key, $db)) {
$object = static::load($key, $db);
} elseif (static::exists($key, $db)) {
throw new DuplicateKeyException(
'Service Template "%s" already exists',
$name
);
} else {
$object = static::create([], $db);
}
// $object->newFields = $properties['fields'];
unset($properties['fields']);
$object->setProperties($properties);
return $object;
}
protected function loadFieldReferences()
{
$db = $this->getDb();
$res = $db->fetchAll(
$db->select()->from([
'sf' => 'icinga_service_field'
], [
'sf.datafield_id',
'sf.is_required',
'sf.var_filter',
])->join(['df' => 'director_datafield'], 'df.id = sf.datafield_id', [])
->where('service_id = ?', $this->get('id'))
->order('varname ASC')
);
if (empty($res)) {
return [];
} else {
foreach ($res as $field) {
$field->datafield_id = (int) $field->datafield_id;
}
return $res;
}
}
/**
* @param string $key
* @return $this

View File

@ -4,11 +4,14 @@ namespace Icinga\Module\Director\Objects;
use Exception;
use Icinga\Data\Filter\Filter;
use Icinga\Module\Director\Db;
use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
use Icinga\Module\Director\Exception\DuplicateKeyException;
use Icinga\Module\Director\IcingaConfig\IcingaConfig;
use InvalidArgumentException;
use RuntimeException;
class IcingaServiceSet extends IcingaObject
class IcingaServiceSet extends IcingaObject implements ExportInterface
{
protected $table = 'icinga_service_set';
@ -110,6 +113,119 @@ class IcingaServiceSet extends IcingaObject
return $services;
}
public function getUniqueIdentifier()
{
return $this->getObjectName();
}
/**
* @return object
* @throws \Icinga\Exception\NotFoundError
*/
public function export()
{
if ($this->get('host_id')) {
return $this->exportSetOnHost();
} else {
return $this->exportTemplate();
}
}
protected function exportSetOnHost()
{
// TODO.
throw new RuntimeException('Not yet');
}
/**
* @return object
* @throws \Icinga\Exception\NotFoundError
*/
protected function exportTemplate()
{
$props = $this->getProperties();
unset($props['id'], $props['host_id']);
$props['services'] = [];
foreach ($this->getServiceObjects() as $serviceObject) {
$props['services'][$serviceObject->getObjectName()] = $serviceObject->export();
}
ksort($props);
return (object) $props;
}
/**
* @param $plain
* @param Db $db
* @param bool $replace
* @return IcingaServiceSet
* @throws DuplicateKeyException
* @throws \Icinga\Exception\NotFoundError
*/
public static function import($plain, Db $db, $replace = false)
{
$properties = (array) $plain;
$name = $properties['object_name'];
if (isset($properties['services'])) {
$services = $properties['services'];
unset($properties['services']);
} else {
$services = [];
}
if ($properties['object_type'] !== 'template') {
throw new InvalidArgumentException(sprintf(
'Can import only Templates, got "%s" for "%s"',
$properties['object_type'],
$name
));
}
if ($replace && static::exists($name, $db)) {
$object = static::load($name, $db);
} elseif (static::exists($name, $db)) {
throw new DuplicateKeyException(
'Service Set "%s" already exists',
$name
);
} else {
$object = static::create([], $db);
}
$object->setProperties($properties);
// This is not how other imports work, but here we need an ID
if (! $object->hasBeenLoadedFromDb()) {
$object->store();
}
$setId = $object->get('id');
$sQuery = $db->getDbAdapter()->select()->from(
['s' => 'icinga_service'],
's.*'
)->where('service_set_id = ?', $setId);
$existingServices = IcingaService::loadAll($db, $sQuery, 'object_name');
foreach ($services as $service) {
if (isset($service->fields)) {
unset($service->fields);
}
$name = $service->object_name;
if (isset($existingServices[$name])) {
$existing = $existingServices[$name];
$existing->setProperties((array) $service);
$existing->set('service_set_id', $setId);
if ($existing->hasBeenModified()) {
$existing->store();
}
} else {
$new = IcingaService::create((array) $service, $db);
$new->set('service_set_id', $setId);
$new->store();
}
}
return $object;
}
public function onDelete()
{
$hostId = $this->get('host_id');

View File

@ -3,9 +3,12 @@
namespace Icinga\Module\Director\Objects;
use Icinga\Exception\ProgrammingError;
use Icinga\Module\Director\Db;
use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
use Icinga\Module\Director\Exception\DuplicateKeyException;
use Icinga\Module\Director\Web\Form\QuickForm;
class IcingaTemplateChoice extends IcingaObject
class IcingaTemplateChoice extends IcingaObject implements ExportInterface
{
protected $objectTable;
@ -28,6 +31,47 @@ class IcingaTemplateChoice extends IcingaObject
return substr(substr($this->table, 0, -16), 7);
}
public function getUniqueIdentifier()
{
return $this->getObjectName();
}
/**
* @param $plain
* @param Db $db
* @param bool $replace
* @return IcingaTemplateChoice
* @throws DuplicateKeyException
* @throws \Icinga\Exception\NotFoundError
*/
public static function import($plain, Db $db, $replace = false)
{
$properties = (array) $plain;
if (isset($properties['originalId'])) {
$id = $properties['originalId'];
unset($properties['originalId']);
} else {
$id = null;
}
$name = $properties['object_name'];
$key = $name;
if ($replace && static::exists($key, $db)) {
$object = static::load($key, $db);
} elseif (static::exists($key, $db)) {
throw new DuplicateKeyException(
'Template Choice "%s" already exists',
$name
);
} else {
$object = static::create([], $db);
}
$object->setProperties($properties);
return $object;
}
public function export()
{
$plain = (object) $this->getProperties();

View File

@ -145,7 +145,10 @@ class ImportRun extends DbObject
public function importSource()
{
if ($this->importSource === null) {
$this->importSource = ImportSource::load($this->get('source_id'), $this->connection);
$this->importSource = ImportSource::loadWithAutoIncId(
(int) $this->get('source_id'),
$this->connection
);
}
return $this->importSource;
}

View File

@ -3,24 +3,27 @@
namespace Icinga\Module\Director\Objects;
use Icinga\Application\Benchmark;
use Icinga\Exception\ConfigurationError;
use Icinga\Exception\NotFoundError;
use Icinga\Module\Director\Data\Db\DbObjectWithSettings;
use Icinga\Module\Director\Db;
use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
use Icinga\Module\Director\Exception\DuplicateKeyException;
use Icinga\Module\Director\Hook\PropertyModifierHook;
use Icinga\Module\Director\Import\Import;
use Icinga\Module\Director\Import\SyncUtils;
use InvalidArgumentException;
use Exception;
class ImportSource extends DbObjectWithSettings
class ImportSource extends DbObjectWithSettings implements ExportInterface
{
protected $table = 'import_source';
protected $keyName = 'id';
protected $keyName = 'source_name';
protected $autoincKeyName = 'id';
protected $protectAutoinc = false;
protected $defaultProperties = [
'id' => null,
'source_name' => null,
@ -51,29 +54,44 @@ class ImportSource extends DbObjectWithSettings
*/
public function export()
{
$plain = (object) $this->getProperties();
$plain->originalId = $plain->id;
unset($plain->id);
$plain = $this->getProperties();
$plain['originalId'] = $plain['id'];
unset($plain['id']);
foreach ($this->stateProperties as $key) {
unset($plain->$key);
unset($plain[$key]);
}
$plain->settings = (object) $this->getSettings();
$plain->modifiers = $this->exportRowModifiers();
$plain['settings'] = (object) $this->getSettings();
$plain['modifiers'] = $this->exportRowModifiers();
ksort($plain);
return $plain;
return (object) $plain;
}
/**
* @param $plain
* @param Db $db
* @param bool $replace
* @return ImportSource
* @throws DuplicateKeyException
* @throws NotFoundError
*/
public static function import($plain, Db $db, $replace = false)
{
$properties = (array) $plain;
$id = $properties['originalId'];
unset($properties['originalId']);
if (isset($properties['originalId'])) {
$id = $properties['originalId'];
unset($properties['originalId']);
} else {
$id = null;
}
$name = $properties['source_name'];
if ($replace && static::existsWithNameAndId($name, $id, $db)) {
$object = static::loadWithAutoIncId($id, $db);
} elseif ($replace && static::exists($name, $db)) {
$object = static::load($name, $db);
} elseif (static::existsWithName($name, $db)) {
throw new DuplicateKeyException(
'Import Source %s already exists',
@ -86,16 +104,36 @@ class ImportSource extends DbObjectWithSettings
$object->newRowModifiers = $properties['modifiers'];
unset($properties['modifiers']);
$object->setProperties($properties);
if ($id !== null) {
$object->reallySet('id', $id);
}
return $object;
}
public function getUniqueIdentifier()
{
return $this->get('source_name');
}
/**
* @param $name
* @param Db $connection
* @return ImportSource
* @throws NotFoundError
*/
public static function loadByName($name, Db $connection)
{
$db = $connection->getDbAdapter();
$properties = $db->fetchRow(
$db->select()->from('import_source')->where('source_name = ?', $name)
);
if ($properties === false) {
throw new NotFoundError(sprintf(
'There is no such Import Source: "%s"',
$name
));
}
return static::create([], $connection)->setDbProperties($properties);
}
@ -111,15 +149,25 @@ class ImportSource extends DbObjectWithSettings
);
}
/**
* @param string $name
* @param int $id
* @param Db $connection
* @api internal
* @return bool
*/
protected static function existsWithNameAndId($name, $id, Db $connection)
{
$db = $connection->getDbAdapter();
$dummy = new static;
$idCol = $dummy->autoincKeyName;
$keyCol = $dummy->keyName;
return (string) $id === (string) $db->fetchOne(
$db->select()
->from('import_source', 'id')
->where('id = ?', $id)
->where('source_name = ?', $name)
->from($dummy->table, $idCol)
->where("$idCol = ?", $id)
->where("$keyCol = ?", $name)
);
}
@ -136,6 +184,7 @@ class ImportSource extends DbObjectWithSettings
/**
* @param bool $required
* @return ImportRun|null
* @throws NotFoundError
*/
public function fetchLastRun($required = false)
{
@ -171,6 +220,7 @@ class ImportSource extends DbObjectWithSettings
* @param $timestamp
* @param bool $required
* @return ImportRun|null
* @throws NotFoundError
*/
public function fetchLastRunBefore($timestamp, $required = false)
{
@ -186,7 +236,7 @@ class ImportSource extends DbObjectWithSettings
$query = $db->select()->from(
['ir' => 'import_run'],
'ir.id'
)->where('ir.source_id = ?', $this->id)
)->where('ir.source_id = ?', $this->get('id'))
->where('ir.start_time < ?', date('Y-m-d H:i:s', $timestamp))
->order('ir.start_time DESC')
->limit(1);
@ -200,12 +250,17 @@ class ImportSource extends DbObjectWithSettings
}
}
/**
* @param $required
* @return null
* @throws NotFoundError
*/
protected function nullUnlessRequired($required)
{
if ($required) {
throw new NotFoundError(
'No data has been imported for "%s" yet',
$this->source_name
$this->get('source_name')
);
}
@ -267,7 +322,7 @@ class ImportSource extends DbObjectWithSettings
$target = $modifier->getTargetProperty($key);
if (strpos($target, '.') !== false) {
throw new ConfigurationError(
throw new InvalidArgumentException(
'Cannot set value for nested key "%s"',
$target
);
@ -309,7 +364,7 @@ class ImportSource extends DbObjectWithSettings
$this->getConnection(),
$db->select()
->from('import_row_modifier')
->where('source_id = ?', $this->id)
->where('source_id = ?', $this->get('id'))
->order('priority ASC')
);
@ -320,7 +375,7 @@ class ImportSource extends DbObjectWithSettings
{
$mods = [];
foreach ($this->fetchRowModifiers() as $mod) {
$mods[] = [$mod->property_name, $mod->getInstance()];
$mods[] = [$mod->get('property_name'), $mod->getInstance()];
}
return $mods;
@ -331,11 +386,12 @@ class ImportSource extends DbObjectWithSettings
$modifiers = [];
foreach ($this->fetchRowModifiers() as $mod) {
if (! array_key_exists($mod->property_name, $modifiers)) {
$modifiers[$mod->property_name] = [];
$name = $mod->get('property_name');
if (! array_key_exists($name, $modifiers)) {
$modifiers[$name] = [];
}
$modifiers[$mod->property_name][] = $mod->getInstance();
$modifiers[$name][] = $mod->getInstance();
}
$this->rowModifiers = $modifiers;
@ -356,32 +412,38 @@ class ImportSource extends DbObjectWithSettings
return array_keys($list);
}
/**
* @param bool $runImport
* @return bool
* @throws DuplicateKeyException
*/
public function checkForChanges($runImport = false)
{
$hadChanges = false;
Benchmark::measure('Starting with import ' . $this->source_name);
$name = $this->get('source_name');
Benchmark::measure("Starting with import $name");
try {
$import = new Import($this);
$this->last_attempt = date('Y-m-d H:i:s');
$this->set('last_attempt', date('Y-m-d H:i:s'));
if ($import->providesChanges()) {
Benchmark::measure('Found changes for ' . $this->source_name);
Benchmark::measure("Found changes for $name");
$hadChanges = true;
$this->import_state = 'pending-changes';
$this->set('import_state', 'pending-changes');
if ($runImport && $import->run()) {
Benchmark::measure('Import succeeded for ' . $this->source_name);
$this->import_state = 'in-sync';
Benchmark::measure("Import succeeded for $name");
$this->set('import_state', 'in-sync');
}
} else {
$this->import_state = 'in-sync';
$this->set('import_state', 'in-sync');
}
$this->last_error_message = null;
$this->set('last_error_message', null);
} catch (Exception $e) {
$this->import_state = 'failing';
Benchmark::measure('Import failed for ' . $this->source_name);
$this->last_error_message = $e->getMessage();
$this->set('import_state', 'failing');
Benchmark::measure("Import failed for $name");
$this->set('last_error_message', $e->getMessage());
}
if ($this->hasBeenModified()) {
@ -391,6 +453,10 @@ class ImportSource extends DbObjectWithSettings
return $hadChanges;
}
/**
* @return bool
* @throws DuplicateKeyException
*/
public function runImport()
{
return $this->checkForChanges(true);

View File

@ -6,19 +6,22 @@ use Icinga\Application\Benchmark;
use Icinga\Data\Filter\Filter;
use Icinga\Module\Director\Data\Db\DbObject;
use Icinga\Module\Director\Db;
use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
use Icinga\Module\Director\Exception\DuplicateKeyException;
use Icinga\Module\Director\Import\PurgeStrategy\PurgeStrategy;
use Icinga\Module\Director\Import\Sync;
use Exception;
class SyncRule extends DbObject
class SyncRule extends DbObject implements ExportInterface
{
protected $table = 'sync_rule';
protected $keyName = 'id';
protected $keyName = 'rule_name';
protected $autoincKeyName = 'id';
protected $protectAutoinc = false;
protected $defaultProperties = [
'id' => null,
'rule_name' => null,
@ -58,6 +61,8 @@ class SyncRule extends DbObject
private $newSyncProperties;
private $originalId;
public function listInvolvedSourceIds()
{
if (! $this->hasBeenLoadedFromDb()) {
@ -76,12 +81,16 @@ class SyncRule extends DbObject
));
}
/**
* @return array
* @throws \Icinga\Exception\NotFoundError
*/
public function fetchInvolvedImportSources()
{
$sources = [];
foreach ($this->listInvolvedSourceIds() as $sourceId) {
$sources[$sourceId] = ImportSource::load($sourceId, $this->getConnection());
$sources[$sourceId] = ImportSource::loadWithAutoIncId($sourceId, $this->getConnection());
}
return $sources;
@ -130,6 +139,11 @@ class SyncRule extends DbObject
return $this->filter()->matches($row);
}
/**
* @param bool $apply
* @return bool
* @throws DuplicateKeyException
*/
public function checkForChanges($apply = false)
{
$hadChanges = false;
@ -170,12 +184,17 @@ class SyncRule extends DbObject
/**
* @return IcingaObject[]
* @throws Exception
*/
public function getExpectedModifications()
{
return $this->sync()->getExpectedModifications();
}
/**
* @return bool
* @throws DuplicateKeyException
*/
public function applyChanges()
{
return $this->checkForChanges(true);
@ -246,16 +265,17 @@ class SyncRule extends DbObject
public function export()
{
$plain = (object) $this->getProperties();
$plain->originalId = $plain->id;
unset($plain->id);
$plain = $this->getProperties();
$plain['originalId'] = $plain['id'];
unset($plain['id']);
foreach ($this->stateProperties as $key) {
unset($plain->$key);
unset($plain[$key]);
}
$plain->properties = $this->exportSyncProperties();
$plain['properties'] = $this->exportSyncProperties();
ksort($plain);
return $plain;
return (object) $plain;
}
/**
@ -269,15 +289,21 @@ class SyncRule extends DbObject
public static function import($plain, Db $db, $replace = false)
{
$properties = (array) $plain;
$id = $properties['originalId'];
unset($properties['originalId']);
if (isset($properties['originalId'])) {
$id = $properties['originalId'];
unset($properties['originalId']);
} else {
$id = null;
}
$name = $properties['rule_name'];
if ($replace && static::existsWithNameAndId($name, $id, $db)) {
$object = static::loadWithAutoIncId($id, $db);
} elseif ($replace && static::exists($name, $db)) {
$object = static::load($name, $db);
} elseif (static::existsWithName($name, $db)) {
throw new DuplicateKeyException(
'Import Source %s already exists',
'Sync Rule %s already exists',
$name
);
} else {
@ -287,10 +313,19 @@ class SyncRule extends DbObject
$object->newSyncProperties = $properties['properties'];
unset($properties['properties']);
$object->setProperties($properties);
if ($id !== null && (int) $id !== (int) $object->get('id')) {
$object->originalId = $object->get('id');
$object->reallySet('id', $id);
}
return $object;
}
public function getUniqueIdentifier()
{
return $this->get('rule_name');
}
/**
* @throws DuplicateKeyException
*/
@ -301,9 +336,15 @@ class SyncRule extends DbObject
$connection = $this->getConnection();
$db = $connection->getDbAdapter();
$myId = $this->get('id');
if ($this->originalId === null) {
$originalId = $myId;
} else {
$originalId = $this->originalId;
$this->originalId = null;
}
if ($this->hasBeenLoadedFromDb()) {
$db->delete(
'sync_rule_property',
'sync_property',
$db->quoteInto('rule_id = ?', $myId)
);
}
@ -331,6 +372,7 @@ class SyncRule extends DbObject
unset($properties['id']);
unset($properties['rule_id']);
unset($properties['source_id']);
ksort($properties);
$all[] = (object) $properties;
}
@ -491,8 +533,6 @@ class SyncRule extends DbObject
}
/**
* TODO: idem
*
* @param string $name
* @param int $id
* @param Db $connection
@ -502,12 +542,15 @@ class SyncRule extends DbObject
protected static function existsWithNameAndId($name, $id, Db $connection)
{
$db = $connection->getDbAdapter();
$dummy = new static;
$idCol = $dummy->autoincKeyName;
$keyCol = $dummy->keyName;
return (string) $id === (string) $db->fetchOne(
$db->select()
->from('sync_rule', 'id')
->where('id = ?', $id)
->where('rule_name = ?', $name)
->from($dummy->table, $idCol)
->where("$idCol = ?", $id)
->where("$keyCol = ?", $name)
);
}
}

View File

@ -183,7 +183,8 @@ abstract class ActionController extends Controller implements ControlsAndContent
$viewRenderer = null;
}
if ($this->getRequest()->isApiRequest()) {
$cType = $this->getResponse()->getHeader('Content-Type', true);
if ($this->getRequest()->isApiRequest() || ($cType !== null && $cType !== 'text/html')) {
$this->_helper->layout()->disableLayout();
if ($viewRenderer) {
$viewRenderer->disable();

View File

@ -6,6 +6,7 @@ use Icinga\Exception\IcingaException;
use Icinga\Exception\InvalidPropertyException;
use Icinga\Exception\NotFoundError;
use Icinga\Module\Director\Deployment\DeploymentInfo;
use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
use Icinga\Module\Director\Forms\DeploymentLinkForm;
use Icinga\Module\Director\Forms\IcingaCloneObjectForm;
use Icinga\Module\Director\Forms\IcingaObjectFieldForm;
@ -79,6 +80,9 @@ abstract class ObjectController extends ActionController
}
}
/**
* @throws NotFoundError
*/
public function indexAction()
{
if (! $this->getRequest()->isApiRequest()) {
@ -111,6 +115,9 @@ abstract class ObjectController extends ActionController
$this->content()->add($form);
}
/**
* @throws NotFoundError
*/
public function editAction()
{
$object = $this->requireObject();
@ -118,9 +125,14 @@ abstract class ObjectController extends ActionController
$this->addObjectTitle()
->addObjectForm($object)
->addActionClone()
->addActionUsage();
->addActionUsage()
->addActionBasket();
}
/**
* @throws NotFoundError
* @throws \Icinga\Security\SecurityException
*/
public function renderAction()
{
$this->assertTypePermission()
@ -133,6 +145,9 @@ abstract class ObjectController extends ActionController
$preview->renderTo($this);
}
/**
* @throws NotFoundError
*/
public function cloneAction()
{
$this->assertTypePermission();
@ -151,6 +166,10 @@ abstract class ObjectController extends ActionController
->content()->add($form);
}
/**
* @throws NotFoundError
* @throws \Icinga\Security\SecurityException
*/
public function fieldsAction()
{
$this->assertPermission('director/admin');
@ -187,6 +206,10 @@ abstract class ObjectController extends ActionController
$table->renderTo($this);
}
/**
* @throws NotFoundError
* @throws \Icinga\Security\SecurityException
*/
public function historyAction()
{
$this
@ -206,6 +229,9 @@ abstract class ObjectController extends ActionController
->renderTo($this);
}
/**
* @throws NotFoundError
*/
public function membershipAction()
{
$object = $this->requireObject();
@ -224,6 +250,10 @@ abstract class ObjectController extends ActionController
->renderTo($this);
}
/**
* @return $this
* @throws NotFoundError
*/
protected function addObjectTitle()
{
$object = $this->requireObject();
@ -271,6 +301,37 @@ abstract class ObjectController extends ActionController
return $this;
}
/**
* @return $this
*/
protected function addActionBasket()
{
if ($this->hasBasketSupport()) {
$object = $this->object;
if ($object instanceof ExportInterface) {
if ($object->isTemplate()) {
$type = $this->getType() . 'Template';
} elseif ($object->isGroup()) {
$type = ucfirst($this->getType());
} else {
// Command? Sure?
$type = ucfirst($this->getType());
}
$this->actions()->add(Link::create(
$this->translate('Add to Basket'),
'director/basket/add',
[
'type' => $type,
'names' => $object->getUniqueIdentifier()
],
['class' => 'icon-tag']
));
}
}
return $this;
}
protected function addTemplate()
{
$this->assertPermission('director/admin');
@ -450,10 +511,20 @@ abstract class ObjectController extends ActionController
return $form;
}
protected function hasBasketSupport()
{
return $this->object->isTemplate() || $this->object->isGroup();
}
protected function onObjectFormLoaded(DirectorObjectForm $form)
{
}
/**
* @return IcingaObject
* @throws NotFoundError
* @throws \Zend_Controller_Response_Exception
*/
protected function requireObject()
{
if (! $this->object) {

View File

@ -56,7 +56,6 @@ abstract class ObjectsController extends ActionController
/**
* @return IcingaObjectsHandler
* @throws \Icinga\Exception\ConfigurationError
* @throws NotFoundError
*/
protected function apiRequestHandler()
@ -82,7 +81,6 @@ abstract class ObjectsController extends ActionController
}
/**
* @throws \Icinga\Exception\ConfigurationError
* @throws \Icinga\Exception\Http\HttpNotFoundException
* @throws NotFoundError
*/
@ -124,7 +122,6 @@ abstract class ObjectsController extends ActionController
/**
* @return ObjectsTable
* @throws \Icinga\Exception\ConfigurationError
*/
protected function getTable()
{
@ -134,9 +131,24 @@ abstract class ObjectsController extends ActionController
/**
* @throws NotFoundError
* @throws \Icinga\Exception\ConfigurationError
*/
public function edittemplatesAction()
{
$this->commonForEdit();
}
/**
* @throws NotFoundError
*/
public function editAction()
{
$this->commonForEdit();
}
/**
* @throws NotFoundError
*/
public function commonForEdit()
{
$type = ucfirst($this->getType());
@ -293,7 +305,7 @@ abstract class ObjectsController extends ActionController
/**
* @return array
* @throws \Icinga\Exception\ConfigurationError
* @throws NotFoundError
*/
protected function loadMultiObjectsFromParams()
{
@ -341,7 +353,6 @@ abstract class ObjectsController extends ActionController
/**
* @param ZfQueryBasedTable $table
* @return ZfQueryBasedTable
* @throws \Icinga\Exception\ConfigurationError
* @throws NotFoundError
*/
protected function eventuallyFilterCommand(ZfQueryBasedTable $table)

View File

@ -2,6 +2,7 @@
namespace Icinga\Module\Director\Web\Controller;
use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
use Icinga\Module\Director\Objects\IcingaObject;
use Icinga\Module\Director\Web\Controller\Extension\DirectorDb;
use Icinga\Module\Director\Web\Table\ApplyRulesTable;
@ -115,6 +116,18 @@ abstract class TemplateController extends CompatController
['class' => 'icon-edit']
)
]);
if ($template instanceof ExportInterface) {
$this->actions()->add(Link::create(
$this->translate('Add to Basket'),
'director/basket/add',
[
'type' => ucfirst($this->getType()) . 'Template',
'names' => $template->getUniqueIdentifier()
],
['class' => 'icon-tag']
));
}
$list = new UnorderedList([], [
'class' => 'vertical-action-list'
]);

View File

@ -29,8 +29,7 @@ class ObjectPreview
/**
* @param ControlsAndContent $cc
* @throws \Icinga\Exception\IcingaException
* @throws \Icinga\Exception\ProgrammingError
* @throws \Icinga\Exception\NotFoundError
*/
public function renderTo(ControlsAndContent $cc)
{

View File

@ -0,0 +1,123 @@
<?php
namespace Icinga\Module\Director\Web\Table;
use dipl\Html\Html;
use dipl\Html\Link;
use dipl\Web\Table\ZfQueryBasedTable;
use Icinga\Date\DateFormatter;
use Icinga\Module\Director\Core\Json;
use Icinga\Module\Director\DirectorObject\Automation\Basket;
use RuntimeException;
class BasketSnapshotTable extends ZfQueryBasedTable
{
protected $searchColumns = [
'basket_name',
'summary'
];
/** @var Basket */
protected $basket;
public function setBasket(Basket $basket)
{
$this->basket = $basket;
$this->searchColumns = [];
return $this;
}
public function renderRow($row)
{
$this->splitByDay($row->ts_create_seconds);
$link = $this->linkToSnapshot($this->renderSummary($row->summary), $row);
if ($this->basket === null) {
$columns = [
[
new Link(
Html::tag('strong', $row->basket_name),
'director/basket',
['name' => $row->basket_name]
),
Html::tag('br'),
$link,
],
DateFormatter::formatTime($row->ts_create / 1000),
];
} else {
$columns = [
$link,
DateFormatter::formatTime($row->ts_create / 1000),
];
}
return $this::row($columns);
}
protected function renderSummary($summary)
{
$summary = Json::decode($summary);
if ($summary === null) {
return '-';
}
$result = [];
if (! is_object($summary) && ! is_array($summary)) {
throw new RuntimeException(sprintf(
'Got invalid basket summary: %s ',
var_export($summary, 1)
));
}
foreach ($summary as $type => $count) {
$result[] = sprintf(
'%dx %s',
$count,
$type
);
}
if (empty($result)) {
return '-';
}
return implode(', ', $result);
}
protected function linkToSnapshot($caption, $row)
{
return new Link($caption, 'director/basket/snapshot', [
'checksum' => bin2hex($row->content_checksum),
'ts' => $row->ts_create,
'name' => $row->basket_name,
]);
}
public function prepareQuery()
{
$query = $this->db()->select()->from([
'b' => 'director_basket'
], [
'b.uuid',
'b.basket_name',
'bs.ts_create',
'ts_create_seconds' => '(bs.ts_create / 1000)',
'bs.content_checksum',
'bc.summary',
])->join(
['bs' => 'director_basket_snapshot'],
'bs.basket_uuid = b.uuid',
[]
)->join(
['bc' => 'director_basket_content'],
'bc.checksum = bs.content_checksum',
[]
)->order('bs.ts_create DESC');
if ($this->basket !== null) {
$query->where('b.uuid = ?', $this->basket->get('uuid'));
}
return $query;
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace Icinga\Module\Director\Web\Table;
use dipl\Html\Link;
use dipl\Web\Table\ZfQueryBasedTable;
class BasketTable extends ZfQueryBasedTable
{
protected $searchColumns = [
'basket_name',
];
public function renderRow($row)
{
$hexUuid = bin2hex($row->uuid);
$tr = $this::row([
new Link(
$row->basket_name,
'director/basket',
['name' => $row->basket_name]
),
$row->cnt_snapshots
]);
return $tr;
}
public function getColumnsToBeRendered()
{
return [
$this->translate('Basket'),
$this->translate('Snapshots'),
];
}
public function prepareQuery()
{
return $this->db()->select()->from([
'b' => 'director_basket'
], [
'b.uuid',
'b.basket_name',
'cnt_snapshots' => 'COUNT(bs.basket_uuid)',
])->joinLeft(
['bs' => 'director_basket_snapshot'],
'bs.basket_uuid = b.uuid',
[]
)->group('b.uuid');
}
}

View File

@ -2,6 +2,7 @@
namespace Icinga\Module\Director\Web\Table;
use dipl\Web\Table\Extension\MultiSelect;
use Icinga\Authentication\Auth;
use Icinga\Data\Filter\Filter;
use Icinga\Module\Director\Db;
@ -17,6 +18,8 @@ use Zend_Db_Select as ZfSelect;
class TemplatesTable extends ZfQueryBasedTable
{
use MultiSelect;
protected $searchColumns = ['o.object_name'];
private $type;
@ -28,6 +31,16 @@ class TemplatesTable extends ZfQueryBasedTable
return $table;
}
protected function assemble()
{
$type = $this->type;
$this->enableMultiSelect(
"director/${type}s/edittemplates",
"director/${type}template",
['name']
);
}
public function getType()
{
return $this->type;

View File

@ -0,0 +1,9 @@
ALTER TABLE import_source
ADD UNIQUE INDEX source_name (source_name);
ALTER TABLE sync_rule
ADD UNIQUE INDEX rule_name (rule_name);
INSERT INTO director_schema_migration
(schema_version, migration_time)
VALUES (152, NOW());

View File

@ -0,0 +1,42 @@
CREATE TABLE director_basket (
uuid VARBINARY(16) NOT NULL,
basket_name VARCHAR(64) NOT NULL,
owner_type ENUM(
'user',
'usergroup',
'role'
) NOT NULL,
owner_value VARCHAR(255) NOT NULL,
objects MEDIUMTEXT NOT NULL, -- json-encoded
PRIMARY KEY (uuid),
UNIQUE INDEX basket_name (basket_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_bin;
CREATE TABLE director_basket_content (
checksum VARBINARY(20) NOT NULL,
summary VARCHAR(255) NOT NULL, -- json
content MEDIUMTEXT NOT NULL, -- json
PRIMARY KEY (checksum)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_bin;
CREATE TABLE director_basket_snapshot (
basket_uuid VARBINARY(16) NOT NULL,
ts_create BIGINT(20) NOT NULL,
content_checksum VARBINARY(20) NOT NULL,
PRIMARY KEY (basket_uuid, ts_create),
INDEX sort_idx (ts_create),
CONSTRAINT basked_snapshot_basket
FOREIGN KEY director_basket_snapshot (basket_uuid)
REFERENCES director_basket (uuid)
ON DELETE CASCADE
ON UPDATE RESTRICT,
CONSTRAINT basked_snapshot_content
FOREIGN KEY content_checksum (content_checksum)
REFERENCES director_basket_content (checksum)
ON DELETE RESTRICT
ON UPDATE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_bin;
INSERT INTO director_schema_migration
(schema_version, migration_time)
VALUES (153, NOW());

View File

@ -28,6 +28,45 @@ CREATE TABLE director_activity_log (
INDEX checksum (checksum)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE director_basket (
uuid VARBINARY(16) NOT NULL,
basket_name VARCHAR(64) NOT NULL,
owner_type ENUM(
'user',
'usergroup',
'role'
) NOT NULL,
owner_value VARCHAR(255) NOT NULL,
objects MEDIUMTEXT NOT NULL, -- json-encoded
PRIMARY KEY (uuid),
UNIQUE INDEX basket_name (basket_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_bin;
CREATE TABLE director_basket_content (
checksum VARBINARY(20) NOT NULL,
summary VARCHAR(255) NOT NULL, -- json
content MEDIUMTEXT NOT NULL, -- json
PRIMARY KEY (checksum)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_bin;
CREATE TABLE director_basket_snapshot (
basket_uuid VARBINARY(16) NOT NULL,
ts_create BIGINT(20) NOT NULL,
content_checksum VARBINARY(20) NOT NULL,
PRIMARY KEY (basket_uuid, ts_create),
INDEX sort_idx (ts_create),
CONSTRAINT basked_snapshot_basket
FOREIGN KEY director_basket_snapshot (basket_uuid)
REFERENCES director_basket (uuid)
ON DELETE CASCADE
ON UPDATE RESTRICT,
CONSTRAINT basked_snapshot_content
FOREIGN KEY content_checksum (content_checksum)
REFERENCES director_basket_content (checksum)
ON DELETE RESTRICT
ON UPDATE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_bin;
CREATE TABLE director_generated_config (
checksum VARBINARY(20) NOT NULL COMMENT 'SHA1(last_activity_checksum;file_path=checksum;file_path=checksum;...)',
director_version VARCHAR(64) DEFAULT NULL,
@ -1296,6 +1335,7 @@ CREATE TABLE import_source (
last_attempt DATETIME DEFAULT NULL,
description TEXT DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE INDEX source_name (source_name),
INDEX search_idx (key_column)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
@ -1441,7 +1481,8 @@ CREATE TABLE sync_rule (
last_error_message TEXT DEFAULT NULL,
last_attempt DATETIME DEFAULT NULL,
description TEXT DEFAULT NULL,
PRIMARY KEY (id)
PRIMARY KEY (id),
UNIQUE INDEX rule_name (rule_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE sync_property (
@ -1730,4 +1771,4 @@ CREATE TABLE icinga_timeperiod_exclude (
INSERT INTO director_schema_migration
(schema_version, migration_time)
VALUES (151, NOW());
VALUES (153, NOW());

View File

@ -0,0 +1,7 @@
CREATE UNIQUE INDEX import_source_name ON import_source (source_name);
CREATE UNIQUE INDEX sync_rule_name ON sync_rule (rule_name);
INSERT INTO director_schema_migration
(schema_version, migration_time)
VALUES (152, NOW());

View File

@ -0,0 +1,45 @@
CREATE TYPE enum_owner_type AS ENUM('user', 'usergroup', 'role');
CREATE TABLE director_basket (
uuid bytea CHECK(LENGTH(uuid) = 16) NOT NULL,
basket_name VARCHAR(64) NOT NULL,
owner_type enum_owner_type NOT NULL,
owner_value VARCHAR(255) NOT NULL,
objects text NOT NULL, -- json-encoded
PRIMARY KEY (uuid)
);
CREATE UNIQUE INDEX basket_basket_name ON director_basket (basket_name);
CREATE TABLE director_basket_content (
checksum bytea CHECK(LENGTH(checksum) = 20) NOT NULL,
summary VARCHAR(255) NOT NULL, -- json
content text NOT NULL, -- json
PRIMARY KEY (checksum)
);
CREATE TABLE director_basket_snapshot (
basket_uuid bytea CHECK(LENGTH(basket_uuid) = 16) NOT NULL,
ts_create bigint NOT NULL,
content_checksum bytea CHECK(LENGTH(content_checksum) = 20) NOT NULL,
PRIMARY KEY (basket_uuid, ts_create),
CONSTRAINT basked_snapshot_basket
FOREIGN KEY (basket_uuid)
REFERENCES director_basket (uuid)
ON DELETE CASCADE
ON UPDATE RESTRICT,
CONSTRAINT basked_snapshot_content
FOREIGN KEY (content_checksum)
REFERENCES director_basket_content (checksum)
ON DELETE RESTRICT
ON UPDATE RESTRICT
);
CREATE INDEX basket_snapshot_sort_idx ON director_basket_snapshot (ts_create);
INSERT INTO director_schema_migration
(schema_version, migration_time)
VALUES (153, NOW());

View File

@ -42,6 +42,7 @@ CREATE TYPE enum_sync_state AS ENUM(
'failing'
);
CREATE TYPE enum_host_service AS ENUM('host', 'service');
CREATE TYPE enum_owner_type AS ENUM('user', 'usergroup', 'role');
CREATE OR REPLACE FUNCTION unix_timestamp(timestamp with time zone) RETURNS bigint AS '
@ -71,6 +72,46 @@ COMMENT ON COLUMN director_activity_log.old_properties IS 'Property hash, JSON';
COMMENT ON COLUMN director_activity_log.new_properties IS 'Property hash, JSON';
CREATE TABLE director_basket (
uuid bytea CHECK(LENGTH(uuid) = 16) NOT NULL,
basket_name VARCHAR(64) NOT NULL,
owner_type enum_owner_type NOT NULL,
owner_value VARCHAR(255) NOT NULL,
objects text NOT NULL, -- json-encoded
PRIMARY KEY (uuid)
);
CREATE UNIQUE INDEX basket_basket_name ON director_basket (basket_name);
CREATE TABLE director_basket_content (
checksum bytea CHECK(LENGTH(checksum) = 20) NOT NULL,
summary VARCHAR(255) NOT NULL, -- json
content text NOT NULL, -- json
PRIMARY KEY (checksum)
);
CREATE TABLE director_basket_snapshot (
basket_uuid bytea CHECK(LENGTH(basket_uuid) = 16) NOT NULL,
ts_create bigint NOT NULL,
content_checksum bytea CHECK(LENGTH(content_checksum) = 20) NOT NULL,
PRIMARY KEY (basket_uuid, ts_create),
CONSTRAINT basked_snapshot_basket
FOREIGN KEY (basket_uuid)
REFERENCES director_basket (uuid)
ON DELETE CASCADE
ON UPDATE RESTRICT,
CONSTRAINT basked_snapshot_content
FOREIGN KEY (content_checksum)
REFERENCES director_basket_content (checksum)
ON DELETE RESTRICT
ON UPDATE RESTRICT
);
CREATE INDEX basket_snapshot_sort_idx ON director_basket_snapshot (ts_create);
CREATE TABLE director_generated_config (
checksum bytea CHECK(LENGTH(checksum) = 20),
director_version character varying(64) DEFAULT NULL,
@ -1439,6 +1480,7 @@ CREATE TABLE import_source (
);
CREATE INDEX import_source_search_idx ON import_source (key_column);
CREATE UNIQUE INDEX import_source_name ON import_source (source_name);
CREATE TABLE import_source_setting (
@ -1594,6 +1636,7 @@ CREATE TABLE sync_rule (
PRIMARY KEY (id)
);
CREATE UNIQUE INDEX sync_rule_name ON sync_rule (rule_name);
CREATE TABLE sync_property (
id serial,
@ -2025,4 +2068,4 @@ CREATE TABLE icinga_timeperiod_exclude (
INSERT INTO director_schema_migration
(schema_version, migration_time)
VALUES (151, NOW());
VALUES (153, NOW());