icingaweb2-module-director/library/Director/Import/Sync.php

821 lines
24 KiB
PHP
Raw Normal View History

<?php
namespace Icinga\Module\Director\Import;
use Exception;
use Icinga\Application\Benchmark;
use Icinga\Data\Filter\Filter;
use Icinga\Module\Director\Application\MemoryLimit;
2017-08-21 21:53:19 +02:00
use Icinga\Module\Director\Data\Db\DbObject;
use Icinga\Module\Director\Db;
2016-10-31 17:20:17 +01:00
use Icinga\Module\Director\Db\Cache\PrefetchCache;
use Icinga\Module\Director\Objects\HostGroupMembershipResolver;
use Icinga\Module\Director\Objects\IcingaHost;
use Icinga\Module\Director\Objects\IcingaHostGroup;
use Icinga\Module\Director\Objects\IcingaObject;
use Icinga\Module\Director\Objects\ImportSource;
use Icinga\Module\Director\Objects\IcingaService;
use Icinga\Module\Director\Objects\SyncProperty;
use Icinga\Module\Director\Objects\SyncRule;
2016-02-24 12:24:19 +01:00
use Icinga\Module\Director\Objects\SyncRun;
use Icinga\Module\Director\Util;
2015-10-20 22:21:48 +02:00
use Icinga\Exception\IcingaException;
class Sync
{
/** @var SyncRule */
protected $rule;
/** @var Db */
protected $db;
/** @var array Related ImportSource objects */
protected $sources;
/** @var array Source columns we want to fetch from our sources */
2016-02-23 21:05:09 +01:00
protected $sourceColumns;
/** @var array Imported data */
2016-02-23 17:35:47 +01:00
protected $imported;
/** @var IcingaObject[] Objects to work with */
2016-02-23 17:47:18 +01:00
protected $objects;
/** @var bool Whether we already prepared your sync */
2016-02-24 12:24:19 +01:00
protected $isPrepared = false;
protected $modify = [];
2015-12-23 15:10:37 +01:00
protected $remove = [];
2015-12-23 15:10:37 +01:00
protected $create = [];
2015-12-23 15:10:37 +01:00
protected $errors = [];
2015-12-23 15:10:37 +01:00
/** @var SyncProperty[] */
protected $syncProperties;
protected $replaceVars = false;
protected $hasPropertyDisabled = false;
protected $serviceOverrideKeyName;
/**
* @var SyncRun
*/
2016-02-24 12:24:19 +01:00
protected $run;
protected $runStartTime;
/** @var Filter[] */
protected $columnFilters = [];
/** @var HostGroupMembershipResolver|bool */
protected $hostGroupMembershipResolver;
/**
* @param SyncRule $rule
*/
public function __construct(SyncRule $rule)
{
$this->rule = $rule;
$this->db = $rule->getConnection();
}
/**
* Whether the given sync rule would apply modifications
*
* @return boolean
* @throws Exception
*/
public function hasModifications()
{
return count($this->getExpectedModifications()) > 0;
}
/**
* Retrieve modifications a given SyncRule would apply
*
* @return array Array of IcingaObject elements
* @throws Exception
*/
public function getExpectedModifications()
{
$modified = [];
$objects = $this->prepare();
foreach ($objects as $object) {
if ($object->hasBeenModified()) {
$modified[] = $object;
} elseif ($object->shouldBeRemoved()) {
2016-04-22 14:47:49 +02:00
$modified[] = $object;
}
}
return $modified;
}
/**
* Transform the given value to an array
*
* @param array|string|null $value
*
* @return array
*/
2015-08-28 23:56:54 +02:00
protected function wantArray($value)
{
if (is_array($value)) {
return $value;
} elseif ($value === null) {
return [];
2015-08-28 23:56:54 +02:00
} else {
return [$value];
2015-08-28 23:56:54 +02:00
}
}
/**
* Raise PHP resource limits
*
* @return self;
*/
protected function raiseLimits()
{
MemoryLimit::raiseTo('1024M');
ini_set('max_execution_time', 0);
return $this;
}
2016-02-24 12:24:19 +01:00
/**
* Initialize run summary measurements
*
* @return self;
*/
protected function startMeasurements()
{
$this->run = SyncRun::start($this->rule);
$this->runStartTime = microtime(true);
Benchmark::measure('Starting sync');
2016-02-24 12:24:19 +01:00
return $this;
}
/**
* Fetch the configured properties involved in this sync
*
* @return self
*/
protected function fetchSyncProperties()
{
$this->syncProperties = $this->rule->getSyncProperties();
foreach ($this->syncProperties as $key => $prop) {
if ($prop->destination_field === 'vars' && $prop->merge_policy === 'override') {
$this->replaceVars = true;
}
if ($prop->destination_field === 'disabled') {
$this->hasPropertyDisabled = true;
}
if (! strlen($prop->filter_expression)) {
continue;
}
$this->columnFilters[$key] = Filter::fromQueryString(
$prop->filter_expression
);
}
2016-02-24 12:24:19 +01:00
return $this;
}
protected function rowMatchesPropertyFilter($row, $key)
{
if (!array_key_exists($key, $this->columnFilters)) {
return true;
}
return $this->columnFilters[$key]->matches($row);
}
/**
* Instantiates all related ImportSource objects
*
2016-02-23 17:35:47 +01:00
* @return self
* @throws IcingaException
* @throws \Icinga\Exception\NotFoundError
*/
protected function prepareRelatedImportSources()
{
$this->sources = [];
foreach ($this->syncProperties as $p) {
$id = $p->source_id;
2016-02-23 17:47:18 +01:00
if (! array_key_exists($id, $this->sources)) {
$this->sources[$id] = ImportSource::load($id, $this->db);
}
}
2016-02-23 17:35:47 +01:00
return $this;
}
2016-02-23 21:05:09 +01:00
/**
* Prepare the source columns we want to fetch
*
* @return self
*/
protected function prepareSourceColumns()
{
// $fieldMap = [];
$this->sourceColumns = [];
foreach ($this->syncProperties as $p) {
$sourceId = $p->source_id;
2016-02-23 21:05:09 +01:00
if (! array_key_exists($sourceId, $this->sourceColumns)) {
$this->sourceColumns[$sourceId] = [];
}
2016-07-13 13:52:15 +02:00
foreach (SyncUtils::extractVariableNames($p->source_expression) as $varname) {
2016-02-23 21:05:09 +01:00
$this->sourceColumns[$sourceId][$varname] = $varname;
// -> ? $fieldMap[
}
}
2016-02-23 21:05:09 +01:00
return $this;
}
2016-02-23 17:35:47 +01:00
/**
* Fetch latest imported data rows from all involved import sources
* @return Sync
* @throws IcingaException
2016-02-23 17:35:47 +01:00
*/
protected function fetchImportedData()
{
Benchmark::measure('Begin loading imported data');
if ($this->rule->object_type === 'host') {
$this->serviceOverrideKeyName = $this->db->settings()->override_services_varname;
}
$this->imported = [];
$sourceKeyPattern = $this->rule->getSourceKeyPattern();
$combinedKey = $this->rule->hasCombinedKey();
2016-02-23 17:35:47 +01:00
foreach ($this->sources as $source) {
/** @var ImportSource $source */
$sourceId = $source->id;
2016-02-23 21:05:09 +01:00
// Provide an alias column for our key. TODO: double-check this!
$key = $source->key_column;
2016-02-23 21:05:09 +01:00
$this->sourceColumns[$sourceId][$key] = $key;
$run = $source->fetchLastRun(true);
$usedColumns = SyncUtils::getRootVariables($this->sourceColumns[$sourceId]);
$filterColumns = [];
foreach ($this->columnFilters as $filter) {
foreach ($filter->listFilteredColumns() as $column) {
$filterColumns[$column] = $column;
}
}
if (($ruleFilter = $this->rule->filter()) !== null) {
foreach ($ruleFilter->listFilteredColumns() as $column) {
$filterColumns[$column] = $column;
}
}
if (! empty($filterColumns)) {
foreach (SyncUtils::getRootVariables($filterColumns) as $column) {
$usedColumns[$column] = $column;
}
}
Benchmark::measure(sprintf('Done pre-processing columns for source %s', $source->source_name));
$rows = $run->fetchRows($usedColumns);
Benchmark::measure(sprintf('Fetched source %s', $source->source_name));
$this->imported[$sourceId] = [];
foreach ($rows as $row) {
if ($combinedKey) {
$key = SyncUtils::fillVariables($sourceKeyPattern, $row);
2016-07-13 13:52:15 +02:00
2016-02-23 17:35:47 +01:00
if (array_key_exists($key, $this->imported[$sourceId])) {
throw new IcingaException(
'Trying to import row "%s" (%s) twice: %s VS %s',
$key,
$sourceKeyPattern,
2016-02-23 17:35:47 +01:00
json_encode($this->imported[$sourceId][$key]),
json_encode($row)
);
}
} else {
if (! property_exists($row, $key)) {
throw new IcingaException(
'There is no key column "%s" in this row from "%s": %s',
$key,
$source->source_name,
json_encode($row)
);
}
}
2016-02-23 21:05:09 +01:00
if (! $this->rule->matches($row)) {
2015-12-04 10:59:25 +01:00
continue;
}
if ($combinedKey) {
2016-02-23 17:35:47 +01:00
$this->imported[$sourceId][$key] = $row;
} else {
2016-02-23 17:35:47 +01:00
$this->imported[$sourceId][$row->$key] = $row;
}
}
unset($rows);
}
Benchmark::measure('Done loading imported data');
2016-02-23 17:35:47 +01:00
return $this;
}
/**
* TODO: This is rubbish, we need to filter at fetch time
*
* @throws IcingaException
*/
2016-02-23 17:47:18 +01:00
protected function removeForeignListEntries()
2016-02-19 12:42:02 +01:00
{
$listId = null;
foreach ($this->syncProperties as $prop) {
2016-02-19 12:42:02 +01:00
if ($prop->destination_field === 'list_id') {
$listId = (int) $prop->source_expression;
}
}
if ($listId === null) {
throw new IcingaException(
'Cannot sync datalist entry without list_ist'
);
}
$no = [];
2016-02-23 17:47:18 +01:00
foreach ($this->objects as $k => $o) {
if ((int) $o->list_id !== (int) $listId) {
2016-02-19 12:42:02 +01:00
$no[] = $k;
}
}
foreach ($no as $k) {
2016-02-23 17:47:18 +01:00
unset($this->objects[$k]);
2016-02-19 12:42:02 +01:00
}
}
/**
* @return $this
* @throws IcingaException
*/
2016-02-23 17:47:18 +01:00
protected function loadExistingObjects()
{
Benchmark::measure('Begin loading existing objects');
2016-02-23 17:47:18 +01:00
// TODO: Make object_type (template, object...) and object_name mandatory?
if ($this->rule->hasCombinedKey()) {
$this->objects = [];
$destinationKeyPattern = $this->rule->getDestinationKeyPattern();
foreach (IcingaObject::loadAllByType(
$this->rule->object_type,
$this->db
) as $object) {
if ($object instanceof IcingaService) {
if (strstr($destinationKeyPattern, '${host}') && $object->host_id === null) {
continue;
2017-01-13 19:47:54 +01:00
} elseif (strstr($destinationKeyPattern, '${service_set}') && $object->service_set_id === null) {
2016-02-26 11:58:37 +01:00
continue;
}
}
2016-07-13 13:52:15 +02:00
$key = SyncUtils::fillVariables(
$destinationKeyPattern,
$object
);
if (array_key_exists($key, $this->objects)) {
throw new IcingaException(
'Combined destination key "%s" is not unique, got "%s" twice',
$destinationKeyPattern,
$key
);
}
$this->objects[$key] = $object;
}
} else {
$this->objects = IcingaObject::loadAllByType(
$this->rule->object_type,
$this->db
);
}
2016-02-23 17:47:18 +01:00
// TODO: should be obsoleted by a better "loadFiltered" method
if ($this->rule->object_type === 'datalistEntry') {
$this->removeForeignListEntries();
2016-02-23 17:47:18 +01:00
}
Benchmark::measure('Done loading existing objects');
2016-02-23 17:47:18 +01:00
return $this;
}
/**
* @return array
* @throws IcingaException
* @throws \Icinga\Exception\NotFoundError
* @throws \Icinga\Exception\ProgrammingError
*/
2016-02-23 17:35:47 +01:00
protected function prepareNewObjects()
{
$objects = [];
2016-02-23 17:35:47 +01:00
foreach ($this->sources as $source) {
$sourceId = $source->id;
2016-02-23 17:35:47 +01:00
foreach ($this->imported[$sourceId] as $key => $row) {
if (! array_key_exists($key, $objects)) {
// Safe default values for object_type and object_name
if ($this->rule->object_type === 'datalistEntry') {
$props = [];
} else {
$props = [
'object_type' => 'object',
'object_name' => $key
];
}
$objects[$key] = IcingaObject::createByType(
$this->rule->object_type,
$props,
$this->db
2015-12-10 12:57:11 +01:00
);
}
$object = $objects[$key];
$this->prepareNewObject($row, $object, $sourceId);
}
}
2015-12-10 12:57:11 +01:00
return $objects;
}
/**
* @param $row
* @param DbObject $object
* @param $sourceId
* @throws IcingaException
* @throws \Icinga\Exception\NotFoundError
* @throws \Icinga\Exception\ProgrammingError
*/
protected function prepareNewObject($row, DbObject $object, $sourceId)
{
foreach ($this->syncProperties as $propertyKey => $p) {
if ($p->source_id !== $sourceId) {
continue;
}
if (! $this->rowMatchesPropertyFilter($row, $propertyKey)) {
continue;
}
$prop = $p->destination_field;
$val = SyncUtils::fillVariables($p->source_expression, $row);
if ($object instanceof IcingaObject) {
if ($prop === 'import') {
if ($val !== null) {
$object->imports()->add($val);
}
} elseif ($prop === 'groups') {
if ($val !== null) {
$object->groups()->add($val);
}
} elseif (substr($prop, 0, 5) === 'vars.') {
$varName = substr($prop, 5);
if (substr($varName, -2) === '[]') {
$varName = substr($varName, 0, -2);
$current = $this->wantArray($object->vars()->$varName);
$object->vars()->$varName = array_merge(
$current,
$this->wantArray($val)
);
} else {
$object->vars()->$varName = $val;
}
} else {
if ($val !== null) {
$object->set($prop, $val);
}
}
} else {
if ($val !== null) {
$object->set($prop, $val);
}
}
}
}
/**
* @return $this
* @throws IcingaException
*/
protected function deferResolvers()
{
if (in_array($this->rule->get('object_type'), ['host', 'hostgroup'])) {
$resolver = $this->getHostGroupMembershipResolver();
$resolver->defer()->setUseTransactions(false);
}
return $this;
}
/**
2017-08-21 21:53:19 +02:00
* @param DbObject $object
* @return $this
* @throws IcingaException
*/
protected function setResolver($object)
{
if (! ($object instanceof IcingaHost || $object instanceof IcingaHostGroup)) {
return $this;
}
if ($resolver = $this->getHostGroupMembershipResolver()) {
$object->setHostGroupMembershipResolver($resolver);
}
return $this;
}
/**
* @return $this
* @throws IcingaException
*/
protected function notifyResolvers()
{
if ($resolver = $this->getHostGroupMembershipResolver()) {
$resolver->refreshDb(true);
}
return $this;
}
/**
* @return bool|HostGroupMembershipResolver
* @throws IcingaException
*/
protected function getHostGroupMembershipResolver()
{
if ($this->hostGroupMembershipResolver === null) {
if (in_array(
$this->rule->get('object_type'),
['host', 'hostgroup']
)) {
$this->hostGroupMembershipResolver = new HostGroupMembershipResolver(
$this->db
);
} else {
$this->hostGroupMembershipResolver = false;
}
}
return $this->hostGroupMembershipResolver;
}
/**
* Evaluates a SyncRule and returns a list of modified objects
*
* TODO: Split this into smaller methods
*
* @return DbObject|IcingaObject[] List of modified IcingaObjects
* @throws Exception
*/
protected function prepare()
{
2016-02-24 12:24:19 +01:00
if ($this->isPrepared) {
return $this->objects;
}
$this->raiseLimits()
2016-02-24 12:24:19 +01:00
->startMeasurements()
->prepareCache()
2016-02-24 12:24:19 +01:00
->fetchSyncProperties()
->prepareRelatedImportSources()
2016-02-23 21:05:09 +01:00
->prepareSourceColumns()
->loadExistingObjects()
->fetchImportedData()
->deferResolvers();
Benchmark::measure('Begin preparing updated objects');
$newObjects = $this->prepareNewObjects();
Benchmark::measure('Ready to process objects');
/** @var DbObject|IcingaObject $object */
foreach ($newObjects as $key => $object) {
$this->processObject($key, $object);
}
Benchmark::measure('Modified objects are ready, applying purge strategy');
$noAction = [];
foreach ($this->rule->purgeStrategy()->listObjectsToPurge() as $key) {
2016-02-22 11:01:37 +01:00
if (array_key_exists($key, $newObjects)) {
// Object has been touched, do not delete
continue;
}
if (array_key_exists($key, $this->objects)) {
$object = $this->objects[$key];
if (! $object->hasBeenModified()) {
$object->markForRemoval();
}
}
}
2016-02-22 11:01:37 +01:00
Benchmark::measure('Done marking objects for purge');
foreach ($this->objects as $key => $object) {
if (! $object->hasBeenModified() && ! $object->shouldBeRemoved()) {
2016-02-22 11:01:37 +01:00
$noAction[] = $key;
}
}
2016-02-22 11:01:37 +01:00
foreach ($noAction as $key) {
2016-02-23 17:47:18 +01:00
unset($this->objects[$key]);
2016-02-22 11:01:37 +01:00
}
2016-02-24 12:24:19 +01:00
$this->isPrepared = true;
Benchmark::measure('Done preparing objects');
2016-02-23 17:47:18 +01:00
return $this->objects;
}
/**
* @param $key
* @param DbObject|IcingaObject $object
* @throws IcingaException
* @throws \Icinga\Exception\ProgrammingError
*/
protected function processObject($key, $object)
{
if (array_key_exists($key, $this->objects)) {
$this->refreshObject($key, $object);
} else {
$this->addNewObject($key, $object);
}
}
/**
* @param $key
* @param DbObject|IcingaObject $object
* @throws IcingaException
* @throws \Icinga\Exception\ProgrammingError
*/
protected function refreshObject($key, $object)
{
$policy = $this->rule->get('update_policy');
switch ($policy) {
case 'override':
$this->objects[$key]->replaceWith($object);
break;
case 'merge':
// TODO: re-evaluate merge settings. vars.x instead of
// just "vars" might suffice.
$this->objects[$key]->merge($object, $this->replaceVars);
if (! $this->hasPropertyDisabled && $object->hasProperty('disabled')) {
$this->objects[$key]->resetProperty('disabled');
}
break;
default:
// policy 'ignore', no action
}
if ($policy === 'override' || $policy === 'merge') {
if ($object instanceof IcingaHost) {
$keyName = $this->serviceOverrideKeyName;
if (! $object->hasInitializedVars() || ! isset($object->vars()->$key)) {
$this->objects[$key]->vars()->restoreStoredVar($keyName);
}
}
}
}
/**
* @param $key
* @param DbObject|IcingaObject $object
*/
protected function addNewObject($key, $object)
{
$this->objects[$key] = $object;
}
/**
* Runs a SyncRule and applies all resulting changes
* @return int
* @throws Exception
* @throws IcingaException
*/
public function apply()
{
Benchmark::measure('Begin applying objects');
2016-02-23 13:41:19 +01:00
$objects = $this->prepare();
2016-02-24 12:24:19 +01:00
$db = $this->db;
$dba = $db->getDbAdapter();
$dba->beginTransaction();
$object = null;
try {
$formerActivityChecksum = Util::hex2binary(
$db->getLastActivityChecksum()
);
$created = 0;
$modified = 0;
$deleted = 0;
$failed = 0;
foreach ($objects as $object) {
$this->setResolver($object);
if ($object->shouldBeRemoved()) {
2017-08-21 21:53:19 +02:00
$object->delete();
$deleted++;
continue;
}
2015-12-04 10:24:54 +01:00
if ($object->hasBeenModified()) {
$existing = $object->hasBeenLoadedFromDb();
$object->store($db);
if ($existing) {
$modified++;
} else {
$created++;
}
2016-02-24 12:24:19 +01:00
}
2015-11-02 09:29:03 +01:00
}
2015-10-20 22:22:58 +02:00
$runProperties = [
'objects_created' => $created,
'objects_deleted' => $deleted,
'objects_modified' => $modified,
];
2016-02-24 12:24:19 +01:00
if ($created + $deleted + $modified > 0) {
// TODO: What if this has been the very first activity?
$runProperties['last_former_activity'] = $db->quoteBinary($formerActivityChecksum);
$runProperties['last_related_activity'] = $db->quoteBinary(Util::hex2binary(
$db->getLastActivityChecksum()
));
}
$this->run->setProperties($runProperties)->store();
$this->notifyResolvers();
$dba->commit();
2016-02-24 12:24:19 +01:00
// Store duration after commit, as the commit might take some time
$this->run->set('duration_ms', (int) round(
(microtime(true) - $this->runStartTime) * 1000
))->store();
Benchmark::measure('Done applying objects');
} catch (Exception $e) {
$dba->rollBack();
if ($object !== null && $object instanceof IcingaObject) {
throw new IcingaException(
'Exception while syncing %s %s: %s',
2017-01-13 19:47:54 +01:00
get_class($object),
$object->get('object_name'),
$e->getMessage(),
$e
);
2017-01-13 19:47:54 +01:00
} else {
throw $e;
}
}
2016-02-24 12:24:19 +01:00
return $this->run->id;
}
protected function prepareCache()
{
PrefetchCache::initialize($this->db);
$dummy = IcingaObject::createByType($this->rule->object_type);
if ($dummy instanceof IcingaObject) {
IcingaObject::prefetchAllRelationsByType($this->rule->object_type, $this->db);
}
return $this;
}
}