summaryrefslogtreecommitdiffstats
path: root/library/Director/Db/Branch/BranchActivity.php
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--library/Director/Db/Branch/BranchActivity.php390
1 files changed, 390 insertions, 0 deletions
diff --git a/library/Director/Db/Branch/BranchActivity.php b/library/Director/Db/Branch/BranchActivity.php
new file mode 100644
index 0000000..3812e75
--- /dev/null
+++ b/library/Director/Db/Branch/BranchActivity.php
@@ -0,0 +1,390 @@
+<?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\DirectorActivityLog;
+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 = DirectorActivityLog::ACTION_CREATE;
+ const ACTION_MODIFY = DirectorActivityLog::ACTION_MODIFY;
+ const ACTION_DELETE = DirectorActivityLog::ACTION_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 static function fixFakeTimestamp($timestampNs)
+ {
+ if ($timestampNs < 1600000000 * 1000000) {
+ // fake TS for cloned branch in sync preview
+ return (int) $timestampNs * 1000000;
+ }
+
+ return $timestampNs;
+ }
+
+ 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(BranchActivity::fixFakeTimestamp($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);
+ }
+}