diff options
Diffstat (limited to 'library/Director/Web/Table')
62 files changed, 6578 insertions, 0 deletions
diff --git a/library/Director/Web/Table/ActivityLogTable.php b/library/Director/Web/Table/ActivityLogTable.php new file mode 100644 index 0000000..5460bc2 --- /dev/null +++ b/library/Director/Web/Table/ActivityLogTable.php @@ -0,0 +1,294 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use gipfl\Format\LocalTimeFormat; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; +use Icinga\Module\Director\Util; +use ipl\Html\Html; +use ipl\Html\HtmlElement; + +class ActivityLogTable extends ZfQueryBasedTable +{ + protected $filters = []; + + protected $lastDeployedId; + + protected $extraParams = []; + + protected $columnCount; + + protected $hasObjectFilter = false; + + protected $searchColumns = [ + 'author', + 'object_name', + 'object_type', + ]; + + /** @var LocalTimeFormat */ + protected $timeFormat; + + protected $ranges = []; + + /** @var ?object */ + protected $currentRange = null; + /** @var ?HtmlElement */ + protected $currentRangeCell = null; + /** @var int */ + protected $rangeRows = 0; + protected $continueRange = false; + protected $currentRow; + + public function __construct($db) + { + parent::__construct($db); + $this->timeFormat = new LocalTimeFormat(); + } + + public function assemble() + { + $this->getAttributes()->add('class', 'activity-log'); + } + + public function setLastDeployedId($id) + { + $this->lastDeployedId = $id; + return $this; + } + + protected function fetchQueryRows() + { + $rows = parent::fetchQueryRows(); + // Hint -> DESC, that's why they are inverted + if (empty($rows)) { + return $rows; + } + $last = $rows[0]->id; + $first = $rows[count($rows) - 1]->id; + $db = $this->db(); + $this->ranges = $db->fetchAll( + $db->select() + ->from('director_activity_log_remark') + ->where('first_related_activity <= ?', $last) + ->where('last_related_activity >= ?', $first) + ); + + return $rows; + } + + + public function renderRow($row) + { + $this->currentRow = $row; + $this->splitByDay($row->ts_change_time); + $action = 'action-' . $row->action. ' '; + if ($row->id > $this->lastDeployedId) { + $action .= 'undeployed'; + } else { + $action .= 'deployed'; + } + + $columns = [ + $this::td($this->makeLink($row))->setSeparator(' '), + ]; + if (! $this->hasObjectFilter) { + $columns[] = $this->makeRangeInfo($row->id); + } + $columns[] = $this::td($this->timeFormat->getTime($row->ts_change_time)); + + return $this::tr($columns)->addAttributes(['class' => $action]); + } + + /** + * Hint: cloned from parent class and modified + * @param int $timestamp + */ + protected function renderDayIfNew($timestamp) + { + $day = $this->getDateFormatter()->getFullDay($timestamp); + + if ($this->lastDay !== $day) { + $this->nextHeader()->add( + $this::th($day, [ + 'colspan' => $this->hasObjectFilter ? 2 : 3, + 'class' => 'table-header-day' + ]) + ); + + $this->lastDay = $day; + if ($this->currentRangeCell) { + if ($this->currentRange->first_related_activity <= $this->currentRow->id) { + $this->currentRangeCell->addAttributes(['class' => 'continuing']); + $this->continueRange = true; + } else { + $this->continueRange = false; + } + } + $this->currentRangeCell = null; + $this->currentRange = null; + $this->rangeRows = 0; + $this->nextBody(); + } + } + + protected function makeRangeInfo($id) + { + $range = $this->getRangeForId($id); + if ($range === null) { + if ($this->currentRangeCell) { + $this->currentRangeCell->getAttributes()->remove('class', 'continuing'); + } + $this->currentRange = null; + $this->currentRangeCell = null; + $this->rangeRows = 0; + return $this::td(); + } + + if ($range === $this->currentRange) { + $this->growCurrentRange(); + return null; + } + $this->startRange($range); + + return $this->currentRangeCell; + } + + protected function startRange($range) + { + $this->currentRangeCell = $this::td($this->renderRangeComment($range), [ + 'colspan' => $this->rangeRows = 1, + 'class' => 'comment-cell' + ]); + if ($this->continueRange) { + $this->currentRangeCell->addAttributes(['class' => 'continued']); + $this->continueRange = false; + } + $this->currentRange = $range; + } + + protected function renderRangeComment($range) + { + // The only purpose of this container is to avoid hovered rows from influencing + // the comments background color, as we're using the alpha channel to lighten it + // This can be replaced once we get theme-safe colors for such messages + return Html::tag('div', [ + 'class' => 'range-comment-container', + ], Link::create($this->continueRange ? '' : $range->remark, '#', null, [ + 'title' => $range->remark, + 'class' => 'range-comment' + ])); + } + + protected function growCurrentRange() + { + $this->rangeRows++; + $this->currentRangeCell->setAttribute('rowspan', $this->rangeRows); + } + + protected function getRangeForId($id) + { + foreach ($this->ranges as $range) { + if ($id >= $range->first_related_activity && $id <= $range->last_related_activity) { + return $range; + } + } + + return null; + } + + protected function makeLink($row) + { + $type = $row->object_type; + $name = $row->object_name; + if (substr($type, 0, 7) === 'icinga_') { + $type = substr($type, 7); + } + + if (Util::hasPermission('director/showconfig')) { + // Later on replacing, service_set -> serviceset + + // multi column key :( + if ($type === 'service' || $this->hasObjectFilter) { + $object = "\"$name\""; + } else { + $object = Link::create( + "\"$name\"", + 'director/' . str_replace('_', '', $type), + ['name' => $name], + ['title' => $this->translate('Jump to this object')] + ); + } + + return [ + '[' . $row->author . ']', + Link::create( + $row->action, + 'director/config/activity', + array_merge(['id' => $row->id], $this->extraParams), + ['title' => $this->translate('Show details related to this change')] + ), + str_replace('_', ' ', $type), + $object + ]; + } else { + return sprintf( + '[%s] %s %s "%s"', + $row->author, + $row->action, + $type, + $name + ); + } + } + + public function filterObject($type, $name) + { + $this->hasObjectFilter = true; + $this->filters[] = ['l.object_type = ?', $type]; + $this->filters[] = ['l.object_name = ?', $name]; + + return $this; + } + + public function filterHost($name) + { + $db = $this->db(); + $filter = '%"host":' . json_encode($name) . '%'; + $this->filters[] = ['(' + . $db->quoteInto('l.old_properties LIKE ?', $filter) + . ' OR ' + . $db->quoteInto('l.new_properties LIKE ?', $filter) + . ')', null]; + + return $this; + } + + public function getColumns() + { + return [ + 'author' => 'l.author', + 'action' => 'l.action_name', + 'object_name' => 'l.object_name', + 'object_type' => 'l.object_type', + 'id' => 'l.id', + 'change_time' => 'l.change_time', + 'ts_change_time' => 'UNIX_TIMESTAMP(l.change_time)', + ]; + } + + public function prepareQuery() + { + $query = $this->db()->select()->from( + ['l' => 'director_activity_log'], + $this->getColumns() + )->order('change_time DESC')->order('id DESC')->limit(100); + + foreach ($this->filters as $filter) { + $query->where($filter[0], $filter[1]); + } + + return $query; + } +} diff --git a/library/Director/Web/Table/ApplyRulesTable.php b/library/Director/Web/Table/ApplyRulesTable.php new file mode 100644 index 0000000..a861bac --- /dev/null +++ b/library/Director/Web/Table/ApplyRulesTable.php @@ -0,0 +1,240 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Icinga\Authentication\Auth; +use Icinga\Data\Filter\Filter; +use Icinga\Exception\IcingaException; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Db\DbUtil; +use Icinga\Module\Director\Db\IcingaObjectFilterHelper; +use Icinga\Module\Director\IcingaConfig\AssignRenderer; +use Icinga\Module\Director\Objects\IcingaObject; +use gipfl\IcingaWeb2\Icon; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; +use gipfl\IcingaWeb2\Url; +use gipfl\IcingaWeb2\Zf1\Db\FilterRenderer; +use Ramsey\Uuid\Uuid; +use Zend_Db_Select as ZfSelect; + +class ApplyRulesTable extends ZfQueryBasedTable +{ + protected $searchColumns = [ + 'o.object_name', + 'o.assign_filter', + ]; + + private $type; + + /** @var IcingaObject */ + protected $dummyObject; + + protected $baseObjectUrl; + + protected $linkWithName = false; + + public static function create($type, Db $db) + { + $table = new static($db); + $table->setType($type); + return $table; + } + + public function setType($type) + { + $this->type = $type; + + return $this; + } + + public function setBaseObjectUrl($url) + { + $this->baseObjectUrl = $url; + + return $this; + } + + public function createLinksWithNames($linksWithName = true) + { + $this->linkWithName = (bool) $linksWithName; + + return $this; + } + + public function getType() + { + return $this->type; + } + + public function getColumnsToBeRendered() + { + return ['Name', 'assign where'/*, 'Actions'*/]; + } + + public function renderRow($row) + { + $row->uuid = DbUtil::binaryResult($row->uuid); + if ($this->linkWithName) { + $params = ['name' => $row->object_name]; + } else { + $params = ['uuid' => Uuid::fromBytes($row->uuid)->toString()]; + } + $url = Url::fromPath("director/{$this->baseObjectUrl}/edit", $params); + + $assignWhere = $this->renderApplyFilter($row->assign_filter); + + if (! empty($row->apply_for)) { + $assignWhere = sprintf('apply for %s / %s', $row->apply_for, $assignWhere); + } + + $tr = static::tr([ + static::td(Link::create($row->object_name, $url)), + static::td($assignWhere), + // NOT (YET) static::td($this->createActionLinks($row))->setSeparator(' ') + ]); + + if ($row->disabled === 'y') { + $tr->getAttributes()->add('class', 'disabled'); + } + + return $tr; + } + + /** + * Should be triggered from renderRow, still unused. + * + * @param IcingaObject $template + * @param string $inheritance + * @return $this + * @throws \Icinga\Exception\ProgrammingError + */ + public function filterTemplate( + IcingaObject $template, + $inheritance = IcingaObjectFilterHelper::INHERIT_DIRECT + ) { + IcingaObjectFilterHelper::filterByTemplate( + $this->getQuery(), + $template, + 'o', + $inheritance + ); + + return $this; + } + + protected function renderApplyFilter($assignFilter) + { + try { + $string = AssignRenderer::forFilter( + Filter::fromQueryString($assignFilter) + )->renderAssign(); + // Do not prefix it + $string = preg_replace('/^assign where /', '', $string); + } catch (IcingaException $e) { + // ignore errors in filter rendering + $string = 'Error in Filter rendering: ' . $e->getMessage(); + } + + return $string; + } + + public function createActionLinks($row) + { + $params = ['uuid' => Uuid::fromBytes($row->uuid)->toString()]; + $baseUrl = 'director/' . $this->baseObjectUrl; + $links = []; + $links[] = Link::create( + Icon::create('sitemap'), + "${baseUrl}template/applytargets", + ['id' => $row->id], + ['title' => $this->translate('Show affected Objects')] + ); + + $links[] = Link::create( + Icon::create('edit'), + "$baseUrl/edit", + $params, + ['title' => $this->translate('Modify this Apply Rule')] + ); + + $links[] = Link::create( + Icon::create('doc-text'), + "$baseUrl/render", + $params, + ['title' => $this->translate('Apply Rule rendering preview')] + ); + + $links[] = Link::create( + Icon::create('history'), + "$baseUrl/history", + $params, + ['title' => $this->translate('Apply rule history')] + ); + + return $links; + } + + protected function applyRestrictions(ZfSelect $query) + { + $auth = Auth::getInstance(); + $type = $this->type; + // TODO: Centralize this logic + if ($type === 'scheduledDowntime') { + $type = 'scheduled-downtime'; + } + $restrictions = $auth->getRestrictions("director/$type/apply/filter-by-name"); + if (empty($restrictions)) { + return $query; + } + + $filter = Filter::matchAny(); + foreach ($restrictions as $restriction) { + $filter->addFilter(Filter::where('o.object_name', $restriction)); + } + + return FilterRenderer::applyToQuery($filter, $query); + } + + + /** + * @return IcingaObject + */ + protected function getDummyObject() + { + if ($this->dummyObject === null) { + $type = $this->type; + $this->dummyObject = IcingaObject::createByType($type); + } + return $this->dummyObject; + } + + public function prepareQuery() + { + $table = $this->getDummyObject()->getTableName(); + $columns = [ + 'id' => 'o.id', + 'uuid' => 'o.uuid', + 'object_name' => 'o.object_name', + 'disabled' => 'o.disabled', + 'assign_filter' => 'o.assign_filter', + 'apply_for' => '(NULL)', + ]; + + if ($table === 'icinga_service') { + $columns['apply_for'] = 'o.apply_for'; + } + $query = $this->db()->select()->from( + ['o' => $table], + $columns + )->where( + "object_type = 'apply'" + )->order('o.object_name'); + + if ($this->type === 'service') { + $query->where('service_set_id IS NULL'); + } + + return $this->applyRestrictions($query); + } +} diff --git a/library/Director/Web/Table/BasketSnapshotTable.php b/library/Director/Web/Table/BasketSnapshotTable.php new file mode 100644 index 0000000..08f808a --- /dev/null +++ b/library/Director/Web/Table/BasketSnapshotTable.php @@ -0,0 +1,125 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use ipl\Html\Html; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; +use Icinga\Date\DateFormatter; +use Icinga\Module\Director\Core\Json; +use Icinga\Module\Director\DirectorObject\Automation\Basket; +use RuntimeException; + +class BasketSnapshotTable extends ZfQueryBasedTable +{ + use DbHelper; + + protected $searchColumns = [ + 'basket_name', + 'summary' + ]; + + /** @var Basket */ + protected $basket; + + public function setBasket(Basket $basket) + { + $this->basket = $basket; + $this->searchColumns = []; + + return $this; + } + + public function renderRow($row) + { + $this->splitByDay($row->ts_create_seconds); + $link = $this->linkToSnapshot($this->renderSummary($row->summary), $row); + + if ($this->basket === null) { + $columns = [ + [ + new Link( + Html::tag('strong', $row->basket_name), + 'director/basket', + ['name' => $row->basket_name] + ), + Html::tag('br'), + $link, + ], + DateFormatter::formatTime($row->ts_create / 1000), + ]; + } else { + $columns = [ + $link, + DateFormatter::formatTime($row->ts_create / 1000), + ]; + } + return $this::row($columns); + } + + protected function renderSummary($summary) + { + $summary = Json::decode($summary); + if ($summary === null) { + return '-'; + } + $result = []; + if (! is_object($summary) && ! is_array($summary)) { + throw new RuntimeException(sprintf( + 'Got invalid basket summary: %s ', + var_export($summary, 1) + )); + } + + foreach ($summary as $type => $count) { + $result[] = sprintf( + '%dx %s', + $count, + $type + ); + } + + if (empty($result)) { + return '-'; + } + + return implode(', ', $result); + } + + protected function linkToSnapshot($caption, $row) + { + return new Link($caption, 'director/basket/snapshot', [ + 'checksum' => bin2hex($this->wantBinaryValue($row->content_checksum)), + 'ts' => $row->ts_create, + 'name' => $row->basket_name, + ]); + } + + public function prepareQuery() + { + $query = $this->db()->select()->from([ + 'b' => 'director_basket' + ], [ + 'b.uuid', + 'b.basket_name', + 'bs.ts_create', + 'ts_create_seconds' => '(bs.ts_create / 1000)', + 'bs.content_checksum', + 'bc.summary', + ])->join( + ['bs' => 'director_basket_snapshot'], + 'bs.basket_uuid = b.uuid', + [] + )->join( + ['bc' => 'director_basket_content'], + 'bc.checksum = bs.content_checksum', + [] + )->order('bs.ts_create DESC'); + + if ($this->basket !== null) { + $query->where('b.uuid = ?', $this->quoteBinary($this->basket->get('uuid'))); + } + + return $query; + } +} diff --git a/library/Director/Web/Table/BasketTable.php b/library/Director/Web/Table/BasketTable.php new file mode 100644 index 0000000..25e37e0 --- /dev/null +++ b/library/Director/Web/Table/BasketTable.php @@ -0,0 +1,50 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; + +class BasketTable extends ZfQueryBasedTable +{ + protected $searchColumns = [ + 'basket_name', + ]; + + public function renderRow($row) + { + $tr = $this::row([ + new Link( + $row->basket_name, + 'director/basket', + ['name' => $row->basket_name] + ), + $row->cnt_snapshots + ]); + + return $tr; + } + + public function getColumnsToBeRendered() + { + return [ + $this->translate('Basket'), + $this->translate('Snapshots'), + ]; + } + + public function prepareQuery() + { + return $this->db()->select()->from([ + 'b' => 'director_basket' + ], [ + 'b.uuid', + 'b.basket_name', + 'cnt_snapshots' => 'COUNT(bs.basket_uuid)', + ])->joinLeft( + ['bs' => 'director_basket_snapshot'], + 'bs.basket_uuid = b.uuid', + [] + )->group('b.uuid')->order('b.basket_name'); + } +} diff --git a/library/Director/Web/Table/BranchActivityTable.php b/library/Director/Web/Table/BranchActivityTable.php new file mode 100644 index 0000000..e7131ef --- /dev/null +++ b/library/Director/Web/Table/BranchActivityTable.php @@ -0,0 +1,116 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use gipfl\Format\LocalTimeFormat; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Db\Branch\BranchActivity; +use Icinga\Module\Director\Util; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; +use Ramsey\Uuid\UuidInterface; + +class BranchActivityTable extends ZfQueryBasedTable +{ + protected $extraParams = []; + + /** @var UuidInterface */ + protected $branchUuid; + + /** @var ?UuidInterface */ + protected $objectUuid; + + /** @var LocalTimeFormat */ + protected $timeFormat; + + protected $linkToObject = true; + + public function __construct(UuidInterface $branchUuid, $db, UuidInterface $objectUuid = null) + { + $this->branchUuid = $branchUuid; + $this->objectUuid = $objectUuid; + $this->timeFormat = new LocalTimeFormat(); + parent::__construct($db); + } + + public function assemble() + { + $this->getAttributes()->add('class', 'activity-log'); + } + + public function renderRow($row) + { + $ts = (int) floor(BranchActivity::fixFakeTimestamp($row->timestamp_ns) / 1000000); + $this->splitByDay($ts); + $activity = BranchActivity::fromDbRow($row); + return $this::tr([ + $this::td($this->makeBranchLink($activity))->setSeparator(' '), + $this::td($this->timeFormat->getTime($ts)) + ])->addAttributes(['class' => ['action-' . $activity->getAction(), 'branched']]); + } + + public function disableObjectLink() + { + $this->linkToObject = false; + return $this; + } + + protected function linkObject(BranchActivity $activity) + { + if (! $this->linkToObject) { + return $activity->getObjectName(); + } + // $type, UuidInterface $uuid + // Later on replacing, service_set -> serviceset + $type = preg_replace('/^icinga_/', '', $activity->getObjectTable()); + return Link::create( + $activity->getObjectName(), + 'director/' . str_replace('_', '', $type), + ['uuid' => $activity->getObjectUuid()->toString()], + ['title' => $this->translate('Jump to this object')] + ); + } + + protected function makeBranchLink(BranchActivity $activity) + { + $type = preg_replace('/^icinga_/', '', $activity->getObjectTable()); + + if (Util::hasPermission('director/showconfig')) { + // Later on replacing, service_set -> serviceset + return [ + '[' . $activity->getAuthor() . ']', + Link::create( + $activity->getAction(), + 'director/branch/activity', + array_merge(['ts' => $activity->getTimestampNs()], $this->extraParams), + ['title' => $this->translate('Show details related to this change')] + ), + str_replace('_', ' ', $type), + $this->linkObject($activity) + ]; + } else { + return sprintf( + '[%s] %s %s "%s"', + $activity->getAuthor(), + $activity->getAction(), + $type, + $activity->getObjectName() + ); + } + } + + public function prepareQuery() + { + /** @var Db $connection */ + $connection = $this->connection(); + $query = $this->db()->select()->from(['ba' => 'director_branch_activity'], 'ba.*') + ->join(['b' => 'director_branch'], 'b.uuid = ba.branch_uuid', ['b.owner']) + ->where('branch_uuid = ?', $connection->quoteBinary($this->branchUuid->getBytes())) + ->order('timestamp_ns DESC'); + if ($this->objectUuid) { + $query->where('ba.object_uuid = ?', $connection->quoteBinary($this->objectUuid->getBytes())); + } + + return $query; + } +} diff --git a/library/Director/Web/Table/BranchedIcingaCommandArgumentTable.php b/library/Director/Web/Table/BranchedIcingaCommandArgumentTable.php new file mode 100644 index 0000000..3d5dbcb --- /dev/null +++ b/library/Director/Web/Table/BranchedIcingaCommandArgumentTable.php @@ -0,0 +1,78 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use gipfl\IcingaWeb2\Data\SimpleQueryPaginationAdapter; +use gipfl\IcingaWeb2\Table\QueryBasedTable; +use Icinga\Data\DataArray\ArrayDatasource; +use Icinga\Module\Director\Db\Branch\Branch; +use Icinga\Module\Director\Objects\IcingaCommand; +use gipfl\IcingaWeb2\Link; + +class BranchedIcingaCommandArgumentTable extends QueryBasedTable +{ + /** @var IcingaCommand */ + protected $command; + + /** @var Branch */ + protected $branch; + + protected $searchColumns = [ + 'ca.argument_name', + 'ca.argument_value', + ]; + + public function __construct(IcingaCommand $command, Branch $branch) + { + $this->command = $command; + $this->branch = $branch; + $this->getAttributes()->set('data-base-target', '_self'); + } + + public function renderRow($row) + { + return $this::row([ + Link::create($row->argument_name, 'director/command/arguments', [ + 'argument' => $row->argument_name, + 'uuid' => $this->command->getUniqueId()->toString(), + ]), + $row->argument_value + ]); + } + + public function getColumnsToBeRendered() + { + return [ + $this->translate('Argument'), + $this->translate('Value'), + ]; + } + + protected function getPaginationAdapter() + { + return new SimpleQueryPaginationAdapter($this->getQuery()); + } + + public function getQuery() + { + return $this->prepareQuery(); + } + + protected function fetchQueryRows() + { + return $this->getQuery()->fetchAll(); + } + + protected function prepareQuery() + { + $list = []; + foreach ($this->command->arguments()->toPlainObject() as $name => $argument) { + $new = (object) []; + $new->argument_name = $name; + $new->argument_value = isset($argument->value) ? $argument->value : null; + $list[] = $new; + } + + return (new ArrayDatasource($list))->select(); + } +} diff --git a/library/Director/Web/Table/ChoicesTable.php b/library/Director/Web/Table/ChoicesTable.php new file mode 100644 index 0000000..4ba2460 --- /dev/null +++ b/library/Director/Web/Table/ChoicesTable.php @@ -0,0 +1,65 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Icinga\Module\Director\Db; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; +use gipfl\IcingaWeb2\Url; + +class ChoicesTable extends ZfQueryBasedTable +{ + protected $searchColumns = ['o.object_name']; + + protected $type; + + /** + * @param $type + * @param Db $db + * @return static + */ + public static function create($type, Db $db) + { + $class = __NAMESPACE__ . '\\ChoicesTable' . ucfirst($type); + if (! class_exists($class)) { + $class = __CLASS__; + } + + /** @var static $table */ + $table = new $class($db); + $table->type = $type; + return $table; + } + + public function getType() + { + return $this->type; + } + + public function getColumnsToBeRendered() + { + return [$this->translate('Name')]; + } + + public function renderRow($row) + { + $type = $this->getType(); + $url = Url::fromPath("director/templatechoice/${type}", [ + 'name' => $row->object_name + ]); + + return $this::row([ + Link::create($row->object_name, $url) + ]); + } + + protected function prepareQuery() + { + $type = $this->getType(); + $table = "icinga_${type}_template_choice"; + return $this->db() + ->select() + ->from(['o' => $table], 'object_name') + ->order('o.object_name'); + } +} diff --git a/library/Director/Web/Table/ConfigFileDiffTable.php b/library/Director/Web/Table/ConfigFileDiffTable.php new file mode 100644 index 0000000..1d14d5e --- /dev/null +++ b/library/Director/Web/Table/ConfigFileDiffTable.php @@ -0,0 +1,140 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Util; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; + +class ConfigFileDiffTable extends ZfQueryBasedTable +{ + use DbHelper; + + protected $leftChecksum; + + protected $rightChecksum; + + /** + * @param $leftSum + * @param $rightSum + * @param Db $connection + * @return static + */ + public static function load($leftSum, $rightSum, Db $connection) + { + $table = new static($connection); + $table->getAttributes()->add('class', 'config-diff'); + return $table->setLeftChecksum($leftSum) + ->setRightChecksum($rightSum); + } + + public function renderRow($row) + { + $tr = $this::row([ + $this->getFileFiffLink($row), + $row->file_path, + ]); + + $tr->getAttributes()->add('class', 'file-' . $row->file_action); + return $tr; + } + + protected function getFileFiffLink($row) + { + $params = array('file_path' => $row->file_path); + + if ($row->file_checksum_left === $row->file_checksum_right) { + $params['config_checksum'] = $row->config_checksum_right; + } elseif ($row->file_checksum_left === null) { + $params['config_checksum'] = $row->config_checksum_right; + } elseif ($row->file_checksum_right === null) { + $params['config_checksum'] = $row->config_checksum_left; + } else { + $params['left'] = $row->config_checksum_left; + $params['right'] = $row->config_checksum_right; + return Link::create( + $row->file_action, + 'director/config/filediff', + $params + ); + } + + return Link::create($row->file_action, 'director/config/file', $params); + } + + public function setLeftChecksum($checksum) + { + $this->leftChecksum = $checksum; + return $this; + } + + public function setRightChecksum($checksum) + { + $this->rightChecksum = $checksum; + return $this; + } + + public function getTitles() + { + return array( + $this->translate('Action'), + $this->translate('File'), + ); + } + + public function prepareQuery() + { + $db = $this->db(); + + $left = $db->select() + ->from( + array('cfl' => 'director_generated_config_file'), + array( + 'file_path' => 'COALESCE(cfl.file_path, cfr.file_path)', + 'config_checksum_left' => $this->dbHexFunc('cfl.config_checksum'), + 'config_checksum_right' => $this->dbHexFunc('cfr.config_checksum'), + 'file_checksum_left' => $this->dbHexFunc('cfl.file_checksum'), + 'file_checksum_right' => $this->dbHexFunc('cfr.file_checksum'), + 'file_action' => '(CASE WHEN cfr.config_checksum IS NULL' + . " THEN 'removed' WHEN cfl.file_checksum = cfr.file_checksum" + . " THEN 'unmodified' ELSE 'modified' END)", + ) + )->joinLeft( + array('cfr' => 'director_generated_config_file'), + $db->quoteInto( + 'cfl.file_path = cfr.file_path AND cfr.config_checksum = ?', + $this->quoteBinary(hex2bin($this->rightChecksum)) + ), + array() + )->where( + 'cfl.config_checksum = ?', + $this->quoteBinary(hex2bin($this->leftChecksum)) + ); + + $right = $db->select() + ->from( + array('cfl' => 'director_generated_config_file'), + array( + 'file_path' => 'COALESCE(cfr.file_path, cfl.file_path)', + 'config_checksum_left' => $this->dbHexFunc('cfl.config_checksum'), + 'config_checksum_right' => $this->dbHexFunc('cfr.config_checksum'), + 'file_checksum_left' => $this->dbHexFunc('cfl.file_checksum'), + 'file_checksum_right' => $this->dbHexFunc('cfr.file_checksum'), + 'file_action' => "('created')", + ) + )->joinRight( + array('cfr' => 'director_generated_config_file'), + $db->quoteInto( + 'cfl.file_path = cfr.file_path AND cfl.config_checksum = ?', + $this->quoteBinary(hex2bin($this->leftChecksum)) + ), + array() + )->where( + 'cfr.config_checksum = ?', + $this->quoteBinary(hex2bin($this->rightChecksum)) + )->where('cfl.file_checksum IS NULL'); + + return $db->select()->union(array($left, $right))->order('file_path'); + } +} diff --git a/library/Director/Web/Table/CoreApiFieldsTable.php b/library/Director/Web/Table/CoreApiFieldsTable.php new file mode 100644 index 0000000..24a6521 --- /dev/null +++ b/library/Director/Web/Table/CoreApiFieldsTable.php @@ -0,0 +1,106 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use gipfl\IcingaWeb2\Link; +use ipl\Html\Table; +use gipfl\Translation\TranslationHelper; +use gipfl\IcingaWeb2\Url; + +class CoreApiFieldsTable extends Table +{ + use TranslationHelper; + + protected $defaultAttributes = [ + 'class' => ['common-table'/*, 'table-row-selectable'*/], + //'data-base-target' => '_next', + ]; + + protected $fields; + + /** @var Url */ + protected $url; + + public function __construct($fields, Url $url) + { + $this->url = $url; + $this->fields = $fields; + } + + public function assemble() + { + if (empty($this->fields)) { + return; + } + $this->add(Html::tag('thead', Html::tag('tr', Html::wrapEach($this->getColumnsToBeRendered(), 'th')))); + foreach ($this->fields as $name => $field) { + $tr = $this::tr([ + $this::td($name), + $this::td(Link::create( + $field->type, + $this->url->with('type', $field->type) + )), + $this::td($field->id) + // $this::td($field->array_rank), + // $this::td($this->renderKeyValue($field->attributes)) + ]); + $this->addAttributeColumns($tr, $field->attributes); + $this->add($tr); + } + } + + protected function addAttributeColumns(BaseHtmlElement $tr, $attrs) + { + $tr->add([ + $this->makeBooleanColumn($attrs->state), + $this->makeBooleanColumn($attrs->config), + $this->makeBooleanColumn($attrs->required), + $this->makeBooleanColumn(isset($attrs->deprecated) ? $attrs->deprecated : null), + $this->makeBooleanColumn($attrs->no_user_modify), + $this->makeBooleanColumn($attrs->no_user_view), + $this->makeBooleanColumn($attrs->navigation), + ]); + } + + protected function makeBooleanColumn($value) + { + if ($value === null) { + return $this::td('-'); + } + + return $this::td($value ? Html::tag('strong', 'true') : 'false'); + } + + public function getColumnsToBeRendered() + { + return [ + $this->translate('Name'), + $this->translate('Type'), + $this->translate('Id'), + // $this->translate('Array Rank'), + // $this->translate('Attributes') + $this->translate('State'), + $this->translate('Config'), + $this->translate('Required'), + $this->translate('Deprecated'), + $this->translate('Protected'), + $this->translate('Hidden'), + $this->translate('Nav'), + ]; + } + + protected function renderKeyValue($values) + { + $parts = []; + foreach ((array) $values as $key => $value) { + if (is_bool($value)) { + $value = $value ? 'true' : 'false'; + } + $parts[] = "$key: $value"; + } + + return implode(', ', $parts); + } +} diff --git a/library/Director/Web/Table/CoreApiObjectsTable.php b/library/Director/Web/Table/CoreApiObjectsTable.php new file mode 100644 index 0000000..c2cefea --- /dev/null +++ b/library/Director/Web/Table/CoreApiObjectsTable.php @@ -0,0 +1,60 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Icinga\Module\Director\Objects\IcingaEndpoint; +use gipfl\IcingaWeb2\Link; +use ipl\Html\Html; +use ipl\Html\Table; +use gipfl\Translation\TranslationHelper; + +class CoreApiObjectsTable extends Table +{ + use TranslationHelper; + + protected $defaultAttributes = [ + 'class' => ['common-table', 'table-row-selectable'], + 'data-base-target' => '_next', + ]; + + /** @var IcingaEndpoint */ + protected $endpoint; + + protected $objects; + + protected $type; + + public function __construct($objects, IcingaEndpoint $endpoint, $type) + { + $this->objects = $objects; + $this->endpoint = $endpoint; + $this->type = $type; + } + + public function assemble() + { + if (empty($this->objects)) { + return; + } + $this->add(Html::tag('thead', Html::tag('tr', Html::wrapEach($this->getColumnsToBeRendered(), 'th')))); + foreach ($this->objects as $name) { + $this->add($this::tr($this::td(Link::create( + str_replace('!', ': ', $name), + 'director/inspect/object', + [ + 'name' => $name, + 'type' => $this->type->name, + 'plural' => $this->type->plural_name, + 'endpoint' => $this->endpoint->getObjectName() + ] + )))); + } + } + + public function getColumnsToBeRendered() + { + return [ + $this->translate('Name'), + ]; + } +} diff --git a/library/Director/Web/Table/CoreApiPrototypesTable.php b/library/Director/Web/Table/CoreApiPrototypesTable.php new file mode 100644 index 0000000..78fd964 --- /dev/null +++ b/library/Director/Web/Table/CoreApiPrototypesTable.php @@ -0,0 +1,43 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use ipl\Html\Html; +use ipl\Html\Table; +use gipfl\Translation\TranslationHelper; + +class CoreApiPrototypesTable extends Table +{ + use TranslationHelper; + + protected $defaultAttributes = ['class' => ['common-table']]; + + protected $prototypes; + + protected $typeName; + + public function __construct($prototypes, $typeName) + { + $this->prototypes = $prototypes; + $this->typeName = $typeName; + } + + public function assemble() + { + if (empty($this->prototypes)) { + return; + } + $this->add(Html::tag('thead', Html::tag('tr', Html::wrapEach($this->getColumnsToBeRendered(), 'th')))); + $type = $this->typeName; + foreach ($this->prototypes as $name) { + $this->add($this::tr($this::td("$type.$name()"))); + } + } + + public function getColumnsToBeRendered() + { + return [ + $this->translate('Name'), + ]; + } +} diff --git a/library/Director/Web/Table/CustomvarTable.php b/library/Director/Web/Table/CustomvarTable.php new file mode 100644 index 0000000..f9a3844 --- /dev/null +++ b/library/Director/Web/Table/CustomvarTable.php @@ -0,0 +1,102 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use ipl\Html\Html; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; +use Zend_Db_Adapter_Abstract as ZfDbAdapter; +use Zend_Db_Select as ZfDbSelect; + +class CustomvarTable extends ZfQueryBasedTable +{ + protected $searchColumns = array( + 'varname', + ); + + public function renderRow($row) + { + $tr = $this::row([ + new Link( + $row->varname, + 'director/customvar/variants', + ['name' => $row->varname] + ) + ]); + + foreach ($this->getObjectTypes() as $type) { + $tr->add($this::td(Html::tag('nobr', null, sprintf( + $this->translate('%d / %d'), + $row->{"cnt_$type"}, + $row->{"distinct_$type"} + )))); + } + + return $tr; + } + + public function getColumnsToBeRendered() + { + return array( + $this->translate('Variable name'), + $this->translate('Distinct Commands'), + $this->translate('Hosts'), + $this->translate('Services'), + $this->translate('Service Sets'), + $this->translate('Notifications'), + $this->translate('Users'), + ); + } + + protected function getObjectTypes() + { + return ['command', 'host', 'service', 'service_set', 'notification', 'user']; + } + + public function prepareQuery() + { + $db = $this->db(); + $varsColumns = ['varname' => 'v.varname']; + $varsTypes = $this->getObjectTypes(); + foreach ($varsTypes as $type) { + $varsColumns["cnt_$type"] = '(0)'; + $varsColumns["distinct_$type"] = '(0)'; + } + $varsQueries = []; + foreach ($varsTypes as $type) { + $varsQueries[] = $this->makeVarSub($type, $varsColumns, $db); + } + + $union = $db->select()->union($varsQueries, ZfDbSelect::SQL_UNION_ALL); + + $columns = ['varname' => 'u.varname']; + foreach ($varsTypes as $column) { + $columns["cnt_$column"] = "SUM(u.cnt_$column)"; + $columns["distinct_$column"] = "SUM(u.distinct_$column)"; + } + return $db->select()->from( + array('u' => $union), + $columns + )->group('u.varname')->order('u.varname ASC')->limit(100); + } + + /** + * @param string $type + * @param array $columns + * @param ZfDbAdapter $db + * @return ZfDbSelect + */ + protected function makeVarSub($type, array $columns, ZfDbAdapter $db) + { + $columns["cnt_$type"] = 'COUNT(*)'; + $columns["distinct_$type"] = 'COUNT(DISTINCT varvalue)'; + return $db->select()->from( + ['v' => "icinga_${type}_var"], + $columns + )->join( + ['o' => "icinga_${type}"], + "o.id = v.${type}_id", + [] + )->where('o.object_type != ?', 'external_object')->group('varname'); + } +} diff --git a/library/Director/Web/Table/CustomvarVariantsTable.php b/library/Director/Web/Table/CustomvarVariantsTable.php new file mode 100644 index 0000000..80fca70 --- /dev/null +++ b/library/Director/Web/Table/CustomvarVariantsTable.php @@ -0,0 +1,125 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Icinga\Module\Director\Db; +use Icinga\Module\Director\PlainObjectRenderer; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; +use Zend_Db_Adapter_Abstract as ZfDbAdapter; +use Zend_Db_Select as ZfDbSelect; + +class CustomvarVariantsTable extends ZfQueryBasedTable +{ + protected $searchColumns = ['varvalue']; + + protected $varName; + + public static function create(Db $db, $varName) + { + $table = new static($db); + $table->varName = $varName; + $table->getAttributes()->set('class', 'common-table'); + return $table; + } + + public function renderRow($row) + { + if ($row->format === 'json') { + $value = PlainObjectRenderer::render(json_decode($row->varvalue)); + } else { + $value = $row->varvalue; + } + $tr = $this::row([ + /* new Link( + $value, + 'director/customvar/value', + ['name' => $row->varvalue] + )*/ + $value + ]); + + foreach ($this->getObjectTypes() as $type) { + $cnt = (int) $row->{"cnt_$type"}; + if ($cnt === 0) { + $cnt = '-'; + } + $tr->add($this::td($cnt)); + } + + return $tr; + } + + public function getColumnsToBeRendered() + { + return array( + $this->translate('Variable Value'), + $this->translate('Commands'), + $this->translate('Hosts'), + $this->translate('Services'), + $this->translate('Service Sets'), + $this->translate('Notifications'), + $this->translate('Users'), + ); + } + + protected function getObjectTypes() + { + return ['command', 'host', 'service', 'service_set', 'notification', 'user']; + } + + public function prepareQuery() + { + $db = $this->db(); + $varsColumns = ['varvalue' => 'v.varvalue']; + $varsTypes = $this->getObjectTypes(); + foreach ($varsTypes as $type) { + $varsColumns["cnt_$type"] = '(0)'; + } + $varsQueries = []; + foreach ($varsTypes as $type) { + $varsQueries[] = $this->makeVarSub($type, $varsColumns, $db); + } + + $union = $db->select()->union($varsQueries, ZfDbSelect::SQL_UNION_ALL); + + $columns = [ + 'varvalue' => 'u.varvalue', + 'format' => 'u.format', + ]; + foreach ($varsTypes as $column) { + $columns["cnt_$column"] = "SUM(u.cnt_$column)"; + } + return $db->select()->from(['u' => $union], $columns) + ->group('u.varvalue')->group('u.format') + ->order('u.varvalue ASC') + ->order('u.format ASC') + ->limit(100); + } + + /** + * @param string $type + * @param array $columns + * @param ZfDbAdapter $db + * @return ZfDbSelect + */ + protected function makeVarSub($type, array $columns, ZfDbAdapter $db) + { + $columns["cnt_$type"] = 'COUNT(*)'; + $columns['format'] = 'v.format'; + return $db->select()->from( + ['v' => "icinga_${type}_var"], + $columns + )->join( + ['o' => "icinga_${type}"], + "o.id = v.${type}_id", + [] + )->where( + 'v.varname = ?', + $this->varName + )->where( + 'o.object_type != ?', + 'external_object' + )->group('varvalue')->group('v.format'); + } +} diff --git a/library/Director/Web/Table/DatafieldCategoryTable.php b/library/Director/Web/Table/DatafieldCategoryTable.php new file mode 100644 index 0000000..6f07939 --- /dev/null +++ b/library/Director/Web/Table/DatafieldCategoryTable.php @@ -0,0 +1,64 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; +use ipl\Html\Html; + +class DatafieldCategoryTable extends ZfQueryBasedTable +{ + protected $searchColumns = [ + 'dfc.category_name', + 'dfc.description', + ]; + + public function getColumns() + { + return array( + 'id' => 'dfc.id', + 'category_name' => 'dfc.category_name', + 'description' => 'dfc.description', + 'assigned_fields' => 'COUNT(df.id)', + ); + } + + public function renderRow($row) + { + $main = [Link::create( + $row->category_name, + 'director/datafieldcategory/edit', + ['name' => $row->category_name] + )]; + + if ($row->description !== null && strlen($row->description)) { + $main[] = Html::tag('br'); + $main[] = Html::tag('small', $row->description); + } + return $this::tr([ + $this::td($main), + $this::td($row->assigned_fields) + ]); + } + + public function getColumnsToBeRendered() + { + return [ + $this->translate('Category Name'), + $this->translate('# Used'), + ]; + } + + public function prepareQuery() + { + $db = $this->db(); + return $db->select()->from( + ['dfc' => 'director_datafield_category'], + $this->getColumns() + )->joinLeft( + ['df' => 'director_datafield'], + 'df.category_id = dfc.id', + [] + )->group('dfc.id')->group('dfc.category_name')->order('category_name ASC'); + } +} diff --git a/library/Director/Web/Table/DatafieldTable.php b/library/Director/Web/Table/DatafieldTable.php new file mode 100644 index 0000000..4b321d7 --- /dev/null +++ b/library/Director/Web/Table/DatafieldTable.php @@ -0,0 +1,118 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; +use Zend_Db_Adapter_Abstract as ZfDbAdapter; +use Zend_Db_Select as ZfDbSelect; + +class DatafieldTable extends ZfQueryBasedTable +{ + protected $searchColumns = [ + 'df.varname', + 'df.caption', + ]; + + public function getColumns() + { + return [ + 'id' => 'df.id', + 'varname' => 'df.varname', + 'caption' => 'df.caption', + 'description' => 'df.description', + 'datatype' => 'df.datatype', + 'category' => 'dfc.category_name', + 'assigned_fields' => 'SUM(used_fields.cnt)', + 'assigned_vars' => 'SUM(used_vars.cnt)', + ]; + } + + public function renderRow($row) + { + return $this::tr([ + $this::td(Link::create( + $row->caption, + 'director/datafield/edit', + ['id' => $row->id] + )), + $this::td($row->varname), + $this::td($row->category), + $this::td($row->assigned_fields), + $this::td($row->assigned_vars) + ]); + } + + public function getColumnsToBeRendered() + { + return [ + $this->translate('Label'), + $this->translate('Field name'), + $this->translate('Category'), + $this->translate('# Used'), + $this->translate('# Vars'), + ]; + } + + public function prepareQuery() + { + $db = $this->db(); + $fieldTypes = ['command', 'host', 'notification', 'service', 'user']; + $varsTypes = ['command', 'host', 'notification', 'service', 'service_set', 'user']; + + $fieldsQueries = []; + foreach ($fieldTypes as $type) { + $fieldsQueries[] = $this->makeDatafieldSub($type, $db); + } + + $varsQueries = []; + foreach ($varsTypes as $type) { + $varsQueries[] = $this->makeVarSub($type, $db); + } + + return $db->select()->from( + ['df' => 'director_datafield'], + $this->getColumns() + )->joinLeft( + ['dfc' => 'director_datafield_category'], + 'df.category_id = dfc.id', + [] + )->joinLeft( + ['used_fields' => $db->select()->union($fieldsQueries, ZfDbSelect::SQL_UNION_ALL)], + 'used_fields.datafield_id = df.id', + [] + )->joinLeft( + ['used_vars' => $db->select()->union($varsQueries, ZfDbSelect::SQL_UNION_ALL)], + 'used_vars.varname = df.varname', + [] + )->group('df.id')->group('df.varname')->group('dfc.category_name')->order('caption ASC'); + } + + /** + * @param $type + * @param ZfDbAdapter $db + * + * @return ZfDbSelect + */ + protected function makeDatafieldSub($type, ZfDbAdapter $db) + { + return $db->select()->from("icinga_${type}_field", [ + 'cnt' => 'COUNT(*)', + 'datafield_id' + ])->group('datafield_id'); + } + + /** + * @param $type + * @param ZfDbAdapter $db + * + * @return ZfDbSelect + */ + protected function makeVarSub($type, ZfDbAdapter $db) + { + return $db->select()->from("icinga_${type}_var", [ + 'cnt' => 'COUNT(*)', + 'varname' + ])->group('varname'); + } +} diff --git a/library/Director/Web/Table/DatalistEntryTable.php b/library/Director/Web/Table/DatalistEntryTable.php new file mode 100644 index 0000000..70167c7 --- /dev/null +++ b/library/Director/Web/Table/DatalistEntryTable.php @@ -0,0 +1,73 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Icinga\Module\Director\Objects\DirectorDatalist; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; + +class DatalistEntryTable extends ZfQueryBasedTable +{ + protected $datalist; + + protected $searchColumns = [ + 'entry_name', + 'entry_value' + ]; + + public function setList(DirectorDatalist $list) + { + $this->datalist = $list; + + return $this; + } + + public function getList() + { + return $this->datalist; + } + + public function getColumns() + { + return [ + 'list_name' => 'l.list_name', + 'list_id' => 'le.list_id', + 'entry_name' => 'le.entry_name', + 'entry_value' => 'le.entry_value', + ]; + } + + public function renderRow($row) + { + return $this::tr([ + $this::td(Link::create($row->entry_name, 'director/data/listentry/edit', [ + 'list' => $row->list_name, + 'entry_name' => $row->entry_name, + ])), + $this::td($row->entry_value) + ]); + } + + public function getColumnsToBeRendered() + { + return [ + 'entry_name' => $this->translate('Key'), + 'entry_value' => $this->translate('Label'), + ]; + } + + public function prepareQuery() + { + return $this->db()->select()->from( + ['le' => 'director_datalist_entry'], + $this->getColumns() + )->join( + ['l' => 'director_datalist'], + 'l.id = le.list_id', + [] + )->where( + 'le.list_id = ?', + $this->getList()->id + )->order('le.entry_name ASC'); + } +} diff --git a/library/Director/Web/Table/DatalistTable.php b/library/Director/Web/Table/DatalistTable.php new file mode 100644 index 0000000..7b35fe0 --- /dev/null +++ b/library/Director/Web/Table/DatalistTable.php @@ -0,0 +1,41 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; + +class DatalistTable extends ZfQueryBasedTable +{ + protected $searchColumns = ['list_name']; + + public function getColumns() + { + return [ + 'id' => 'l.id', + 'list_name' => 'l.list_name', + ]; + } + + public function renderRow($row) + { + return $this::tr($this::td(Link::create( + $row->list_name, + 'director/data/listentry', + array('list' => $row->list_name) + ))); + } + + public function getColumnsToBeRendered() + { + return [$this->translate('List name')]; + } + + public function prepareQuery() + { + return $this->db()->select()->from( + ['l' => 'director_datalist'], + $this->getColumns() + )->order('list_name ASC'); + } +} diff --git a/library/Director/Web/Table/DbHelper.php b/library/Director/Web/Table/DbHelper.php new file mode 100644 index 0000000..573f946 --- /dev/null +++ b/library/Director/Web/Table/DbHelper.php @@ -0,0 +1,67 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Zend_Db_Expr as Expr; + +trait DbHelper +{ + public function dbHexFunc($column) + { + if ($this->isPgsql()) { + return sprintf("LOWER(ENCODE(%s, 'hex'))", $column); + } else { + return sprintf("LOWER(HEX(%s))", $column); + } + } + + public function quoteBinary($binary) + { + if ($binary === '') { + return ''; + } + + if (is_array($binary)) { + return array_map([$this, 'quoteBinary'], $binary); + } + + if ($this->isPgsql()) { + return new Expr("'\\x" . bin2hex($binary) . "'"); + } + + return new Expr('0x' . bin2hex($binary)); + } + + public function isPgsql() + { + return $this->db() instanceof \Zend_Db_Adapter_Pdo_Pgsql; + } + + public function isMysql() + { + return $this->db() instanceof \Zend_Db_Adapter_Pdo_Mysql; + } + + public function wantBinaryValue($value) + { + if (is_resource($value)) { + return stream_get_contents($value); + } + + return $value; + } + + public function getChecksum($checksum) + { + return bin2hex($this->wantBinaryValue($checksum)); + } + + public function getShortChecksum($checksum) + { + if ($checksum === null) { + return null; + } + + return substr($this->getChecksum($checksum), 0, 7); + } +} diff --git a/library/Director/Web/Table/Dependency/DependencyInfoTable.php b/library/Director/Web/Table/Dependency/DependencyInfoTable.php new file mode 100644 index 0000000..28aa856 --- /dev/null +++ b/library/Director/Web/Table/Dependency/DependencyInfoTable.php @@ -0,0 +1,101 @@ +<?php + +namespace Icinga\Module\Director\Web\Table\Dependency; + +use Icinga\Application\Modules\Module; +use Icinga\Module\Director\Application\DependencyChecker; +use Icinga\Web\Url; + +class DependencyInfoTable +{ + protected $module; + + protected $checker; + + public function __construct(DependencyChecker $checker, Module $module) + { + $this->module = $module; + $this->checker = $checker; + } + + protected function linkToModule($name, $icon) + { + return Html::link( + Html::escape($name), + Html::webUrl('config/module', ['name' => $name]), + [ + 'class' => "icon-$icon" + ] + ); + } + + public function render() + { + $html = '<table class="common-table table-row-selectable"> +<thead> +<tr> + <th>' . Html::escape($this->translate('Module name')) . '</th> + <th>' . Html::escape($this->translate('Required')) . '</th> + <th>' . Html::escape($this->translate('Installed')) . '</th> +</tr> +</thead> +<tbody data-base-target="_next"> +'; + foreach ($this->checker->getDependencies($this->module) as $dependency) { + $name = $dependency->getName(); + $isLibrary = substr($name, 0, 11) === 'icinga-php-'; + $rowAttributes = $isLibrary ? ['data-base-target' => '_self'] : null; + if ($dependency->isSatisfied()) { + if ($dependency->isSatisfied()) { + $icon = 'ok'; + } else { + $icon = 'cancel'; + } + $link = $isLibrary ? $this->noLink($name, $icon) : $this->linkToModule($name, $icon); + $installed = $dependency->getInstalledVersion(); + } elseif ($dependency->isInstalled()) { + $installed = sprintf('%s (%s)', $dependency->getInstalledVersion(), $this->translate('disabled')); + $link = $this->linkToModule($name, 'cancel'); + } else { + $installed = $this->translate('missing'); + $repository = $isLibrary ? $name : "icingaweb2-module-$name"; + $link = sprintf( + '%s (%s)', + $this->noLink($name, 'cancel'), + Html::linkToGitHub(Html::escape($this->translate('more')), 'Icinga', $repository) + ); + } + + $html .= $this->htmlRow([ + $link, + Html::escape($dependency->getRequirement()), + Html::escape($installed) + ], $rowAttributes); + } + + return $html . '</tbody> +</table> +'; + } + + protected function noLink($label, $icon) + { + return Html::link(Html::escape($label), Url::fromRequest()->with('rnd', rand(1, 100000)), [ + 'class' => "icon-$icon" + ]); + } + + protected function translate($string) + { + return \mt('director', $string); + } + + protected function htmlRow(array $cols, $rowAttributes) + { + $content = ''; + foreach ($cols as $escapedContent) { + $content .= Html::tag('td', null, $escapedContent); + } + return Html::tag('tr', $rowAttributes, $content); + } +} diff --git a/library/Director/Web/Table/Dependency/Html.php b/library/Director/Web/Table/Dependency/Html.php new file mode 100644 index 0000000..092f799 --- /dev/null +++ b/library/Director/Web/Table/Dependency/Html.php @@ -0,0 +1,74 @@ +<?php + +namespace Icinga\Module\Director\Web\Table\Dependency; + +use Icinga\Web\Url; +use InvalidArgumentException; + +/** + * Minimal HTML helper, as we might be forced to run without ipl + */ +class Html +{ + public static function tag($tag, $attributes = [], $escapedContent = null) + { + $result = "<$tag"; + if (! empty($attributes)) { + foreach ($attributes as $name => $value) { + if (! preg_match('/^[a-z][a-z0-9:-]*$/i', $name)) { + throw new InvalidArgumentException("Invalid attribute name: '$name'"); + } + + $result .= " $name=\"" . self::escapeAttributeValue($value) . '"'; + } + } + + return "$result>$escapedContent</$tag>"; + } + + public static function webUrl($path, $params) + { + return Url::fromPath($path, $params); + } + + public static function link($escapedLabel, $url, $attributes = []) + { + return static::tag('a', [ + 'href' => $url, + ] + $attributes, $escapedLabel); + } + + public static function linkToGitHub($escapedLabel, $namespace, $repository) + { + return static::link( + $escapedLabel, + 'https://github.com/' . urlencode($namespace) . '/' . urlencode($repository), + [ + 'target' => '_blank', + 'rel' => 'noreferrer', + 'class' => 'icon-forward' + ] + ); + } + + protected static function escapeAttributeValue($value) + { + $value = str_replace('"', '"', $value); + // Escape ambiguous ampersands + return preg_replace_callback('/&[0-9A-Z]+;/i', function ($match) { + $subject = $match[0]; + + if (htmlspecialchars_decode($subject, ENT_COMPAT | ENT_HTML5) === $subject) { + // Ambiguous ampersand + return str_replace('&', '&', $subject); + } + + return $subject; + }, $value); + } + + public static function escape($any) + { + return htmlspecialchars($any); + } +} diff --git a/library/Director/Web/Table/DependencyTemplateUsageTable.php b/library/Director/Web/Table/DependencyTemplateUsageTable.php new file mode 100644 index 0000000..d7537c5 --- /dev/null +++ b/library/Director/Web/Table/DependencyTemplateUsageTable.php @@ -0,0 +1,22 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +class DependencyTemplateUsageTable extends TemplateUsageTable +{ + public function getTypes() + { + return [ + 'templates' => $this->translate('Templates'), + 'applyrules' => $this->translate('Apply Rules'), + ]; + } + + protected function getTypeSummaryDefinitions() + { + return [ + 'templates' => $this->getSummaryLine('template'), + 'applyrules' => $this->getSummaryLine('apply'), + ]; + } +} diff --git a/library/Director/Web/Table/DeploymentLogTable.php b/library/Director/Web/Table/DeploymentLogTable.php new file mode 100644 index 0000000..2d5cb94 --- /dev/null +++ b/library/Director/Web/Table/DeploymentLogTable.php @@ -0,0 +1,90 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; +use Icinga\Date\DateFormatter; + +class DeploymentLogTable extends ZfQueryBasedTable +{ + use DbHelper; + + protected $activeStageName; + + public function setActiveStageName($name) + { + $this->activeStageName = $name; + return $this; + } + + public function assemble() + { + $this->getAttributes()->add('class', 'deployment-log'); + } + + public function renderRow($row) + { + $this->splitByDay($row->start_time); + + $shortSum = $this->getShortChecksum($row->config_checksum); + $tr = $this::tr([ + $this::td(Link::create( + $shortSum === null ? $row->peer_identity : [$row->peer_identity, " ($shortSum)"], + 'director/deployment', + ['id' => $row->id] + )), + $this::td(DateFormatter::formatTime($row->start_time)) + ])->addAttributes(['class' => $this->getMyRowClasses($row)]); + + return $tr; + } + + protected function getMyRowClasses($row) + { + if ($row->startup_succeeded === 'y') { + $classes = ['succeeded']; + } elseif ($row->startup_succeeded === 'n') { + $classes = ['failed']; + } elseif ($row->stage_collected === null) { + $classes = ['pending']; + } elseif ($row->dump_succeeded === 'y') { + $classes = ['sent']; + } else { + // TODO: does this ever be stored? + $classes = ['notsent']; + } + + if ($this->activeStageName !== null + && $row->stage_name === $this->activeStageName + ) { + $classes[] = 'running'; + } + + return $classes; + } + + public function getColumns() + { + $columns = [ + 'id' => 'l.id', + 'peer_identity' => 'l.peer_identity', + 'start_time' => 'UNIX_TIMESTAMP(l.start_time)', + 'stage_collected' => 'l.stage_collected', + 'dump_succeeded' => 'l.dump_succeeded', + 'stage_name' => 'l.stage_name', + 'startup_succeeded' => 'l.startup_succeeded', + 'config_checksum' => 'l.config_checksum', + ]; + + return $columns; + } + + public function prepareQuery() + { + return $this->db()->select()->from( + array('l' => 'director_deployment_log'), + $this->getColumns() + )->order('l.start_time DESC')->limit(100); + } +} diff --git a/library/Director/Web/Table/FilterableByUsage.php b/library/Director/Web/Table/FilterableByUsage.php new file mode 100644 index 0000000..5e8695f --- /dev/null +++ b/library/Director/Web/Table/FilterableByUsage.php @@ -0,0 +1,10 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +interface FilterableByUsage +{ + public function showOnlyUsed(); + + public function showOnlyUnUsed(); +} diff --git a/library/Director/Web/Table/GeneratedConfigFileTable.php b/library/Director/Web/Table/GeneratedConfigFileTable.php new file mode 100644 index 0000000..97f7091 --- /dev/null +++ b/library/Director/Web/Table/GeneratedConfigFileTable.php @@ -0,0 +1,120 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Icinga\Module\Director\Db; +use Icinga\Module\Director\IcingaConfig\IcingaConfig; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; + +class GeneratedConfigFileTable extends ZfQueryBasedTable +{ + use DbHelper; + + protected $searchColumns = ['file_path']; + + protected $deploymentId; + + protected $activeFile; + + /** @var IcingaConfig */ + protected $config; + + public static function load(IcingaConfig $config, Db $db) + { + $table = new static($db); + $table->config = $config; + $table->getAttributes()->set('data-base-target', '_self'); + return $table; + } + + public function renderRow($row) + { + $counts = implode(' / ', [ + $row->cnt_object, + $row->cnt_template, + $row->cnt_apply + ]); + + $tr = $this::row([ + $this->getFileLink($row), + $counts, + $row->size + ]); + + if ($row->file_path === $this->activeFile) { + $tr->getAttributes()->add('class', 'active'); + } + + return $tr; + } + + public function setActiveFilename($filename) + { + $this->activeFile = $filename; + return $this; + } + + protected function getFileLink($row) + { + $params = [ + 'config_checksum' => $row->config_checksum, + 'file_path' => $row->file_path + ]; + + if ($this->deploymentId) { + $params['deployment_id'] = $this->deploymentId; + } + + return Link::create($row->file_path, 'director/config/file', $params); + } + + public function setDeploymentId($id) + { + if ($id) { + $this->deploymentId = (int) $id; + } + + return $this; + } + + public function getColumnsToBeRendered() + { + return [ + $this->translate('File'), + $this->translate('Object/Tpl/Apply'), + $this->translate('Size'), + ]; + } + + public function prepareQuery() + { + $columns = [ + 'file_path' => 'cf.file_path', + 'size' => 'LENGTH(f.content)', + 'cnt_object' => 'f.cnt_object', + 'cnt_template' => 'f.cnt_template', + 'cnt_apply' => 'f.cnt_apply', + 'cnt_all' => "f.cnt_object || ' / ' || f.cnt_template || ' / ' || f.cnt_apply", + 'checksum' => 'LOWER(HEX(f.checksum))', + 'config_checksum' => 'LOWER(HEX(cf.config_checksum))', + ]; + + if ($this->isPgsql()) { + $columns['checksum'] = "LOWER(ENCODE(f.checksum, 'hex'))"; + $columns['config_checksum'] = "LOWER(ENCODE(cf.config_checksum, 'hex'))"; + } + + return $this->db()->select()->from( + ['cf' => 'director_generated_config_file'], + $columns + )->join( + ['f' => 'director_generated_file'], + 'cf.file_checksum = f.checksum', + [] + )->where( + 'config_checksum = ?', + $this->quoteBinary($this->config->getChecksum()) + )->order('cf.file_path ASC'); + } +} diff --git a/library/Director/Web/Table/GroupMemberTable.php b/library/Director/Web/Table/GroupMemberTable.php new file mode 100644 index 0000000..b0814ad --- /dev/null +++ b/library/Director/Web/Table/GroupMemberTable.php @@ -0,0 +1,201 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use gipfl\IcingaWeb2\Table\Extension\MultiSelect; +use Icinga\Data\Filter\Filter; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\IcingaConfig\AssignRenderer; +use Icinga\Module\Director\Objects\IcingaObjectGroup; +use Exception; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; +use gipfl\IcingaWeb2\Url; + +class GroupMemberTable extends ZfQueryBasedTable +{ + use MultiSelect; + + protected $searchColumns = [ + 'o.object_name', + // membership_type + ]; + + protected $type; + + /** @var IcingaObjectGroup */ + protected $group; + + /** + * @param $type + * @param Db $db + * @return static + */ + public static function create($type, Db $db) + { + $class = __NAMESPACE__ . '\\GroupMemberTable' . ucfirst($type); + if (! class_exists($class)) { + $class = __CLASS__; + } + + /** @var static $table */ + $table = new $class($db); + $table->type = $type; + return $table; + } + public function assemble() + { + if ($this->type === 'host') { + $this->enableMultiSelect( + 'director/hosts/edit', + 'director/hosts', + ['name'] + ); + } + } + + public function setGroup(IcingaObjectGroup $group) + { + $this->group = $group; + return $this; + } + + public function getType() + { + return $this->type; + } + + public function getColumnsToBeRendered() + { + if ($this->group === null) { + return [ + $this->translate('Group'), + $this->translate('Member'), + $this->translate('via') + ]; + } else { + return [ + $this->translate('Member'), + $this->translate('via') + ]; + } + } + + public function renderRow($row) + { + $type = $this->getType(); + if ($row->object_type === 'apply') { + $params = [ + 'id' => $row->id + ]; + } elseif (isset($row->host_id)) { + // I would prefer to see host=<name> and set=<name>, but joining + // them here is pointless. We should use DeferredHtml for these, + // remember hosts/sets we need and fetch them in a single query at + // rendering time. For now, this works fine - just... the URLs are + // not so nice + $params = [ + 'name' => $row->object_name, + 'host_id' => $row->host_id + ]; + } elseif (isset($row->service_set_id)) { + $params = [ + 'name' => $row->object_name, + 'set_id' => $row->service_set_id + ]; + } else { + $params = [ + 'name' => $row->object_name + ]; + } + + $url = Url::fromPath("director/${type}", $params); + + $tr = $this::tr(); + + if ($this->group === null) { + $tr->add($this::td($row->group_name)); + } + $link = Link::create($row->object_name, $url); + if ($row->object_type === 'apply') { + $link = [ + $link, + ' (where ', + $this->renderApplyFilter($row->assign_filter), + ')' + ]; + } + + $tr->add([ + $this::td($link), + $this::td($row->membership_type) + ]); + + return $tr; + } + + protected function renderApplyFilter($assignFilter) + { + try { + $string = AssignRenderer::forFilter( + Filter::fromQueryString($assignFilter) + )->renderAssign(); + // Do not prefix it + $string = preg_replace('/^assign where /', '', $string); + } catch (Exception $e) { + // ignore errors in filter rendering + $string = 'Error in Filter rendering: ' . $e->getMessage(); + } + + return $string; + } + + protected function prepareQuery() + { + // select h.object_name, hg.object_name, + // CASE WHEN hgh.host_id IS NULL THEN 'apply' ELSE 'direct' END AS assi + // from icinga_hostgroup_host_resolved hgr join icinga_host h on h.id = hgr.host_id + // join icinga_hostgroup hg on hgr.hostgroup_id = hg.id + // left join icinga_hostgroup_host hgh on hgh.host_id = h.id and hgh.hostgroup_id = hg.id; + + $type = $this->getType(); + $columns = [ + 'o.id', + 'o.object_type', + 'o.object_name', + 'membership_type' => "CASE WHEN go.${type}_id IS NULL THEN 'apply' ELSE 'direct' END" + ]; + + if ($this->group === null) { + $columns = ['group_name' => 'g.object_name'] + $columns; + } + if ($type === 'service') { + $columns[] = 'o.assign_filter'; + $columns[] = 'o.host_id'; + $columns[] = 'o.service_set_id'; + } + + $query = $this->db()->select()->from( + ['gro' => "icinga_${type}group_${type}_resolved"], + $columns + )->join( + ['o' => "icinga_${type}"], + "o.id = gro.${type}_id", + [] + )->join( + ['g' => "icinga_${type}group"], + "gro.${type}group_id = g.id", + [] + )->joinLeft( + ['go' => "icinga_${type}group_${type}"], + "go.${type}_id = o.id AND go.${type}group_id = g.id", + [] + )->order('o.object_name'); + + if ($this->group !== null) { + $query->where('g.id = ?', $this->group->get('id')); + } + + return $query; + } +} diff --git a/library/Director/Web/Table/HostTemplateUsageTable.php b/library/Director/Web/Table/HostTemplateUsageTable.php new file mode 100644 index 0000000..2d1ee2f --- /dev/null +++ b/library/Director/Web/Table/HostTemplateUsageTable.php @@ -0,0 +1,22 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +class HostTemplateUsageTable extends TemplateUsageTable +{ + public function getTypes() + { + return [ + 'templates' => $this->translate('Templates'), + 'objects' => $this->translate('Objects'), + ]; + } + + protected function getTypeSummaryDefinitions() + { + return [ + 'templates' => $this->getSummaryLine('template'), + 'objects' => $this->getSummaryLine('object'), + ]; + } +} diff --git a/library/Director/Web/Table/IcingaAppliedServiceTable.php b/library/Director/Web/Table/IcingaAppliedServiceTable.php new file mode 100644 index 0000000..b669296 --- /dev/null +++ b/library/Director/Web/Table/IcingaAppliedServiceTable.php @@ -0,0 +1,49 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Icinga\Module\Director\Objects\IcingaService; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; + +class IcingaAppliedServiceTable extends ZfQueryBasedTable +{ + protected $service; + + protected $searchColumns = array( + 'service', + ); + + public function setService(IcingaService $service) + { + $this->service = $service; + return $this; + } + + public function renderRow($row) + { + return $this::row([ + new Link($row->service, 'director/service', ['id' => $row->id]) + ]); + } + + public function getColumnsToBeRendered() + { + return [$this->translate('Servicename')]; + } + + public function prepareQuery() + { + return $this->db()->select()->from( + array('s' => 'icinga_service'), + array() + )->joinLeft( + array('si' => 'icinga_service_inheritance'), + 's.id = si.service_id', + array() + )->where( + 'si.parent_service_id = ?', + $this->service->id + )->where('s.object_type = ?', 'apply'); + } +} diff --git a/library/Director/Web/Table/IcingaCommandArgumentTable.php b/library/Director/Web/Table/IcingaCommandArgumentTable.php new file mode 100644 index 0000000..37cbc78 --- /dev/null +++ b/library/Director/Web/Table/IcingaCommandArgumentTable.php @@ -0,0 +1,89 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Icinga\Data\DataArray\ArrayDatasource; +use Icinga\Module\Director\Data\Json; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Db\Branch\Branch; +use Icinga\Module\Director\Db\Branch\BranchModificationStore; +use Icinga\Module\Director\Objects\IcingaCommand; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; + +class IcingaCommandArgumentTable extends ZfQueryBasedTable +{ + /** @var IcingaCommand */ + protected $command; + + /** @var Branch */ + protected $branch; + + protected $searchColumns = [ + 'ca.argument_name', + 'ca.argument_value', + ]; + + public function __construct(IcingaCommand $command, Branch $branch) + { + $this->command = $command; + $this->branch = $branch; + parent::__construct($command->getConnection()); + $this->getAttributes()->set('data-base-target', '_self'); + } + + public function renderRow($row) + { + return $this::row([ + Link::create($row->argument_name, 'director/command/arguments', [ + 'argument' => $row->argument_name, + 'name' => $this->command->getObjectName() + ]), + $row->argument_value + ]); + } + + public function getColumnsToBeRendered() + { + return [ + $this->translate('Argument'), + $this->translate('Value'), + ]; + } + + public function prepareQuery() + { + $db = $this->db(); + if ($this->branch->isBranch()) { + return (new ArrayDatasource((array) $this->command->arguments()->toPlainObject()))->select(); + /** @var Db $connection */ + $connection = $this->connection(); + $store = new BranchModificationStore($connection, 'command'); + $modification = $store->loadOptionalModificationByName( + $this->command->getObjectName(), + $this->branch->getUuid() + ); + if ($modification) { + $props = $modification->getProperties()->jsonSerialize(); + if (isset($props->arguments)) { + return new ArrayDatasource((array) $this->command->arguments()->toPlainObject()); + } + } + } + $id = $this->command->get('id'); + if ($id === null) { + return new ArrayDatasource([]); + } + return $this->db()->select()->from( + ['ca' => 'icinga_command_argument'], + [ + 'id' => 'ca.id', + 'argument_name' => "COALESCE(ca.argument_name, '(none)')", + 'argument_value' => 'ca.argument_value', + ] + )->where( + 'ca.command_id = ?', + $id + )->order('ca.sort_order')->order('ca.argument_name')->limit(100); + } +} diff --git a/library/Director/Web/Table/IcingaHostAppliedForServiceTable.php b/library/Director/Web/Table/IcingaHostAppliedForServiceTable.php new file mode 100644 index 0000000..0d2f8e8 --- /dev/null +++ b/library/Director/Web/Table/IcingaHostAppliedForServiceTable.php @@ -0,0 +1,117 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use ipl\Html\Html; +use Icinga\Data\DataArray\ArrayDatasource; +use Icinga\Module\Director\CustomVariable\CustomVariableDictionary; +use Icinga\Module\Director\Objects\IcingaHost; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\SimpleQueryBasedTable; + +class IcingaHostAppliedForServiceTable extends SimpleQueryBasedTable +{ + protected $title; + + protected $host; + + /** @var CustomVariableDictionary */ + protected $cv; + + protected $searchColumns = [ + 'service', + ]; + + /** @var bool */ + protected $readonly = false; + + /** @var string|null */ + protected $highlightedService; + + /** + * @param IcingaHost $host + * @param CustomVariableDictionary $dict + * @return static + */ + public static function load(IcingaHost $host, CustomVariableDictionary $dict) + { + $table = (new static())->setHost($host)->setDictionary($dict); + $table->getAttributes()->set('data-base-target', '_self'); + return $table; + } + + public function setDictionary(CustomVariableDictionary $dict) + { + $this->cv = $dict; + return $this; + } + + public function setTitle($title) + { + $this->title = $title; + return $this; + } + + public function setHost(IcingaHost $host) + { + $this->host = $host; + return $this; + } + + /** + * Show no related links + * + * @param bool $readonly + * @return $this + */ + public function setReadonly($readonly = true) + { + $this->readonly = (bool) $readonly; + + return $this; + } + + public function highlightService($service) + { + $this->highlightedService = $service; + + return $this; + } + + public function renderRow($row) + { + if ($this->readonly) { + if ($this->highlightedService === $row->service) { + $link = Html::tag('span', ['class' => 'icon-right-big'], $row->service); + } else { + $link = $row->service; + } + } else { + $link = Link::create($row->service, 'director/host/appliedservice', [ + 'name' => $this->host->object_name, + 'service' => $row->service, + ]); + } + + return $this::row([$link]); + } + + public function getColumnsToBeRendered() + { + return [ + $this->title ?: $this->translate('Service name'), + ]; + } + + public function prepareQuery() + { + $data = []; + foreach ($this->cv->getValue() as $key => $var) { + $data[] = (object) array( + 'service' => $key, + ); + } + + return (new ArrayDatasource($data))->select(); + } +} diff --git a/library/Director/Web/Table/IcingaHostAppliedServicesTable.php b/library/Director/Web/Table/IcingaHostAppliedServicesTable.php new file mode 100644 index 0000000..415903b --- /dev/null +++ b/library/Director/Web/Table/IcingaHostAppliedServicesTable.php @@ -0,0 +1,207 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use ipl\Html\Html; +use Icinga\Data\DataArray\ArrayDatasource; +use Icinga\Data\Filter\Filter; +use Icinga\Exception\IcingaException; +use Icinga\Module\Director\IcingaConfig\AssignRenderer; +use Icinga\Module\Director\Objects\HostApplyMatches; +use Icinga\Module\Director\Objects\IcingaHost; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\SimpleQueryBasedTable; + +class IcingaHostAppliedServicesTable extends SimpleQueryBasedTable +{ + protected $title; + + /** @var IcingaHost */ + protected $host; + + /** @var \Zend_Db_Adapter_Abstract */ + protected $db; + + /** @var bool */ + protected $readonly = false; + + /** @var string|null */ + protected $highlightedService; + + private $allApplyRules; + + /** + * @param IcingaHost $host + * @return static + */ + public static function load(IcingaHost $host) + { + $table = (new static())->setHost($host); + $table->getAttributes()->set('data-base-target', '_self'); + return $table; + } + + public function setTitle($title) + { + $this->title = $title; + return $this; + } + + public function getColumnsToBeRendered() + { + return [$this->title]; + } + + public function setHost(IcingaHost $host) + { + $this->host = $host; + $this->db = $host->getDb(); + return $this; + } + + /** + * Show no related links + * + * @param bool $readonly + * @return $this + */ + public function setReadonly($readonly = true) + { + $this->readonly = (bool) $readonly; + + return $this; + } + + public function highlightService($service) + { + $this->highlightedService = $service; + + return $this; + } + + public function renderRow($row) + { + $classes = []; + if ($row->blacklisted === 'y') { + $classes[] = 'strike-links'; + } + if ($row->disabled === 'y') { + $classes[] = 'disabled'; + } + + $attributes = empty($classes) ? null : ['class' => $classes]; + + if ($this->readonly) { + if ($this->highlightedService === $row->name) { + $link = Html::tag('a', ['class' => 'icon-right-big'], $row->name); + } else { + $link = Html::tag('a', $row->name); + } + } else { + $applyFor = ''; + if (! empty($row->apply_for)) { + $applyFor = sprintf('(apply for %s) ', $row->apply_for); + } + + $link = Link::create(sprintf( + $this->translate('%s %s(%s)'), + $row->name, + $applyFor, + $this->renderApplyFilter($row->filter) + ), 'director/host/appliedservice', [ + 'name' => $this->host->getObjectName(), + 'service_id' => $row->id, + ]); + } + + return $this::row([$link], $attributes); + } + + /** + * @param Filter $assignFilter + * + * @return string + */ + protected function renderApplyFilter(Filter $assignFilter) + { + try { + $string = AssignRenderer::forFilter($assignFilter)->renderAssign(); + } catch (IcingaException $e) { + $string = 'Error in Filter rendering: ' . $e->getMessage(); + } + + return $string; + } + + /** + * @return \Icinga\Data\SimpleQuery + */ + public function prepareQuery() + { + $services = []; + $matcher = HostApplyMatches::prepare($this->host); + foreach ($this->getAllApplyRules() as $rule) { + if ($matcher->matchesFilter($rule->filter)) { + $services[] = $rule; + } + } + + $ds = new ArrayDatasource($services); + return $ds->select()->columns([ + 'id' => 'id', + 'uuid' => 'uuid', + 'name' => 'name', + 'filter' => 'filter', + 'disabled' => 'disabled', + 'blacklisted' => 'blacklisted', + 'assign_filter' => 'assign_filter', + 'apply_for' => 'apply_for', + ]); + } + + /*** + * @return array + */ + protected function getAllApplyRules() + { + if ($this->allApplyRules === null) { + $this->allApplyRules = $this->fetchAllApplyRules(); + foreach ($this->allApplyRules as $rule) { + $rule->filter = Filter::fromQueryString($rule->assign_filter); + } + } + + return $this->allApplyRules; + } + + /** + * @return array + */ + protected function fetchAllApplyRules() + { + $db = $this->db; + $hostId = $this->host->get('id'); + $query = $db->select()->from( + ['s' => 'icinga_service'], + [ + 'id' => 's.id', + 'uuid' => 's.uuid', + 'name' => 's.object_name', + 'assign_filter' => 's.assign_filter', + 'apply_for' => 's.apply_for', + 'disabled' => 's.disabled', + 'blacklisted' => $hostId ? "CASE WHEN hsb.service_id IS NULL THEN 'n' ELSE 'y' END" : "('n')", + ] + )->where('object_type = ? AND assign_filter IS NOT NULL', 'apply') + ->order('s.object_name'); + if ($hostId) { + $query->joinLeft( + ['hsb' => 'icinga_host_service_blacklist'], + $db->quoteInto('s.id = hsb.service_id AND hsb.host_id = ?', $hostId), + [] + ); + } + + return $db->fetchAll($query); + } +} diff --git a/library/Director/Web/Table/IcingaHostsMatchingFilterTable.php b/library/Director/Web/Table/IcingaHostsMatchingFilterTable.php new file mode 100644 index 0000000..8d225bf --- /dev/null +++ b/library/Director/Web/Table/IcingaHostsMatchingFilterTable.php @@ -0,0 +1,71 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use gipfl\IcingaWeb2\Data\SimpleQueryPaginationAdapter; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\QueryBasedTable; +use Icinga\Data\DataArray\ArrayDatasource; +use Icinga\Data\Filter\Filter; +use Icinga\Data\SimpleQuery; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Resolver\IcingaHostObjectResolver; + +class IcingaHostsMatchingFilterTable extends QueryBasedTable +{ + protected $searchColumns = [ + 'object_name', + ]; + + /** @var ArrayDatasource */ + protected $dataSource; + + public static function load(Filter $filter, Db $db) + { + $table = new static(); + $table->dataSource = new ArrayDatasource( + (new IcingaHostObjectResolver($db->getDbAdapter())) + ->fetchObjectsMatchingFilter($filter) + ); + + return $table; + } + + public function renderRow($row) + { + return $this::row([ + Link::create( + $row->object_name, + 'director/host', + ['name' => $row->object_name] + ) + ]); + } + + public function getColumnsToBeRendered() + { + return [ + $this->translate('Hostname'), + ]; + } + + protected function getPaginationAdapter() + { + return new SimpleQueryPaginationAdapter($this->getQuery()); + } + + public function getQuery() + { + return $this->prepareQuery(); + } + + protected function fetchQueryRows() + { + return $this->dataSource->fetchAll($this->getQuery()); + } + + protected function prepareQuery() + { + return new SimpleQuery($this->dataSource, ['object_name']); + } +} diff --git a/library/Director/Web/Table/IcingaObjectDatafieldTable.php b/library/Director/Web/Table/IcingaObjectDatafieldTable.php new file mode 100644 index 0000000..f97692e --- /dev/null +++ b/library/Director/Web/Table/IcingaObjectDatafieldTable.php @@ -0,0 +1,87 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Icinga\Data\DataArray\ArrayDatasource; +use Icinga\Module\Director\Objects\IcingaObject; +use Icinga\Module\Director\Web\Form\IcingaObjectFieldLoader; +use Icinga\Web\Url; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\SimpleQueryBasedTable; + +class IcingaObjectDatafieldTable extends SimpleQueryBasedTable +{ + protected $object; + + /** @var int */ + protected $objectId; + + public function __construct(IcingaObject $object) + { + $this->object = $object; + $this->objectId = (int) $object->id; + return $this; + } + + protected $searchColumns = array( + 'varname', + 'caption' + ); + + public function getColumns() + { + return array( + 'object_id', + 'var_filter', + 'is_required', + 'id', + 'varname', + 'caption', + 'description', + 'datatype', + 'format', + ); + } + + public function getColumnsToBeRendered() + { + return array( + 'caption' => $this->translate('Label'), + 'varname' => $this->translate('Field name'), + 'is_required' => $this->translate('Mandatory'), + ); + } + + public function renderRow($row) + { + $definedOnThis = (int) $row->object_id === $this->objectId; + if ($definedOnThis) { + $caption = new Link( + $row->caption, + Url::fromRequest()->with('field_id', $row->id) + ); + } else { + $caption = $row->caption; + } + + $row = $this::row([ + $caption, + $row->varname, + $row->is_required + ]); + + if (! $definedOnThis) { + $row->getAttributes()->add('class', 'disabled'); + } + + return $row; + } + + public function prepareQuery() + { + $loader = new IcingaObjectFieldLoader($this->object); + $fields = $loader->fetchFieldDetailsForObject($this->object); + $ds = new ArrayDatasource($fields); + return $ds->select(); + } +} diff --git a/library/Director/Web/Table/IcingaScheduledDowntimeRangeTable.php b/library/Director/Web/Table/IcingaScheduledDowntimeRangeTable.php new file mode 100644 index 0000000..cd8f8b1 --- /dev/null +++ b/library/Director/Web/Table/IcingaScheduledDowntimeRangeTable.php @@ -0,0 +1,67 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Icinga\Module\Director\Objects\IcingaScheduledDowntime; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; + +class IcingaScheduledDowntimeRangeTable extends ZfQueryBasedTable +{ + /** @var IcingaScheduledDowntime */ + protected $downtime; + + protected $searchColumns = [ + 'range_key', + 'range_value', + ]; + + /** + * @param IcingaScheduledDowntime $downtime + * @return static + */ + public static function load(IcingaScheduledDowntime $downtime) + { + $table = new static($downtime->getConnection()); + $table->downtime = $downtime; + $table->getAttributes()->set('data-base-target', '_self'); + + return $table; + } + + public function renderRow($row) + { + return $this::row([ + Link::create( + $row->range_key, + 'director/scheduled-downtime/ranges', + [ + 'name' => $this->downtime->getObjectName(), + 'range' => $row->range_key, + 'range_type' => 'include' + ] + ), + $row->range_value + ]); + } + + public function getColumnsToBeRendered() + { + return [ + $this->translate('Day(s)'), + $this->translate('Timeperiods'), + ]; + } + + public function prepareQuery() + { + return $this->db()->select()->from( + ['r' => 'icinga_scheduled_downtime_range'], + [ + 'scheduled_downtime_id' => 'r.scheduled_downtime_id', + 'range_key' => 'r.range_key', + 'range_value' => 'r.range_value', + ] + )->where('r.scheduled_downtime_id = ?', $this->downtime->id); + } +} diff --git a/library/Director/Web/Table/IcingaServiceSetHostTable.php b/library/Director/Web/Table/IcingaServiceSetHostTable.php new file mode 100644 index 0000000..9fc3c61 --- /dev/null +++ b/library/Director/Web/Table/IcingaServiceSetHostTable.php @@ -0,0 +1,64 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Icinga\Module\Director\Objects\IcingaServiceSet; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; + +class IcingaServiceSetHostTable extends ZfQueryBasedTable +{ + protected $set; + + protected $searchColumns = array( + 'host', + ); + + public static function load(IcingaServiceSet $set) + { + $table = new static($set->getConnection()); + $table->set = $set; + return $table; + } + + public function renderRow($row) + { + return $this::row([ + Link::create( + $row->host, + 'director/host', + ['name' => $row->host] + ) + ]); + } + + public function getColumnsToBeRendered() + { + return [ + $this->translate('Hostname'), + ]; + } + + public function prepareQuery() + { + return $this->db()->select()->from( + ['h' => 'icinga_host'], + [ + 'id' => 'h.id', + 'host' => 'h.object_name', + 'object_type' => 'h.object_type', + ] + )->joinLeft( + ['ssh' => 'icinga_service_set'], + 'ssh.host_id = h.id', + [] + )->joinLeft( + ['ssih' => 'icinga_service_set_inheritance'], + 'ssih.service_set_id = ssh.id', + [] + )->where( + 'ssih.parent_service_set_id = ?', + $this->set->id + )->order('h.object_name'); + } +} diff --git a/library/Director/Web/Table/IcingaServiceSetServiceTable.php b/library/Director/Web/Table/IcingaServiceSetServiceTable.php new file mode 100644 index 0000000..c205e66 --- /dev/null +++ b/library/Director/Web/Table/IcingaServiceSetServiceTable.php @@ -0,0 +1,259 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Icinga\Module\Director\Data\Db\ServiceSetQueryBuilder; +use Icinga\Module\Director\Db; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use Icinga\Module\Director\Forms\RemoveLinkForm; +use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Objects\IcingaServiceSet; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; +use gipfl\IcingaWeb2\Url; + +class IcingaServiceSetServiceTable extends ZfQueryBasedTable +{ + use TableWithBranchSupport; + + /** @var IcingaServiceSet */ + protected $set; + + protected $title; + + /** @var IcingaHost */ + protected $host; + + /** @var IcingaHost */ + protected $affectedHost; + + protected $searchColumns = [ + 'service', + ]; + + /** @var bool */ + protected $readonly = false; + + /** @var string|null */ + protected $highlightedService; + + /** + * @param IcingaServiceSet $set + * @return static + */ + public static function load(IcingaServiceSet $set) + { + $table = new static($set->getConnection()); + $table->set = $set; + $table->getAttributes()->set('data-base-target', '_self'); + return $table; + } + + /** + * @param string $title + * @return $this + */ + public function setTitle($title) + { + $this->title = $title; + return $this; + } + + /** + * @param IcingaHost $host + * @return $this + */ + public function setHost(IcingaHost $host) + { + $this->host = $host; + return $this; + } + + /** + * @param IcingaHost $host + * @return $this + */ + public function setAffectedHost(IcingaHost $host) + { + $this->affectedHost = $host; + return $this; + } + + /** + * Show no related links + * + * @param bool $readonly + * @return $this + */ + public function setReadonly($readonly = true) + { + $this->readonly = (bool) $readonly; + + return $this; + } + + public function highlightService($service) + { + $this->highlightedService = $service; + + return $this; + } + + /** + * @param $row + * @return BaseHtmlElement + */ + protected function getServiceLink($row) + { + if ($this->readonly) { + if ($this->highlightedService === $row->service) { + return Html::tag('span', ['class' => 'ro-service icon-right-big'], $row->service); + } + + return Html::tag('span', ['class' => 'ro-service'], $row->service); + } + + if ($this->affectedHost) { + $params = [ + 'uuid' => $this->affectedHost->getUniqueId()->toString(), + 'service' => $row->service, + 'set' => $row->service_set + ]; + $url = 'director/host/servicesetservice'; + } else { + $params = [ + 'name' => $row->service, + 'set' => $row->service_set + ]; + $url = 'director/service'; + } + + return Link::create( + $row->service, + $url, + $params + ); + } + + public function renderRow($row) + { + $tr = $this::row([ + $this->getServiceLink($row) + ]); + $classes = $this->getRowClasses($row); + if ($row->disabled === 'y') { + $classes[] = 'disabled'; + } + if ($row->blacklisted === 'y') { + $classes[] = 'strike-links'; + } + if (! empty($classes)) { + $tr->getAttributes()->add('class', $classes); + } + + return $tr; + } + + protected function getRowClasses($row) + { + if ($row->branch_uuid !== null) { + return ['branch_modified']; + } + return []; + } + + protected function getTitle() + { + return $this->title ?: $this->translate('Servicename'); + } + + protected function renderTitleColumns() + { + if (! $this->host || ! $this->affectedHost) { + return Html::tag('th', $this->getTitle()); + } + + if ($this->readonly) { + $link = $this->createFakeRemoveLinkForReadonlyView(); + } elseif ($this->affectedHost->get('id') !== $this->host->get('id')) { + $link = $this->linkToHost($this->host); + } else { + $link = $this->createRemoveLinkForm(); + } + + return $this::th([$this->getTitle(), $link]); + } + + /** + * @return \Zend_Db_Select + * @throws \Zend_Db_Select_Exception + */ + public function prepareQuery() + { + $connection = $this->connection(); + assert($connection instanceof Db); + $builder = new ServiceSetQueryBuilder($connection, $this->branchUuid); + return $builder->selectServicesForSet($this->set)->limit(100); + } + + protected function createFakeRemoveLinkForReadonlyView() + { + return Html::tag('span', [ + 'class' => 'icon-paste', + 'style' => 'float: right; font-weight: normal', + ], $this->host->getObjectName()); + } + + protected function linkToHost(IcingaHost $host) + { + $hostname = $host->getObjectName(); + return Link::create($hostname, 'director/host/services', ['name' => $hostname], [ + 'class' => 'icon-paste', + 'style' => 'float: right; font-weight: normal', + 'data-base-target' => '_next', + 'title' => sprintf( + $this->translate('This set has been inherited from %s'), + $hostname + ) + ]); + } + + protected function createRemoveLinkForm() + { + $deleteLink = new RemoveLinkForm( + $this->translate('Remove'), + sprintf( + $this->translate('Remove "%s" from this host'), + $this->getTitle() + ), + Url::fromPath('director/host/services', [ + 'name' => $this->host->getObjectName() + ]), + ['title' => $this->getTitle()] + ); + $deleteLink->runOnSuccess(function () { + $conn = $this->set->getConnection(); + $db = $conn->getDbAdapter(); + $query = $db->select()->from(['ss' => 'icinga_service_set'], 'ss.id') + ->join(['ssih' => 'icinga_service_set_inheritance'], 'ssih.service_set_id = ss.id', []) + ->where('ssih.parent_service_set_id = ?', $this->set->get('id')) + ->where('ss.host_id = ?', $this->host->get('id')); + IcingaServiceSet::loadWithAutoIncId( + $db->fetchOne($query), + $conn + )->delete(); + }); + $deleteLink->handleRequest(); + return $deleteLink; + } + + public function removeQueryLimit() + { + $query = $this->getQuery(); + $query->reset($query::LIMIT_OFFSET); + $query->reset($query::LIMIT_COUNT); + + return $this; + } +} diff --git a/library/Director/Web/Table/IcingaTimePeriodRangeTable.php b/library/Director/Web/Table/IcingaTimePeriodRangeTable.php new file mode 100644 index 0000000..5870e67 --- /dev/null +++ b/library/Director/Web/Table/IcingaTimePeriodRangeTable.php @@ -0,0 +1,61 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Icinga\Module\Director\Objects\IcingaTimePeriod; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; + +class IcingaTimePeriodRangeTable extends ZfQueryBasedTable +{ + protected $period; + + protected $searchColumns = array( + 'range_key', + 'range_value', + ); + + public static function load(IcingaTimePeriod $period) + { + $table = new static($period->getConnection()); + $table->period = $period; + $table->getAttributes()->set('data-base-target', '_self'); + return $table; + } + + public function renderRow($row) + { + return $this::row([ + Link::create( + $row->range_key, + 'director/timeperiod/ranges', + array( + 'name' => $this->period->object_name, + 'range' => $row->range_key, + 'range_type' => 'include' + ) + ), + $row->range_value + ]); + } + + public function getColumnsToBeRendered() + { + return [ + $this->translate('Day(s)'), + $this->translate('Timeperiods'), + ]; + } + + public function prepareQuery() + { + return $this->db()->select()->from( + ['r' => 'icinga_timeperiod_range'], + [ + 'timeperiod_id' => 'r.timeperiod_id', + 'range_key' => 'r.range_key', + 'range_value' => 'r.range_value', + ] + )->where('r.timeperiod_id = ?', $this->period->id); + } +} diff --git a/library/Director/Web/Table/ImportedrowsTable.php b/library/Director/Web/Table/ImportedrowsTable.php new file mode 100644 index 0000000..d5c9811 --- /dev/null +++ b/library/Director/Web/Table/ImportedrowsTable.php @@ -0,0 +1,103 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use ipl\Html\Html; +use ipl\Html\ValidHtml; +use gipfl\IcingaWeb2\Table\SimpleQueryBasedTable; +use Icinga\Data\DataArray\ArrayDatasource; +use Icinga\Module\Director\Objects\ImportRun; +use Icinga\Module\Director\PlainObjectRenderer; + +class ImportedrowsTable extends SimpleQueryBasedTable +{ + protected $columns; + + /** @var ImportRun */ + protected $importRun; + + protected $keyColumn; + + public static function load(ImportRun $run) + { + $table = new static(); + $table->setImportRun($run); + return $table; + } + + public function setImportRun(ImportRun $run) + { + $this->importRun = $run; + return $this; + } + + public function setColumns($columns) + { + $this->columns = $columns; + return $this; + } + + protected function getKeyColumn() + { + if ($this->keyColumn === null) { + $this->keyColumn = $this->importRun->importSource()->get('key_column'); + } + + return $this->keyColumn; + } + + public function getColumns() + { + if ($this->columns === null) { + $cols = $this->importRun->listColumnNames(); + + $keyColumn = $this->getKeyColumn(); + if ($keyColumn !== null && ($pos = array_search($keyColumn, $cols)) !== false) { + unset($cols[$pos]); + array_unshift($cols, $keyColumn); + } + } else { + $cols = $this->columns; + } + + return array_combine($cols, $cols); + } + + public function renderRow($row) + { + // Find a better place! + if ($row === null) { + return null; + } + $tr = $this::tr(); + + foreach ($this->getColumnsToBeRendered() as $column) { + $td = $this::td(); + if (property_exists($row, $column)) { + if (is_string($row->$column) || $row->$column instanceof ValidHtml) { + $td->setContent($row->$column); + } else { + $html = Html::tag('pre', null, PlainObjectRenderer::render($row->$column)); + $td->setContent($html); + } + } + $tr->add($td); + } + + return $tr; + } + + public function getColumnsToBeRendered() + { + return $this->getColumns(); + } + + public function prepareQuery() + { + $ds = new ArrayDatasource( + $this->importRun->fetchRows($this->columns) + ); + + return $ds->select()->order($this->getKeyColumn()); + } +} diff --git a/library/Director/Web/Table/ImportrunTable.php b/library/Director/Web/Table/ImportrunTable.php new file mode 100644 index 0000000..e6c8a38 --- /dev/null +++ b/library/Director/Web/Table/ImportrunTable.php @@ -0,0 +1,90 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Icinga\Module\Director\Objects\ImportSource; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; + +class ImportrunTable extends ZfQueryBasedTable +{ + use DbHelper; + + /** @var ImportSource */ + protected $source; + + protected $searchColumns = [ + 'source_name', + ]; + + public static function load(ImportSource $source) + { + $table = new static($source->getConnection()); + $table->source = $source; + return $table; + } + + public function getColumnsToBeRendered() + { + return [ + $this->translate('Source name'), + $this->translate('Timestamp'), + $this->translate('Imported rows'), + ]; + } + + public function renderRow($row) + { + return $this::row([ + Link::create( + $row->source_name, + 'director/importrun', + ['id' => $row->id] + ), + $row->start_time, + $row->cnt_rows + ]); + } + + public function prepareQuery() + { + $db = $this->db(); + $columns = array( + 'id' => 'r.id', + 'source_id' => 's.id', + 'source_name' => 's.source_name', + 'start_time' => 'r.start_time', + 'rowset' => 'LOWER(HEX(rs.checksum))', + 'cnt_rows' => 'COUNT(rsr.row_checksum)', + ); + + if ($this->isPgsql()) { + $columns['rowset'] = "LOWER(ENCODE(rs.checksum, 'hex'))"; + } + + // TODO: Store row count to rowset + $query = $db->select()->from( + ['s' => 'import_source'], + $columns + )->join( + ['r' => 'import_run'], + 'r.source_id = s.id', + [] + )->joinLeft( + ['rs' => 'imported_rowset'], + 'rs.checksum = r.rowset_checksum', + [] + )->joinLeft( + ['rsr' => 'imported_rowset_row'], + 'rs.checksum = rsr.rowset_checksum', + [] + )->group('r.id')->group('s.id')->group('rs.checksum') + ->order('r.start_time DESC'); + + if ($this->source) { + $query->where('r.source_id = ?', $this->source->get('id')); + } + + return $query; + } +} diff --git a/library/Director/Web/Table/ImportsourceHookTable.php b/library/Director/Web/Table/ImportsourceHookTable.php new file mode 100644 index 0000000..5ddb6f3 --- /dev/null +++ b/library/Director/Web/Table/ImportsourceHookTable.php @@ -0,0 +1,107 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use ipl\Html\ValidHtml; +use Icinga\Data\DataArray\ArrayDatasource; +use Icinga\Module\Director\Hook\ImportSourceHook; +use Icinga\Module\Director\Import\SyncUtils; +use Icinga\Module\Director\Objects\ImportSource; +use Icinga\Module\Director\PlainObjectRenderer; +use ipl\Html\Html; +use gipfl\IcingaWeb2\Table\SimpleQueryBasedTable; + +class ImportsourceHookTable extends SimpleQueryBasedTable +{ + /** @var ImportSource */ + protected $source; + + protected $columnCache; + + /** @var ImportSourceHook */ + protected $sourceHook; + + protected function assemble() + { + $this->getAttributes()->add('class', 'raw-data-table collapsed'); + } + + public function getColumns() + { + if ($this->columnCache === null) { + $this->columnCache = SyncUtils::getRootVariables(array_merge( + $this->sourceHook()->listColumns(), + $this->source->listModifierTargetProperties() + )); + + sort($this->columnCache); + + // prioritize key column + $keyColumn = $this->source->get('key_column'); + if ($keyColumn !== null && ($pos = array_search($keyColumn, $this->columnCache)) !== false) { + unset($this->columnCache[$pos]); + array_unshift($this->columnCache, $keyColumn); + } + } + + return $this->columnCache; + } + + public function setImportSource(ImportSource $source) + { + $this->source = $source; + return $this; + } + + public function getColumnsToBeRendered() + { + return $this->getColumns(); + } + + public function renderRow($row) + { + // Find a better place! + if ($row === null) { + return null; + } + if (\is_array($row)) { + $row = (object) $row; + } + $tr = $this::tr(); + + foreach ($this->getColumnsToBeRendered() as $column) { + $td = $this::td(); + if (\property_exists($row, $column)) { + if (\is_string($row->$column) || $row->$column instanceof ValidHtml) { + $td->setContent($row->$column); + } else { + $html = Html::tag('pre', null, PlainObjectRenderer::render($row->$column)); + $td->setContent($html); + } + } + $tr->add($td); + } + + return $tr; + } + + protected function sourceHook() + { + if ($this->sourceHook === null) { + $this->sourceHook = ImportSourceHook::forImportSource( + $this->source + ); + } + + return $this->sourceHook; + } + + public function prepareQuery() + { + $data = $this->sourceHook()->fetchData(); + $this->source->applyModifiers($data); + + $ds = new ArrayDatasource($data); + return $ds->select(); + } +} diff --git a/library/Director/Web/Table/ImportsourceTable.php b/library/Director/Web/Table/ImportsourceTable.php new file mode 100644 index 0000000..1a93ef5 --- /dev/null +++ b/library/Director/Web/Table/ImportsourceTable.php @@ -0,0 +1,63 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; + +class ImportsourceTable extends ZfQueryBasedTable +{ + protected $searchColumns = [ + 'source_name', + 'description', + ]; + + public function getColumnsToBeRendered() + { + return [ + $this->translate('Source name'), + ]; + } + + protected function assemble() + { + $this->getAttributes()->add('class', 'syncstate'); + parent::assemble(); + } + + public function renderRow($row) + { + $caption = [Link::create( + $row->source_name, + 'director/importsource', + ['id' => $row->id] + )]; + if ($row->description !== null) { + $caption[] = ': ' . $row->description; + } + + if ($row->import_state === 'failing' && $row->last_error_message) { + $caption[] = ' (' . $row->last_error_message . ')'; + } + + $tr = $this::row([$caption]); + $tr->getAttributes()->add('class', $row->import_state); + + return $tr; + } + + public function prepareQuery() + { + return $this->db()->select()->from( + ['s' => 'import_source'], + [ + 'id' => 's.id', + 'source_name' => 's.source_name', + 'provider_class' => 's.provider_class', + 'import_state' => 's.import_state', + 'last_error_message' => 's.last_error_message', + 'description' => 's.description', + ] + )->order('source_name ASC'); + } +} diff --git a/library/Director/Web/Table/JobTable.php b/library/Director/Web/Table/JobTable.php new file mode 100644 index 0000000..81ba07b --- /dev/null +++ b/library/Director/Web/Table/JobTable.php @@ -0,0 +1,82 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; + +class JobTable extends ZfQueryBasedTable +{ + protected $searchColumns = [ + 'job_name', + ]; + + protected function assemble() + { + $this->getAttributes()->add('class', 'jobs'); + parent::assemble(); + } + + public function renderRow($row) + { + $caption = [Link::create( + $row->job_name, + 'director/job', + ['id' => $row->id] + )]; + + if ($row->last_attempt_succeeded === 'n' && $row->last_error_message) { + $caption[] = ' (' . $row->last_error_message . ')'; + } + + $tr = $this::row([$caption]); + $tr->getAttributes()->add('class', $this->getJobClasses($row)); + + return $tr; + } + + protected function getJobClasses($row) + { + if ($row->unixts_last_attempt === null) { + return 'pending'; + } + + if ($row->unixts_last_attempt + $row->run_interval < time()) { + return 'pending'; + } + + if ($row->last_attempt_succeeded === 'y') { + return 'ok'; + } elseif ($row->last_attempt_succeeded === 'n') { + return 'critical'; + } else { + return 'unknown'; + } + } + + public function getColumnsToBeRendered() + { + return [ + $this->translate('Job name'), + ]; + } + + public function prepareQuery() + { + return $this->db()->select()->from( + ['j' => 'director_job'], + [ + 'id' => 'j.id', + 'job_name' => 'j.job_name', + 'job_class' => 'j.job_class', + 'disabled' => 'j.disabled', + 'run_interval' => 'j.run_interval', + 'last_attempt_succeeded' => 'j.last_attempt_succeeded', + 'ts_last_attempt' => 'j.ts_last_attempt', + 'unixts_last_attempt' => 'UNIX_TIMESTAMP(j.ts_last_attempt)', + 'ts_last_error' => 'j.ts_last_error', + 'last_error_message' => 'j.last_error_message', + ] + )->order('job_name'); + } +} diff --git a/library/Director/Web/Table/NotificationTemplateUsageTable.php b/library/Director/Web/Table/NotificationTemplateUsageTable.php new file mode 100644 index 0000000..da411a3 --- /dev/null +++ b/library/Director/Web/Table/NotificationTemplateUsageTable.php @@ -0,0 +1,22 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +class NotificationTemplateUsageTable extends TemplateUsageTable +{ + public function getTypes() + { + return [ + 'templates' => $this->translate('Templates'), + 'applyrules' => $this->translate('Apply Rules'), + ]; + } + + protected function getTypeSummaryDefinitions() + { + return [ + 'templates' => $this->getSummaryLine('template'), + 'applyrules' => $this->getSummaryLine('apply', 'o.host_id IS NULL'), + ]; + } +} diff --git a/library/Director/Web/Table/ObjectSetTable.php b/library/Director/Web/Table/ObjectSetTable.php new file mode 100644 index 0000000..2773841 --- /dev/null +++ b/library/Director/Web/Table/ObjectSetTable.php @@ -0,0 +1,211 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Icinga\Authentication\Auth; +use Icinga\Module\Director\Db; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; +use gipfl\IcingaWeb2\Url; +use Icinga\Module\Director\Db\DbSelectParenthesis; +use Icinga\Module\Director\Restriction\FilterByNameRestriction; +use ipl\Html\Html; +use Ramsey\Uuid\Uuid; + +class ObjectSetTable extends ZfQueryBasedTable +{ + use TableWithBranchSupport; + + protected $searchColumns = [ + 'os.object_name', + 'os.description', + 'os.assign_filter', + 'o.object_name', + ]; + + private $type; + + /** @var Auth */ + private $auth; + + public static function create($type, Db $db, Auth $auth) + { + $table = new static($db); + $table->type = $type; + $table->auth = $auth; + return $table; + } + + public function getType() + { + return $this->type; + } + + public function getColumnsToBeRendered() + { + return [$this->translate('Name')]; + } + + public function renderRow($row) + { + $type = $this->getType(); + $params = [ + 'uuid' => Uuid::fromBytes(Db\DbUtil::binaryResult($row->uuid))->toString(), + ]; + + $url = Url::fromPath("director/${type}set", $params); + + $classes = $this->getRowClasses($row); + $tr = static::tr([ + static::td([ + Link::create(sprintf( + $this->translate('%s (%d members)'), + $row->object_name, + $row->count_services + ), $url), + $row->description ? [Html::tag('br'), Html::tag('i', $row->description)] : null + ]) + ]); + if (! empty($classes)) { + $tr->getAttributes()->add('class', $classes); + } + + return $tr; + } + + protected function getRowClasses($row) + { + if ($row->branch_uuid !== null) { + return ['branch_modified']; + } + return []; + } + + protected function prepareQuery() + { + $type = $this->getType(); + + $table = "icinga_${type}_set"; + $columns = [ + 'id' => 'os.id', + 'uuid' => 'os.uuid', + 'branch_uuid' => '(NULL)', + 'object_name' => 'os.object_name', + 'object_type' => 'os.object_type', + 'assign_filter' => 'os.assign_filter', + 'description' => 'os.description', + 'count_services' => 'COUNT(DISTINCT o.uuid)', + ]; + if ($this->branchUuid) { + $columns['branch_uuid'] = 'bos.branch_uuid'; + $columns = $this->branchifyColumns($columns); + $this->stripSearchColumnAliases(); + } + + $query = $this->db()->select()->from( + ['os' => $table], + $columns + )->joinLeft( + ['o' => "icinga_${type}"], + "o.${type}_set_id = os.id", + [] + ); + + $nameFilter = new FilterByNameRestriction( + $this->connection(), + $this->auth, + "${type}_set" + ); + $nameFilter->applyToQuery($query, 'os'); + /** @var Db $conn */ + $conn = $this->connection(); + if ($this->branchUuid) { + $right = clone($query); + + $query->joinLeft( + ['bos' => "branched_$table"], + // TODO: PgHexFunc + $this->db()->quoteInto( + 'bos.uuid = os.uuid AND bos.branch_uuid = ?', + $conn->quoteBinary($this->branchUuid->getBytes()) + ), + [] + )->where("(bos.branch_deleted IS NULL OR bos.branch_deleted = 'n')"); + $right->joinRight( + ['bos' => "branched_$table"], + 'bos.uuid = os.uuid', + [] + ) + ->where('os.uuid IS NULL') + ->where('bos.branch_uuid = ?', $conn->quoteBinary($this->branchUuid->getBytes())); + $query->group('COALESCE(os.uuid, bos.uuid)'); + $right->group('COALESCE(os.uuid, bos.uuid)'); + if ($conn->isPgsql()) { + // This is ugly, might want to modify the query - even a subselect looks better + $query->group('bos.uuid')->group('os.uuid')->group('os.id')->group('bos.branch_uuid'); + $right->group('bos.uuid')->group('os.uuid')->group('os.id')->group('bos.branch_uuid'); + } + + $query = $this->db()->select()->union([ + 'l' => new DbSelectParenthesis($query), + 'r' => new DbSelectParenthesis($right), + ]); + $query = $this->db()->select()->from(['u' => $query]); + $query->order('object_name')->limit(100); + + $query + ->group('uuid') + ->where('object_type = ?', 'template') + ->order('object_name'); + if ($conn->isPgsql()) { + // BS. Drop count? Sub-select? Better query? + $query + ->group('uuid') + ->group('id') + ->group('branch_uuid') + ->group('object_name') + ->group('object_type') + ->group('assign_filter') + ->group('description') + ->group('count_services'); + }; + } else { + // Disabled for now, check for correctness: + // $query->joinLeft( + // ['osi' => "icinga_${type}_set_inheritance"], + // "osi.parent_${type}_set_id = os.id", + // [] + // )->joinLeft( + // ['oso' => "icinga_${type}_set"], + // "oso.id = oso.${type}_set_id", + // [] + // ); + // 'count_hosts' => 'COUNT(DISTINCT oso.id)', + + $query + ->group('os.uuid') + ->where('os.object_type = ?', 'template') + ->order('os.object_name'); + if ($conn->isPgsql()) { + // BS. Drop count? Sub-select? Better query? + $query + ->group('os.uuid') + ->group('os.id') + ->group('os.object_name') + ->group('os.object_type') + ->group('os.assign_filter') + ->group('os.description'); + }; + } + + return $query; + } + + /** + * @return Db + */ + public function connection() + { + return parent::connection(); + } +} diff --git a/library/Director/Web/Table/ObjectsTable.php b/library/Director/Web/Table/ObjectsTable.php new file mode 100644 index 0000000..792cb6d --- /dev/null +++ b/library/Director/Web/Table/ObjectsTable.php @@ -0,0 +1,315 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Icinga\Authentication\Auth; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Db\DbSelectParenthesis; +use Icinga\Module\Director\Db\IcingaObjectFilterHelper; +use Icinga\Module\Director\Objects\IcingaObject; +use Icinga\Module\Director\Restriction\FilterByNameRestriction; +use Icinga\Module\Director\Restriction\HostgroupRestriction; +use Icinga\Module\Director\Restriction\ObjectRestriction; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; +use gipfl\IcingaWeb2\Url; +use Ramsey\Uuid\Uuid; +use Zend_Db_Select as ZfSelect; + +class ObjectsTable extends ZfQueryBasedTable +{ + use TableWithBranchSupport; + + /** @var ObjectRestriction[] */ + protected $objectRestrictions; + + protected $columns = [ + 'object_name' => 'o.object_name', + 'object_type' => 'o.object_type', + 'disabled' => 'o.disabled', + 'uuid' => 'o.uuid', + ]; + + protected $searchColumns = ['o.object_name']; + + protected $showColumns = ['object_name' => 'Name']; + + protected $filterObjectType = 'object'; + + protected $type; + + protected $baseObjectUrl; + + /** @var IcingaObject */ + protected $dummyObject; + + protected $leftSubQuery; + + protected $rightSubQuery; + + /** @var Auth */ + private $auth; + + /** + * @param $type + * @param Db $db + * @return static + */ + public static function create($type, Db $db) + { + $class = __NAMESPACE__ . '\\ObjectsTable' . ucfirst($type); + if (! class_exists($class)) { + $class = __CLASS__; + } + + /** @var static $table */ + $table = new $class($db); + $table->type = $type; + return $table; + } + + public function getType() + { + return $this->type; + } + + /** + * @param string $url + * @return $this + */ + public function setBaseObjectUrl($url) + { + $this->baseObjectUrl = $url; + + return $this; + } + + /** + * @return Auth + */ + public function getAuth() + { + return $this->auth; + } + + public function setAuth(Auth $auth) + { + $this->auth = $auth; + return $this; + } + + public function filterObjectType($type) + { + $this->filterObjectType = $type; + return $this; + } + + public function addObjectRestriction(ObjectRestriction $restriction) + { + $this->objectRestrictions[$restriction->getName()] = $restriction; + return $this; + } + + public function getColumns() + { + return $this->columns; + } + + public function getColumnsToBeRendered() + { + return $this->showColumns; + } + + public function filterTemplate( + IcingaObject $template, + $inheritance = Db\IcingaObjectFilterHelper::INHERIT_DIRECT + ) { + IcingaObjectFilterHelper::filterByTemplate( + $this->getQuery(), + $template, + 'o', + $inheritance + ); + + return $this; + } + + protected function getMainLinkLabel($row) + { + return $row->object_name; + } + + protected function renderObjectNameColumn($row) + { + $type = $this->baseObjectUrl; + $url = Url::fromPath("director/${type}", [ + 'uuid' => Uuid::fromBytes($row->uuid)->toString() + ]); + + return static::td(Link::create($this->getMainLinkLabel($row), $url)); + } + + protected function renderExtraColumns($row) + { + $columns = $this->getColumnsToBeRendered(); + unset($columns['object_name']); + $cols = []; + foreach ($columns as $key => & $label) { + $cols[] = static::td($row->$key); + } + + return $cols; + } + + public function renderRow($row) + { + if (isset($row->uuid) && is_resource($row->uuid)) { + $row->uuid = stream_get_contents($row->uuid); + } + $tr = static::tr([ + $this->renderObjectNameColumn($row), + $this->renderExtraColumns($row) + ]); + + $classes = $this->getRowClasses($row); + if ($row->disabled === 'y') { + $classes[] = 'disabled'; + } + if (! empty($classes)) { + $tr->getAttributes()->add('class', $classes); + } + + return $tr; + } + + protected function getRowClasses($row) + { + // TODO: remove isset, to figure out where it is missing + if (isset($row->branch_uuid) && $row->branch_uuid !== null) { + return ['branch_modified']; + } + return []; + } + + protected function applyObjectTypeFilter(ZfSelect $query, ZfSelect $right = null) + { + if ($right) { + $right->where( + 'bo.object_type = ?', + $this->filterObjectType + ); + } + return $query->where( + 'o.object_type = ?', + $this->filterObjectType + ); + } + + protected function applyRestrictions(ZfSelect $query) + { + foreach ($this->getRestrictions() as $restriction) { + $restriction->applyToQuery($query); + } + + return $query; + } + + protected function getRestrictions() + { + if ($this->objectRestrictions === null) { + $this->objectRestrictions = $this->loadRestrictions(); + } + + return $this->objectRestrictions; + } + + protected function loadRestrictions() + { + /** @var Db $db */ + $db = $this->connection(); + $auth = $this->getAuth(); + + return [ + new HostgroupRestriction($db, $auth), + new FilterByNameRestriction($db, $auth, $this->getDummyObject()->getShortTableName()) + ]; + } + + /** + * @return IcingaObject + */ + protected function getDummyObject() + { + if ($this->dummyObject === null) { + $type = $this->getType(); + $this->dummyObject = IcingaObject::createByType($type); + } + return $this->dummyObject; + } + + protected function prepareQuery() + { + $table = $this->getDummyObject()->getTableName(); + if ($this->branchUuid) { + $this->columns['branch_uuid'] = 'bo.branch_uuid'; + } + + $columns = $this->getColumns(); + if ($this->branchUuid) { + $columns = $this->branchifyColumns($columns); + $this->stripSearchColumnAliases(); + } + $query = $this->db()->select()->from(['o' => $table], $columns); + + if ($this->branchUuid) { + $right = clone($query); + // Hint: Right part has only those with object = null + // This means that restrictions on $right would hide all + // new rows. Dedicated restriction logic for the branch-only + // part of thw union are not required, we assume that restrictions + // for new objects have been checked once they have been created + $query = $this->applyRestrictions($query); + /** @var Db $conn */ + $conn = $this->connection(); + $query->joinLeft( + ['bo' => "branched_$table"], + // TODO: PgHexFunc + $this->db()->quoteInto( + 'bo.uuid = o.uuid AND bo.branch_uuid = ?', + $conn->quoteBinary($this->branchUuid->getBytes()) + ), + [] + )->where("(bo.branch_deleted IS NULL OR bo.branch_deleted = 'n')"); + $this->applyObjectTypeFilter($query, $right); + $right->joinRight( + ['bo' => "branched_$table"], + 'bo.uuid = o.uuid', + [] + ) + ->where('o.uuid IS NULL') + ->where('bo.branch_uuid = ?', $conn->quoteBinary($this->branchUuid->getBytes())); + $this->leftSubQuery = $query; + $this->rightSubQuery = $right; + $query = $this->db()->select()->union([ + 'l' => new DbSelectParenthesis($query), + 'r' => new DbSelectParenthesis($right), + ]); + $query = $this->db()->select()->from(['u' => $query]); + $query->order('object_name')->limit(100); + } else { + $this->applyObjectTypeFilter($query); + $query->order('o.object_name')->limit(100); + } + + return $query; + } + + public function removeQueryLimit() + { + $query = $this->getQuery(); + $query->reset($query::LIMIT_OFFSET); + $query->reset($query::LIMIT_COUNT); + + return $this; + } +} diff --git a/library/Director/Web/Table/ObjectsTableApiUser.php b/library/Director/Web/Table/ObjectsTableApiUser.php new file mode 100644 index 0000000..2287c2f --- /dev/null +++ b/library/Director/Web/Table/ObjectsTableApiUser.php @@ -0,0 +1,13 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Zend_Db_Select as ZfSelect; + +class ObjectsTableApiUser extends ObjectsTable +{ + protected function applyObjectTypeFilter(ZfSelect $query, ZfSelect $right = null) + { + return $query->where("o.object_type IN ('object', 'external_object')"); + } +} diff --git a/library/Director/Web/Table/ObjectsTableCommand.php b/library/Director/Web/Table/ObjectsTableCommand.php new file mode 100644 index 0000000..ebd89da --- /dev/null +++ b/library/Director/Web/Table/ObjectsTableCommand.php @@ -0,0 +1,67 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Zend_Db_Select as ZfSelect; + +class ObjectsTableCommand extends ObjectsTable implements FilterableByUsage +{ + // TODO: Notifications separately? + protected $searchColumns = [ + 'o.object_name', + 'o.command', + ]; + + protected $columns = [ + 'uuid' => 'o.uuid', + 'object_name' => 'o.object_name', + 'object_type' => 'o.object_type', + 'disabled' => 'o.disabled', + 'command' => 'o.command', + ]; + + protected $showColumns = [ + 'object_name' => 'Command', + 'command' => 'Command line' + ]; + + private $objectType; + + public function setType($type) + { + $this->getQuery()->where('object_type = ?', $type); + + return $this; + } + + public function showOnlyUsed() + { + $this->getQuery()->where( + '(' + . 'EXISTS (SELECT check_command_id FROM icinga_host WHERE check_command_id = o.id)' + . ' OR EXISTS (SELECT check_command_id FROM icinga_service WHERE check_command_id = o.id)' + . ' OR EXISTS (SELECT event_command_id FROM icinga_host WHERE event_command_id = o.id)' + . ' OR EXISTS (SELECT event_command_id FROM icinga_service WHERE event_command_id = o.id)' + . ' OR EXISTS (SELECT command_id FROM icinga_notification WHERE command_id = o.id)' + . ')' + ); + } + + public function showOnlyUnUsed() + { + $this->getQuery()->where( + '(' + . 'NOT EXISTS (SELECT check_command_id FROM icinga_host WHERE check_command_id = o.id)' + . ' AND NOT EXISTS (SELECT check_command_id FROM icinga_service WHERE check_command_id = o.id)' + . ' AND NOT EXISTS (SELECT event_command_id FROM icinga_host WHERE event_command_id = o.id)' + . ' AND NOT EXISTS (SELECT event_command_id FROM icinga_service WHERE event_command_id = o.id)' + . ' AND NOT EXISTS (SELECT command_id FROM icinga_notification WHERE command_id = o.id)' + . ')' + ); + } + + protected function applyObjectTypeFilter(ZfSelect $query, ZfSelect $right = null) + { + return $query; + } +} diff --git a/library/Director/Web/Table/ObjectsTableEndpoint.php b/library/Director/Web/Table/ObjectsTableEndpoint.php new file mode 100644 index 0000000..f73b38b --- /dev/null +++ b/library/Director/Web/Table/ObjectsTableEndpoint.php @@ -0,0 +1,86 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use gipfl\IcingaWeb2\Icon; +use Zend_Db_Select as ZfSelect; + +class ObjectsTableEndpoint extends ObjectsTable +{ + protected $searchColumns = [ + 'o.object_name', + ]; + + protected $deploymentEndpoint; + + public function getColumnsToBeRendered() + { + return array( + 'object_name' => $this->translate('Endpoint'), + 'host' => $this->translate('Host'), + 'zone' => $this->translate('Zone'), + 'object_type' => $this->translate('Type'), + ); + } + + public function getColumns() + { + return [ + 'uuid' => 'o.uuid', + 'object_name' => 'o.object_name', + 'object_type' => 'o.object_type', + 'disabled' => 'o.disabled', + 'host' => "(CASE WHEN o.host IS NULL THEN NULL ELSE" + . " CONCAT(o.host || ':' || COALESCE(o.port, 5665)) END)", + 'zone' => 'z.object_name', + ]; + } + + protected function getMainLinkLabel($row) + { + if ($row->object_name === $this->deploymentEndpoint) { + return [ + $row->object_name, + ' ', + Icon::create('upload', [ + 'title' => $this->translate( + 'This is your Config master and will receive our Deployments' + ) + ]) + ]; + } else { + return $row->object_name; + } + } + + public function getRowClasses($row) + { + if ($row->object_name === $this->deploymentEndpoint) { + return array_merge(array('deployment-endpoint'), parent::getRowClasses($row)); + } else { + return null; + } + } + + protected function applyObjectTypeFilter(ZfSelect $query, ZfSelect $right = null) + { + return $query->where("o.object_type IN ('object', 'external_object')"); + } + + public function prepareQuery() + { + if ($this->deploymentEndpoint === null) { + /** @var \Icinga\Module\Director\Db $c */ + $c = $this->connection(); + if ($c->hasDeploymentEndpoint()) { + $this->deploymentEndpoint = $c->getDeploymentEndpointName(); + } + } + + return parent::prepareQuery()->joinLeft( + ['z' => 'icinga_zone'], + 'o.zone_id = z.id', + [] + ); + } +} diff --git a/library/Director/Web/Table/ObjectsTableHost.php b/library/Director/Web/Table/ObjectsTableHost.php new file mode 100644 index 0000000..5128e04 --- /dev/null +++ b/library/Director/Web/Table/ObjectsTableHost.php @@ -0,0 +1,40 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use gipfl\IcingaWeb2\Table\Extension\MultiSelect; + +class ObjectsTableHost extends ObjectsTable +{ + use MultiSelect; + + protected $type = 'host'; + + protected $searchColumns = [ + 'o.object_name', + 'o.display_name', + 'o.address', + ]; + + protected $columns = [ + 'object_name' => 'o.object_name', + 'display_name' => 'o.display_name', + 'address' => 'o.address', + 'disabled' => 'o.disabled', + 'uuid' => 'o.uuid', + ]; + + protected $showColumns = [ + 'object_name' => 'Hostname', + 'address' => 'Address' + ]; + + public function assemble() + { + $this->enableMultiSelect( + 'director/hosts/edit', + 'director/hosts', + ['uuid'] + ); + } +} diff --git a/library/Director/Web/Table/ObjectsTableHostTemplateChoice.php b/library/Director/Web/Table/ObjectsTableHostTemplateChoice.php new file mode 100644 index 0000000..929e050 --- /dev/null +++ b/library/Director/Web/Table/ObjectsTableHostTemplateChoice.php @@ -0,0 +1,27 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Zend_Db_Select as ZfSelect; + +class ObjectsTableHostTemplateChoice extends ObjectsTable +{ + protected $columns = [ + 'object_name' => 'o.object_name', + 'templates' => 'GROUP_CONCAT(t.object_name)' + ]; + + protected function applyObjectTypeFilter(ZfSelect $query, ZfSelect $right = null) + { + return $query; + } + + protected function prepareQuery() + { + return parent::prepareQuery()->joinLeft( + ['t' => 'icinga_host'], + 't.template_choice_id = o.id', + [] + )->group('o.id'); + } +} diff --git a/library/Director/Web/Table/ObjectsTableService.php b/library/Director/Web/Table/ObjectsTableService.php new file mode 100644 index 0000000..2d4ad41 --- /dev/null +++ b/library/Director/Web/Table/ObjectsTableService.php @@ -0,0 +1,219 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Icinga\Module\Director\Db\DbUtil; +use Icinga\Module\Director\Objects\IcingaHost; +use ipl\Html\Html; +use gipfl\IcingaWeb2\Table\Extension\MultiSelect; +use gipfl\IcingaWeb2\Link; +use Ramsey\Uuid\Uuid; + +class ObjectsTableService extends ObjectsTable +{ + use MultiSelect; + + /** @var IcingaHost */ + protected $host; + + protected $type = 'service'; + + protected $title; + + /** @var IcingaHost */ + protected $inheritedBy; + + /** @var bool */ + protected $readonly = false; + + /** @var string|null */ + protected $highlightedService; + + protected $columns = [ + 'object_name' => 'o.object_name', + 'disabled' => 'o.disabled', + 'host' => 'h.object_name', + 'host_id' => 'h.id', + 'host_object_type' => 'h.object_type', + 'host_disabled' => 'h.disabled', + 'id' => 'o.id', + 'uuid' => 'o.uuid', + 'blacklisted' => "CASE WHEN hsb.service_id IS NULL THEN 'n' ELSE 'y' END", + ]; + + protected $searchColumns = [ + 'o.object_name', + 'h.object_name' + ]; + + public function assemble() + { + $this->enableMultiSelect( + 'director/services/edit', + 'director/services', + ['uuid'] + ); + } + + public function setTitle($title) + { + $this->title = $title; + return $this; + } + + public function setHost(IcingaHost $host) + { + $this->host = $host; + $this->getAttributes()->set('data-base-target', '_self'); + return $this; + } + + public function setInheritedBy(IcingaHost $host) + { + $this->inheritedBy = $host; + return $this; + } + + /** + * Show no related links + * + * @param bool $readonly + * @return $this + */ + public function setReadonly($readonly = true) + { + $this->readonly = (bool) $readonly; + + return $this; + } + + public function highlightService($service) + { + $this->highlightedService = $service; + + return $this; + } + + public function getColumnsToBeRendered() + { + if ($this->title) { + return [$this->title]; + } + if ($this->host) { + return [$this->translate('Servicename')]; + } + return [ + 'host' => $this->translate('Host'), + 'object_name' => $this->translate('Service Name'), + ]; + } + + public function renderRow($row) + { + $caption = $row->host === null + ? Html::tag('span', ['class' => 'error'], '- none -') + : $row->host; + + $hostField = static::td($caption); + if ($row->host === null) { + $hostField->getAttributes()->add('class', 'error'); + } + if ($this->host) { + $tr = static::tr([ + static::td($this->getServiceLink($row)) + ]); + } else { + $tr = static::tr([ + $hostField, + static::td($this->getServiceLink($row)) + ]); + } + + $attributes = $tr->getAttributes(); + $classes = $this->getRowClasses($row); + if ($row->host_disabled === 'y' || $row->disabled === 'y') { + $classes[] = 'disabled'; + } + if ($row->blacklisted === 'y') { + $classes[] = 'strike-links'; + } + $attributes->add('class', $classes); + + return $tr; + } + + protected function getInheritedServiceLink($row, $target) + { + $params = [ + 'name' => $target->object_name, + 'service' => $row->object_name, + 'inheritedFrom' => $row->host, + ]; + + return Link::create( + $row->object_name, + 'director/host/inheritedservice', + $params + ); + } + + protected function getServiceLink($row) + { + if ($this->readonly) { + if ($this->highlightedService === $row->object_name) { + return Html::tag('span', ['class' => 'icon-right-big'], $row->object_name); + } else { + return $row->object_name; + } + } + + $params = [ + 'uuid' => Uuid::fromBytes(DbUtil::binaryResult($row->uuid))->toString(), + ]; + if ($row->host !== null) { + $params['host'] = $row->host; + } + if ($target = $this->inheritedBy) { + return $this->getInheritedServiceLink($row, $target); + } + + return Link::create( + $row->object_name, + 'director/service/edit', + $params + ); + } + + public function prepareQuery() + { + $query = parent::prepareQuery(); + if ($this->branchUuid) { + $queries = [$this->leftSubQuery, $this->rightSubQuery]; + } else { + $queries = [$query]; + } + + foreach ($queries as $subQuery) { + $subQuery->joinLeft( + ['h' => 'icinga_host'], + 'o.host_id = h.id', + [] + )->joinLeft( + ['hsb' => 'icinga_host_service_blacklist'], + 'hsb.service_id = o.id AND hsb.host_id = o.host_id', + [] + )->where('o.service_set_id IS NULL') + ->order('o.object_name')->order('h.object_name'); + + if ($this->host) { + if ($this->branchUuid) { + $subQuery->where('COALESCE(h.object_name, bo.host) = ?', $this->host->getObjectName()); + } else { + $subQuery->where('h.id = ?', $this->host->get('id')); + } + } + } + + return $query; + } +} diff --git a/library/Director/Web/Table/ObjectsTableZone.php b/library/Director/Web/Table/ObjectsTableZone.php new file mode 100644 index 0000000..602cf0a --- /dev/null +++ b/library/Director/Web/Table/ObjectsTableZone.php @@ -0,0 +1,13 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Zend_Db_Select as ZfSelect; + +class ObjectsTableZone extends ObjectsTable +{ + protected function applyObjectTypeFilter(ZfSelect $query, ZfSelect $right = null) + { + return $query; + } +} diff --git a/library/Director/Web/Table/PropertymodifierTable.php b/library/Director/Web/Table/PropertymodifierTable.php new file mode 100644 index 0000000..bf9e4a3 --- /dev/null +++ b/library/Director/Web/Table/PropertymodifierTable.php @@ -0,0 +1,145 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Error; +use Exception; +use Icinga\Module\Director\Hook\ImportSourceHook; +use Icinga\Module\Director\Objects\ImportSource; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\Extension\ZfSortablePriority; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; +use gipfl\IcingaWeb2\Url; + +class PropertymodifierTable extends ZfQueryBasedTable +{ + use ZfSortablePriority; + + protected $searchColumns = [ + 'property_name', + 'target_property', + ]; + + /** @var ImportSource */ + protected $source; + + /** @var Url */ + protected $url; + + protected $keyColumn = 'id'; + + protected $priorityColumn = 'priority'; + + protected $readOnly = false; + + public static function load(ImportSource $source, Url $url) + { + $table = new static($source->getConnection()); + $table->source = $source; + $table->url = $url; + return $table; + } + + public function setReadOnly($readOnly = true) + { + $this->readOnly = $readOnly; + return $this; + } + + public function render() + { + if ($this->readOnly) { + return parent::render(); + } + return $this->renderWithSortableForm(); + } + + protected function assemble() + { + $this->getAttributes()->set('data-base-target', '_self'); + } + + public function getColumns() + { + return array( + 'id' => 'm.id', + 'source_id' => 'm.source_id', + 'property_name' => 'm.property_name', + 'target_property' => 'm.target_property', + 'description' => 'm.description', + 'provider_class' => 'm.provider_class', + 'priority' => 'm.priority', + ); + } + + public function renderRow($row) + { + $caption = $row->property_name; + if ($row->target_property !== null) { + $caption .= ' -> ' . $row->target_property; + } + if ($row->description === null) { + $class = $row->provider_class; + try { + /** @var ImportSourceHook $hook */ + $hook = new $class; + $caption .= ': ' . $hook->getName(); + } catch (Exception $e) { + $caption = $this->createErrorCaption($caption, $e); + } catch (Error $e) { + $caption = $this->createErrorCaption($caption, $e); + } + } else { + $caption .= ': ' . $row->description; + } + + $renderedRow = $this::row([ + Link::create($caption, 'director/importsource/editmodifier', [ + 'id' => $row->id, + 'source_id' => $row->source_id, + ]), + ]); + if ($this->readOnly) { + return $renderedRow; + } + + return $this->addSortPriorityButtons( + $renderedRow, + $row + ); + } + + /** + * @param $caption + * @param Exception|Error $e + * @return array + */ + protected function createErrorCaption($caption, $e) + { + return [ + $caption, + ': ', + $this::tag('span', ['class' => 'error'], $e->getMessage()) + ]; + } + + public function getColumnsToBeRendered() + { + if ($this->readOnly) { + return [$this->translate('Property')]; + } + return [ + $this->translate('Property'), + $this->getSortPriorityTitle() + ]; + } + + public function prepareQuery() + { + return $this->db()->select()->from( + ['m' => 'import_row_modifier'], + $this->getColumns() + )->where('m.source_id = ?', $this->source->get('id')) + ->order('priority'); + } +} diff --git a/library/Director/Web/Table/QuickTable.php b/library/Director/Web/Table/QuickTable.php new file mode 100644 index 0000000..ff3edcc --- /dev/null +++ b/library/Director/Web/Table/QuickTable.php @@ -0,0 +1,547 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Icinga\Application\Icinga; +use Icinga\Data\Filter\FilterAnd; +use Icinga\Data\Filter\FilterChain; +use Icinga\Data\Filter\FilterExpression; +use Icinga\Data\Filter\FilterNot; +use Icinga\Data\Filter\FilterOr; +use Icinga\Data\Filter\Filter; +use Icinga\Data\Selectable; +use Icinga\Data\Paginatable; +use Icinga\Exception\QueryException; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\PlainObjectRenderer; +use Icinga\Web\Request; +use gipfl\IcingaWeb2\Url; +use Icinga\Web\View; +use Icinga\Web\Widget; +use Icinga\Web\Widget\Paginator; +use ipl\Html\ValidHtml; +use stdClass; +use Zend_Db_Select as ZfDbSelect; + +abstract class QuickTable implements Paginatable, ValidHtml +{ + protected $view; + + /** @var Db */ + protected $connection; + + protected $limit; + + protected $offset; + + /** @var Filter */ + protected $filter; + + protected $enforcedFilters = array(); + + protected $searchColumns = array(); + + protected function getRowClasses($row) + { + return array(); + } + + protected function getRowClassesString($row) + { + return $this->createClassAttribute($this->getRowClasses($row)); + } + + protected function createClassAttribute($classes) + { + $str = $this->createClassesString($classes); + if (strlen($str) > 0) { + return ' class="' . $str . '"'; + } else { + return ''; + } + } + + private function createClassesString($classes) + { + if (is_string($classes)) { + $classes = array($classes); + } + + if (empty($classes)) { + return ''; + } else { + return implode(' ', $classes); + } + } + + protected function getMultiselectProperties() + { + /* array( + * 'url' => 'director/hosts/edit', + * 'sourceUrl' => 'director/hosts', + * 'keys' => 'name' + * ) */ + + return array(); + } + + protected function renderMultiselectAttributes() + { + $props = $this->getMultiselectProperties(); + + if (empty($props)) { + return ''; + } + + $prefix = 'data-icinga-multiselect-'; + $view = $this->view(); + $parts = array(); + $multi = array( + 'url' => $view->href($props['url']), + 'controllers' => $view->href($props['sourceUrl']), + 'data' => implode(',', $props['keys']), + ); + + foreach ($multi as $k => $v) { + $parts[] = $prefix . $k . '="' . $v . '"'; + } + + return ' ' . implode(' ', $parts); + } + + protected function renderRow($row) + { + $htm = " <tr" . $this->getRowClassesString($row) . ">\n"; + $firstCol = true; + + foreach ($this->getTitles() as $key => $title) { + // Support missing columns + if (property_exists($row, $key)) { + $val = $row->$key; + } else { + $val = null; + } + + $value = null; + + if ($firstCol) { + if ($val !== null && $url = $this->getActionUrl($row)) { + $value = $this->view()->qlink($val, $this->getActionUrl($row)); + } + $firstCol = false; + } + + if ($value === null) { + if ($val === null) { + $value = '-'; + } elseif (is_array($val) || $val instanceof stdClass || is_bool($val)) { + $value = '<pre>' + . $this->view()->escape(PlainObjectRenderer::render($val)) + . '</pre>'; + } else { + $value = $this->view()->escape($val); + } + } + + $htm .= ' <td>' . $value . "</td>\n"; + } + + if ($this->hasAdditionalActions()) { + $htm .= ' <td class="actions">' . $this->renderAdditionalActions($row) . "</td>\n"; + } + + return $htm . " </tr>\n"; + } + + abstract protected function getTitles(); + + protected function getActionUrl($row) + { + return false; + } + + public function setConnection(Selectable $connection) + { + $this->connection = $connection; + return $this; + } + + /** + * @return ZfDbSelect + */ + abstract protected function getBaseQuery(); + + public function fetchData() + { + $db = $this->db(); + $query = $this->getBaseQuery()->columns($this->getColumns()); + + if ($this->hasLimit() || $this->hasOffset()) { + $query->limit($this->getLimit(), $this->getOffset()); + } + + $this->applyFiltersToQuery($query); + + return $db->fetchAll($query); + } + + protected function applyFiltersToQuery(ZfDbSelect $query) + { + $filter = null; + $enforced = $this->enforcedFilters; + if ($this->filter && ! $this->filter->isEmpty()) { + $filter = $this->filter; + } elseif (! empty($enforced)) { + $filter = array_shift($enforced); + } + if ($filter) { + foreach ($enforced as $f) { + $filter = $filter->andFilter($f); + } + $query->where($this->renderFilter($filter)); + } + + return $query; + } + + public function getPaginator() + { + $paginator = new Paginator(); + $paginator->setQuery($this); + + return $paginator; + } + + #[\ReturnTypeWillChange] + public function count() + { + $db = $this->db(); + $query = clone($this->getBaseQuery()); + $query->reset('order')->columns(array('COUNT(*)')); + $this->applyFiltersToQuery($query); + + return $db->fetchOne($query); + } + + public function limit($count = null, $offset = null) + { + $this->limit = $count; + $this->offset = $offset; + + return $this; + } + + public function hasLimit() + { + return $this->limit !== null; + } + + public function getLimit() + { + return $this->limit; + } + + public function hasOffset() + { + return $this->offset !== null; + } + + public function getOffset() + { + return $this->offset; + } + + public function hasAdditionalActions() + { + return method_exists($this, 'renderAdditionalActions'); + } + + /** @return Db */ + protected function connection() + { + // TODO: Fail if missing? Require connection in constructor? + return $this->connection; + } + + protected function db() + { + return $this->connection()->getDbAdapter(); + } + + protected function renderTitles($row) + { + $view = $this->view(); + $htm = "<thead>\n <tr>\n"; + + foreach ($row as $title) { + $htm .= ' <th>' . $view->escape($title) . "</th>\n"; + } + + if ($this->hasAdditionalActions()) { + $htm .= ' <th class="actions">' . $view->translate('Actions') . "</th>\n"; + } + + return $htm . " </tr>\n</thead>\n"; + } + + protected function url($url, $params) + { + return Url::fromPath($url, $params); + } + + protected function listTableClasses() + { + $classes = array('simple', 'common-table', 'table-row-selectable'); + $multi = $this->getMultiselectProperties(); + if (! empty($multi)) { + $classes[] = 'multiselect'; + } + + return $classes; + } + + public function render() + { + $data = $this->fetchData(); + + $htm = '<table' + . $this->createClassAttribute($this->listTableClasses()) + . $this->renderMultiselectAttributes() + . '>' . "\n" + . $this->renderTitles($this->getTitles()) + . $this->beginTableBody(); + foreach ($data as $row) { + $htm .= $this->renderRow($row); + } + return $htm . $this->endTableBody() . $this->endTable(); + } + + protected function beginTableBody() + { + return "<tbody>\n"; + } + + protected function endTableBody() + { + return "</tbody>\n"; + } + + protected function endTable() + { + return "</table>\n"; + } + + /** + * @return View + */ + protected function view() + { + if ($this->view === null) { + $this->view = Icinga::app()->getViewRenderer()->view; + } + return $this->view; + } + + + public function setView($view) + { + $this->view = $view; + } + + public function __toString() + { + return $this->render(); + } + + protected function getSearchColumns() + { + return $this->searchColumns; + } + + abstract public function getColumns(); + + public function getFilterColumns() + { + $keys = array_keys($this->getColumns()); + return array_combine($keys, $keys); + } + + public function setFilter($filter) + { + $this->filter = $filter; + return $this; + } + + public function enforceFilter($filter, $expression = null) + { + if (! $filter instanceof Filter) { + $filter = Filter::where($filter, $expression); + } + $this->enforcedFilters[] = $filter; + return $this; + } + + public function getFilterEditor(Request $request) + { + $filterEditor = Widget::create('filterEditor') + ->setColumns(array_keys($this->getColumns())) + ->setSearchColumns($this->getSearchColumns()) + ->preserveParams('limit', 'sort', 'dir', 'view', 'backend', '_dev') + ->ignoreParams('page') + ->handleRequest($request); + + $filter = $filterEditor->getFilter(); + $this->setFilter($filter); + + return $filterEditor; + } + + protected function mapFilterColumn($col) + { + $cols = $this->getColumns(); + return $cols[$col]; + } + + protected function renderFilter(Filter $filter, $level = 0) + { + $str = ''; + if ($filter instanceof FilterChain) { + if ($filter instanceof FilterAnd) { + $op = ' AND '; + } elseif ($filter instanceof FilterOr) { + $op = ' OR '; + } elseif ($filter instanceof FilterNot) { + $op = ' AND '; + $str .= ' NOT '; + } else { + throw new QueryException( + 'Cannot render filter: %s', + $filter + ); + } + $parts = array(); + if (! $filter->isEmpty()) { + foreach ($filter->filters() as $f) { + $filterPart = $this->renderFilter($f, $level + 1); + if ($filterPart !== '') { + $parts[] = $filterPart; + } + } + if (! empty($parts)) { + if ($level > 0) { + $str .= ' (' . implode($op, $parts) . ') '; + } else { + $str .= implode($op, $parts); + } + } + } + } else { + /** @var FilterExpression $filter */ + $str .= $this->whereToSql( + $this->mapFilterColumn($filter->getColumn()), + $filter->getSign(), + $filter->getExpression() + ); + } + + return $str; + } + + protected function escapeForSql($value) + { + // bindParam? bindValue? + if (is_array($value)) { + $ret = array(); + foreach ($value as $val) { + $ret[] = $this->escapeForSql($val); + } + return implode(', ', $ret); + } else { + //if (preg_match('/^\d+$/', $value)) { + // return $value; + //} else { + return $this->db()->quote($value); + //} + } + } + + protected function escapeWildcards($value) + { + return preg_replace('/\*/', '%', $value); + } + + protected function valueToTimestamp($value) + { + // We consider integers as valid timestamps. Does not work for URL params + if (! is_string($value) || ctype_digit($value)) { + return $value; + } + $value = strtotime($value); + if (! $value) { + /* + NOTE: It's too late to throw exceptions, we might finish in __toString + throw new QueryException(sprintf( + '"%s" is not a valid time expression', + $value + )); + */ + } + return $value; + } + + protected function timestampForSql($value) + { + // TODO: do this db-aware + return $this->escapeForSql(date('Y-m-d H:i:s', $value)); + } + + /** + * Check for timestamp fields + * + * TODO: This is not here to do automagic timestamp stuff. One may + * override this function for custom voodoo, IdoQuery right now + * does. IMO we need to split whereToSql functionality, however + * I'd prefer to wait with this unless we understood how other + * backends will work. We probably should also rename this + * function to isTimestampColumn(). + * + * @param string $field Field Field name to checked + * @return bool Whether this field expects timestamps + */ + public function isTimestamp($field) + { + return false; + } + + public function whereToSql($col, $sign, $expression) + { + if ($this->isTimestamp($col)) { + $expression = $this->valueToTimestamp($expression); + } + + if (is_array($expression) && $sign === '=') { + // TODO: Should we support this? Doesn't work for blub* + return $col . ' IN (' . $this->escapeForSql($expression) . ')'; + } elseif ($sign === '=' && strpos($expression, '*') !== false) { + if ($expression === '*') { + // We'll ignore such filters as it prevents index usage and because "*" means anything, anything means + // all whereas all means that whether we use a filter to match anything or no filter at all makes no + // difference, except for performance reasons... + return ''; + } + + return $col . ' LIKE ' . $this->escapeForSql($this->escapeWildcards($expression)); + } elseif ($sign === '!=' && strpos($expression, '*') !== false) { + if ($expression === '*') { + // We'll ignore such filters as it prevents index usage and because "*" means nothing, so whether we're + // using a real column with a valid comparison here or just an expression which cannot be evaluated to + // true makes no difference, except for performance reasons... + return $this->escapeForSql(0); + } + + return $col . ' NOT LIKE ' . $this->escapeForSql($this->escapeWildcards($expression)); + } else { + return $col . ' ' . $sign . ' ' . $this->escapeForSql($expression); + } + } +} diff --git a/library/Director/Web/Table/ReadOnlyFormAvpTable.php b/library/Director/Web/Table/ReadOnlyFormAvpTable.php new file mode 100644 index 0000000..c3b44f3 --- /dev/null +++ b/library/Director/Web/Table/ReadOnlyFormAvpTable.php @@ -0,0 +1,113 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Icinga\Module\Director\PlainObjectRenderer; +use Icinga\Module\Director\Web\Form\QuickForm; +use Zend_Form_Element as ZfElement; +use Zend_Form_DisplayGroup as ZfDisplayGroup; + +class ReadOnlyFormAvpTable +{ + protected $form; + + public function __construct(QuickForm $form) + { + $this->form = $form; + } + + protected function renderDisplayGroups(QuickForm $form) + { + $html = ''; + + foreach ($form->getDisplayGroups() as $group) { + $elements = $this->filterGroupElements($group); + + if (empty($elements)) { + continue; + } + + $html .= '<tr><th colspan="2" style="text-align: right">' . $group->getLegend() . '</th></tr>'; + $html .= $this->renderElements($elements); + } + + return $html; + } + + /** + * @param ZfDisplayGroup $group + * @return ZfElement[] + */ + protected function filterGroupElements(ZfDisplayGroup $group) + { + $blacklist = array('disabled', 'assign_filter'); + $elements = array(); + /** @var ZfElement $element */ + foreach ($group->getElements() as $element) { + if ($element->getValue() === null) { + continue; + } + + if ($element->getType() === 'Zend_Form_Element_Hidden') { + continue; + } + + if (in_array($element->getName(), $blacklist)) { + continue; + } + + + $elements[] = $element; + } + + return $elements; + } + + protected function renderElements($elements) + { + $html = ''; + foreach ($elements as $element) { + $html .= $this->renderElement($element); + } + + return $html; + } + + /** + * @param ZfElement $element + * + * @return string + */ + protected function renderElement(ZfElement $element) + { + $value = $element->getValue(); + return '<tr><th>' + . $this->escape($element->getLabel()) + . '</th><td>' + . $this->renderValue($value) + . '</td></tr>'; + } + + protected function renderValue($value) + { + if (is_string($value)) { + return $this->escape($value); + } elseif (is_array($value)) { + return $this->escape(implode(', ', $value)); + } + return $this->escape(PlainObjectRenderer::render($value)); + } + + protected function escape($string) + { + return htmlspecialchars($string); + } + + public function render() + { + $this->form->initializeForObject(); + return '<table class="name-value-table">' . "\n" + . $this->renderDisplayGroups($this->form) + . '</table>'; + } +} diff --git a/library/Director/Web/Table/ServiceTemplateUsageTable.php b/library/Director/Web/Table/ServiceTemplateUsageTable.php new file mode 100644 index 0000000..82f9643 --- /dev/null +++ b/library/Director/Web/Table/ServiceTemplateUsageTable.php @@ -0,0 +1,27 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +class ServiceTemplateUsageTable extends TemplateUsageTable +{ + public function getTypes() + { + return [ + 'templates' => $this->translate('Templates'), + 'objects' => $this->translate('Objects'), + 'applyrules' => $this->translate('Apply Rules'), + // 'setmembers' => $this->translate('Set Members'), + ]; + } + + protected function getTypeSummaryDefinitions() + { + return [ + 'templates' => $this->getSummaryLine('template'), + 'objects' => $this->getSummaryLine('object'), + 'applyrules' => $this->getSummaryLine('apply', 'o.service_set_id IS NULL'), + // TODO: re-enable + // 'setmembers' => $this->getSummaryLine('apply', 'o.service_set_id IS NOT NULL'), + ]; + } +} diff --git a/library/Director/Web/Table/SyncRunTable.php b/library/Director/Web/Table/SyncRunTable.php new file mode 100644 index 0000000..e08aad7 --- /dev/null +++ b/library/Director/Web/Table/SyncRunTable.php @@ -0,0 +1,90 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use gipfl\Format\LocalTimeFormat; +use Icinga\Module\Director\Objects\SyncRule; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; + +class SyncRunTable extends ZfQueryBasedTable +{ + /** @var SyncRule */ + protected $rule; + + protected $timeFormat; + + public function __construct(SyncRule $rule) + { + parent::__construct($rule->getConnection()); + $this->timeFormat = new LocalTimeFormat(); + $this->getAttributes() + ->set('data-base-target', '_self') + ->add('class', 'history'); + $this->rule = $rule; + } + + public function renderRow($row) + { + $time = strtotime($row->start_time); + $this->renderDayIfNew($time); + return $this::tr([ + $this::td($this->makeSummary($row)), + $this::td(new Link( + $this->timeFormat->getTime($time), + 'director/syncrule/history', + [ + 'id' => $row->rule_id, + 'run_id' => $row->id, + ] + )) + ]); + } + + protected function makeSummary($row) + { + $parts = []; + if ($row->objects_created > 0) { + $parts[] = sprintf( + $this->translate('%d created'), + $row->objects_created + ); + } + if ($row->objects_modified > 0) { + $parts[] = sprintf( + $this->translate('%d modified'), + $row->objects_modified + ); + } + if ($row->objects_deleted > 0) { + $parts[] = sprintf( + $this->translate('%d deleted'), + $row->objects_deleted + ); + } + + return implode(', ', $parts); + } + + public function prepareQuery() + { + return $this->db()->select()->from( + array('sr' => 'sync_run'), + [ + 'id' => 'sr.id', + 'rule_id' => 'sr.rule_id', + 'rule_name' => 'sr.rule_name', + 'start_time' => 'sr.start_time', + 'duration_ms' => 'sr.duration_ms', + 'objects_deleted' => 'sr.objects_deleted', + 'objects_created' => 'sr.objects_created', + 'objects_modified' => 'sr.objects_modified', + 'last_former_activity' => 'sr.last_former_activity', + 'last_related_activity' => 'sr.last_related_activity', + ] + )->where( + 'sr.rule_id = ?', + $this->rule->get('id') + )->order('start_time DESC'); + } +} diff --git a/library/Director/Web/Table/SyncpropertyTable.php b/library/Director/Web/Table/SyncpropertyTable.php new file mode 100644 index 0000000..79461ce --- /dev/null +++ b/library/Director/Web/Table/SyncpropertyTable.php @@ -0,0 +1,97 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Icinga\Module\Director\Objects\SyncRule; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\Extension\ZfSortablePriority; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; + +class SyncpropertyTable extends ZfQueryBasedTable +{ + use ZfSortablePriority; + + /** @var SyncRule */ + protected $rule; + + protected $searchColumns = [ + 'source_expression', + 'destination_field', + ]; + + protected $keyColumn = 'id'; + + protected $priorityColumn = 'priority'; + + public static function create(SyncRule $rule) + { + $table = new static($rule->getConnection()); + $table->getAttributes()->set('data-base-target', '_self'); + $table->rule = $rule; + return $table; + } + + public function render() + { + return $this->renderWithSortableForm(); + } + + public function renderRow($row) + { + return $this->addSortPriorityButtons( + $this::row([ + $row->source_name, + $row->source_expression, + new Link( + $row->destination_field, + 'director/syncrule/editproperty', + [ + 'id' => $row->id, + 'rule_id' => $row->rule_id, + ] + ), + ]), + $row + ); + } + + public function getColumnsToBeRendered() + { + return [ + $this->translate('Source name'), + $this->translate('Source field'), + $this->translate('Destination'), + $this->getSortPriorityTitle() + ]; + } + + public function prepareQuery() + { + return $this->db()->select()->from( + ['p' => 'sync_property'], + [ + 'id' => 'p.id', + 'rule_id' => 'p.rule_id', + 'rule_name' => 'r.rule_name', + 'source_id' => 'p.source_id', + 'source_name' => 's.source_name', + 'source_expression' => 'p.source_expression', + 'destination_field' => 'p.destination_field', + 'priority' => 'p.priority', + 'filter_expression' => 'p.filter_expression', + 'merge_policy' => 'p.merge_policy' + ] + )->join( + ['r' => 'sync_rule'], + 'r.id = p.rule_id', + [] + )->join( + ['s' => 'import_source'], + 's.id = p.source_id', + [] + )->where( + 'p.rule_id = ?', + $this->rule->get('id') + )->order('p.priority'); + } +} diff --git a/library/Director/Web/Table/SyncruleTable.php b/library/Director/Web/Table/SyncruleTable.php new file mode 100644 index 0000000..4a8e4e5 --- /dev/null +++ b/library/Director/Web/Table/SyncruleTable.php @@ -0,0 +1,67 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; + +class SyncruleTable extends ZfQueryBasedTable +{ + protected $searchColumns = [ + 'rule_name', + 'description', + ]; + + protected function assemble() + { + $this->getAttributes()->add('class', 'syncstate'); + parent::assemble(); + } + + public function renderRow($row) + { + $caption = [Link::create( + $row->rule_name, + 'director/syncrule', + ['id' => $row->id] + )]; + if ($row->description !== null) { + $caption[] = ': ' . $row->description; + } + + if ($row->sync_state === 'failing' && $row->last_error_message) { + $caption[] = ' (' . $row->last_error_message . ')'; + } + + $tr = $this::row([$caption, $row->object_type]); + $tr->getAttributes()->add('class', $row->sync_state); + + return $tr; + } + + public function getColumnsToBeRendered() + { + return [ + $this->translate('Rule name'), + $this->translate('Object type'), + ]; + } + + public function prepareQuery() + { + return $this->db()->select()->from( + ['s' => 'sync_rule'], + [ + 'id' => 's.id', + 'rule_name' => 's.rule_name', + 'sync_state' => 's.sync_state', + 'object_type' => 's.object_type', + 'update_policy' => 's.update_policy', + 'purge_existing' => 's.purge_existing', + 'filter_expression' => 's.filter_expression', + 'last_error_message' => 's.last_error_message', + 'description' => 's.description', + ] + )->order('rule_name'); + } +} diff --git a/library/Director/Web/Table/TableLoader.php b/library/Director/Web/Table/TableLoader.php new file mode 100644 index 0000000..f7e378b --- /dev/null +++ b/library/Director/Web/Table/TableLoader.php @@ -0,0 +1,34 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Icinga\Application\Icinga; +use Icinga\Application\Modules\Module; +use Icinga\Exception\ProgrammingError; + +class TableLoader +{ + /** @return QuickTable */ + public static function load($name, Module $module = null) + { + if ($module === null) { + $basedir = Icinga::app()->getApplicationDir('tables'); + $ns = '\\Icinga\\Web\\Tables\\'; + } else { + $basedir = $module->getBaseDir() . '/application/tables'; + $ns = '\\Icinga\\Module\\' . ucfirst($module->getName()) . '\\Tables\\'; + } + if (preg_match('~^[a-z0-9/]+$~i', $name)) { + $parts = preg_split('~/~', $name); + $class = ucfirst(array_pop($parts)) . 'Table'; + $file = sprintf('%s/%s/%s.php', rtrim($basedir, '/'), implode('/', $parts), $class); + if (file_exists($file)) { + require_once($file); + /** @var QuickTable $class */ + $class = $ns . $class; + return new $class(); + } + } + throw new ProgrammingError(sprintf('Cannot load %s (%s), no such table', $name, $file)); + } +} diff --git a/library/Director/Web/Table/TableWithBranchSupport.php b/library/Director/Web/Table/TableWithBranchSupport.php new file mode 100644 index 0000000..7c5b15c --- /dev/null +++ b/library/Director/Web/Table/TableWithBranchSupport.php @@ -0,0 +1,69 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Icinga\Module\Director\Db\Branch\Branch; +use Ramsey\Uuid\UuidInterface; + +trait TableWithBranchSupport +{ + + /** @var UuidInterface|null */ + protected $branchUuid; + + /** + * Convenience method, only UUID is required + * + * @param Branch|null $branch + * @return $this + */ + public function setBranch(Branch $branch = null) + { + if ($branch && $branch->isBranch()) { + $this->setBranchUuid($branch->getUuid()); + } + + return $this; + } + + public function setBranchUuid(UuidInterface $uuid = null) + { + $this->branchUuid = $uuid; + + return $this; + } + + protected function branchifyColumns($columns) + { + $result = [ + 'uuid' => 'COALESCE(o.uuid, bo.uuid)' + ]; + $ignore = ['o.id', 'os.id', 'o.service_set_id', 'os.host_id']; + foreach ($columns as $alias => $column) { + if (substr($column, 0, 2) === 'o.' && ! in_array($column, $ignore)) { + // bo.column, o.column + $column = "COALESCE(b$column, $column)"; + } + if (substr($column, 0, 3) === 'os.' && ! in_array($column, $ignore)) { + // bo.column, o.column + $column = "COALESCE(b$column, $column)"; + } + + // Used in Service Tables: + if ($column === 'h.object_name' && $alias = 'host') { + $column = "COALESCE(bo.host, $column)"; + } + + $result[$alias] = $column; + } + + return $result; + } + + protected function stripSearchColumnAliases() + { + foreach ($this->searchColumns as &$column) { + $column = preg_replace('/^[a-z]+\./', '', $column); + } + } +} diff --git a/library/Director/Web/Table/TemplateUsageTable.php b/library/Director/Web/Table/TemplateUsageTable.php new file mode 100644 index 0000000..66e56ea --- /dev/null +++ b/library/Director/Web/Table/TemplateUsageTable.php @@ -0,0 +1,157 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Icinga\Exception\ProgrammingError; +use Icinga\Module\Director\Objects\IcingaObject; +use Icinga\Module\Director\Resolver\TemplateTree; +use gipfl\IcingaWeb2\Link; +use ipl\Html\Table; +use gipfl\Translation\TranslationHelper; + +class TemplateUsageTable extends Table +{ + use TranslationHelper; + + protected $defaultAttributes = ['class' => 'pivot']; + + protected $objectType; + + public function getTypes() + { + return [ + 'templates' => $this->translate('Templates'), + 'objects' => $this->translate('Objects'), + ]; + } + + protected function getTypeSummaryDefinitions() + { + return [ + 'templates' => $this->getSummaryLine('template'), + 'objects' => $this->getSummaryLine('object'), + ]; + } + + /** + * @param IcingaObject $template + * @return TemplateUsageTable + */ + public static function forTemplate(IcingaObject $template) + { + $type = ucfirst($template->getShortTableName()); + $class = __NAMESPACE__ . "\\${type}TemplateUsageTable"; + if (class_exists($class)) { + return new $class($template); + } else { + return new static($template); + } + } + + public function getColumnsToBeRendered() + { + return [ + '', + $this->translate('Direct'), + $this->translate('Indirect'), + $this->translate('Total') + ]; + } + + protected function __construct(IcingaObject $template) + { + + if ($template->get('object_type') !== 'template') { + throw new ProgrammingError( + 'TemplateUsageTable expects a template, got %s', + $template->get('object_type') + ); + } + + $this->objectType = $objectType = $template->getShortTableName(); + $types = $this->getTypes(); + $usage = $this->getUsageSummary($template); + + $used = false; + $rows = []; + foreach ($types as $type => $typeTitle) { + $tr = Table::tr(Table::th($typeTitle)); + foreach (['direct', 'indirect', 'total'] as $inheritance) { + $count = $usage->$inheritance->$type; + if (! $used && $count > 0) { + $used = true; + } + $tr->add( + Table::td( + Link::create( + $count, + "director/${objectType}template/$type", + [ + 'name' => $template->getObjectName(), + 'inheritance' => $inheritance + ] + ) + ) + ); + } + $rows[] = $tr; + } + + if ($used) { + $this->add($rows); + } else { + $this->add($this->translate('This template is not in use')); + } + } + + protected function getUsageSummary(IcingaObject $template) + { + $id = $template->getAutoincId(); + $connection = $template->getConnection(); + $db = $connection->getDbAdapter(); + $oType = $this->objectType; + $tree = new TemplateTree($oType, $connection); + $ids = $tree->listDescendantIdsFor($template); + if (empty($ids)) { + $ids = [0]; + } + + $baseQuery = $db->select()->from( + ['o' => 'icinga_' . $oType], + $this->getTypeSummaryDefinitions() + )->joinLeft( + ['oi' => "icinga_${oType}_inheritance"], + "oi.${oType}_id = o.id", + [] + ); + + $query = clone($baseQuery); + $direct = $db->fetchRow( + $query->where("oi.parent_${oType}_id = ?", $id) + ); + $query = clone($baseQuery); + $indirect = $db->fetchRow( + $query->where("oi.parent_${oType}_id IN (?)", $ids) + ); + //$indirect->templates = count($ids) - 1; + $total = []; + $types = array_keys($this->getTypes()); + foreach ($types as $type) { + $total[$type] = $direct->$type + $indirect->$type; + } + + return (object) [ + 'direct' => $direct, + 'indirect' => $indirect, + 'total' => (object) $total + ]; + } + + protected function getSummaryLine($type, $extra = null) + { + if ($extra !== null) { + $extra = " AND $extra"; + } + return "COALESCE(SUM(CASE WHEN o.object_type = '${type}'${extra} THEN 1 ELSE 0 END), 0)"; + } +} diff --git a/library/Director/Web/Table/TemplatesTable.php b/library/Director/Web/Table/TemplatesTable.php new file mode 100644 index 0000000..be195b2 --- /dev/null +++ b/library/Director/Web/Table/TemplatesTable.php @@ -0,0 +1,156 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Icinga\Authentication\Auth; +use Icinga\Data\Filter\Filter; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Db\IcingaObjectFilterHelper; +use Icinga\Module\Director\Objects\IcingaObject; +use ipl\Html\Html; +use gipfl\IcingaWeb2\Icon; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\Extension\MultiSelect; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; +use gipfl\IcingaWeb2\Url; +use gipfl\IcingaWeb2\Zf1\Db\FilterRenderer; +use Ramsey\Uuid\Uuid; +use Zend_Db_Select as ZfSelect; + +class TemplatesTable extends ZfQueryBasedTable implements FilterableByUsage +{ + use MultiSelect; + + protected $searchColumns = ['o.object_name']; + + private $type; + + public static function create($type, Db $db) + { + $table = new static($db); + $table->type = strtolower($type); + return $table; + } + + protected function assemble() + { + $type = $this->type; + $this->enableMultiSelect( + "director/${type}s/edittemplates", + "director/${type}template", + ['name'] + ); + } + + public function getType() + { + return $this->type; + } + + public function getColumnsToBeRendered() + { + return [$this->translate('Template Name')]; + } + + public function renderRow($row) + { + $name = $row->object_name; + $type = str_replace('_', '-', $this->getType()); + $caption = $row->is_used === 'y' ? $name : [ + $name, + Html::tag( + 'span', + ['style' => 'font-style: italic'], + $this->translate(' - not in use -') + ) + ]; + + $url = Url::fromPath("director/${type}template/usage", [ + 'name' => $name + ]); + + return $this::row([ + new Link($caption, $url), + [ + new Link(new Icon('plus'), "director/$type/add", [ + 'type' => 'object', + 'imports' => $name + ]), + new Link(new Icon('history'), "director/$type/history", [ + 'uuid' => Uuid::fromBytes(Db\DbUtil::binaryResult($row->uuid))->toString(), + ]) + ] + ]); + } + + public function filterTemplate( + IcingaObject $template, + $inheritance = IcingaObjectFilterHelper::INHERIT_DIRECT + ) { + IcingaObjectFilterHelper::filterByTemplate( + $this->getQuery(), + $template, + 'o', + $inheritance + ); + + return $this; + } + + public function showOnlyUsed() + { + $type = $this->getType(); + $this->getQuery()->where( + "(EXISTS (SELECT ${type}_id FROM icinga_${type}_inheritance" + . " WHERE parent_${type}_id = o.id))" + ); + } + + public function showOnlyUnUsed() + { + $type = $this->getType(); + $this->getQuery()->where( + "(NOT EXISTS (SELECT ${type}_id FROM icinga_${type}_inheritance" + . " WHERE parent_${type}_id = o.id))" + ); + } + + protected function applyRestrictions(ZfSelect $query) + { + $auth = Auth::getInstance(); + $type = $this->type; + $restrictions = $auth->getRestrictions("director/$type/template/filter-by-name"); + if (empty($restrictions)) { + return $query; + } + + $filter = Filter::matchAny(); + foreach ($restrictions as $restriction) { + $filter->addFilter(Filter::where('o.object_name', $restriction)); + } + + return FilterRenderer::applyToQuery($filter, $query); + } + + protected function prepareQuery() + { + $type = $this->getType(); + $used = "CASE WHEN EXISTS(SELECT 1 FROM icinga_${type}_inheritance oi" + . " WHERE oi.parent_${type}_id = o.id) THEN 'y' ELSE 'n' END"; + + $columns = [ + 'object_name' => 'o.object_name', + 'uuid' => 'o.uuid', + 'id' => 'o.id', + 'is_used' => $used, + ]; + $query = $this->db()->select()->from( + ['o' => "icinga_${type}"], + $columns + )->where( + "o.object_type = 'template'" + )->order('o.object_name'); + + return $this->applyRestrictions($query); + } +} |