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.
zabbix/ui/include/classes/import/CConfigurationImportcompare...

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