You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
565 lines
15 KiB
565 lines
15 KiB
1 year ago
|
<?php declare(strict_types = 0);
|
||
|
/*
|
||
|
** Zabbix
|
||
|
** Copyright (C) 2001-2023 Zabbix SIA
|
||
|
**
|
||
|
** This program is free software; you can redistribute it and/or modify
|
||
|
** it under the terms of the GNU General Public License as published by
|
||
|
** the Free Software Foundation; either version 2 of the License, or
|
||
|
** (at your option) any later version.
|
||
|
**
|
||
|
** This program is distributed in the hope that it will be useful,
|
||
|
** but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
** GNU General Public License for more details.
|
||
|
**
|
||
|
** You should have received a copy of the GNU General Public License
|
||
|
** along with this program; if not, write to the Free Software
|
||
|
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||
|
**/
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Class for preparing difference between import file and system.
|
||
|
*/
|
||
|
class CConfigurationImportcompare {
|
||
|
/**
|
||
|
* @var array
|
||
|
*/
|
||
|
protected $options;
|
||
|
|
||
|
/**
|
||
|
* @var array shows which elements in import array tree contains uuid
|
||
|
*/
|
||
|
protected $uuid_structure;
|
||
|
|
||
|
/**
|
||
|
* @var array contains unique fields path for import entities
|
||
|
*/
|
||
|
protected $unique_fields_keys_by_type;
|
||
|
|
||
|
/**
|
||
|
* CConfigurationImportcompare constructor.
|
||
|
*
|
||
|
* @param array $options import options "createMissing", "updateExisting" and "deleteMissing"
|
||
|
*/
|
||
|
public function __construct(array $options) {
|
||
|
$this->uuid_structure = [
|
||
|
'template_groups' => [],
|
||
|
'host_groups' => [],
|
||
|
'templates' => [
|
||
|
'items' => [
|
||
|
'triggers' => []
|
||
|
],
|
||
|
'discovery_rules' => [
|
||
|
'item_prototypes' => [
|
||
|
'trigger_prototypes' => []
|
||
|
],
|
||
|
'trigger_prototypes' => [],
|
||
|
'graph_prototypes' => [],
|
||
|
'host_prototypes' => []
|
||
|
],
|
||
|
'dashboards' => [],
|
||
|
'httptests' => [],
|
||
|
'valuemaps' => []
|
||
|
],
|
||
|
'triggers' => [],
|
||
|
'graphs' => []
|
||
|
];
|
||
|
|
||
|
$this->unique_fields_keys_by_type = [
|
||
|
'template_groups' => ['name'],
|
||
|
'host_groups' => ['name'],
|
||
|
'templates' => ['template'],
|
||
|
'items' => ['name', 'key'],
|
||
|
'triggers' => ['name', 'expression', 'recovery_expression'],
|
||
|
'dashboards' => ['name'],
|
||
|
'httptests' => ['name'],
|
||
|
'valuemaps' => ['name'],
|
||
|
'discovery_rules' => ['name', 'key'],
|
||
|
'item_prototypes' => ['name', 'key'],
|
||
|
'trigger_prototypes' => ['name', 'expression', 'recovery_expression'],
|
||
|
'graph_prototypes' => ['name', ['graph_items' => ['numeric_keys' => ['item' => 'host']]]],
|
||
|
'host_prototypes' => ['host'],
|
||
|
'graphs' => ['name', ['graph_items' => ['numeric_keys' => ['item' => 'host']]]]
|
||
|
];
|
||
|
|
||
|
$this->options = $options;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Performs comparison of import and export arrays and returns combined array that shows what was changed.
|
||
|
*
|
||
|
* @param array $export data exported from current system
|
||
|
* @param array $import data from import file
|
||
|
*
|
||
|
* @return array
|
||
|
*/
|
||
|
public function importcompare(array $export, array $import): array {
|
||
|
// Leave only template related keys.
|
||
|
$export = array_intersect_key($export, $this->uuid_structure);
|
||
|
$import = array_intersect_key($import, $this->uuid_structure);
|
||
|
|
||
|
return $this->compareByStructure($this->uuid_structure, $export, $import, $this->options);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create separate comparison for each structured object.
|
||
|
* Warning: Recursion.
|
||
|
*
|
||
|
* @param array $structure
|
||
|
* @param array $before
|
||
|
* @param array $after
|
||
|
* @param array $options
|
||
|
*
|
||
|
* @return array
|
||
|
*/
|
||
|
protected function compareByStructure(array $structure, array $before, array $after, array $options): array {
|
||
|
$result = [];
|
||
|
|
||
|
foreach ($structure as $key => $sub_structure) {
|
||
|
if ((!array_key_exists($key, $before) || !$before[$key])
|
||
|
&& (!array_key_exists($key, $after) || !$after[$key])) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Make sure, $key exists in both arrays.
|
||
|
$before += [$key => []];
|
||
|
$after += [$key => []];
|
||
|
|
||
|
$diff = $this->compareArrayByUniqueness($before[$key], $after[$key], $key);
|
||
|
|
||
|
|
||
|
if (array_key_exists('added', $diff)) {
|
||
|
foreach ($diff['added'] as &$entity) {
|
||
|
$entity = $this->compareByStructure($sub_structure, [], $entity, $options);
|
||
|
}
|
||
|
unset($entity);
|
||
|
}
|
||
|
|
||
|
if (array_key_exists('removed', $diff)) {
|
||
|
foreach ($diff['removed'] as &$entity) {
|
||
|
$entity = $this->compareByStructure($sub_structure, $entity, [], $options);
|
||
|
}
|
||
|
unset($entity);
|
||
|
}
|
||
|
|
||
|
if ($sub_structure && array_key_exists('updated', $diff)) {
|
||
|
foreach ($diff['updated'] as &$entity) {
|
||
|
$entity = $this->compareByStructure($sub_structure, $entity['before'], $entity['after'], $options);
|
||
|
}
|
||
|
unset($entity);
|
||
|
}
|
||
|
|
||
|
$diff = $this->applyOptions($options, $key, $diff);
|
||
|
|
||
|
unset($before[$key], $after[$key]);
|
||
|
|
||
|
if ($diff) {
|
||
|
$result[$key] = $diff;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$object = [];
|
||
|
|
||
|
if ($before) {
|
||
|
$object['before'] = $before;
|
||
|
}
|
||
|
|
||
|
if ($after) {
|
||
|
$object['after'] = $after;
|
||
|
}
|
||
|
|
||
|
if ($object) {
|
||
|
// Insert 'before' and/or 'after' at the beginning of array.
|
||
|
$result = array_merge($object, $result);
|
||
|
}
|
||
|
|
||
|
return $result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Compare two entities and separate all their keys into added/removed/updated.
|
||
|
* First entities gets compared by uuid then by its unique field values.
|
||
|
*
|
||
|
* @param array $before
|
||
|
* @param array $after
|
||
|
* @param string $type
|
||
|
*
|
||
|
* @return array
|
||
|
*/
|
||
|
protected function compareArrayByUniqueness(array $before, array $after, string $type): array {
|
||
|
if (!$before && !$after) {
|
||
|
return [];
|
||
|
}
|
||
|
|
||
|
$diff = [
|
||
|
'added' => [],
|
||
|
'removed' => [],
|
||
|
'updated' => []
|
||
|
];
|
||
|
|
||
|
$before = $this->addUniquenessParameterByEntityType($before, $type);
|
||
|
$after = $this->addUniquenessParameterByEntityType($after, $type);
|
||
|
|
||
|
$same_entities = [];
|
||
|
foreach ($after as $a_key => $after_entity) {
|
||
|
if (!array_key_exists('uuid', $after_entity)) {
|
||
|
unset($after[$a_key]);
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
foreach ($before as $b_key => $before_entity) {
|
||
|
if (array_key_exists('uuid', $before_entity) && $before_entity['uuid'] === $after_entity['uuid']) {
|
||
|
unset($before_entity['uniqueness'], $after_entity['uniqueness']);
|
||
|
|
||
|
$same_entities[$b_key]['before'] = $before_entity;
|
||
|
$same_entities[$b_key]['after'] = $after_entity;
|
||
|
|
||
|
unset($before[$b_key], $after[$a_key]);
|
||
|
continue 2;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
foreach ($before as $b_key => $before_entity) {
|
||
|
if ($before_entity['uniqueness'] === $after_entity['uniqueness']) {
|
||
|
unset($before_entity['uniqueness'], $after_entity['uniqueness']);
|
||
|
$before_entity['uuid'] = $after_entity['uuid'];
|
||
|
|
||
|
$same_entities[$b_key]['before'] = $before_entity;
|
||
|
$same_entities[$b_key]['after'] = $after_entity;
|
||
|
|
||
|
unset($before[$b_key], $after[$a_key]);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$removed_entities = $before;
|
||
|
$added_entities = $after;
|
||
|
|
||
|
foreach ($added_entities as $entity) {
|
||
|
unset($entity['uniqueness']);
|
||
|
|
||
|
$diff['added'][] = $entity;
|
||
|
}
|
||
|
|
||
|
foreach ($removed_entities as $entity) {
|
||
|
unset($entity['uniqueness']);
|
||
|
|
||
|
$diff['removed'][] = $entity;
|
||
|
}
|
||
|
|
||
|
foreach ($same_entities as $entity) {
|
||
|
$uuid = ['uuid' => null];
|
||
|
|
||
|
if (array_diff_key($entity['before'], $uuid) != array_diff_key($entity['after'], $uuid)) {
|
||
|
$diff['updated'][] = [
|
||
|
'before' => $entity['before'],
|
||
|
'after' => $entity['after']
|
||
|
];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
foreach (['added', 'removed', 'updated'] as $key) {
|
||
|
if (!$diff[$key]) {
|
||
|
unset($diff[$key]);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $diff;
|
||
|
}
|
||
|
|
||
|
private function addUniquenessParameterByEntityType(array $entities, string $type): array {
|
||
|
foreach ($entities as &$entity) {
|
||
|
foreach ($this->unique_fields_keys_by_type[$type] as $unique_field_key) {
|
||
|
$unique_values = $this->getUniqueValuesByFieldPath($entity, $unique_field_key);
|
||
|
|
||
|
$entity['uniqueness'][] = $unique_values;
|
||
|
}
|
||
|
// To make unique entity string, get result values, get rid of value duplicates and sort them.
|
||
|
$entity['uniqueness'] = array_unique($this->flatten($entity['uniqueness']));
|
||
|
sort($entity['uniqueness']);
|
||
|
|
||
|
$entity['uniqueness'] = implode('/', $entity['uniqueness']);
|
||
|
}
|
||
|
unset($entity);
|
||
|
|
||
|
return $entities;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get entity field values by giving field key path constructed.
|
||
|
*
|
||
|
* @param array $entity Entity.
|
||
|
* @param string|array $field_key Field key or field key path given.
|
||
|
*/
|
||
|
private function getUniqueValuesByFieldPath(array $entity, $field_key_path) {
|
||
|
if (is_array($field_key_path)) {
|
||
|
foreach ($field_key_path as $sub_key => $sub_field) {
|
||
|
if ($sub_key !== 'numeric_keys') {
|
||
|
$sub_entities = $entity[$sub_key];
|
||
|
}
|
||
|
else {
|
||
|
if (is_array($sub_field)) {
|
||
|
foreach ($sub_field as $key => $field) {
|
||
|
foreach ($entity as $sub_entity) {
|
||
|
$sub_entities[] = $sub_entity[$key];
|
||
|
}
|
||
|
|
||
|
$sub_field = $field;
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
$sub_entities = $entity;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$result = $this->getUniqueValuesByFieldPath($sub_entities, $sub_field);
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
if (array_key_exists($field_key_path, $entity)){
|
||
|
$result = $entity[$field_key_path];
|
||
|
}
|
||
|
else {
|
||
|
$result = array_column($entity, $field_key_path);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return multidimensional array as one dimensional array.
|
||
|
*
|
||
|
* @param array $array
|
||
|
*
|
||
|
* @return array
|
||
|
*/
|
||
|
private function flatten(array $array): array {
|
||
|
$result = [];
|
||
|
|
||
|
foreach ($array as $value) {
|
||
|
if (is_array($value)) {
|
||
|
$result = array_merge($result, self::flatten($value));
|
||
|
}
|
||
|
else {
|
||
|
$result[] = $value;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Compare two entities and separate all their keys into added/removed/updated.
|
||
|
*
|
||
|
* @param array $options import options
|
||
|
* @param string $entity_key key of entity being processed
|
||
|
* @param array $diff diff for this entity
|
||
|
*
|
||
|
* @return array
|
||
|
*/
|
||
|
protected function applyOptions(array $options, string $entity_key, array $diff): array {
|
||
|
$option_key_map = [
|
||
|
'template_groups' => 'template_groups',
|
||
|
'host_groups' => 'host_groups',
|
||
|
'group_links' => 'host_groups',
|
||
|
'groups' => 'template_groups',
|
||
|
'templates' => 'templates',
|
||
|
'items' => 'items',
|
||
|
'triggers' => 'triggers',
|
||
|
'discovery_rules' => 'discoveryRules',
|
||
|
'item_prototypes' => 'discoveryRules',
|
||
|
'trigger_prototypes' => 'discoveryRules',
|
||
|
'graph_prototypes' => 'discoveryRules',
|
||
|
'host_prototypes' => 'discoveryRules',
|
||
|
'dashboards' => 'templateDashboards',
|
||
|
'httptests' => 'httptests',
|
||
|
'valuemaps' => 'valueMaps',
|
||
|
'graphs' => 'graphs'
|
||
|
];
|
||
|
|
||
|
if (!array_key_exists($option_key_map[$entity_key], $options)) {
|
||
|
return [];
|
||
|
}
|
||
|
|
||
|
$entity_options = $options[$option_key_map[$entity_key]];
|
||
|
$stored_changes = [];
|
||
|
|
||
|
if ($entity_key === 'templates' && array_key_exists('updated', $diff)) {
|
||
|
$updated_count = count($diff['updated']);
|
||
|
for ($key = 0; $key < $updated_count; $key++) {
|
||
|
$entity = $diff['updated'][$key];
|
||
|
$has_before_templates = array_key_exists('templates', $entity['before']);
|
||
|
$has_after_templates = array_key_exists('templates', $entity['after']);
|
||
|
|
||
|
if (!$has_before_templates && !$has_after_templates) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if ($has_before_templates && !$has_after_templates) {
|
||
|
$entity['after']['templates'] = [];
|
||
|
|
||
|
// Make sure that processed entry is last in both arrays. Otherwise, it will break the comparison.
|
||
|
$before_templates = $entity['before']['templates'];
|
||
|
unset($entity['before']['templates']);
|
||
|
$entity['before']['templates'] = $before_templates;
|
||
|
}
|
||
|
elseif ($has_after_templates && !$has_before_templates) {
|
||
|
$entity['before']['templates'] = [];
|
||
|
}
|
||
|
|
||
|
if ($entity['before']['templates'] === $entity['after']['templates']) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if (!$options['templateLinkage']['createMissing'] && !$options['templateLinkage']['deleteMissing']) {
|
||
|
$entity['after']['templates'] = $entity['before']['templates'];
|
||
|
}
|
||
|
elseif ($options['templateLinkage']['createMissing'] && !$options['templateLinkage']['deleteMissing']) {
|
||
|
$entity['after']['templates'] = $this->afterForInnerCreateMissing($entity['before']['templates'],
|
||
|
$entity['after']['templates']);
|
||
|
}
|
||
|
elseif ($options['templateLinkage']['deleteMissing'] && !$options['templateLinkage']['createMissing']) {
|
||
|
$entity['after']['templates'] = $this->afterForInnerDeleteMissing($entity['before']['templates'],
|
||
|
$entity['after']['templates']);
|
||
|
}
|
||
|
|
||
|
if ($entity['before'] === $entity['after'] && count($entity) === 2) {
|
||
|
unset($diff['updated'][$key]);
|
||
|
}
|
||
|
else {
|
||
|
$stored_changes[$key]['templates'] = $entity['after']['templates'];
|
||
|
}
|
||
|
}
|
||
|
unset($entity);
|
||
|
|
||
|
if (!$diff['updated']) {
|
||
|
unset($diff['updated']);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!array_key_exists('createMissing', $entity_options) || !$entity_options['createMissing']) {
|
||
|
unset($diff['added']);
|
||
|
}
|
||
|
|
||
|
if (!array_key_exists('deleteMissing', $entity_options) || !$entity_options['deleteMissing']) {
|
||
|
unset($diff['removed']);
|
||
|
}
|
||
|
|
||
|
if (array_key_exists('updated', $diff)) {
|
||
|
$new_updated = [];
|
||
|
|
||
|
foreach ($diff['updated'] as $key => $entity) {
|
||
|
$has_inner_entities = array_flip(array_keys($entity));
|
||
|
unset($has_inner_entities['before'], $has_inner_entities['after']);
|
||
|
$has_inner_entities = count($has_inner_entities) > 0;
|
||
|
|
||
|
if ($has_inner_entities || array_key_exists($key, $stored_changes)
|
||
|
|| $entity['after'] !== $entity['before']) {
|
||
|
if (!array_key_exists('updateExisting', $entity_options) || !$entity_options['updateExisting']) {
|
||
|
$entity['after'] = $entity['before'];
|
||
|
}
|
||
|
$new_updated[] = $entity;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ($new_updated) {
|
||
|
$diff['updated'] = $new_updated;
|
||
|
}
|
||
|
else {
|
||
|
unset($diff['updated']);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ($stored_changes) {
|
||
|
foreach ($stored_changes as $key => $stored_entry) {
|
||
|
$entry = $diff['updated'][$key]['after'];
|
||
|
|
||
|
foreach ($stored_entry as $entry_key => $entry_after_value) {
|
||
|
if ($entry_after_value !== []) {
|
||
|
$entry[$entry_key] = $entry_after_value;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$diff['updated'][$key]['after'] = $entry;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (array_key_exists('updated', $diff)) {
|
||
|
// Reset keys.
|
||
|
$diff['updated'] = array_values($diff['updated']);
|
||
|
|
||
|
// Make sure, key order is same in 'before' and 'after' arrays.
|
||
|
foreach ($diff['updated'] as &$entity) {
|
||
|
$order = array_flip(array_keys($entity['before']));
|
||
|
$order = array_intersect_key($order, $entity['after']);
|
||
|
$entity['after'] = array_merge($order, $entity['after']);
|
||
|
}
|
||
|
unset($entity);
|
||
|
}
|
||
|
|
||
|
return $diff;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create "after" that contains all entries from "before" and "after" combined.
|
||
|
*
|
||
|
* @param array $before
|
||
|
* @param array $after
|
||
|
*
|
||
|
* @return array
|
||
|
*/
|
||
|
protected function afterForInnerCreateMissing(array $before, array $after): array {
|
||
|
$missing = [];
|
||
|
|
||
|
foreach ($after as $after_entity) {
|
||
|
$found = false;
|
||
|
|
||
|
foreach ($before as $before_entity) {
|
||
|
if ($before_entity === $after_entity) {
|
||
|
$found = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!$found) {
|
||
|
$missing[] = $after_entity;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return array_merge($before, $missing);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create "after" that contains only entries from "after" that were also present in "before".
|
||
|
*
|
||
|
* @param array $before
|
||
|
* @param array $after
|
||
|
*
|
||
|
* @return array
|
||
|
*/
|
||
|
protected function afterForInnerDeleteMissing(array $before, array $after): array {
|
||
|
$new_after = [];
|
||
|
|
||
|
foreach ($after as $after_entity) {
|
||
|
$found = false;
|
||
|
|
||
|
foreach ($before as $before_entity) {
|
||
|
if ($before_entity === $after_entity) {
|
||
|
$found = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ($found) {
|
||
|
$new_after[] = $after_entity;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $new_after;
|
||
|
}
|
||
|
}
|