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