380 lines
10 KiB
PHP
380 lines
10 KiB
PHP
<?php
|
|
|
|
namespace Icinga\Module\Director\Db\Branch;
|
|
|
|
use Icinga\Authentication\Auth;
|
|
use Icinga\Exception\NotFoundError;
|
|
use Icinga\Module\Director\Data\Db\DbObject;
|
|
use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry;
|
|
use Icinga\Module\Director\Data\Json;
|
|
use Icinga\Module\Director\Data\SerializableValue;
|
|
use Icinga\Module\Director\Db;
|
|
use Icinga\Module\Director\Objects\IcingaObject;
|
|
use InvalidArgumentException;
|
|
use Ramsey\Uuid\Uuid;
|
|
use Ramsey\Uuid\UuidInterface;
|
|
use RuntimeException;
|
|
|
|
class BranchActivity
|
|
{
|
|
const DB_TABLE = 'director_branch_activity';
|
|
|
|
const ACTION_CREATE = 'create';
|
|
const ACTION_MODIFY = 'modify';
|
|
const ACTION_DELETE = 'delete';
|
|
|
|
/** @var int */
|
|
protected $timestampNs;
|
|
|
|
/** @var UuidInterface */
|
|
protected $objectUuid;
|
|
|
|
/** @var UuidInterface */
|
|
protected $branchUuid;
|
|
|
|
/** @var string create, modify, delete */
|
|
protected $action;
|
|
|
|
/** @var string */
|
|
protected $objectTable;
|
|
|
|
/** @var string */
|
|
protected $author;
|
|
|
|
/** @var SerializableValue */
|
|
protected $modifiedProperties;
|
|
|
|
/** @var ?SerializableValue */
|
|
protected $formerProperties;
|
|
|
|
public function __construct(
|
|
UuidInterface $objectUuid,
|
|
UuidInterface $branchUuid,
|
|
$action,
|
|
$objectType,
|
|
$author,
|
|
SerializableValue $modifiedProperties,
|
|
SerializableValue $formerProperties
|
|
) {
|
|
$this->objectUuid = $objectUuid;
|
|
$this->branchUuid = $branchUuid;
|
|
$this->action = $action;
|
|
$this->objectTable = $objectType;
|
|
$this->author = $author;
|
|
$this->modifiedProperties = $modifiedProperties;
|
|
$this->formerProperties = $formerProperties;
|
|
}
|
|
|
|
public static function deleteObject(DbObject $object, Branch $branch)
|
|
{
|
|
return new static(
|
|
$object->getUniqueId(),
|
|
$branch->getUuid(),
|
|
self::ACTION_DELETE,
|
|
$object->getTableName(),
|
|
Auth::getInstance()->getUser()->getUsername(),
|
|
SerializableValue::fromSerialization(null),
|
|
SerializableValue::fromSerialization(self::getFormerObjectProperties($object))
|
|
);
|
|
}
|
|
|
|
public static function forDbObject(DbObject $object, Branch $branch)
|
|
{
|
|
if (! $object->hasBeenModified()) {
|
|
throw new InvalidArgumentException('Cannot get modifications for unmodified object');
|
|
}
|
|
if (! $branch->isBranch()) {
|
|
throw new InvalidArgumentException('Branch activity requires an active branch');
|
|
}
|
|
|
|
$author = Auth::getInstance()->getUser()->getUsername();
|
|
if ($object instanceof IcingaObject && $object->shouldBeRemoved()) {
|
|
$action = self::ACTION_DELETE;
|
|
$old = self::getFormerObjectProperties($object);
|
|
$new = null;
|
|
} elseif ($object->hasBeenLoadedFromDb()) {
|
|
$action = self::ACTION_MODIFY;
|
|
$old = self::getFormerObjectProperties($object);
|
|
$new = self::getObjectProperties($object);
|
|
} else {
|
|
$action = self::ACTION_CREATE;
|
|
$old = null;
|
|
$new = self::getObjectProperties($object);
|
|
}
|
|
|
|
if ($new !== null) {
|
|
$new = PlainObjectPropertyDiff::calculate(
|
|
$old,
|
|
$new
|
|
);
|
|
}
|
|
|
|
return new static(
|
|
$object->getUniqueId(),
|
|
$branch->getUuid(),
|
|
$action,
|
|
$object->getTableName(),
|
|
$author,
|
|
SerializableValue::fromSerialization($new),
|
|
SerializableValue::fromSerialization($old)
|
|
);
|
|
}
|
|
|
|
public function applyToDbObject(DbObject $object)
|
|
{
|
|
if (!$this->isActionModify()) {
|
|
throw new RuntimeException('Only BranchActivity instances with action=modify can be applied');
|
|
}
|
|
|
|
foreach ($this->getModifiedProperties()->jsonSerialize() as $key => $value) {
|
|
$object->set($key, $value);
|
|
}
|
|
|
|
return $object;
|
|
}
|
|
|
|
/**
|
|
* Hint: $connection is required, because setting groups triggered loading them.
|
|
* Should be investigated, as in theory $hostWithoutConnection->groups = 'group'
|
|
* is expected to work
|
|
* @param Db $connection
|
|
* @return DbObject|string
|
|
*/
|
|
public function createDbObject(Db $connection)
|
|
{
|
|
if (!$this->isActionCreate()) {
|
|
throw new RuntimeException('Only BranchActivity instances with action=create can create objects');
|
|
}
|
|
|
|
$class = DbObjectTypeRegistry::classByType($this->getObjectTable());
|
|
$object = $class::create([], $connection);
|
|
$object->setUniqueId($this->getObjectUuid());
|
|
foreach ($this->getModifiedProperties()->jsonSerialize() as $key => $value) {
|
|
$object->set($key, $value);
|
|
}
|
|
|
|
return $object;
|
|
}
|
|
|
|
public function deleteDbObject(DbObject $object)
|
|
{
|
|
if (!$this->isActionDelete()) {
|
|
throw new RuntimeException('Only BranchActivity instances with action=delete can delete objects');
|
|
}
|
|
|
|
return $object->delete();
|
|
}
|
|
|
|
public static function load($ts, Db $connection)
|
|
{
|
|
$db = $connection->getDbAdapter();
|
|
$row = $db->fetchRow(
|
|
$db->select()->from('director_branch_activity')->where('timestamp_ns = ?', $ts)
|
|
);
|
|
|
|
if ($row) {
|
|
return static::fromDbRow($row);
|
|
}
|
|
|
|
throw new NotFoundError('Not found');
|
|
}
|
|
|
|
protected static function fixPgResource(&$value)
|
|
{
|
|
if (is_resource($value)) {
|
|
$value = stream_get_contents($value);
|
|
}
|
|
}
|
|
|
|
public static function fromDbRow($row)
|
|
{
|
|
static::fixPgResource($row->object_uuid);
|
|
static::fixPgResource($row->branch_uuid);
|
|
$activity = new static(
|
|
Uuid::fromBytes($row->object_uuid),
|
|
Uuid::fromBytes($row->branch_uuid),
|
|
$row->action,
|
|
$row->object_table,
|
|
$row->author,
|
|
SerializableValue::fromSerialization(Json::decodeOptional($row->modified_properties)),
|
|
SerializableValue::fromSerialization(Json::decodeOptional($row->former_properties))
|
|
);
|
|
$activity->timestampNs = $row->timestamp_ns;
|
|
|
|
return $activity;
|
|
}
|
|
|
|
/**
|
|
* Must be run in a transaction! Repeatable read?
|
|
* @param Db $connection
|
|
* @throws \Icinga\Module\Director\Exception\JsonEncodeException
|
|
* @throws \Zend_Db_Adapter_Exception
|
|
*/
|
|
public function store(Db $connection)
|
|
{
|
|
if ($this->timestampNs !== null) {
|
|
throw new InvalidArgumentException(sprintf(
|
|
'Cannot store activity with a given timestamp: %s',
|
|
$this->timestampNs
|
|
));
|
|
}
|
|
$db = $connection->getDbAdapter();
|
|
$last = $db->fetchRow(
|
|
$db->select()->from('director_branch_activity', ['timestamp_ns' => 'MAX(timestamp_ns)'])
|
|
);
|
|
if (PHP_INT_SIZE !== 8) {
|
|
throw new RuntimeException('PHP with 64bit integer support is required');
|
|
}
|
|
$timestampNs = (int) floor(microtime(true) * 1000000);
|
|
if ($last) {
|
|
if ($last->timestamp_ns >= $timestampNs) {
|
|
$timestampNs = $last + 1;
|
|
}
|
|
}
|
|
$old = Json::encode($this->formerProperties);
|
|
$new = Json::encode($this->modifiedProperties);
|
|
|
|
$db->insert(self::DB_TABLE, [
|
|
'timestamp_ns' => $timestampNs,
|
|
'object_uuid' => $connection->quoteBinary($this->objectUuid->getBytes()),
|
|
'branch_uuid' => $connection->quoteBinary($this->branchUuid->getBytes()),
|
|
'action' => $this->action,
|
|
'object_table' => $this->objectTable,
|
|
'author' => $this->author,
|
|
'former_properties' => $old,
|
|
'modified_properties' => $new,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @return int
|
|
*/
|
|
public function getTimestampNs()
|
|
{
|
|
return $this->timestampNs;
|
|
}
|
|
|
|
/**
|
|
* @return int
|
|
*/
|
|
public function getTimestamp()
|
|
{
|
|
return (int) floor($this->timestampNs / 1000000);
|
|
}
|
|
|
|
/**
|
|
* @return UuidInterface
|
|
*/
|
|
public function getObjectUuid()
|
|
{
|
|
return $this->objectUuid;
|
|
}
|
|
|
|
/**
|
|
* @return UuidInterface
|
|
*/
|
|
public function getBranchUuid()
|
|
{
|
|
return $this->branchUuid;
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function getObjectName()
|
|
{
|
|
return $this->getProperty('object_name', 'unknown object name');
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function getAction()
|
|
{
|
|
return $this->action;
|
|
}
|
|
|
|
public function isActionDelete()
|
|
{
|
|
return $this->action === self::ACTION_DELETE;
|
|
}
|
|
|
|
public function isActionCreate()
|
|
{
|
|
return $this->action === self::ACTION_CREATE;
|
|
}
|
|
|
|
public function isActionModify()
|
|
{
|
|
return $this->action === self::ACTION_MODIFY;
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function getObjectTable()
|
|
{
|
|
return $this->objectTable;
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function getAuthor()
|
|
{
|
|
return $this->author;
|
|
}
|
|
|
|
/**
|
|
* @return ?SerializableValue
|
|
*/
|
|
public function getModifiedProperties()
|
|
{
|
|
return $this->modifiedProperties;
|
|
}
|
|
|
|
/**
|
|
* @return ?SerializableValue
|
|
*/
|
|
public function getFormerProperties()
|
|
{
|
|
return $this->formerProperties;
|
|
}
|
|
|
|
public function getProperty($key, $default = null)
|
|
{
|
|
if ($this->modifiedProperties) {
|
|
$properties = $this->modifiedProperties->jsonSerialize();
|
|
if (isset($properties->$key)) {
|
|
return $properties->$key;
|
|
}
|
|
}
|
|
if ($this->formerProperties) {
|
|
$properties = $this->formerProperties->jsonSerialize();
|
|
if (isset($properties->$key)) {
|
|
return $properties->$key;
|
|
}
|
|
}
|
|
|
|
return $default;
|
|
}
|
|
|
|
protected static function getFormerObjectProperties(DbObject $object)
|
|
{
|
|
if (! $object instanceof IcingaObject) {
|
|
throw new RuntimeException('Plain object helpers for DbObject must be implemented');
|
|
}
|
|
|
|
return (array) $object->getPlainUnmodifiedObject();
|
|
}
|
|
|
|
protected static function getObjectProperties(DbObject $object)
|
|
{
|
|
if (! $object instanceof IcingaObject) {
|
|
throw new RuntimeException('Plain object helpers for DbObject must be implemented');
|
|
}
|
|
|
|
return (array) $object->toPlainObject(false, true);
|
|
}
|
|
}
|