summaryrefslogtreecommitdiffstats
path: root/library/Businessprocess/BpConfig.php
diff options
context:
space:
mode:
Diffstat (limited to 'library/Businessprocess/BpConfig.php')
-rw-r--r--library/Businessprocess/BpConfig.php1117
1 files changed, 1117 insertions, 0 deletions
diff --git a/library/Businessprocess/BpConfig.php b/library/Businessprocess/BpConfig.php
new file mode 100644
index 0000000..c9e70fd
--- /dev/null
+++ b/library/Businessprocess/BpConfig.php
@@ -0,0 +1,1117 @@
+<?php
+
+namespace Icinga\Module\Businessprocess;
+
+use Exception;
+use Icinga\Application\Modules\Module;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Businessprocess\Exception\NestingError;
+use Icinga\Module\Businessprocess\Modification\ProcessChanges;
+use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport;
+use Icinga\Module\Businessprocess\Storage\LegacyStorage;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+use ipl\Sql\Connection as IcingaDbConnection;
+
+class BpConfig
+{
+ const SOFT_STATE = 0;
+
+ const HARD_STATE = 1;
+
+ /**
+ * Name of the configured monitoring backend
+ *
+ * @var string
+ */
+ protected $backendName;
+
+ /**
+ * Backend to retrieve states from
+ *
+ * @var MonitoringBackend|IcingaDbConnection
+ */
+ protected $backend;
+
+ /**
+ * @var LegacyStorage
+ */
+ protected $storage;
+
+ /** @var Metadata */
+ protected $metadata;
+
+ /**
+ * Business process name
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * Business process title
+ *
+ * @var string
+ */
+ protected $title;
+
+ /**
+ * State type, soft or hard
+ *
+ * @var int
+ */
+ protected $state_type;
+
+ /**
+ * Warnings, usually filled at process build time
+ *
+ * @var array
+ */
+ protected $warnings = array();
+
+ /**
+ * Errors, usually filled at process build time
+ *
+ * @var array
+ */
+ protected $errors = array();
+
+ /**
+ * All used node objects
+ *
+ * @var array
+ */
+ protected $nodes = array();
+
+ /**
+ * Root node objects
+ *
+ * @var array
+ */
+ protected $root_nodes = array();
+
+ /**
+ * Imported nodes
+ *
+ * @var ImportedNode[]
+ */
+ protected $importedNodes = [];
+
+ /**
+ * Imported configs
+ *
+ * @var BpConfig[]
+ */
+ protected $importedConfigs = [];
+
+ /**
+ * All host names { 'hostA' => true, ... }
+ *
+ * @var array
+ */
+ protected $hosts = array();
+
+ /** @var bool Whether catchable errors should be thrown nonetheless */
+ protected $throwErrors = false;
+
+ protected $loopDetection = array();
+
+ /**
+ * Applied state simulation
+ *
+ * @var Simulation
+ */
+ protected $simulation;
+
+ protected $changeCount = 0;
+
+ protected $simulationCount = 0;
+
+ /** @var ProcessChanges */
+ protected $appliedChanges;
+
+ /** @var bool Whether the config is faulty */
+ protected $isFaulty = false;
+
+ public function __construct()
+ {
+ }
+
+ /**
+ * Retrieve metadata for this configuration
+ *
+ * @return Metadata
+ */
+ public function getMetadata()
+ {
+ if ($this->metadata === null) {
+ $this->metadata = new Metadata($this->name);
+ }
+
+ return $this->metadata;
+ }
+
+ /**
+ * Set metadata
+ *
+ * @param Metadata $metadata
+ *
+ * @return $this
+ */
+ public function setMetadata(Metadata $metadata)
+ {
+ $this->metadata = $metadata;
+ return $this;
+ }
+
+ /**
+ * Apply pending process changes
+ *
+ * @param ProcessChanges $changes
+ *
+ * @return $this
+ */
+ public function applyChanges(ProcessChanges $changes)
+ {
+ $cnt = 0;
+ foreach ($changes->getChanges() as $change) {
+ $cnt++;
+ $change->applyTo($this);
+ }
+ $this->changeCount = $cnt;
+
+ $this->appliedChanges = $changes;
+
+ return $this;
+ }
+
+ /**
+ * Apply a state simulation
+ *
+ * @param Simulation $simulation
+ *
+ * @return $this
+ */
+ public function applySimulation(Simulation $simulation)
+ {
+ $cnt = 0;
+
+ foreach ($simulation->simulations() as $node => $s) {
+ if (! $this->hasNode($node)) {
+ continue;
+ }
+ $cnt++;
+ $this->getNode($node)
+ ->setState($s->state)
+ ->setAck($s->acknowledged)
+ ->setDowntime($s->in_downtime)
+ ->setMissing(false);
+ }
+
+ $this->simulationCount = $cnt;
+
+ return $this;
+ }
+
+ /**
+ * Number of applied changes
+ *
+ * @return int
+ */
+ public function countChanges()
+ {
+ return $this->changeCount;
+ }
+
+ /**
+ * Whether changes have been applied to this configuration
+ *
+ * @return bool
+ */
+ public function hasChanges()
+ {
+ return $this->countChanges() > 0;
+ }
+
+ /**
+ * @param $name
+ *
+ * @return $this
+ */
+ public function setName($name)
+ {
+ $this->name = $name;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * @return string
+ */
+ public function getHtmlId()
+ {
+ return 'businessprocess-' . preg_replace('/[\r\n\t\s]/', '_', $this->getName());
+ }
+
+ public function setTitle($title)
+ {
+ $this->title = $title;
+ return $this;
+ }
+
+ public function getTitle()
+ {
+ return $this->getMetadata()->getTitle();
+ }
+
+ public function hasTitle()
+ {
+ return $this->getMetadata()->has('Title');
+ }
+
+ public function getBackendName()
+ {
+ return $this->getMetadata()->get('Backend');
+ }
+
+ public function hasBackendName()
+ {
+ return $this->getMetadata()->has('Backend');
+ }
+
+ public function setBackend($backend)
+ {
+ $this->backend = $backend;
+ return $this;
+ }
+
+ public function getBackend()
+ {
+ if ($this->backend === null) {
+ if (Module::exists('icingadb')
+ && (! $this->hasBackendName() && IcingadbSupport::useIcingaDbAsBackend())) {
+ $this->backend = IcingaDbObject::fetchDb();
+ } else {
+ $this->backend = MonitoringBackend::instance(
+ $this->getBackendName()
+ );
+ }
+ }
+
+ return $this->backend;
+ }
+
+ public function isReferenced()
+ {
+ foreach ($this->storage()->listProcessNames() as $bpName) {
+ if ($bpName !== $this->getName()) {
+ $bp = $this->storage()->loadProcess($bpName);
+ foreach ($bp->getImportedNodes() as $importedNode) {
+ if ($importedNode->getConfigName() === $this->getName()) {
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public function hasBackend()
+ {
+ return $this->backend !== null;
+ }
+
+ public function hasBeenChanged()
+ {
+ return false;
+ }
+
+ public function hasSimulations()
+ {
+ return $this->countSimulations() > 0;
+ }
+
+ public function countSimulations()
+ {
+ return $this->simulationCount;
+ }
+
+ public function clearAppliedChanges()
+ {
+ if ($this->appliedChanges !== null) {
+ $this->appliedChanges->clear();
+ }
+ return $this;
+ }
+
+ public function getStateType()
+ {
+ if ($this->state_type === null) {
+ if ($this->getMetadata()->has('Statetype')) {
+ switch ($this->getMetadata()->get('Statetype')) {
+ case 'hard':
+ $this->state_type = self::HARD_STATE;
+ break;
+ case 'soft':
+ $this->state_type = self::SOFT_STATE;
+ break;
+ }
+ } else {
+ $this->state_type = self::HARD_STATE;
+ }
+ }
+
+ return $this->state_type;
+ }
+
+ public function useSoftStates()
+ {
+ $this->state_type = self::SOFT_STATE;
+ return $this;
+ }
+
+ public function useHardStates()
+ {
+ $this->state_type = self::HARD_STATE;
+ return $this;
+ }
+
+ public function usesSoftStates()
+ {
+ return $this->getStateType() === self::SOFT_STATE;
+ }
+
+ public function usesHardStates()
+ {
+ return $this->getStateType() === self::HARD_STATE;
+ }
+
+ public function addRootNode($name)
+ {
+ $this->root_nodes[$name] = $this->getNode($name);
+ return $this;
+ }
+
+ public function removeRootNode($name)
+ {
+ if ($this->isRootNode($name)) {
+ unset($this->root_nodes[$name]);
+ }
+
+ return $this;
+ }
+
+ public function isRootNode($name)
+ {
+ return array_key_exists($name, $this->root_nodes);
+ }
+
+ /**
+ * @return BpNode[]
+ */
+ public function getChildren()
+ {
+ return $this->getRootNodes();
+ }
+
+ /**
+ * @return int
+ */
+ public function countChildren()
+ {
+ return count($this->root_nodes);
+ }
+
+ /**
+ * @return BpNode[]
+ */
+ public function getRootNodes()
+ {
+ return $this->root_nodes;
+ }
+
+ public function listRootNodes()
+ {
+ return array_keys($this->root_nodes);
+ }
+
+ public function getNodes()
+ {
+ return $this->nodes;
+ }
+
+ public function hasNode($name)
+ {
+ if (array_key_exists($name, $this->nodes)) {
+ return true;
+ } elseif ($name[0] === '@') {
+ list($configName, $nodeName) = preg_split('~:\s*~', substr($name, 1), 2);
+ return $this->getImportedConfig($configName)->hasNode($nodeName);
+ }
+
+ return false;
+ }
+
+ public function hasRootNode($name)
+ {
+ return array_key_exists($name, $this->root_nodes);
+ }
+
+ public function createService($host, $service)
+ {
+ $node = new ServiceNode(
+ (object) array(
+ 'hostname' => $host,
+ 'service' => $service
+ )
+ );
+ $node->setBpConfig($this);
+ $this->nodes[$node->getName()] = $node;
+ $this->hosts[$host] = true;
+ return $node;
+ }
+
+ public function createHost($host)
+ {
+ $node = new HostNode((object) array('hostname' => $host));
+ $node->setBpConfig($this);
+ $this->nodes[$node->getName()] = $node;
+ $this->hosts[$host] = true;
+ return $node;
+ }
+
+ public function calculateAllStates()
+ {
+ foreach ($this->getRootNodes() as $node) {
+ $node->getState();
+ }
+
+ return $this;
+ }
+
+ public function clearAllStates()
+ {
+ foreach ($this->getBpNodes() as $node) {
+ $node->clearState();
+ }
+
+ return $this;
+ }
+
+ public function listInvolvedHostNames(&$usedConfigs = null)
+ {
+ $hosts = $this->hosts;
+ if (! empty($this->importedNodes)) {
+ $usedConfigs[$this->getName()] = true;
+ foreach ($this->importedNodes as $node) {
+ if (isset($usedConfigs[$node->getConfigName()])) {
+ continue;
+ }
+
+ $hosts += array_flip($node->getBpConfig()->listInvolvedHostNames($usedConfigs));
+ }
+ }
+
+ return array_keys($hosts);
+ }
+
+ /**
+ * Create and attach a new process (BpNode)
+ *
+ * @param string $name Process name
+ * @param string $operator Operator (defaults to &)
+ *
+ * @return BpNode
+ */
+ public function createBp($name, $operator = '&')
+ {
+ $node = new BpNode((object) array(
+ 'name' => $name,
+ 'operator' => $operator,
+ 'child_names' => array(),
+ ));
+ $node->setBpConfig($this);
+
+ $this->addNode($name, $node);
+ return $node;
+ }
+
+ public function createMissingBp($name)
+ {
+ return $this->createBp($name)->setMissing();
+ }
+
+ public function getMissingChildren()
+ {
+ $missing = array();
+ foreach ($this->getRootNodes() as $root) {
+ $missing += $root->getMissingChildren();
+ }
+
+ return $missing;
+ }
+
+ public function createImportedNode($config, $name = null)
+ {
+ $params = (object) array('configName' => $config);
+ if ($name !== null) {
+ $params->node = $name;
+ }
+
+ $node = new ImportedNode($this, $params);
+ $this->importedNodes[$node->getName()] = $node;
+ $this->nodes[$node->getName()] = $node;
+ return $node;
+ }
+
+ public function getImportedNodes()
+ {
+ return $this->importedNodes;
+ }
+
+ public function getImportedConfig($name)
+ {
+ if (! isset($this->importedConfigs[$name])) {
+ try {
+ $import = $this->storage()->loadProcess($name);
+ } catch (Exception $e) {
+ $import = (new static())
+ ->setName($name)
+ ->setFaulty();
+ }
+
+ if ($this->usesSoftStates()) {
+ $import->useSoftStates();
+ } else {
+ $import->useHardStates();
+ }
+
+ $this->importedConfigs[$name] = $import;
+ }
+
+ return $this->importedConfigs[$name];
+ }
+
+ public function listInvolvedConfigs(&$configs = null)
+ {
+ if ($configs === null) {
+ $configs[$this->getName()] = $this;
+ }
+
+ foreach ($this->importedNodes as $node) {
+ if (! isset($configs[$node->getConfigName()])) {
+ $config = $node->getBpConfig();
+ $configs[$node->getConfigName()] = $config;
+ $config->listInvolvedConfigs($configs);
+ }
+ }
+
+ return $configs;
+ }
+
+ /**
+ * @return LegacyStorage
+ */
+ protected function storage()
+ {
+ if ($this->storage === null) {
+ $this->storage = LegacyStorage::getInstance();
+ }
+
+ return $this->storage;
+ }
+
+ /**
+ * @param string $name
+ * @return MonitoredNode|BpNode
+ * @throws Exception
+ */
+ public function getNode($name)
+ {
+ if ($name === '__unbound__') {
+ return $this->getUnboundBaseNode();
+ }
+
+ if (array_key_exists($name, $this->nodes)) {
+ return $this->nodes[$name];
+ }
+
+ if ($name[0] === '@') {
+ list($configName, $nodeName) = preg_split('~:\s*~', substr($name, 1), 2);
+ return $this->getImportedConfig($configName)->getNode($nodeName);
+ }
+
+ // Fallback: if it is a service, create an empty one:
+ $this->warn(sprintf('The node "%s" doesn\'t exist', $name));
+
+ [$name, $suffix] = self::splitNodeName($name);
+ if ($suffix !== null) {
+ if ($suffix === 'Hoststatus') {
+ return $this->createHost($name);
+ } else {
+ return $this->createService($name, $suffix);
+ }
+ }
+
+ throw new Exception(
+ sprintf('The node "%s" doesn\'t exist', $name)
+ );
+ }
+
+ /**
+ * @return BpNode
+ */
+ public function getUnboundBaseNode()
+ {
+ // Hint: state is useless here, but triggers parent/child "calculation"
+ // This is an ugly workaround and should be made obsolete
+ $this->calculateAllStates();
+
+ $names = array_keys($this->getUnboundNodes());
+ $bp = new BpNode((object) array(
+ 'name' => '__unbound__',
+ 'operator' => '&',
+ 'child_names' => $names
+ ));
+ $bp->setBpConfig($this);
+ $bp->setAlias($this->translate('Unbound nodes'));
+ return $bp;
+ }
+
+ /**
+ * @param $name
+ * @return BpNode
+ *
+ * @throws NotFoundError
+ */
+ public function getBpNode($name)
+ {
+ if ($this->hasBpNode($name)) {
+ return $this->nodes[$name];
+ }
+
+ $msg = $this->isFaulty()
+ ? sprintf(
+ t('Trying to import node "%s" from faulty config file "%s.conf"'),
+ self::unescapeName($name),
+ $this->getName()
+ )
+ : sprintf(t('Trying to access a missing business process node "%s"'), $name);
+
+ throw new NotFoundError($msg);
+ }
+
+ /**
+ * @param $name
+ *
+ * @return bool
+ */
+ public function hasBpNode($name)
+ {
+ return array_key_exists($name, $this->nodes)
+ && $this->nodes[$name] instanceof BpNode;
+ }
+
+ /**
+ * Set the state for a specific node
+ *
+ * @param string $name Node name
+ * @param int $state Desired state
+ *
+ * @return $this
+ */
+ public function setNodeState($name, $state)
+ {
+ $this->getNode($name)->setState($state);
+ return $this;
+ }
+
+ /**
+ * Add the given node to the given BpNode
+ *
+ * @param $name
+ * @param BpNode $node
+ *
+ * @return $this
+ */
+ public function addNode($name, BpNode $node)
+ {
+ if (array_key_exists($name, $this->nodes)) {
+ $this->warn(
+ sprintf(
+ mt('businessprocess', 'Node "%s" has been defined twice'),
+ $name
+ )
+ );
+ }
+
+ $this->nodes[$name] = $node;
+
+ if ($node->getDisplay() > 0) {
+ if (! $this->isRootNode($name)) {
+ $this->addRootNode($name);
+ }
+ } else {
+ if ($this->isRootNode($name)) {
+ $this->removeRootNode($name);
+ }
+ }
+
+
+ return $this;
+ }
+
+ /**
+ * Remove all occurrences of a specific node by name
+ *
+ * @param $name
+ */
+ public function removeNode($name)
+ {
+ unset($this->nodes[$name]);
+ if (array_key_exists($name, $this->root_nodes)) {
+ unset($this->root_nodes[$name]);
+ }
+
+ foreach ($this->getBpNodes() as $node) {
+ if ($node->hasChild($name)) {
+ $node->removeChild($name);
+ }
+ }
+ }
+
+ /**
+ * Get all business process nodes
+ *
+ * @return BpNode[]
+ */
+ public function getBpNodes()
+ {
+ $nodes = array();
+
+ foreach ($this->nodes as $node) {
+ if ($node instanceof BpNode) {
+ $nodes[$node->getName()] = $node;
+ }
+ }
+
+ return $nodes;
+ }
+
+ /**
+ * List all business process node names
+ *
+ * @return array
+ */
+ public function listBpNodes()
+ {
+ $nodes = array();
+
+ foreach ($this->getBpNodes() as $name => $node) {
+ $alias = $node->getAlias();
+ $nodes[$name] = $name === $alias ? $name : sprintf('%s (%s)', $alias, $node);
+ }
+
+ return $nodes;
+ }
+
+ /**
+ * All business process nodes defined in this config but not
+ * assigned to any parent
+ *
+ * @return BpNode[]
+ */
+ public function getUnboundNodes()
+ {
+ $nodes = array();
+
+ foreach ($this->getBpNodes() as $name => $node) {
+ if ($node->hasParents()) {
+ continue;
+ }
+
+ if ($node->getDisplay() === 0) {
+ $nodes[$name] = $node;
+ }
+ }
+
+ return $nodes;
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasWarnings()
+ {
+ return ! empty($this->warnings);
+ }
+
+ /**
+ * @return array
+ */
+ public function getWarnings()
+ {
+ return $this->warnings;
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasErrors()
+ {
+ return ! empty($this->errors) || $this->isEmpty();
+ }
+
+ /**
+ * @return array
+ */
+ public function getErrors()
+ {
+ $errors = $this->errors;
+ if ($this->isEmpty()) {
+ $errors[] = sprintf(
+ $this->translate(
+ 'No business process nodes for "%s" have been defined yet'
+ ),
+ $this->getTitle()
+ );
+ }
+ return $errors;
+ }
+
+ /**
+ * Translation helper
+ *
+ * @param $msg
+ *
+ * @return mixed|string
+ */
+ public function translate($msg)
+ {
+ return mt('businessprocess', $msg);
+ }
+
+ /**
+ * Add a message to our warning stack
+ *
+ * @param $msg
+ */
+ protected function warn($msg)
+ {
+ $args = func_get_args();
+ array_shift($args);
+ $this->warnings[] = vsprintf($msg, $args);
+ }
+
+ /**
+ * @param string $msg,...
+ *
+ * @return $this
+ *
+ * @throws IcingaException
+ */
+ public function addError($msg)
+ {
+ $args = func_get_args();
+ array_shift($args);
+ if (! empty($args)) {
+ $msg = vsprintf($msg, $args);
+ }
+ if ($this->throwErrors) {
+ throw new IcingaException($msg);
+ }
+
+ if (! in_array($msg, $this->errors)) {
+ $this->errors[] = $msg;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Decide whether errors should be thrown or collected
+ *
+ * @param bool $throw
+ *
+ * @return $this
+ */
+ public function throwErrors($throw = true)
+ {
+ $this->throwErrors = $throw;
+ return $this;
+ }
+
+ /**
+ * Begin loop detection for the given name
+ *
+ * Will throw a NestingError in case this node will be met again below itself
+ *
+ * @param $name
+ *
+ * @throws NestingError
+ */
+ public function beginLoopDetection($name)
+ {
+ // echo "Begin loop $name\n";
+ if (array_key_exists($name, $this->loopDetection)) {
+ $loop = array_keys($this->loopDetection);
+ $loop[] = $name;
+ $this->loopDetection = array();
+ throw new NestingError('Loop detected: %s', implode(' -> ', $loop));
+ }
+
+ $this->loopDetection[$name] = true;
+ }
+
+ /**
+ * Remove the given name from the loop detection stack
+ *
+ * @param $name
+ */
+ public function endLoopDetection($name)
+ {
+ // echo "End loop $this->name\n";
+ unset($this->loopDetection[$name]);
+ }
+
+ /**
+ * Whether this configuration has any Nodes
+ *
+ * @return bool
+ */
+ public function isEmpty()
+ {
+ // This is faster
+ if (! empty($this->root_nodes)) {
+ return false;
+ }
+
+ return count($this->listBpNodes()) === 0;
+ }
+
+ /**
+ * Export the config to array
+ *
+ * @param bool $flat If false, children will be added to the array key children, else the array will be flat
+ *
+ * @return array
+ */
+ public function toArray($flat = false)
+ {
+ $data = [
+ 'name' => $this->getTitle(),
+ 'path' => $this->getTitle()
+ ];
+
+ $children = [];
+
+ foreach ($this->getChildren() as $node) {
+ if ($flat) {
+ $children = array_merge($children, $node->toArray($data, $flat));
+ } else {
+ $children[] = $node->toArray($data, $flat);
+ }
+ }
+
+ if ($flat) {
+ $data = [$data];
+
+ if (! empty($children)) {
+ $data = array_merge($data, $children);
+ }
+ } else {
+ $data['children'] = $children;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Escape the given node name
+ *
+ * @param string $name
+ *
+ * @return string
+ */
+ public static function escapeName(string $name): string
+ {
+ return preg_replace('/((?<!\\\\);)/', '\\\\$1', $name);
+ }
+
+ /**
+ * Unescape the given node name
+ *
+ * @param string $name
+ *
+ * @return string
+ */
+ public static function unescapeName(string $name): string
+ {
+ return str_replace('\\;', ';', $name);
+ }
+
+ /**
+ * Join the given two name parts together
+ *
+ * The used separator is the semicolon. If a semicolon exists in either part, it's escaped.
+ *
+ * @param string $name
+ * @param ?string $suffix
+ *
+ * @return string
+ */
+ public static function joinNodeName(string $name, ?string $suffix = null): string
+ {
+ return self::escapeName($name) . ($suffix ? ";$suffix" : '');
+ }
+
+ /**
+ * Split the given node name into two parts
+ *
+ * The first part is always a string, with any semicolons unescaped.
+ * The second part may be null or a string otherwise.
+ *
+ * @param string $nodeName
+ *
+ * @return array
+ */
+ public static function splitNodeName(string $nodeName): array
+ {
+ $parts = preg_split('/(?<!\\\\);/', $nodeName, 2);
+ $parts[0] = self::unescapeName($parts[0]);
+
+ return array_pad($parts, 2, null);
+ }
+
+ /**
+ * Set whether the config is faulty
+ *
+ * @param bool $isFaulty
+ *
+ * @return $this
+ */
+ public function setFaulty(bool $isFaulty = true): self
+ {
+ $this->isFaulty = $isFaulty;
+
+ return $this;
+ }
+
+ /**
+ * Get whether the config is faulty
+ *
+ * @return bool
+ */
+ public function isFaulty(): bool
+ {
+ return $this->isFaulty;
+ }
+}