482 lines
13 KiB
PHP
482 lines
13 KiB
PHP
<?php
|
|
|
|
namespace Icinga\Module\Director\Import;
|
|
|
|
use Exception;
|
|
use Icinga\Application\Benchmark;
|
|
use Icinga\Exception\IcingaException;
|
|
use Icinga\Module\Director\Data\RecursiveUtf8Validator;
|
|
use Icinga\Module\Director\Db;
|
|
use Icinga\Module\Director\Hook\ImportSourceHook;
|
|
use Icinga\Module\Director\Objects\ImportSource;
|
|
use Icinga\Module\Director\Util;
|
|
use stdClass;
|
|
|
|
class Import
|
|
{
|
|
/**
|
|
* @var ImportSource
|
|
*/
|
|
protected $source;
|
|
|
|
/**
|
|
* @var Db
|
|
*/
|
|
protected $connection;
|
|
|
|
/**
|
|
* @var \Zend_Db_Adapter_Abstract
|
|
*/
|
|
protected $db;
|
|
|
|
/**
|
|
* Raw data that should be imported, array of stdClass objects
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $data;
|
|
|
|
/**
|
|
* Checksum of the rowset that should be imported
|
|
*
|
|
* @var string
|
|
*/
|
|
private $rowsetChecksum;
|
|
|
|
/**
|
|
* Checksum-indexed rows
|
|
*
|
|
* @var array
|
|
*/
|
|
private $rows;
|
|
|
|
/**
|
|
* Checksum-indexed row -> property
|
|
*
|
|
* @var array
|
|
*/
|
|
private $rowProperties;
|
|
|
|
/**
|
|
* Whether this rowset exists, for caching purposes
|
|
*
|
|
* @var boolean
|
|
*/
|
|
private $rowsetExists;
|
|
|
|
protected $properties = array();
|
|
|
|
/**
|
|
* Checksums of all rows
|
|
*/
|
|
private $rowChecksums;
|
|
|
|
public function __construct(ImportSource $source)
|
|
{
|
|
$this->source = $source;
|
|
$this->connection = $source->getConnection();
|
|
$this->db = $this->connection->getDbAdapter();
|
|
}
|
|
|
|
/**
|
|
* Whether this import provides modified data
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function providesChanges()
|
|
{
|
|
return ! $this->rowsetExists()
|
|
|| ! $this->lastRowsetIs($this->rowsetChecksum());
|
|
}
|
|
|
|
/**
|
|
* Trigger an import run
|
|
*
|
|
* @return int Last import run ID
|
|
*/
|
|
public function run()
|
|
{
|
|
if ($this->providesChanges() && ! $this->rowsetExists()) {
|
|
$this->storeRowset();
|
|
}
|
|
|
|
$this->db->insert(
|
|
'import_run',
|
|
array(
|
|
'source_id' => $this->source->get('id'),
|
|
'rowset_checksum' => $this->quoteBinary($this->rowsetChecksum()),
|
|
'start_time' => date('Y-m-d H:i:s'),
|
|
'succeeded' => 'y'
|
|
)
|
|
);
|
|
if ($this->connection->isPgsql()) {
|
|
return $this->db->lastInsertId('import_run', 'id');
|
|
} else {
|
|
return $this->db->lastInsertId();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Whether there are no rows to be fetched from import source
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function isEmpty()
|
|
{
|
|
$rows = $this->checksummedRows();
|
|
return empty($rows);
|
|
}
|
|
|
|
/**
|
|
* Checksum of all available rows
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function & rowsetChecksum()
|
|
{
|
|
if ($this->rowsetChecksum === null) {
|
|
$this->prepareChecksummedRows();
|
|
}
|
|
|
|
return $this->rowsetChecksum;
|
|
}
|
|
|
|
/**
|
|
* All rows
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function & checksummedRows()
|
|
{
|
|
if ($this->rows === null) {
|
|
$this->prepareChecksummedRows();
|
|
}
|
|
|
|
return $this->rows;
|
|
}
|
|
|
|
/**
|
|
* Checksum of all available rows
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function & rawData()
|
|
{
|
|
if ($this->data === null) {
|
|
$this->data = ImportSourceHook::forImportSource(
|
|
$this->source
|
|
)->fetchData();
|
|
Benchmark::measure('Fetched all data from Import Source');
|
|
$this->source->applyModifiers($this->data);
|
|
Benchmark::measure('Applied Property Modifiers to imported data');
|
|
}
|
|
|
|
return $this->data;
|
|
}
|
|
|
|
/**
|
|
* Prepare and remember an ImportedProperty
|
|
*
|
|
* @param string $key
|
|
* @param mixed $rawValue
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function prepareImportedProperty($key, $rawValue)
|
|
{
|
|
if (is_array($rawValue) || is_bool($rawValue) || is_int($rawValue) || is_float($rawValue)) {
|
|
$value = json_encode($rawValue);
|
|
$format = 'json';
|
|
} elseif ($rawValue instanceof stdClass) {
|
|
$value = json_encode($this->sortObject($rawValue));
|
|
$format = 'json';
|
|
} else {
|
|
$value = $rawValue;
|
|
$format = 'string';
|
|
}
|
|
|
|
$checksum = sha1(sprintf('%s=(%s)%s', $key, $format, $value), true);
|
|
|
|
if (! array_key_exists($checksum, $this->properties)) {
|
|
$this->properties[$checksum] = array(
|
|
'checksum' => $this->quoteBinary($checksum),
|
|
'property_name' => $key,
|
|
'property_value' => $value,
|
|
'format' => $format
|
|
);
|
|
}
|
|
|
|
return $this->properties[$checksum];
|
|
}
|
|
|
|
/**
|
|
* Walk through each row, prepare properties and calculate checksums
|
|
*/
|
|
protected function prepareChecksummedRows()
|
|
{
|
|
$keyColumn = $this->source->get('key_column');
|
|
$this->rows = array();
|
|
$this->rowProperties = array();
|
|
$objects = array();
|
|
$rowCount = 0;
|
|
|
|
foreach ($this->rawData() as $row) {
|
|
$rowCount++;
|
|
|
|
// Key column must be set
|
|
if (! isset($row->$keyColumn)) {
|
|
throw new IcingaException(
|
|
'No key column "%s" in row %d',
|
|
$keyColumn,
|
|
$rowCount
|
|
);
|
|
}
|
|
|
|
$object_name = $row->$keyColumn;
|
|
|
|
// Check for name collision
|
|
if (array_key_exists($object_name, $objects)) {
|
|
throw new IcingaException(
|
|
'Duplicate entry: %s',
|
|
$object_name
|
|
);
|
|
}
|
|
|
|
$rowChecksums = array();
|
|
$keys = array_keys((array) $row);
|
|
sort($keys);
|
|
|
|
foreach ($keys as $key) {
|
|
// TODO: Specify how to treat NULL values. Ignoring for now.
|
|
// One option might be to import null (checksum '(null)')
|
|
// and to provide a flag at sync time
|
|
if ($row->$key === null) {
|
|
continue;
|
|
}
|
|
|
|
$property = $this->prepareImportedProperty($key, $row->$key);
|
|
$rowChecksums[] = $property['checksum'];
|
|
}
|
|
|
|
$checksum = sha1($object_name . ';' . implode(';', $rowChecksums), true);
|
|
if (array_key_exists($checksum, $this->rows)) {
|
|
die('WTF, collision?');
|
|
}
|
|
|
|
$this->rows[$checksum] = array(
|
|
'checksum' => $this->quoteBinary($checksum),
|
|
'object_name' => $object_name
|
|
);
|
|
|
|
$this->rowProperties[$checksum] = $rowChecksums;
|
|
|
|
$objects[$object_name] = $checksum;
|
|
}
|
|
|
|
$this->rowChecksums = array_keys($this->rows);
|
|
$this->rowsetChecksum = sha1(implode(';', $this->rowChecksums), true);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Store our new rowset
|
|
*/
|
|
protected function storeRowset()
|
|
{
|
|
$db = $this->db;
|
|
$rowset = $this->rowsetChecksum();
|
|
$rows = $this->checksummedRows();
|
|
|
|
$db->beginTransaction();
|
|
|
|
try {
|
|
if ($this->isEmpty()) {
|
|
$newRows = array();
|
|
$newProperties = array();
|
|
} else {
|
|
$newRows = $this->newChecksums('imported_row', $this->rowChecksums);
|
|
$newProperties = $this->newChecksums('imported_property', array_keys($this->properties));
|
|
}
|
|
|
|
$db->insert('imported_rowset', array('checksum' => $this->quoteBinary($rowset)));
|
|
|
|
foreach ($newProperties as $checksum) {
|
|
$db->insert('imported_property', $this->properties[$checksum]);
|
|
}
|
|
|
|
foreach ($newRows as $row) {
|
|
try {
|
|
$db->insert('imported_row', $rows[$row]);
|
|
foreach ($this->rowProperties[$row] as $property) {
|
|
$db->insert('imported_row_property', array(
|
|
'row_checksum' => $this->quoteBinary($row),
|
|
'property_checksum' => $property
|
|
));
|
|
}
|
|
} catch (Exception $e) {
|
|
throw new IcingaException(
|
|
"Error while storing a row for '%s' into database: %s",
|
|
$rows[$row]['object_name'],
|
|
$e->getMessage()
|
|
);
|
|
}
|
|
}
|
|
|
|
foreach (array_keys($rows) as $row) {
|
|
$db->insert(
|
|
'imported_rowset_row',
|
|
array(
|
|
'rowset_checksum' => $this->quoteBinary($rowset),
|
|
'row_checksum' => $this->quoteBinary($row)
|
|
)
|
|
);
|
|
}
|
|
|
|
$db->commit();
|
|
|
|
$this->rowsetExists = true;
|
|
} catch (Exception $e) {
|
|
try {
|
|
$db->rollBack();
|
|
} catch (Exception $e) {
|
|
// Well...
|
|
}
|
|
// Eventually throws details for invalid UTF8 characters
|
|
RecursiveUtf8Validator::validateRows($this->data);
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Whether the last run of this import matches the given checksum
|
|
*
|
|
* @param string $checksum Binary checksum
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function lastRowsetIs($checksum)
|
|
{
|
|
return $this->connection->getLatestImportedChecksum($this->source->get('id'))
|
|
=== bin2hex($checksum);
|
|
}
|
|
|
|
/**
|
|
* Whether our rowset already exists in the database
|
|
*
|
|
* @return boolean
|
|
*/
|
|
protected function rowsetExists()
|
|
{
|
|
if (null === $this->rowsetExists) {
|
|
$this->rowsetExists = 0 === count(
|
|
$this->newChecksums(
|
|
'imported_rowset',
|
|
array($this->rowsetChecksum())
|
|
)
|
|
);
|
|
}
|
|
|
|
return $this->rowsetExists;
|
|
}
|
|
|
|
/**
|
|
* Finde new checksums for a specific table
|
|
*
|
|
* Accepts an array of checksums and gives you an array with those checksums
|
|
* that are missing in the given table
|
|
*
|
|
* @param string $table Database table name
|
|
* @param array $checksums Array with the checksums that should be verified
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function newChecksums($table, $checksums)
|
|
{
|
|
$db = $this->db;
|
|
|
|
// TODO: The following is a quickfix for binary data corrpution reported
|
|
// in https://github.com/zendframework/zf1/issues/655 caused by
|
|
// https://github.com/zendframework/zf1/commit/2ac9c30f
|
|
//
|
|
// Should be reverted once fixed, eventually with a check continueing
|
|
// to use this workaround for specific ZF versions (1.12.16 and 1.12.17
|
|
// so far). Alternatively we could also use a custom quoteInto method.
|
|
|
|
// The former query looked as follows:
|
|
//
|
|
// $query = $db->select()->from($table, 'checksum')
|
|
// ->where('checksum IN (?)', $checksums)
|
|
// ...
|
|
// return array_diff($checksums, $existing);
|
|
|
|
$hexed = array_map('bin2hex', $checksums);
|
|
|
|
$conn = $this->connection;
|
|
$query = $db
|
|
->select()
|
|
->from(
|
|
array('c' => $table),
|
|
array('checksum' => $conn->dbHexFunc('c.checksum'))
|
|
)->where(
|
|
$conn->dbHexFunc('c.checksum') . ' IN (?)',
|
|
$hexed
|
|
);
|
|
|
|
$existing = $db->fetchCol($query);
|
|
$new = array_diff($hexed, $existing);
|
|
|
|
return array_map('hex2bin', $new);
|
|
}
|
|
|
|
/**
|
|
* Sort a given stdClass object by property name
|
|
*
|
|
* @param stdClass $object
|
|
*
|
|
* @return object
|
|
*/
|
|
protected function sortObject($object)
|
|
{
|
|
$array = (array) $object;
|
|
foreach ($array as $key => $val) {
|
|
$this->sortElement($val);
|
|
}
|
|
ksort($array);
|
|
return (object) $array;
|
|
}
|
|
|
|
/**
|
|
* Walk through a given array and sort all children
|
|
*
|
|
* Please note that the array itself will NOT be sorted, as arrays must
|
|
* keep their ordering
|
|
*
|
|
* @param array $array
|
|
*/
|
|
protected function sortArrayObject(&$array)
|
|
{
|
|
foreach ($array as $key => $val) {
|
|
$this->sortElement($val);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Recursively sort a given property
|
|
*
|
|
* @param mixed $el
|
|
*/
|
|
protected function sortElement(&$el)
|
|
{
|
|
if (is_array($el)) {
|
|
$this->sortArrayObject($el);
|
|
} elseif ($el instanceof stdClass) {
|
|
$el = $this->sortObject($el);
|
|
}
|
|
}
|
|
|
|
protected function quoteBinary($bin)
|
|
{
|
|
return $this->connection->quoteBinary($bin);
|
|
}
|
|
}
|