summaryrefslogtreecommitdiffstats
path: root/library/Icingadb/Command
diff options
context:
space:
mode:
Diffstat (limited to 'library/Icingadb/Command')
-rw-r--r--library/Icingadb/Command/IcingaApiCommand.php128
-rw-r--r--library/Icingadb/Command/IcingaCommand.php22
-rw-r--r--library/Icingadb/Command/Instance/ToggleInstanceFeatureCommand.php109
-rw-r--r--library/Icingadb/Command/Object/AcknowledgeProblemCommand.php140
-rw-r--r--library/Icingadb/Command/Object/AddCommentCommand.php42
-rw-r--r--library/Icingadb/Command/Object/CommandAuthor.php45
-rw-r--r--library/Icingadb/Command/Object/DeleteCommentCommand.php52
-rw-r--r--library/Icingadb/Command/Object/DeleteDowntimeCommand.php58
-rw-r--r--library/Icingadb/Command/Object/GetObjectCommand.php71
-rw-r--r--library/Icingadb/Command/Object/ObjectCommand.php51
-rw-r--r--library/Icingadb/Command/Object/ProcessCheckResultCommand.php140
-rw-r--r--library/Icingadb/Command/Object/PropagateHostDowntimeCommand.php42
-rw-r--r--library/Icingadb/Command/Object/RemoveAcknowledgementCommand.php13
-rw-r--r--library/Icingadb/Command/Object/ScheduleCheckCommand.php86
-rw-r--r--library/Icingadb/Command/Object/ScheduleHostDowntimeCommand.php42
-rw-r--r--library/Icingadb/Command/Object/ScheduleServiceDowntimeCommand.php196
-rw-r--r--library/Icingadb/Command/Object/SendCustomNotificationCommand.php44
-rw-r--r--library/Icingadb/Command/Object/ToggleObjectFeatureCommand.php108
-rw-r--r--library/Icingadb/Command/Object/WithCommentCommand.php50
-rw-r--r--library/Icingadb/Command/Renderer/IcingaApiCommandRenderer.php322
-rw-r--r--library/Icingadb/Command/Renderer/IcingaCommandRendererInterface.php12
-rw-r--r--library/Icingadb/Command/Transport/ApiCommandException.php14
-rw-r--r--library/Icingadb/Command/Transport/ApiCommandTransport.php353
-rw-r--r--library/Icingadb/Command/Transport/CommandTransport.php130
-rw-r--r--library/Icingadb/Command/Transport/CommandTransportConfig.php31
-rw-r--r--library/Icingadb/Command/Transport/CommandTransportException.php14
-rw-r--r--library/Icingadb/Command/Transport/CommandTransportInterface.php23
27 files changed, 2338 insertions, 0 deletions
diff --git a/library/Icingadb/Command/IcingaApiCommand.php b/library/Icingadb/Command/IcingaApiCommand.php
new file mode 100644
index 0000000..f3f0c33
--- /dev/null
+++ b/library/Icingadb/Command/IcingaApiCommand.php
@@ -0,0 +1,128 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Command;
+
+class IcingaApiCommand
+{
+ /**
+ * Command data
+ *
+ * @var array
+ */
+ protected $data;
+
+ /**
+ * Name of the endpoint
+ *
+ * @var string
+ */
+ protected $endpoint;
+
+ /**
+ * HTTP method to use
+ *
+ * @var string
+ */
+ protected $method = 'POST';
+
+ /**
+ * Create a new Icinga 2 API command
+ *
+ * @param string $endpoint
+ * @param array $data
+ *
+ * @return static
+ */
+ public static function create(string $endpoint, array $data): self
+ {
+ return (new static())
+ ->setEndpoint($endpoint)
+ ->setData($data);
+ }
+
+ /**
+ * Get the command data
+ *
+ * @return array
+ */
+ public function getData(): array
+ {
+ if ($this->data === null) {
+ throw new \LogicException(
+ 'You are accessing an unset property. Please make sure to set it beforehand.'
+ );
+ }
+
+ return $this->data;
+ }
+
+ /**
+ * Set the command data
+ *
+ * @param array $data
+ *
+ * @return $this
+ */
+ public function setData(array $data): self
+ {
+ $this->data = $data;
+
+ return $this;
+ }
+
+ /**
+ * Get the name of the endpoint
+ *
+ * @return string
+ */
+ public function getEndpoint(): string
+ {
+ if ($this->endpoint === null) {
+ throw new \LogicException(
+ 'You are accessing an unset property. Please make sure to set it beforehand.'
+ );
+ }
+
+ return $this->endpoint;
+ }
+
+ /**
+ * Set the name of the endpoint
+ *
+ * @param string $endpoint
+ *
+ * @return $this
+ */
+ public function setEndpoint(string $endpoint): self
+ {
+ $this->endpoint = $endpoint;
+
+ return $this;
+ }
+
+ /**
+ * Get the HTTP method to use
+ *
+ * @return string
+ */
+ public function getMethod(): string
+ {
+ return $this->method;
+ }
+
+ /**
+ * Set the HTTP method to use
+ *
+ * @param string $method All uppercase HTTP method name. Case-sensitive.
+ *
+ * @return $this
+ */
+ public function setMethod(string $method): self
+ {
+ $this->method = $method;
+
+ return $this;
+ }
+}
diff --git a/library/Icingadb/Command/IcingaCommand.php b/library/Icingadb/Command/IcingaCommand.php
new file mode 100644
index 0000000..7b5c5cf
--- /dev/null
+++ b/library/Icingadb/Command/IcingaCommand.php
@@ -0,0 +1,22 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Command;
+
+/**
+ * Base class for commands sent to an Icinga instance
+ */
+abstract class IcingaCommand
+{
+ /**
+ * Get the name of the command
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ $nsParts = explode('\\', get_called_class());
+ return substr_replace(end($nsParts), '', -7); // Remove 'Command' Suffix
+ }
+}
diff --git a/library/Icingadb/Command/Instance/ToggleInstanceFeatureCommand.php b/library/Icingadb/Command/Instance/ToggleInstanceFeatureCommand.php
new file mode 100644
index 0000000..d275d9b
--- /dev/null
+++ b/library/Icingadb/Command/Instance/ToggleInstanceFeatureCommand.php
@@ -0,0 +1,109 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Command\Instance;
+
+use Icinga\Module\Icingadb\Command\IcingaCommand;
+
+/**
+ * Enable or disable a feature of an Icinga instance
+ */
+class ToggleInstanceFeatureCommand extends IcingaCommand
+{
+ /**
+ * Feature for enabling or disabling active host checks on an Icinga instance
+ */
+ const FEATURE_ACTIVE_HOST_CHECKS = 'active_host_checks_enabled';
+
+ /**
+ * Feature for enabling or disabling active service checks on an Icinga instance
+ */
+ const FEATURE_ACTIVE_SERVICE_CHECKS = 'active_service_checks_enabled';
+
+ /**
+ * Feature for enabling or disabling host and service event handlers on an Icinga instance
+ */
+ const FEATURE_EVENT_HANDLERS = 'event_handlers_enabled';
+
+ /**
+ * Feature for enabling or disabling host and service flap detection on an Icinga instance
+ */
+ const FEATURE_FLAP_DETECTION = 'flap_detection_enabled';
+
+ /**
+ * Feature for enabling or disabling host and service notifications on an Icinga instance
+ */
+ const FEATURE_NOTIFICATIONS = 'notifications_enabled';
+
+ /**
+ * Feature for enabling or disabling the processing of host and service performance data on an Icinga instance
+ */
+ const FEATURE_PERFORMANCE_DATA = 'process_performance_data';
+
+ /**
+ * Feature that is to be enabled or disabled
+ *
+ * @var string
+ */
+ protected $feature;
+
+ /**
+ * Whether the feature should be enabled or disabled
+ *
+ * @var bool
+ */
+ protected $enabled;
+
+ /**
+ * Set the feature that is to be enabled or disabled
+ *
+ * @param string $feature
+ *
+ * @return $this
+ */
+ public function setFeature(string $feature): self
+ {
+ $this->feature = $feature;
+
+ return $this;
+ }
+
+ /**
+ * Get the feature that is to be enabled or disabled
+ *
+ * @return string
+ */
+ public function getFeature(): string
+ {
+ if ($this->feature === null) {
+ throw new \LogicException('You have to set the feature first before getting it.');
+ }
+
+ return $this->feature;
+ }
+
+ /**
+ * Set whether the feature should be enabled or disabled
+ *
+ * @param bool $enabled
+ *
+ * @return $this
+ */
+ public function setEnabled(bool $enabled = true): self
+ {
+ $this->enabled = $enabled;
+
+ return $this;
+ }
+
+ /**
+ * Get whether the feature should be enabled or disabled
+ *
+ * @return ?bool
+ */
+ public function getEnabled()
+ {
+ return $this->enabled;
+ }
+}
diff --git a/library/Icingadb/Command/Object/AcknowledgeProblemCommand.php b/library/Icingadb/Command/Object/AcknowledgeProblemCommand.php
new file mode 100644
index 0000000..baae24c
--- /dev/null
+++ b/library/Icingadb/Command/Object/AcknowledgeProblemCommand.php
@@ -0,0 +1,140 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Command\Object;
+
+/**
+ * Acknowledge a host or service problem
+ */
+class AcknowledgeProblemCommand extends WithCommentCommand
+{
+ /**
+ * Whether the acknowledgement is sticky
+ *
+ * Sticky acknowledgements remain until the host or service recovers. Non-sticky acknowledgements will be
+ * automatically removed when the host or service state changes.
+ *
+ * @var bool
+ */
+ protected $sticky = false;
+
+ /**
+ * Whether to send a notification about the acknowledgement
+
+ * @var bool
+ */
+ protected $notify = false;
+
+ /**
+ * Whether the comment associated with the acknowledgement is persistent
+ *
+ * Persistent comments are not lost the next time the monitoring host restarts.
+ *
+ * @var bool
+ */
+ protected $persistent = false;
+
+ /**
+ * Optional time when the acknowledgement should expire
+ *
+ * @var int
+ */
+ protected $expireTime;
+
+ /**
+ * Set whether the acknowledgement is sticky
+ *
+ * @param bool $sticky
+ *
+ * @return $this
+ */
+ public function setSticky(bool $sticky = true): self
+ {
+ $this->sticky = $sticky;
+
+ return $this;
+ }
+
+ /**
+ * Is the acknowledgement sticky?
+ *
+ * @return bool
+ */
+ public function getSticky(): bool
+ {
+ return $this->sticky;
+ }
+
+ /**
+ * Set whether to send a notification about the acknowledgement
+ *
+ * @param bool $notify
+ *
+ * @return $this
+ */
+ public function setNotify(bool $notify = true): self
+ {
+ $this->notify = $notify;
+
+ return $this;
+ }
+
+ /**
+ * Get whether to send a notification about the acknowledgement
+ *
+ * @return bool
+ */
+ public function getNotify(): bool
+ {
+ return $this->notify;
+ }
+
+ /**
+ * Set whether the comment associated with the acknowledgement is persistent
+ *
+ * @param bool $persistent
+ *
+ * @return $this
+ */
+ public function setPersistent(bool $persistent = true): self
+ {
+ $this->persistent = $persistent;
+
+ return $this;
+ }
+
+ /**
+ * Is the comment associated with the acknowledgement is persistent?
+ *
+ * @return bool
+ */
+ public function getPersistent(): bool
+ {
+ return $this->persistent;
+ }
+
+ /**
+ * Set the time when the acknowledgement should expire
+ *
+ * @param int $expireTime
+ *
+ * @return $this
+ */
+ public function setExpireTime(int $expireTime): self
+ {
+ $this->expireTime = $expireTime;
+
+ return $this;
+ }
+
+ /**
+ * Get the time when the acknowledgement should expire
+ *
+ * @return ?int
+ */
+ public function getExpireTime()
+ {
+ return $this->expireTime;
+ }
+}
diff --git a/library/Icingadb/Command/Object/AddCommentCommand.php b/library/Icingadb/Command/Object/AddCommentCommand.php
new file mode 100644
index 0000000..c853b25
--- /dev/null
+++ b/library/Icingadb/Command/Object/AddCommentCommand.php
@@ -0,0 +1,42 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Command\Object;
+
+/**
+ * Add a comment to a host or service
+ */
+class AddCommentCommand extends WithCommentCommand
+{
+ /**
+ * Optional time when the acknowledgement should expire
+ *
+ * @var int
+ */
+ protected $expireTime;
+
+ /**
+ * Set the time when the acknowledgement should expire
+ *
+ * @param int $expireTime
+ *
+ * @return $this
+ */
+ public function setExpireTime(int $expireTime): self
+ {
+ $this->expireTime = $expireTime;
+
+ return $this;
+ }
+
+ /**
+ * Get the time when the acknowledgement should expire
+ *
+ * @return ?int
+ */
+ public function getExpireTime()
+ {
+ return $this->expireTime;
+ }
+}
diff --git a/library/Icingadb/Command/Object/CommandAuthor.php b/library/Icingadb/Command/Object/CommandAuthor.php
new file mode 100644
index 0000000..f323b63
--- /dev/null
+++ b/library/Icingadb/Command/Object/CommandAuthor.php
@@ -0,0 +1,45 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Command\Object;
+
+trait CommandAuthor
+{
+ /**
+ * Author of the command
+ *
+ * @var string
+ */
+ protected $author;
+
+ /**
+ * Set the author
+ *
+ * @param string $author
+ *
+ * @return $this
+ */
+ public function setAuthor(string $author): self
+ {
+ $this->author = $author;
+
+ return $this;
+ }
+
+ /**
+ * Get the author
+ *
+ * @return string
+ */
+ public function getAuthor(): string
+ {
+ if ($this->author === null) {
+ throw new \LogicException(
+ 'You are accessing an unset property. Please make sure to set it beforehand.'
+ );
+ }
+
+ return $this->author;
+ }
+}
diff --git a/library/Icingadb/Command/Object/DeleteCommentCommand.php b/library/Icingadb/Command/Object/DeleteCommentCommand.php
new file mode 100644
index 0000000..8bfc2a3
--- /dev/null
+++ b/library/Icingadb/Command/Object/DeleteCommentCommand.php
@@ -0,0 +1,52 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Command\Object;
+
+use Icinga\Module\Icingadb\Command\IcingaCommand;
+
+/**
+ * Delete a host or service comment
+ */
+class DeleteCommentCommand extends IcingaCommand
+{
+ use CommandAuthor;
+
+ /**
+ * Name of the comment
+ *
+ * @var string
+ */
+ protected $commentName;
+
+ /**
+ * Get the name of the comment
+ *
+ * @return string
+ */
+ public function getCommentName(): string
+ {
+ if ($this->commentName === null) {
+ throw new \LogicException(
+ 'You are accessing an unset property. Please make sure to set it beforehand.'
+ );
+ }
+
+ return $this->commentName;
+ }
+
+ /**
+ * Set the name of the comment
+ *
+ * @param string $commentName
+ *
+ * @return $this
+ */
+ public function setCommentName(string $commentName): self
+ {
+ $this->commentName = $commentName;
+
+ return $this;
+ }
+}
diff --git a/library/Icingadb/Command/Object/DeleteDowntimeCommand.php b/library/Icingadb/Command/Object/DeleteDowntimeCommand.php
new file mode 100644
index 0000000..a0867fa
--- /dev/null
+++ b/library/Icingadb/Command/Object/DeleteDowntimeCommand.php
@@ -0,0 +1,58 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Command\Object;
+
+use Icinga\Module\Icingadb\Command\IcingaCommand;
+
+/**
+ * Delete a host or service downtime
+ */
+class DeleteDowntimeCommand extends IcingaCommand
+{
+ use CommandAuthor;
+
+ /**
+ * Name of the downtime (Icinga 2.4+)
+ *
+ * Required for removing the downtime via Icinga 2's API.
+ *
+ * @var string
+ */
+ protected $downtimeName;
+
+ /**
+ * Get the name of the downtime (Icinga 2.4+)
+ *
+ * Required for removing the downtime via Icinga 2's API.
+ *
+ * @return string
+ */
+ public function getDowntimeName(): string
+ {
+ if ($this->downtimeName === null) {
+ throw new \LogicException(
+ 'You are accessing an unset property. Please make sure to set it beforehand.'
+ );
+ }
+
+ return $this->downtimeName;
+ }
+
+ /**
+ * Set the name of the downtime (Icinga 2.4+)
+ *
+ * Required for removing the downtime via Icinga 2's API.
+ *
+ * @param string $downtimeName
+ *
+ * @return $this
+ */
+ public function setDowntimeName(string $downtimeName): self
+ {
+ $this->downtimeName = $downtimeName;
+
+ return $this;
+ }
+}
diff --git a/library/Icingadb/Command/Object/GetObjectCommand.php b/library/Icingadb/Command/Object/GetObjectCommand.php
new file mode 100644
index 0000000..372a555
--- /dev/null
+++ b/library/Icingadb/Command/Object/GetObjectCommand.php
@@ -0,0 +1,71 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Command\Object;
+
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Module\Icingadb\Model\Service;
+use LogicException;
+
+class GetObjectCommand extends ObjectCommand
+{
+ /** @var array */
+ protected $attributes;
+
+ /**
+ * Get the object name used in the Icinga 2 API
+ *
+ * @return string
+ */
+ public function getObjectName(): string
+ {
+ switch (true) {
+ case $this->object instanceof Service:
+ return $this->object->host->name . '!' . $this->object->name;
+ default:
+ return $this->object->name;
+ }
+ }
+
+ /**
+ * Get the sub-route of the endpoint for this object
+ *
+ * @return string
+ */
+ public function getObjectPluralType(): string
+ {
+ switch (true) {
+ case $this->object instanceof Host:
+ return 'hosts';
+ case $this->object instanceof Service:
+ return 'services';
+ default:
+ throw new LogicException(sprintf('Invalid object type %s provided', get_class($this->object)));
+ }
+ }
+
+ /**
+ * Get the attributes to query
+ *
+ * @return ?array
+ */
+ public function getAttributes()
+ {
+ return $this->attributes;
+ }
+
+ /**
+ * Set the attributes to query
+ *
+ * @param array $attributes
+ *
+ * @return $this
+ */
+ public function setAttributes(array $attributes): self
+ {
+ $this->attributes = $attributes;
+
+ return $this;
+ }
+}
diff --git a/library/Icingadb/Command/Object/ObjectCommand.php b/library/Icingadb/Command/Object/ObjectCommand.php
new file mode 100644
index 0000000..7dae799
--- /dev/null
+++ b/library/Icingadb/Command/Object/ObjectCommand.php
@@ -0,0 +1,51 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Command\Object;
+
+use Icinga\Module\Icingadb\Command\IcingaCommand;
+use ipl\Orm\Model;
+
+/**
+ * Base class for commands that involve a monitored object, i.e. a host or service
+ */
+abstract class ObjectCommand extends IcingaCommand
+{
+ /**
+ * Involved object
+ *
+ * @var Model
+ */
+ protected $object;
+
+ /**
+ * Set the involved object
+ *
+ * @param Model $object
+ *
+ * @return $this
+ */
+ public function setObject(Model $object): self
+ {
+ $this->object = $object;
+
+ return $this;
+ }
+
+ /**
+ * Get the involved object
+ *
+ * @return Model
+ */
+ public function getObject(): Model
+ {
+ if ($this->object === null) {
+ throw new \LogicException(
+ 'You are accessing an unset property. Please make sure to set it beforehand.'
+ );
+ }
+
+ return $this->object;
+ }
+}
diff --git a/library/Icingadb/Command/Object/ProcessCheckResultCommand.php b/library/Icingadb/Command/Object/ProcessCheckResultCommand.php
new file mode 100644
index 0000000..3d7e956
--- /dev/null
+++ b/library/Icingadb/Command/Object/ProcessCheckResultCommand.php
@@ -0,0 +1,140 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Command\Object;
+
+/**
+ * Submit a passive check result for a host or service
+ */
+class ProcessCheckResultCommand extends ObjectCommand
+{
+ /**
+ * Host up
+ */
+ const HOST_UP = 0;
+
+ /**
+ * Host down
+ */
+ const HOST_DOWN = 1;
+
+ /**
+ * Service ok
+ */
+ const SERVICE_OK = 0;
+
+ /**
+ * Service warning
+ */
+ const SERVICE_WARNING = 1;
+
+ /**
+ * Service critical
+ */
+ const SERVICE_CRITICAL = 2;
+
+ /**
+ * Service unknown
+ */
+ const SERVICE_UNKNOWN = 3;
+
+ /**
+ * Status code of the host or service check result
+ *
+ * @var int
+ */
+ protected $status;
+
+ /**
+ * Text output of the host or service check result
+ *
+ * @var string
+ */
+ protected $output;
+
+ /**
+ * Optional performance data of the host or service check result
+ *
+ * @var string
+ */
+ protected $performanceData;
+
+ /**
+ * Set the status code of the host or service check result
+ *
+ * @param int $status
+ *
+ * @return $this
+ */
+ public function setStatus(int $status): self
+ {
+ $this->status = $status;
+
+ return $this;
+ }
+
+ /**
+ * Get the status code of the host or service check result
+ *
+ * @return int
+ */
+ public function getStatus(): int
+ {
+ if ($this->status === null) {
+ throw new \LogicException(
+ 'You are accessing an unset property. Please make sure to set it beforehand.'
+ );
+ }
+
+ return $this->status;
+ }
+
+ /**
+ * Set the text output of the host or service check result
+ *
+ * @param string $output
+ *
+ * @return $this
+ */
+ public function setOutput(string $output): self
+ {
+ $this->output = $output;
+
+ return $this;
+ }
+
+ /**
+ * Get the text output of the host or service check result
+ *
+ * @return ?string
+ */
+ public function getOutput()
+ {
+ return $this->output;
+ }
+
+ /**
+ * Set the performance data of the host or service check result
+ *
+ * @param string|null $performanceData
+ *
+ * @return $this
+ */
+ public function setPerformanceData(string $performanceData = null): self
+ {
+ $this->performanceData = $performanceData;
+
+ return $this;
+ }
+
+ /**
+ * Get the performance data of the host or service check result
+ *
+ * @return ?string
+ */
+ public function getPerformanceData()
+ {
+ return $this->performanceData;
+ }
+}
diff --git a/library/Icingadb/Command/Object/PropagateHostDowntimeCommand.php b/library/Icingadb/Command/Object/PropagateHostDowntimeCommand.php
new file mode 100644
index 0000000..88964fb
--- /dev/null
+++ b/library/Icingadb/Command/Object/PropagateHostDowntimeCommand.php
@@ -0,0 +1,42 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Command\Object;
+
+/**
+ * Schedule and propagate host downtime
+ */
+class PropagateHostDowntimeCommand extends ScheduleHostDowntimeCommand
+{
+ /**
+ * Whether the downtime for child hosts are all set to be triggered by this' host downtime
+ *
+ * @var bool
+ */
+ protected $triggered = false;
+
+ /**
+ * Set whether the downtime for child hosts are all set to be triggered by this' host downtime
+ *
+ * @param bool $triggered
+ *
+ * @return $this
+ */
+ public function setTriggered(bool $triggered = true): self
+ {
+ $this->triggered = $triggered;
+
+ return $this;
+ }
+
+ /**
+ * Get whether the downtime for child hosts are all set to be triggered by this' host downtime
+ *
+ * @return bool
+ */
+ public function getTriggered(): bool
+ {
+ return $this->triggered;
+ }
+}
diff --git a/library/Icingadb/Command/Object/RemoveAcknowledgementCommand.php b/library/Icingadb/Command/Object/RemoveAcknowledgementCommand.php
new file mode 100644
index 0000000..49a22a3
--- /dev/null
+++ b/library/Icingadb/Command/Object/RemoveAcknowledgementCommand.php
@@ -0,0 +1,13 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Command\Object;
+
+/**
+ * Remove a problem acknowledgement from a host or service
+ */
+class RemoveAcknowledgementCommand extends ObjectCommand
+{
+ use CommandAuthor;
+}
diff --git a/library/Icingadb/Command/Object/ScheduleCheckCommand.php b/library/Icingadb/Command/Object/ScheduleCheckCommand.php
new file mode 100644
index 0000000..c3f2097
--- /dev/null
+++ b/library/Icingadb/Command/Object/ScheduleCheckCommand.php
@@ -0,0 +1,86 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Command\Object;
+
+/**
+ * Schedule a check
+ */
+class ScheduleCheckCommand extends ObjectCommand
+{
+ /**
+ * Time when the next check of a host or service is to be scheduled
+ *
+ * If active checks are disabled on a host- or service-specific or program-wide basis or the host or service is
+ * already scheduled to be checked at an earlier time, etc. The check may not actually be scheduled at the time
+ * specified. This behaviour can be overridden by setting `ScheduledCheck::$forced' to true.
+ *
+ * @var int Unix timestamp
+ */
+ protected $checkTime;
+
+ /**
+ * Whether the check is forced
+ *
+ * Forced checks are performed regardless of what time it is (e.g. time period restrictions are ignored) and whether
+ * or not active checks are enabled on a host- or service-specific or program-wide basis.
+ *
+ * @var bool
+ */
+ protected $forced = false;
+
+ /**
+ * Set the time when the next check of a host or service is to be scheduled
+ *
+ * @param int $checkTime Unix timestamp
+ *
+ * @return $this
+ */
+ public function setCheckTime(int $checkTime): self
+ {
+ $this->checkTime = $checkTime;
+
+ return $this;
+ }
+
+ /**
+ * Get the time when the next check of a host or service is to be scheduled
+ *
+ * @return int Unix timestamp
+ */
+ public function getCheckTime(): int
+ {
+ if ($this->checkTime === null) {
+ throw new \LogicException(
+ 'You are accessing an unset property. Please make sure to set it beforehand.'
+ );
+ }
+
+ return $this->checkTime;
+ }
+
+ /**
+ * Set whether the check is forced
+ *
+ * @param bool $forced
+ *
+ * @return $this
+ */
+ public function setForced(bool $forced = true): self
+ {
+ $this->forced = $forced;
+
+ return $this;
+ }
+
+ /**
+ * Get whether the check is forced
+ *
+ * @return bool
+ */
+ public function getForced(): bool
+ {
+ return $this->forced;
+ }
+}
diff --git a/library/Icingadb/Command/Object/ScheduleHostDowntimeCommand.php b/library/Icingadb/Command/Object/ScheduleHostDowntimeCommand.php
new file mode 100644
index 0000000..0e4d84f
--- /dev/null
+++ b/library/Icingadb/Command/Object/ScheduleHostDowntimeCommand.php
@@ -0,0 +1,42 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Command\Object;
+
+/**
+ * Schedule a host downtime
+ */
+class ScheduleHostDowntimeCommand extends ScheduleServiceDowntimeCommand
+{
+ /**
+ * Whether to schedule a downtime for all services associated with a particular host
+ *
+ * @var bool
+ */
+ protected $forAllServices = false;
+
+ /**
+ * Set whether to schedule a downtime for all services associated with a particular host
+ *
+ * @param bool $forAllServices
+ *
+ * @return $this
+ */
+ public function setForAllServices(bool $forAllServices = true): self
+ {
+ $this->forAllServices = $forAllServices;
+
+ return $this;
+ }
+
+ /**
+ * Get whether to schedule a downtime for all services associated with a particular host
+ *
+ * @return bool
+ */
+ public function getForAllServices(): bool
+ {
+ return $this->forAllServices;
+ }
+}
diff --git a/library/Icingadb/Command/Object/ScheduleServiceDowntimeCommand.php b/library/Icingadb/Command/Object/ScheduleServiceDowntimeCommand.php
new file mode 100644
index 0000000..3bad28e
--- /dev/null
+++ b/library/Icingadb/Command/Object/ScheduleServiceDowntimeCommand.php
@@ -0,0 +1,196 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Command\Object;
+
+/**
+ * Schedule a service downtime
+ */
+class ScheduleServiceDowntimeCommand extends AddCommentCommand
+{
+ /**
+ * Downtime starts at the exact time specified
+ *
+ * If `Downtime::$fixed' is set to false, the time between `Downtime::$start' and `Downtime::$end' at which a
+ * host or service transitions to a problem state determines the time at which the downtime actually starts.
+ * The downtime will then last for `Downtime::$duration' seconds.
+ *
+ * @var int Unix timestamp
+ */
+ protected $start;
+
+ /**
+ * Downtime ends at the exact time specified
+ *
+ * If `Downtime::$fixed' is set to false, the time between `Downtime::$start' and `Downtime::$end' at which a
+ * host or service transitions to a problem state determines the time at which the downtime actually starts.
+ * The downtime will then last for `Downtime::$duration' seconds.
+ *
+ * @var int Unix timestamp
+ */
+ protected $end;
+
+ /**
+ * Whether it's a fixed or flexible downtime
+ *
+ * @var bool
+ */
+ protected $fixed = true;
+
+ /**
+ * ID of the downtime which triggers this downtime
+ *
+ * The start of this downtime is triggered by the start of the other scheduled host or service downtime.
+ *
+ * @var int|null
+ */
+ protected $triggerId;
+
+ /**
+ * The duration in seconds the downtime must last if it's a flexible downtime
+ *
+ * If `Downtime::$fixed' is set to false, the downtime will last for the duration in seconds specified, even
+ * if the host or service recovers before the downtime expires.
+ *
+ * @var int|null
+ */
+ protected $duration;
+
+ /**
+ * Set the time when the downtime should start
+ *
+ * @param int $start Unix timestamp
+ *
+ * @return $this
+ */
+ public function setStart(int $start): self
+ {
+ $this->start = $start;
+
+ return $this;
+ }
+
+ /**
+ * Get the time when the downtime should start
+ *
+ * @return int Unix timestamp
+ */
+ public function getStart(): int
+ {
+ if ($this->start === null) {
+ throw new \LogicException(
+ 'You are accessing an unset property. Please make sure to set it beforehand.'
+ );
+ }
+
+ return $this->start;
+ }
+
+ /**
+ * Set the time when the downtime should end
+ *
+ * @param int $end Unix timestamp
+ *
+ * @return $this
+ */
+ public function setEnd(int $end): self
+ {
+ $this->end = $end;
+
+ return $this;
+ }
+
+ /**
+ * Get the time when the downtime should end
+ *
+ * @return int Unix timestamp
+ */
+ public function getEnd(): int
+ {
+ if ($this->start === null) {
+ throw new \LogicException(
+ 'You are accessing an unset property. Please make sure to set it beforehand.'
+ );
+ }
+
+ return $this->end;
+ }
+
+ /**
+ * Set whether it's a fixed or flexible downtime
+ *
+ * @param boolean $fixed
+ *
+ * @return $this
+ */
+ public function setFixed(bool $fixed = true): self
+ {
+ $this->fixed = $fixed;
+
+ return $this;
+ }
+
+ /**
+ * Is the downtime fixed?
+ *
+ * @return boolean
+ */
+ public function getFixed(): bool
+ {
+ return $this->fixed;
+ }
+
+ /**
+ * Set the ID of the downtime which triggers this downtime
+ *
+ * @param int $triggerId
+ *
+ * @return $this
+ */
+ public function setTriggerId(int $triggerId): self
+ {
+ $this->triggerId = $triggerId;
+
+ return $this;
+ }
+
+ /**
+ * Get the ID of the downtime which triggers this downtime
+ *
+ * @return int|null
+ */
+ public function getTriggerId()
+ {
+ return $this->triggerId;
+ }
+
+ /**
+ * Set the duration in seconds the downtime must last if it's a flexible downtime
+ *
+ * @param int $duration
+ *
+ * @return $this
+ */
+ public function setDuration(int $duration): self
+ {
+ $this->duration = $duration;
+
+ return $this;
+ }
+
+ /**
+ * Get the duration in seconds the downtime must last if it's a flexible downtime
+ *
+ * @return int|null
+ */
+ public function getDuration()
+ {
+ return $this->duration;
+ }
+
+ public function getName(): string
+ {
+ return 'ScheduleDowntime';
+ }
+}
diff --git a/library/Icingadb/Command/Object/SendCustomNotificationCommand.php b/library/Icingadb/Command/Object/SendCustomNotificationCommand.php
new file mode 100644
index 0000000..de90620
--- /dev/null
+++ b/library/Icingadb/Command/Object/SendCustomNotificationCommand.php
@@ -0,0 +1,44 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Command\Object;
+
+/**
+ * Send custom notifications for a host or service
+ */
+class SendCustomNotificationCommand extends WithCommentCommand
+{
+ /**
+ * Whether the notification is forced
+ *
+ * Forced notifications are sent out regardless of time restrictions and whether or not notifications are enabled.
+ *
+ * @var bool
+ */
+ protected $forced;
+
+ /**
+ * Get whether to force the notification
+ *
+ * @return ?bool
+ */
+ public function getForced()
+ {
+ return $this->forced;
+ }
+
+ /**
+ * Set whether to force the notification
+ *
+ * @param bool $forced
+ *
+ * @return $this
+ */
+ public function setForced(bool $forced = true): self
+ {
+ $this->forced = $forced;
+
+ return $this;
+ }
+}
diff --git a/library/Icingadb/Command/Object/ToggleObjectFeatureCommand.php b/library/Icingadb/Command/Object/ToggleObjectFeatureCommand.php
new file mode 100644
index 0000000..699a113
--- /dev/null
+++ b/library/Icingadb/Command/Object/ToggleObjectFeatureCommand.php
@@ -0,0 +1,108 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Command\Object;
+
+/**
+ * Enable or disable a feature of an Icinga object, i.e. host or service
+ */
+class ToggleObjectFeatureCommand extends ObjectCommand
+{
+ /**
+ * Feature for enabling or disabling active checks of a host or service
+ */
+ const FEATURE_ACTIVE_CHECKS = 'active_checks_enabled';
+
+ /**
+ * Feature for enabling or disabling passive checks of a host or service
+ */
+ const FEATURE_PASSIVE_CHECKS = 'passive_checks_enabled';
+
+ /**
+ * Feature for enabling or disabling notifications for a host or service
+ *
+ * Notifications will be sent out only if notifications are enabled on a program-wide basis as well.
+ */
+ const FEATURE_NOTIFICATIONS = 'notifications_enabled';
+
+ /**
+ * Feature for enabling or disabling event handler for a host or service
+ */
+ const FEATURE_EVENT_HANDLER = 'event_handler_enabled';
+
+ /**
+ * Feature for enabling or disabling flap detection for a host or service.
+ *
+ * In order to enable flap detection flap detection must be enabled on a program-wide basis as well.
+ */
+ const FEATURE_FLAP_DETECTION = 'flapping_enabled';
+
+ /**
+ * Feature that is to be enabled or disabled
+ *
+ * @var string
+ */
+ protected $feature;
+
+ /**
+ * Whether the feature should be enabled or disabled
+ *
+ * @var bool
+ */
+ protected $enabled;
+
+ /**
+ * Set the feature that is to be enabled or disabled
+ *
+ * @param string $feature
+ *
+ * @return $this
+ */
+ public function setFeature(string $feature): self
+ {
+ $this->feature = $feature;
+
+ return $this;
+ }
+
+ /**
+ * Get the feature that is to be enabled or disabled
+ *
+ * @return string
+ */
+ public function getFeature(): string
+ {
+ if ($this->feature === null) {
+ throw new \LogicException(
+ 'You are accessing an unset property. Please make sure to set it beforehand.'
+ );
+ }
+
+ return $this->feature;
+ }
+
+ /**
+ * Set whether the feature should be enabled or disabled
+ *
+ * @param bool $enabled
+ *
+ * @return $this
+ */
+ public function setEnabled(bool $enabled = true): self
+ {
+ $this->enabled = $enabled;
+
+ return $this;
+ }
+
+ /**
+ * Get whether the feature should be enabled or disabled
+ *
+ * @return ?bool
+ */
+ public function getEnabled()
+ {
+ return $this->enabled;
+ }
+}
diff --git a/library/Icingadb/Command/Object/WithCommentCommand.php b/library/Icingadb/Command/Object/WithCommentCommand.php
new file mode 100644
index 0000000..299c998
--- /dev/null
+++ b/library/Icingadb/Command/Object/WithCommentCommand.php
@@ -0,0 +1,50 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Command\Object;
+
+/**
+ * Base class for commands adding comments
+ */
+abstract class WithCommentCommand extends ObjectCommand
+{
+ use CommandAuthor;
+
+ /**
+ * Comment
+ *
+ * @var string
+ */
+ protected $comment;
+
+ /**
+ * Set the comment
+ *
+ * @param string $comment
+ *
+ * @return $this
+ */
+ public function setComment(string $comment): self
+ {
+ $this->comment = $comment;
+
+ return $this;
+ }
+
+ /**
+ * Get the comment
+ *
+ * @return string
+ */
+ public function getComment(): string
+ {
+ if ($this->comment === null) {
+ throw new \LogicException(
+ 'You are accessing an unset property. Please make sure to set it beforehand.'
+ );
+ }
+
+ return $this->comment;
+ }
+}
diff --git a/library/Icingadb/Command/Renderer/IcingaApiCommandRenderer.php b/library/Icingadb/Command/Renderer/IcingaApiCommandRenderer.php
new file mode 100644
index 0000000..aa1cdbe
--- /dev/null
+++ b/library/Icingadb/Command/Renderer/IcingaApiCommandRenderer.php
@@ -0,0 +1,322 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Command\Renderer;
+
+use Icinga\Module\Icingadb\Command\IcingaApiCommand;
+use Icinga\Module\Icingadb\Command\Object\GetObjectCommand;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Module\Icingadb\Model\Service;
+use Icinga\Module\Icingadb\Command\Instance\ToggleInstanceFeatureCommand;
+use Icinga\Module\Icingadb\Command\Object\AcknowledgeProblemCommand;
+use Icinga\Module\Icingadb\Command\Object\AddCommentCommand;
+use Icinga\Module\Icingadb\Command\Object\DeleteCommentCommand;
+use Icinga\Module\Icingadb\Command\Object\DeleteDowntimeCommand;
+use Icinga\Module\Icingadb\Command\Object\ProcessCheckResultCommand;
+use Icinga\Module\Icingadb\Command\Object\PropagateHostDowntimeCommand;
+use Icinga\Module\Icingadb\Command\Object\RemoveAcknowledgementCommand;
+use Icinga\Module\Icingadb\Command\Object\ScheduleHostDowntimeCommand;
+use Icinga\Module\Icingadb\Command\Object\ScheduleCheckCommand;
+use Icinga\Module\Icingadb\Command\Object\ScheduleServiceDowntimeCommand;
+use Icinga\Module\Icingadb\Command\Object\SendCustomNotificationCommand;
+use Icinga\Module\Icingadb\Command\Object\ToggleObjectFeatureCommand;
+use Icinga\Module\Icingadb\Command\IcingaCommand;
+use InvalidArgumentException;
+use ipl\Orm\Model;
+
+/**
+ * Icinga command renderer for the Icinga command file
+ */
+class IcingaApiCommandRenderer implements IcingaCommandRendererInterface
+{
+ /**
+ * Name of the Icinga application object
+ *
+ * @var string
+ */
+ protected $app = 'app';
+
+ /**
+ * Get the name of the Icinga application object
+ *
+ * @return string
+ */
+ public function getApp(): string
+ {
+ return $this->app;
+ }
+
+ /**
+ * Set the name of the Icinga application object
+ *
+ * @param string $app
+ *
+ * @return $this
+ */
+ public function setApp(string $app): self
+ {
+ $this->app = $app;
+
+ return $this;
+ }
+
+ /**
+ * Apply filter to query data
+ *
+ * @param array $data
+ * @param Model $object
+ *
+ * @return void
+ */
+ protected function applyFilter(array &$data, Model $object)
+ {
+ if ($object instanceof Host) {
+ $data['host'] = $object->name;
+ } else {
+ /** @var Service $object */
+ $data['service'] = sprintf('%s!%s', $object->host->name, $object->name);
+ }
+ }
+
+ /**
+ * Render a command
+ *
+ * @param IcingaCommand $command
+ *
+ * @return IcingaApiCommand
+ */
+ public function render(IcingaCommand $command): IcingaApiCommand
+ {
+ $renderMethod = 'render' . $command->getName();
+ if (! method_exists($this, $renderMethod)) {
+ throw new InvalidArgumentException(
+ sprintf('Can\'t render command. Method %s not found', $renderMethod)
+ );
+ }
+
+ return $this->$renderMethod($command);
+ }
+
+ public function renderGetObject(GetObjectCommand $command): IcingaApiCommand
+ {
+ $endpoint = sprintf(
+ 'objects/%s/%s',
+ $command->getObjectPluralType(),
+ rawurlencode($command->getObjectName())
+ );
+
+ $data = [
+ 'all_joins' => 1,
+ 'attrs' => $command->getAttributes() ?: []
+ ];
+
+ return IcingaApiCommand::create($endpoint, $data)->setMethod('GET');
+ }
+
+ public function renderAddComment(AddCommentCommand $command): IcingaApiCommand
+ {
+ $endpoint = 'actions/add-comment';
+ $data = [
+ 'author' => $command->getAuthor(),
+ 'comment' => $command->getComment()
+ ];
+
+ if ($command->getExpireTime() !== null) {
+ $data['expiry'] = $command->getExpireTime();
+ }
+
+ $this->applyFilter($data, $command->getObject());
+ return IcingaApiCommand::create($endpoint, $data);
+ }
+
+ public function renderSendCustomNotification(SendCustomNotificationCommand $command): IcingaApiCommand
+ {
+ $endpoint = 'actions/send-custom-notification';
+ $data = [
+ 'author' => $command->getAuthor(),
+ 'comment' => $command->getComment(),
+ 'force' => $command->getForced()
+ ];
+
+ $this->applyFilter($data, $command->getObject());
+ return IcingaApiCommand::create($endpoint, $data);
+ }
+
+ public function renderProcessCheckResult(ProcessCheckResultCommand $command): IcingaApiCommand
+ {
+ $endpoint = 'actions/process-check-result';
+ $data = [
+ 'exit_status' => $command->getStatus(),
+ 'plugin_output' => $command->getOutput(),
+ 'performance_data' => $command->getPerformanceData()
+ ];
+
+ $this->applyFilter($data, $command->getObject());
+ return IcingaApiCommand::create($endpoint, $data);
+ }
+
+ public function renderScheduleCheck(ScheduleCheckCommand $command): IcingaApiCommand
+ {
+ $endpoint = 'actions/reschedule-check';
+ $data = [
+ 'next_check' => $command->getCheckTime(),
+ 'force' => $command->getForced()
+ ];
+
+ $this->applyFilter($data, $command->getObject());
+ return IcingaApiCommand::create($endpoint, $data);
+ }
+
+ public function renderScheduleDowntime(ScheduleServiceDowntimeCommand $command): IcingaApiCommand
+ {
+ $endpoint = 'actions/schedule-downtime';
+ $data = [
+ 'author' => $command->getAuthor(),
+ 'comment' => $command->getComment(),
+ 'start_time' => $command->getStart(),
+ 'end_time' => $command->getEnd(),
+ 'duration' => $command->getDuration(),
+ 'fixed' => $command->getFixed(),
+ 'trigger_name' => $command->getTriggerId()
+ ];
+
+ if ($command instanceof PropagateHostDowntimeCommand) {
+ $data['child_options'] = $command->getTriggered() ? 1 : 2;
+ }
+
+ if ($command instanceof ScheduleHostDowntimeCommand && $command->getForAllServices()) {
+ $data['all_services'] = true;
+ }
+
+ $this->applyFilter($data, $command->getObject());
+ return IcingaApiCommand::create($endpoint, $data);
+ }
+
+ public function renderAcknowledgeProblem(AcknowledgeProblemCommand $command): IcingaApiCommand
+ {
+ $endpoint = 'actions/acknowledge-problem';
+ $data = [
+ 'author' => $command->getAuthor(),
+ 'comment' => $command->getComment(),
+ 'sticky' => $command->getSticky(),
+ 'notify' => $command->getNotify(),
+ 'persistent' => $command->getPersistent()
+ ];
+
+ if ($command->getExpireTime() !== null) {
+ $data['expiry'] = $command->getExpireTime();
+ }
+
+ $this->applyFilter($data, $command->getObject());
+ return IcingaApiCommand::create($endpoint, $data);
+ }
+
+ public function renderToggleObjectFeature(ToggleObjectFeatureCommand $command): IcingaApiCommand
+ {
+ switch ($command->getFeature()) {
+ case ToggleObjectFeatureCommand::FEATURE_ACTIVE_CHECKS:
+ $attr = 'enable_active_checks';
+ break;
+ case ToggleObjectFeatureCommand::FEATURE_PASSIVE_CHECKS:
+ $attr = 'enable_passive_checks';
+ break;
+ case ToggleObjectFeatureCommand::FEATURE_NOTIFICATIONS:
+ $attr = 'enable_notifications';
+ break;
+ case ToggleObjectFeatureCommand::FEATURE_EVENT_HANDLER:
+ $attr = 'enable_event_handler';
+ break;
+ case ToggleObjectFeatureCommand::FEATURE_FLAP_DETECTION:
+ $attr = 'enable_flapping';
+ break;
+ default:
+ throw new InvalidArgumentException($command->getFeature());
+ }
+
+ $endpoint = 'objects/';
+ $object = $command->getObject();
+ if ($object instanceof Host) {
+ $endpoint .= 'hosts';
+ } else {
+ /** @var Service $object */
+ $endpoint .= 'services';
+ }
+
+ $data = [
+ 'attrs' => [
+ $attr => $command->getEnabled()
+ ]
+ ];
+
+ $this->applyFilter($data, $object);
+ return IcingaApiCommand::create($endpoint, $data);
+ }
+
+ public function renderDeleteComment(DeleteCommentCommand $command): IcingaApiCommand
+ {
+ $endpoint = 'actions/remove-comment';
+ $data = [
+ 'author' => $command->getAuthor(),
+ 'comment' => $command->getCommentName()
+ ];
+
+ return IcingaApiCommand::create($endpoint, $data);
+ }
+
+ public function renderDeleteDowntime(DeleteDowntimeCommand $command): IcingaApiCommand
+ {
+ $endpoint = 'actions/remove-downtime';
+ $data = [
+ 'author' => $command->getAuthor(),
+ 'downtime' => $command->getDowntimeName()
+ ];
+
+ return IcingaApiCommand::create($endpoint, $data);
+ }
+
+ public function renderRemoveAcknowledgement(RemoveAcknowledgementCommand $command): IcingaApiCommand
+ {
+ $endpoint = 'actions/remove-acknowledgement';
+ $data = ['author' => $command->getAuthor()];
+
+ $this->applyFilter($data, $command->getObject());
+ return IcingaApiCommand::create($endpoint, $data);
+ }
+
+ public function renderToggleInstanceFeature(ToggleInstanceFeatureCommand $command): IcingaApiCommand
+ {
+ $endpoint = 'objects/icingaapplications/' . $this->getApp();
+
+ switch ($command->getFeature()) {
+ case ToggleInstanceFeatureCommand::FEATURE_ACTIVE_HOST_CHECKS:
+ $attr = 'enable_host_checks';
+ break;
+ case ToggleInstanceFeatureCommand::FEATURE_ACTIVE_SERVICE_CHECKS:
+ $attr = 'enable_service_checks';
+ break;
+ case ToggleInstanceFeatureCommand::FEATURE_EVENT_HANDLERS:
+ $attr = 'enable_event_handlers';
+ break;
+ case ToggleInstanceFeatureCommand::FEATURE_FLAP_DETECTION:
+ $attr = 'enable_flapping';
+ break;
+ case ToggleInstanceFeatureCommand::FEATURE_NOTIFICATIONS:
+ $attr = 'enable_notifications';
+ break;
+ case ToggleInstanceFeatureCommand::FEATURE_PERFORMANCE_DATA:
+ $attr = 'enable_perfdata';
+ break;
+ default:
+ throw new InvalidArgumentException($command->getFeature());
+ }
+
+ $data = [
+ 'attrs' => [
+ $attr => $command->getEnabled()
+ ]
+ ];
+
+ return IcingaApiCommand::create($endpoint, $data);
+ }
+}
diff --git a/library/Icingadb/Command/Renderer/IcingaCommandRendererInterface.php b/library/Icingadb/Command/Renderer/IcingaCommandRendererInterface.php
new file mode 100644
index 0000000..50dd90c
--- /dev/null
+++ b/library/Icingadb/Command/Renderer/IcingaCommandRendererInterface.php
@@ -0,0 +1,12 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Command\Renderer;
+
+/**
+ * Interface for Icinga command renderer
+ */
+interface IcingaCommandRendererInterface
+{
+}
diff --git a/library/Icingadb/Command/Transport/ApiCommandException.php b/library/Icingadb/Command/Transport/ApiCommandException.php
new file mode 100644
index 0000000..5449a7d
--- /dev/null
+++ b/library/Icingadb/Command/Transport/ApiCommandException.php
@@ -0,0 +1,14 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Command\Transport;
+
+use Icinga\Exception\IcingaException;
+
+/**
+ * Exception thrown if a command was not successful
+ */
+class ApiCommandException extends IcingaException
+{
+}
diff --git a/library/Icingadb/Command/Transport/ApiCommandTransport.php b/library/Icingadb/Command/Transport/ApiCommandTransport.php
new file mode 100644
index 0000000..370d705
--- /dev/null
+++ b/library/Icingadb/Command/Transport/ApiCommandTransport.php
@@ -0,0 +1,353 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Command\Transport;
+
+use GuzzleHttp\Client;
+use GuzzleHttp\Exception\GuzzleException;
+use Icinga\Application\Hook\AuditHook;
+use Icinga\Application\Logger;
+use Icinga\Exception\Json\JsonDecodeException;
+use Icinga\Module\Icingadb\Command\IcingaApiCommand;
+use Icinga\Module\Icingadb\Command\IcingaCommand;
+use Icinga\Module\Icingadb\Command\Renderer\IcingaApiCommandRenderer;
+use Icinga\Util\Json;
+
+/**
+ * Command transport over Icinga 2's REST API
+ */
+class ApiCommandTransport implements CommandTransportInterface
+{
+ /**
+ * Transport identifier
+ */
+ const TRANSPORT = 'api';
+
+ /**
+ * API host
+ *
+ * @var string
+ */
+ protected $host;
+
+ /**
+ * API password
+ *
+ * @var string
+ */
+ protected $password;
+
+ /**
+ * API port
+ *
+ * @var int
+ */
+ protected $port = 5665;
+
+ /**
+ * Command renderer
+ *
+ * @var IcingaApiCommandRenderer
+ */
+ protected $renderer;
+
+ /**
+ * API username
+ *
+ * @var string
+ */
+ protected $username;
+
+ /**
+ * Create a new API command transport
+ */
+ public function __construct()
+ {
+ $this->renderer = new IcingaApiCommandRenderer();
+ }
+
+ /**
+ * Set the name of the Icinga application object
+ *
+ * @param string $app
+ *
+ * @return $this
+ */
+ public function setApp(string $app): self
+ {
+ $this->renderer->setApp($app);
+
+ return $this;
+ }
+
+ /**
+ * Get the API host
+ *
+ * @return string
+ */
+ public function getHost(): string
+ {
+ if ($this->host === null) {
+ throw new \LogicException(
+ 'You are accessing an unset property. Please make sure to set it beforehand.'
+ );
+ }
+
+ return $this->host;
+ }
+
+ /**
+ * Set the API host
+ *
+ * @param string $host
+ *
+ * @return $this
+ */
+ public function setHost(string $host): self
+ {
+ $this->host = $host;
+
+ return $this;
+ }
+
+ /**
+ * Get the API password
+ *
+ * @return string
+ */
+ public function getPassword(): string
+ {
+ if ($this->password === null) {
+ throw new \LogicException(
+ 'You are accessing an unset property. Please make sure to set it beforehand.'
+ );
+ }
+
+ return $this->password;
+ }
+
+ /**
+ * Set the API password
+ *
+ * @param string $password
+ *
+ * @return $this
+ */
+ public function setPassword(string $password): self
+ {
+ $this->password = $password;
+
+ return $this;
+ }
+
+ /**
+ * Get the API port
+ *
+ * @return int
+ */
+ public function getPort(): int
+ {
+ return $this->port;
+ }
+
+ /**
+ * Set the API port
+ *
+ * @param int $port
+ *
+ * @return $this
+ */
+ public function setPort(int $port): self
+ {
+ $this->port = $port;
+
+ return $this;
+ }
+
+ /**
+ * Get the API username
+ *
+ * @return string
+ */
+ public function getUsername(): string
+ {
+ if ($this->username === null) {
+ throw new \LogicException(
+ 'You are accessing an unset property. Please make sure to set it beforehand.'
+ );
+ }
+
+ return $this->username;
+ }
+
+ /**
+ * Set the API username
+ *
+ * @param string $username
+ *
+ * @return $this
+ */
+ public function setUsername(string $username): self
+ {
+ $this->username = $username;
+
+ return $this;
+ }
+
+ /**
+ * Get URI for endpoint
+ *
+ * @param string $endpoint
+ *
+ * @return string
+ */
+ protected function getUriFor(string $endpoint): string
+ {
+ return sprintf('https://%s:%u/v1/%s', $this->getHost(), $this->getPort(), $endpoint);
+ }
+
+ /**
+ * Send the given command to the icinga2's REST API
+ *
+ * @param IcingaApiCommand $command
+ *
+ * @return mixed
+ */
+ protected function sendCommand(IcingaApiCommand $command)
+ {
+ Logger::debug(
+ 'Sending Icinga command "%s" to the API "%s:%u"',
+ $command->getEndpoint(),
+ $this->getHost(),
+ $this->getPort()
+ );
+
+ $data = $command->getData();
+ $payload = Json::encode($data);
+ AuditHook::logActivity(
+ 'monitoring/command',
+ "Issued command {$command->getEndpoint()} with the following payload: $payload",
+ $data
+ );
+
+ $headers = ['Accept' => 'application/json'];
+ if ($command->getMethod() !== 'POST') {
+ $headers['X-HTTP-Method-Override'] = $command->getMethod();
+ }
+
+ try {
+ $response = (new Client())
+ ->post($this->getUriFor($command->getEndpoint()), [
+ 'auth' => [$this->getUsername(), $this->getPassword()],
+ 'headers' => $headers,
+ 'json' => $command->getData(),
+ 'http_errors' => false,
+ 'verify' => false
+ ]);
+ } catch (GuzzleException $e) {
+ throw new CommandTransportException(
+ 'Can\'t connect to the Icinga 2 API: %u %s',
+ $e->getCode(),
+ $e->getMessage()
+ );
+ }
+
+ try {
+ $responseData = Json::decode((string) $response->getBody(), true);
+ } catch (JsonDecodeException $e) {
+ throw new CommandTransportException(
+ 'Got invalid JSON response from the Icinga 2 API: %s',
+ $e->getMessage()
+ );
+ }
+
+ if (! isset($responseData['results']) || empty($responseData['results'])) {
+ if (isset($responseData['error'])) {
+ throw new ApiCommandException(
+ 'Can\'t send external Icinga command: %u %s',
+ $responseData['error'],
+ $responseData['status']
+ );
+ }
+
+ return;
+ }
+
+ $errorResult = $responseData['results'][0];
+ if (isset($errorResult['code']) && ($errorResult['code'] < 200 || $errorResult['code'] >= 300)) {
+ throw new ApiCommandException(
+ 'Can\'t send external Icinga command: %u %s',
+ $errorResult['code'],
+ $errorResult['status']
+ );
+ }
+
+ return $responseData['results'];
+ }
+
+ /**
+ * Send the Icinga command over the Icinga 2 API
+ *
+ * @param IcingaCommand $command
+ * @param int|null $now
+ *
+ * @throws CommandTransportException
+ *
+ * @return mixed
+ */
+ public function send(IcingaCommand $command, int $now = null)
+ {
+ return $this->sendCommand($this->renderer->render($command));
+ }
+
+ /**
+ * Try to connect to the API
+ *
+ * @return void
+ *
+ * @throws CommandTransportException In case the connection was not successful
+ */
+ public function probe()
+ {
+ try {
+ $response = (new Client(['timeout' => 15]))
+ ->get($this->getUriFor(''), [
+ 'auth' => [$this->getUsername(), $this->getPassword()],
+ 'headers' => ['Accept' => 'application/json'],
+ 'http_errors' => false,
+ 'verify' => false
+ ]);
+ } catch (GuzzleException $e) {
+ throw new CommandTransportException(
+ 'Can\'t connect to the Icinga 2 API: %u %s',
+ $e->getCode(),
+ $e->getMessage()
+ );
+ }
+
+ try {
+ $responseData = Json::decode((string) $response->getBody(), true);
+ } catch (JsonDecodeException $e) {
+ throw new CommandTransportException(
+ 'Got invalid JSON response from the Icinga 2 API: %s',
+ $e->getMessage()
+ );
+ }
+
+ if (! isset($responseData['results']) || empty($responseData['results'])) {
+ throw new CommandTransportException(
+ 'Got invalid response from the Icinga 2 API: %s',
+ JSON::encode($responseData)
+ );
+ }
+
+ $result = array_pop($responseData['results']);
+ if (! isset($result['user']) || $result['user'] !== $this->getUsername()) {
+ throw new CommandTransportException(
+ 'Got invalid response from the Icinga 2 API: %s',
+ JSON::encode($responseData)
+ );
+ }
+ }
+}
diff --git a/library/Icingadb/Command/Transport/CommandTransport.php b/library/Icingadb/Command/Transport/CommandTransport.php
new file mode 100644
index 0000000..ea125bc
--- /dev/null
+++ b/library/Icingadb/Command/Transport/CommandTransport.php
@@ -0,0 +1,130 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Command\Transport;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Application\Logger;
+use Icinga\Data\ConfigObject;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Module\Icingadb\Command\IcingaCommand;
+
+/**
+ * Command transport
+ */
+class CommandTransport implements CommandTransportInterface
+{
+ /**
+ * Transport configuration
+ *
+ * @var Config
+ */
+ protected static $config;
+
+ /**
+ * Get transport configuration
+ *
+ * @return Config
+ *
+ * @throws ConfigurationError
+ */
+ public static function getConfig(): Config
+ {
+ if (static::$config === null) {
+ $config = Config::module('icingadb', 'commandtransports');
+ if ($config->isEmpty()) {
+ throw new ConfigurationError(
+ t('No command transports have been configured in "%s".'),
+ $config->getConfigFile()
+ );
+ }
+
+ static::$config = $config;
+ }
+
+ return static::$config;
+ }
+
+ /**
+ * Create a transport from config
+ *
+ * @param ConfigObject $config
+ *
+ * @return ApiCommandTransport
+ *
+ * @throws ConfigurationError
+ */
+ public static function createTransport(ConfigObject $config): ApiCommandTransport
+ {
+ $config = clone $config;
+ switch (strtolower($config->transport)) {
+ case ApiCommandTransport::TRANSPORT:
+ $transport = new ApiCommandTransport();
+ break;
+ default:
+ throw new ConfigurationError(
+ t('Cannot create command transport "%s". Invalid transport defined in "%s". Use one of: %s.'),
+ $config->transport,
+ static::getConfig()->getConfigFile(),
+ join(', ', [ApiCommandTransport::TRANSPORT])
+ );
+ }
+
+ unset($config->transport);
+ foreach ($config as $key => $value) {
+ $method = 'set' . ucfirst($key);
+ if (! method_exists($transport, $method)) {
+ // Ignore settings from config that don't have a setter on the transport instead of throwing an
+ // exception here because the transport should throw an exception if it's not fully set up
+ // when being about to send a command
+ continue;
+ }
+
+ $transport->$method($value);
+ }
+
+ return $transport;
+ }
+
+ /**
+ * Send the given command over an appropriate Icinga command transport
+ *
+ * This will try one configured transport after another until the command has been successfully sent.
+ *
+ * @param IcingaCommand $command The command to send
+ * @param int|null $now Timestamp of the command or null for now
+ *
+ * @throws CommandTransportException If sending the Icinga command failed
+ *
+ * @return mixed
+ */
+ public function send(IcingaCommand $command, int $now = null)
+ {
+ $errors = [];
+
+ foreach (static::getConfig() as $name => $transportConfig) {
+ $transport = static::createTransport($transportConfig);
+
+ try {
+ $result = $transport->send($command, $now);
+ } catch (CommandTransportException $e) {
+ Logger::error($e);
+ $errors[] = sprintf('%s: %s.', $name, rtrim($e->getMessage(), '.'));
+ continue; // Try the next transport
+ }
+
+ return $result; // The command was successfully sent
+ }
+
+ if (! empty($errors)) {
+ throw new CommandTransportException(implode("\n", $errors));
+ }
+
+ throw new CommandTransportException(t(
+ 'Failed to send external Icinga command. No transport has been configured'
+ . ' for this instance. Please contact your Icinga Web administrator.'
+ ));
+ }
+}
diff --git a/library/Icingadb/Command/Transport/CommandTransportConfig.php b/library/Icingadb/Command/Transport/CommandTransportConfig.php
new file mode 100644
index 0000000..e17fa04
--- /dev/null
+++ b/library/Icingadb/Command/Transport/CommandTransportConfig.php
@@ -0,0 +1,31 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Command\Transport;
+
+use Icinga\Repository\IniRepository;
+
+class CommandTransportConfig extends IniRepository
+{
+ protected $configs = [
+ 'transport' => [
+ 'name' => 'commandtransports',
+ 'module' => 'icingadb',
+ 'keyColumn' => 'name'
+ ]
+ ];
+
+ protected $queryColumns = [
+ 'transport' => [
+ 'name',
+ 'transport',
+
+ // API options
+ 'host',
+ 'port',
+ 'username',
+ 'password'
+ ]
+ ];
+}
diff --git a/library/Icingadb/Command/Transport/CommandTransportException.php b/library/Icingadb/Command/Transport/CommandTransportException.php
new file mode 100644
index 0000000..2ca89d9
--- /dev/null
+++ b/library/Icingadb/Command/Transport/CommandTransportException.php
@@ -0,0 +1,14 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Command\Transport;
+
+use Icinga\Exception\IcingaException;
+
+/**
+ * Exception thrown if a command was not sent
+ */
+class CommandTransportException extends IcingaException
+{
+}
diff --git a/library/Icingadb/Command/Transport/CommandTransportInterface.php b/library/Icingadb/Command/Transport/CommandTransportInterface.php
new file mode 100644
index 0000000..ad07cb9
--- /dev/null
+++ b/library/Icingadb/Command/Transport/CommandTransportInterface.php
@@ -0,0 +1,23 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Command\Transport;
+
+use Icinga\Module\Icingadb\Command\IcingaCommand;
+
+/**
+ * Interface for Icinga command transports
+ */
+interface CommandTransportInterface
+{
+ /**
+ * Send an Icinga command over the Icinga command transport
+ *
+ * @param IcingaCommand $command The command to send
+ * @param int|null $now Timestamp of the command or null for now
+ *
+ * @throws CommandTransportException If sending the Icinga command failed
+ */
+ public function send(IcingaCommand $command, int $now = null);
+}