Compare commits

...

18 Commits

Author SHA1 Message Date
Johannes Meyer
1c1f5e306e Prepare version 1.8.2 2024-02-07 15:12:09 +01:00
Ravi Kumar Kempapura Srinivasa
e49cccd13c Remove gipfl/format package dependency 2024-02-07 14:53:47 +01:00
Thomas Gelf
087ce05ebe Prepare v1.8.1 2021-07-13 10:18:55 +02:00
Thomas Gelf
babf42c764 DependencyChecker: new implemenation
This is now also able to give help for web 2.9.x

fixes #2354
fixes #2350
2021-07-13 10:01:39 +02:00
Thomas Gelf
05dfef5ccc ActionController: check view->compact
fixes #2141
2021-07-13 01:02:23 +02:00
Thomas Gelf
19df9eef8c OverriddenVarsResolver: deal with root templates
fixes #2333
2021-07-13 00:50:07 +02:00
Thomas Gelf
2527541326 IcingaServiceSetServiceTable: show "deactivated"
...in RO users overview table

fixes #2344
2021-07-13 00:45:17 +02:00
Thomas Gelf
cda5f56038 ObjectApplyMatches: fetch allied host groups
fixes #2313
2021-07-13 00:29:44 +02:00
Thomas Gelf
9de28e7c79 ObjectPreview: fix inline Service Template links...
...for Service Sets

fixes #2334
2021-07-13 00:24:10 +02:00
Thomas Gelf
62a684b928 IcingaServiceForm: show Override button also in...
...case all fields belong to categories

fixes #2303
2021-07-13 00:16:35 +02:00
Eric Lippmann
98b07b6dbe Use Icinga 2's generate-ticket API 2021-07-13 00:04:18 +02:00
Thomas Gelf
367fb811b1 IcingaConfig: store config file relations in a...
...transaction: all or nothing

fixes #2351
2021-07-12 23:44:33 +02:00
Sebastian Gumprich
d87057e43d IcingaObject: alias scheduled_downtime
Fixes an error when trying to create scheduled_downtime via api

fixes #1879
2021-07-12 23:28:24 +02:00
Thomas Gelf
3a2f019e18 ImportRunBasedPurgeStrategy: fixed combined keys
fixes #2339
2021-07-12 23:17:46 +02:00
Thomas Gelf
575c8fad4e DirectorObjectForm: render inherited scalars only
fixes #2288
2021-07-12 23:17:13 +02:00
Thomas Gelf
b80fdc9c37 IcingaObject/Imports: better error message wording
fixes #2224
2021-07-12 23:16:55 +02:00
Thomas Gelf
032b043e3a IcingaHostForm: strike only HTML elements
fixes #2253
2020-12-16 06:00:57 +01:00
Thomas Gelf
ea2c4fdaf7 doc/changelog: mention Icinga for Windows 2020-12-16 06:00:43 +01:00
38 changed files with 715 additions and 206 deletions

View File

@ -3,7 +3,8 @@
namespace Icinga\Module\Director\Controllers;
use Icinga\Application\Icinga;
use Icinga\Application\Modules\Manager;
use Icinga\Module\Director\Application\DependencyChecker;
use Icinga\Module\Director\Web\Table\Dependency\DependencyInfoTable;
use Icinga\Web\Controller;
class PhperrorController extends Controller
@ -24,39 +25,19 @@ class PhperrorController extends Controller
public function dependenciesAction()
{
$dependencies = $this->view->dependencies = $this->Module()->getDependencies();
$modules = $this->view->modules = Icinga::app()->getModuleManager();
// Hint: we're duplicating some code here
$satisfied = true;
foreach ($dependencies as $module => $required) {
/** @var Manager $this ->modules */
if ($modules->hasEnabled($module)) {
$installed = $modules->getModule($module, false)->getVersion();
$installed = \ltrim($installed, 'v'); // v0.6.0 VS 0.6.0
if (\preg_match('/^([<>=]+)\s*v?(\d+\.\d+\.\d+)$/', $required, $match)) {
$operator = $match[1];
$vRequired = $match[2];
if (\version_compare($installed, $vRequired, $operator)) {
continue;
}
}
}
$satisfied = false;
}
if ($satisfied) {
$checker = new DependencyChecker(Icinga::app());
if ($checker->satisfiesDependencies($this->Module())) {
$this->redirectNow('director');
}
$this->setAutorefreshInterval(15);
$this->getTabs()->add('error', array(
$this->getTabs()->add('error', [
'label' => $this->translate('Error'),
'url' => $this->getRequest()->getUrl()
))->activate('error');
$msg = $this->translate(
])->activate('error');
$this->view->title = $this->translate('Unsatisfied dependencies');
$this->view->table = (new DependencyInfoTable($checker, $this->Module()))->render();
$this->view->message = $this->translate(
"Icinga Director depends on the following modules, please install/upgrade as required"
);
$this->view->title = $this->translate('Unsatisfied dependencies');
$this->view->message = sprintf($msg, PHP_VERSION);
}
}

View File

@ -136,12 +136,7 @@ class SelfServiceController extends ActionController
throw new NotFoundError('The host "%s" is not an agent', $name);
}
$this->sendPowerShellResponse(
Util::getIcingaTicket(
$name,
$this->api()->getTicketSalt()
)
);
$this->sendPowerShellResponse($this->api()->getTicket($name));
} catch (Exception $e) {
if ($e instanceof NotFoundError) {
$this->sendPowerShellError($e->getMessage(), 404);

View File

@ -230,12 +230,14 @@ class IcingaHostForm extends DirectorObjectForm
$links->addAttributes(['class' => 'strike-links']);
/** @var BaseHtmlElement $link */
foreach ($links->getContent() as $link) {
$link->addAttributes([
'title' => $this->translate(
'Group has been inherited, but will be overridden'
. ' by locally assigned group(s)'
)
]);
if ($link instanceof BaseHtmlElement) {
$link->addAttributes([
'title' => $this->translate(
'Group has been inherited, but will be overridden'
. ' by locally assigned group(s)'
)
]);
}
}
}
$this->addElement('simpleNote', 'inherited_groups', [

View File

@ -139,6 +139,14 @@ class IcingaServiceForm extends DirectorObjectForm
} else {
$this->addOverrideHint();
$group = $this->getDisplayGroup('custom_fields');
if (! $group) {
foreach ($this->getDisplayGroups() as $groupName => $eventualGroup) {
if (preg_match('/^custom_fields:/', $groupName)) {
$group = $eventualGroup;
break;
}
}
}
if ($group) {
$elements = $group->getElements();
$group->setElements([$this->getElement('inheritance_hint')]);

View File

@ -10,62 +10,5 @@ use Icinga\Application\Modules\Manager;
<div class="content">
<p class="legacy-error"><?= $this->escape($this->message) ?></p>
<table class="common-table table-row-selectable">
<thead>
<tr>
<th><?= $this->translate('Module name') ?></th>
<th><?= $this->translate('Required') ?></th>
<th><?= $this->translate('Installed') ?></th>
</tr>
</thead>
<tbody data-base-target="_next">
<?php
foreach ($this->dependencies as $module => $required) {
/** @var Manager $this->modules */
if ($modules->hasEnabled($module)) {
$installed = $modules->getModule($module, false)->getVersion();
$installed = \ltrim($installed, 'v'); // v0.6.0 VS 0.6.0
if (\preg_match('/^([<>=]+)\s*v?(\d+\.\d+\.\d+)$/', $required, $match)) {
$operator = $match[1];
$vRequired = $match[2];
if (\version_compare($installed, $vRequired, $operator)) {
$icon = 'ok';
} else {
$icon = 'cancel';
}
} else {
$icon = 'cancel';
}
$link = $this->qlink(
$module,
'config/module',
['name' => $module],
['class' => "icon-$icon"]
);
} elseif ($modules->hasInstalled($module)) {
$installed = $this->translate('disabled');
$link = $this->qlink($module, 'config/module', ['name' => $module], ['class' => 'icon-cancel']);
} else {
$installed = $this->translate('missing');
$link = sprintf(
'<a href="#" class="icon-cancel">%s</a> (<a href="https://github.com/Icinga/icingaweb2-module-%s"'
. ' target="_blank" rel="noreferrer">%s</a>)',
$this->escape($module),
$this->escape($module),
$this->translate('more')
);
}
\printf(
'<tr><td>%s</a></td><td>%s</td><td>%s</td></tr>',
$link,
$this->escape($required),
$this->escape($installed)
);
}
?>
</tbody>
</table>
<?= $this->table ?>
</div>

View File

@ -20,7 +20,7 @@ Requirements
might show smaller UI bugs and are not actively tested
* The following Icinga modules must be installed and enabled:
* [ipl](https://github.com/Icinga/icingaweb2-module-ipl) (>=0.3.0)
* [incubator](https://github.com/Icinga/icingaweb2-module-incubator) (>=0.5.0)
* [incubator](https://github.com/Icinga/icingaweb2-module-incubator) (>=0.21.0)
* [reactbundle](https://github.com/Icinga/icingaweb2-module-reactbundle) (>=0.7.0)
* A database, MySQL (&gt;= 5.1) or PostgreSQL (&gt;= 9.1). MariaDB and other
MySQL forks are also fine. Mentioned versions are the required minimum,
@ -102,7 +102,7 @@ You might want to use a script as follows for this task:
ICINGAWEB_MODULEPATH="/usr/share/icingaweb2/modules"
REPO_URL="https://github.com/icinga/icingaweb2-module-director"
TARGET_DIR="${ICINGAWEB_MODULEPATH}/director"
MODULE_VERSION="1.8.0"
MODULE_VERSION="1.8.2"
URL="${REPO_URL}/archive/v${MODULE_VERSION}.tar.gz"
install -d -m 0755 "${TARGET_DIR}"
wget -q -O - "$URL" | tar xfz - -C "${TARGET_DIR}" --strip-components 1
@ -119,7 +119,7 @@ It will be immediately ready for use:
ICINGAWEB_MODULEPATH="/usr/share/icingaweb2/modules"
REPO_URL="https://github.com/icinga/icingaweb2-module-director"
TARGET_DIR="${ICINGAWEB_MODULEPATH}/director"
MODULE_VERSION="1.8.0"
MODULE_VERSION="1.8.2"
git clone "${REPO_URL}" "${TARGET_DIR}" --branch v${MODULE_VERSION}
You can now directly use our current GIT master or check out a specific version.

View File

@ -43,7 +43,7 @@ pending database migrations to an imported old database snapshot.
-------------------------------------------------
Before upgrading, please upgrade the [incubator module](https://github.com/Icinga/icingaweb2-module-incubator)
to (at least) v0.6.0. As always, you'll be prompted to apply pending Database
to (at least) v0.21.0. As always, you'll be prompted to apply pending Database
Migrations.
<a name="upgrade-to-1.7.x"></a>Upgrading to 1.7.x

View File

@ -4,6 +4,45 @@
Please make sure to always read our [Upgrading](05-Upgrading.md) documentation
before switching to a new version.
1.8.2
-----
### UI
* FIX: The activity log now avoids a bug in PHP introduced with version 8.1.25 (#2828)
1.8.1
-----
### Fixed issues
* You can find issues and feature requests related to this release on our
[roadmap](https://github.com/Icinga/icingaweb2-module-director/milestone/24?closed=1)
### User Interface
* FIX: show Override button when all Fields belong to Field Categories (#2303)
* FIX: don't fail when showing a Host overriding multiple inherited groups (#2253)
* FIX: deal with inherited values which are invalid for a select box (#2288)
* FIX: Service Set preview inline Service Template links (#2334)
* FIX: show Services applied with Rules involving applied Hostgroups (#2313)
* FIX: show "deactivated" services as such also for read-only users (#2344)
* FIX: Overrides for Services belonging to Sets on root Host Templates (#2333)
* FIX: show no header tabs for search result in web 2.8+ (#2141)
* FIX: show and link dependencies for web 2.9+ (#2354)
### Icinga Configuration
* FIX: rare race condition, where generated config might miss some files (#2351)
### Icinga API
* FIX: use Icinga 2's generate-ticket API, required for v2.13.0 (#2348)
### Import and Sync
* FIX: Purge didn't remove more than 1000 services at once (#2339)
### Automation, User Interface
* FIX: error message wording on failing related (or parent) object ref (#2224)
### REST API
* FIX: creating scheduled downtime via api failed (#1879)
1.8.0
-----
@ -87,6 +126,7 @@ before switching to a new version.
### Icinga Agent handling
* FIX: Linux Agent installer now fails when unable to retrieve a certificate
* FEATURE: Linux Agent installer now supports Alpine Linux (#2216)
* FEATURE: Icinga for Windows support (#2147)
### REST API
* FEATURE: Self Service API ignores empty/missing properties (e.g. no address)

View File

@ -0,0 +1,113 @@
<?php
namespace Icinga\Module\Director\Application;
class Dependency
{
/** @var string */
protected $name;
/** @var string|null */
protected $installedVersion;
/** @var bool|null */
protected $enabled;
/** @var string */
protected $operator;
/** @var string */
protected $requiredVersion;
/** @var string */
protected $requirement;
/**
* Dependency constructor.
* @param string $name Usually a module name
* @param string $requirement e.g. >=1.7.0
* @param string $installedVersion
* @param bool $enabled
*/
public function __construct($name, $requirement, $installedVersion = null, $enabled = null)
{
$this->name = $name;
$this->setRequirement($requirement);
if ($installedVersion !== null) {
$this->setInstalledVersion($installedVersion);
}
if ($enabled !== null) {
$this->setEnabled($enabled);
}
}
public function setRequirement($requirement)
{
if (preg_match('/^([<>=]+)\s*v?(\d+\.\d+\.\d+)$/', $requirement, $match)) {
$this->operator = $match[1];
$this->requiredVersion = $match[2];
$this->requirement = $requirement;
} else {
throw new \InvalidArgumentException("'$requirement' is not a valid version constraint");
}
}
/**
* @return bool
*/
public function isInstalled()
{
return $this->installedVersion !== null;
}
/**
* @return string|null
*/
public function getInstalledVersion()
{
return $this->installedVersion;
}
/**
* @param string $version
*/
public function setInstalledVersion($version)
{
$this->installedVersion = ltrim($version, 'v'); // v0.6.0 VS 0.6.0
}
/**
* @return bool
*/
public function isEnabled()
{
return $this->enabled === true;
}
/**
* @param bool $enabled
*/
public function setEnabled($enabled = true)
{
$this->enabled = $enabled;
}
public function isSatisfied()
{
if (! $this->isInstalled() || ! $this->isEnabled()) {
return false;
}
return version_compare($this->installedVersion, $this->requiredVersion, $this->operator);
}
public function getName()
{
return $this->name;
}
public function getRequirement()
{
return $this->requirement;
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace Icinga\Module\Director\Application;
use Icinga\Application\ApplicationBootstrap;
use Icinga\Application\Modules\Module;
use Icinga\Application\Version;
class DependencyChecker
{
/** @var ApplicationBootstrap */
protected $app;
/** @var \Icinga\Application\Modules\Manager */
protected $modules;
public function __construct(ApplicationBootstrap $app)
{
$this->app = $app;
$this->modules = $app->getModuleManager();
}
/**
* @param Module $module
* @return Dependency[]
*/
public function getDependencies(Module $module)
{
$dependencies = [];
$isV290 = version_compare(Version::VERSION, '2.9.0', '>=');
foreach ($module->getDependencies() as $moduleName => $required) {
if ($isV290 && in_array($moduleName, ['ipl', 'reactbundle'], true)) {
continue;
}
$dependency = new Dependency($moduleName, $required);
$dependency->setEnabled($this->modules->hasEnabled($moduleName));
if ($this->modules->hasInstalled($moduleName)) {
$dependency->setInstalledVersion($this->modules->getModule($moduleName, false)->getVersion());
}
$dependencies[] = $dependency;
}
if ($isV290) {
$libs = $this->app->getLibraries();
foreach ($module->getRequiredLibraries() as $libraryName => $required) {
$dependency = new Dependency($libraryName, $required);
if ($libs->has($libraryName)) {
$dependency->setInstalledVersion($libs->get($libraryName)->getVersion());
$dependency->setEnabled();
}
$dependencies[] = $dependency;
}
}
return $dependencies;
}
// if (version_compare(Version::VERSION, '2.9.0', 'ge')) {
// }
/**
* @param Module $module
* @return bool
*/
public function satisfiesDependencies(Module $module)
{
foreach ($this->getDependencies($module) as $dependency) {
if (! $dependency->isSatisfied()) {
return false;
}
}
return true;
}
}

View File

@ -123,15 +123,38 @@ class CoreApi implements DeploymentApiInterface
return $res[$name];
}
public function getTicketSalt()
/**
* Get a PKI ticket for CSR auto-signing
*
* @param string $cn The hosts common name for which the ticket should be generated
*
* @return string|null
*/
public function getTicket($cn)
{
// TODO: api must not be the name!
$api = $this->getObject('api', 'ApiListeners', array('ticket_salt'));
if (isset($api->attrs->ticket_salt)) {
return $api->attrs->ticket_salt;
$r = $this->client()->post(
'actions/generate-ticket',
['cn' => $cn]
);
if (! $r->succeeded()) {
throw new RuntimeException($r->getErrorMessage());
}
return null;
$ticket = $r->getRaw('ticket');
if ($ticket === null) {
// RestApiResponse::succeeded() returns true if Icinga 2 reports an error in the results key, e.g.
// {
// "results": [
// {
// "code": 500.0,
// "status": "Ticket salt is not configured in ApiListener object"
// }
// ]
// }
throw new RuntimeException($r->getRaw('status', 'Ticket is empty'));
}
return $ticket;
}
public function checkHostNow($host)

View File

@ -8,6 +8,7 @@ use Icinga\Module\Director\Objects\IcingaEndpoint;
use Icinga\Module\Director\Objects\IcingaHost;
use Icinga\Module\Director\Objects\IcingaZone;
use Icinga\Module\Director\Util;
use LogicException;
class AgentWizard
{
@ -15,12 +16,13 @@ class AgentWizard
protected $host;
protected $salt;
protected $parentZone;
protected $parentEndpoints;
/** @var string PKI ticket */
protected $ticket;
public function __construct(IcingaHost $host)
{
$this->host = $host;
@ -96,29 +98,32 @@ class AgentWizard
);
}
public function setTicketSalt($salt)
{
$this->salt = $salt;
return $this;
}
/**
* Get the PKI ticket
*
* @return string
*
* @throws LogicException If ticket has not been set
*/
protected function getTicket()
{
return Util::getIcingaTicket(
$this->getCertName(),
$this->getTicketSalt()
);
}
protected function getTicketSalt()
{
if ($this->salt === null) {
throw new ProgrammingError('Requesting salt, but got none');
// TODO: No API, not yet. Pass in constructor or throw, still tbd
// $this->salt = $this->api()->getTicketSalt();
if ($this->ticket === null) {
throw new LogicException('Ticket is null');
}
return $this->salt;
return $this->ticket;
}
/**
* Set the PKI ticket
*
* @param string $ticket
*
* @return $this
*/
public function setTicket($ticket)
{
$this->ticket = $ticket;
}
protected function getCertName()

View File

@ -411,25 +411,31 @@ class IcingaConfig
}
$activity = $this->dbBin($this->getLastActivityChecksum());
$this->db->insert(
self::$table,
array(
$this->db->beginTransaction();
try {
$this->db->insert(self::$table, [
'duration' => $this->generationTime,
'first_activity_checksum' => $activity,
'last_activity_checksum' => $activity,
'checksum' => $this->dbBin($this->getChecksum()),
)
);
/** @var IcingaConfigFile $file */
foreach ($this->files as $name => $file) {
$this->db->insert(
'director_generated_config_file',
array(
]);
/** @var IcingaConfigFile $file */
foreach ($this->files as $name => $file) {
$this->db->insert('director_generated_config_file', [
'config_checksum' => $this->dbBin($this->getChecksum()),
'file_checksum' => $this->dbBin($file->getChecksum()),
'file_path' => $name,
)
);
]);
}
$this->db->commit();
} catch (\Exception $e) {
try {
$this->db->rollBack();
} catch (\Exception $ignored) {
// Well...
}
throw $e;
}
return $this;

View File

@ -71,14 +71,14 @@ class ImportRunBasedPurgeStrategy extends PurgeStrategy
$columns = SyncUtils::getRootVariables(
SyncUtils::extractVariableNames($pattern)
);
$resultForCombinedKey = array();
foreach (array_chunk($result, 1000) as $keys) {
$rows = $runA->fetchRows($columns, null, $keys);
$result = array();
foreach ($rows as $row) {
$result[] = SyncUtils::fillVariables($pattern, $row);
$resultForCombinedKey[] = SyncUtils::fillVariables($pattern, $row);
}
}
$result = $resultForCombinedKey;
}
if (empty($result)) {

View File

@ -535,7 +535,13 @@ abstract class IcingaObject extends DbObject implements IcingaConfigRenderer
$this->connection
);
} catch (NotFoundError $e) {
throw new RuntimeException($e->getMessage(), 0, $e);
// Hint: eventually a NotFoundError would be better
throw new RuntimeException(sprintf(
'Unable to load object referenced from %s "%s", %s',
$this->getShortTableName(),
$this->getObjectName(),
lcfirst($e->getMessage())
), $e->getCode(), $e);
}
$this->reallySet($name, $object->get('id'));
@ -2553,7 +2559,7 @@ abstract class IcingaObject extends DbObject implements IcingaConfigRenderer
$type = 'templateChoiceHost';
} elseif ($type === 'service_template_choice') {
$type = 'TemplateChoiceService';
} elseif ($type === 'scheduled_downtime') {
} elseif ($type === 'scheduled_downtime' || $type === 'scheduled-downtime') {
$type = 'ScheduledDowntime';
}

View File

@ -4,6 +4,7 @@ namespace Icinga\Module\Director\Objects;
use Countable;
use Exception;
use Icinga\Exception\NotFoundError;
use Iterator;
use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
use Icinga\Module\Director\IcingaConfig\IcingaConfigRenderer;
@ -251,14 +252,23 @@ class IcingaObjectImports implements Iterator, Countable, IcingaConfigRenderer
$connection = $this->object->getConnection();
/** @var IcingaObject $class */
$class = $this->getImportClass();
if (is_array($this->object->getKeyName())) {
// Services only
$import = $class::load([
'object_name' => $name,
'object_type' => 'template'
], $connection);
} else {
$import = $class::load($name, $connection);
try {
if (is_array($this->object->getKeyName())) {
// Services only
$import = $class::load([
'object_name' => $name,
'object_type' => 'template'
], $connection);
} else {
$import = $class::load($name, $connection);
}
} catch (NotFoundError $e) {
throw new NotFoundError(sprintf(
'Unable to load parent referenced from %s "%s", %s',
$this->object->getShortTableName(),
$this->object->getObjectName(),
lcfirst($e->getMessage())
), $e->getCode(), $e);
}
return $this->objects[$import->getObjectName()] = $import;

View File

@ -213,10 +213,26 @@ abstract class ObjectApplyMatches
protected function __construct(IcingaObject $object)
{
$this->object = $object;
$this->flatObject = $object->toPlainObject(true, false);
$flat = $object->toPlainObject(true, false);
// Sure, we are flat - but we might still want to match templates.
unset($this->flatObject->imports);
$this->flatObject->templates = $object->listFlatResolvedImportNames();
static::flattenVars($this->flatObject);
unset($flat->imports);
$flat->templates = $object->listFlatResolvedImportNames();
$this->addAppliedGroupsToFlatObject($flat, $object);
static::flattenVars($flat);
$this->flatObject = $flat;
}
protected function addAppliedGroupsToFlatObject($flat, IcingaObject $object)
{
if ($object instanceof IcingaHost) {
$appliedGroups = $object->getAppliedGroups();
if (! empty($appliedGroups)) {
if (isset($flat->groups)) {
$flat->groups = array_merge($flat->groups, $appliedGroups);
} else {
$flat->groups = $appliedGroups;
}
}
}
}
}

View File

@ -23,7 +23,11 @@ class OverriddenVarsResolver
public function resolveFor(IcingaHost $host, IcingaService $service = null)
{
$overrides = [];
$parents = $host->listFlatResolvedImportNames();
if (empty($parents)) {
return $overrides;
}
$query = $this->db->select()->from(['hv' => 'icinga_host_var'], [
'host_name' => 'h.object_name',
'varvalue' => 'hv.varvalue',
@ -32,7 +36,6 @@ class OverriddenVarsResolver
'h.id = hv.host_id',
[]
)->where('hv.varname = ?', $this->varName)->where('h.object_name IN (?)', $parents);
$overrides = [];
foreach ($this->db->fetchAll($query) as $row) {
if ($row->varvalue === null) {
continue;

View File

@ -105,12 +105,7 @@ class IcingaObjectHandler extends RequestHandler
throw new NotFoundError('The host "%s" is not an agent', $host->getObjectName());
}
$this->sendJson(
Util::getIcingaTicket(
$host->getObjectName(),
$this->api->getTicketSalt()
)
);
$this->sendJson($this->api->getTicket($host->getObjectName()));
// TODO: find a better way to shut down. Currently, this avoids
// "not found" errors:

View File

@ -81,11 +81,6 @@ class Util
return bin2hex(substr($out, 0, $length));
}
public static function getIcingaTicket($certname, $salt)
{
return self::pbkdf2('sha1', $certname, $salt, 50000, 20);
}
public static function auth()
{
if (self::$auth === null) {

View File

@ -216,7 +216,8 @@ abstract class ActionController extends Controller implements ControlsAndContent
$viewRenderer = new SimpleViewRenderer();
$viewRenderer->replaceZendViewRenderer();
$this->view = $viewRenderer->view;
if ($this->getOriginalUrl()->getParam('view') === 'compact') {
// Hint -> $this->view->compact is the only way since v2.8.0
if ($this->view->compact || $this->getOriginalUrl()->getParam('view') === 'compact') {
if ($this->view->controls) {
$this->controls()->getAttributes()->add('style', 'display: none;');
}

View File

@ -17,7 +17,7 @@ class CsrfToken
return false;
}
list($seed, $token) = explode('|', $elementValue);
list($seed, $token) = explode('|', $token);
if (!is_numeric($seed)) {
return false;

View File

@ -629,7 +629,7 @@ abstract class DirectorObjectForm extends DirectorForm
if (is_bool($inherited)) {
$inherited = $inherited ? 'y' : 'n';
}
if (array_key_exists($inherited, $multi)) {
if (is_scalar($inherited) && array_key_exists($inherited, $multi)) {
$multi[null] = $multi[$inherited] . sprintf($txtInherited, $inheritedFrom);
} else {
$multi[null] = $this->translate($this->translate('- inherited -'));

View File

@ -619,6 +619,10 @@ abstract class QuickForm extends QuickBaseForm
$this->hasBeenSent = true;
} elseif ($req->isPost()) {
$post = $req->getPost();
if (! CsrfToken::isValid($post[self::CSRF])) {
throw new Exception('Invalid CSRF token provided');
}
$this->hasBeenSent = array_key_exists(self::ID, $post) &&
$post[self::ID] === $this->getName();
} else {

View File

@ -141,9 +141,13 @@ class ObjectPreview
return $match[1] . $match[2] . $match[3];
}
$urlObjectType = $this->object->getShortTableName();
if ($urlObjectType === 'service_set') {
$urlObjectType = 'service';
}
return $match[1] . Link::create(
$match[2],
sprintf('director/' . $this->object->getShortTableName()),
sprintf("director/$urlObjectType"),
['name' => $match[2]]
)->render() . $match[3];
}

View File

@ -226,9 +226,9 @@ class SelfService
$certname = $host->getObjectName();
try {
$ticket = Util::getIcingaTicket($certname, $this->api->getTicketSalt());
$ticket = $this->api->getTicket($host->getObjectName());
$wizard = new AgentWizard($host);
$wizard->setTicketSalt($this->api->getTicketSalt());
$wizard->setTicket($ticket);
} catch (Exception $e) {
$c->add(Hint::error(sprintf(
$this->translate(
@ -278,7 +278,7 @@ class SelfService
public function handleLegacyAgentDownloads($os)
{
$wizard = new AgentWizard($this->host);
$wizard->setTicketSalt($this->api->getTicketSalt());
$wizard->setTicket($this->api->getTicket($this->host->getObjectName()));
switch ($os) {
case 'windows-kickstart':

View File

@ -5,9 +5,8 @@ namespace Icinga\Module\Director\Web\Table;
use Icinga\Module\Director\Util;
use ipl\Html\BaseHtmlElement;
use gipfl\IcingaWeb2\Link;
use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
class ActivityLogTable extends ZfQueryBasedTable
class ActivityLogTable extends IntlZfQueryBasedTable
{
protected $filters = [];
@ -55,7 +54,7 @@ class ActivityLogTable extends ZfQueryBasedTable
return $this::tr([
$this::td($this->makeLink($row))->setSeparator(' '),
$this::td(strftime('%H:%M:%S', $row->ts_change_time))
$this::td($this->getTime($row->ts_change_time))
])->addAttributes(['class' => $action]);
}

View File

@ -4,13 +4,12 @@ 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
class BasketSnapshotTable extends IntlZfQueryBasedTable
{
use DbHelper;

View File

@ -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);
}
}

View File

@ -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('"', '&quot;', $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('&', '&amp;', $subject);
}
return $subject;
}, $value);
}
public static function escape($any)
{
return htmlspecialchars($any);
}
}

View File

@ -3,10 +3,9 @@
namespace Icinga\Module\Director\Web\Table;
use gipfl\IcingaWeb2\Link;
use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
use Icinga\Date\DateFormatter;
class DeploymentLogTable extends ZfQueryBasedTable
class DeploymentLogTable extends IntlZfQueryBasedTable
{
use DbHelper;

View File

@ -105,10 +105,10 @@ class IcingaServiceSetServiceTable extends ZfQueryBasedTable
{
if ($this->readonly) {
if ($this->highlightedService === $row->service) {
return Html::tag('span', ['class' => 'icon-right-big'], $row->service);
} else {
return $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) {

View File

@ -0,0 +1,51 @@
<?php
namespace Icinga\Module\Director\Web\Table;
use DateTime;
use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
use IntlDateFormatter;
use Locale;
abstract class IntlZfQueryBasedTable extends ZfQueryBasedTable
{
protected function getDateFormatter()
{
return (new IntlDateFormatter(
Locale::getDefault(),
IntlDateFormatter::FULL,
IntlDateFormatter::NONE
));
}
/**
* @param int $timestamp
*/
protected function renderDayIfNew($timestamp)
{
$day = $this->getDateFormatter()->format((new DateTime())->setTimestamp($timestamp));
if ($this->lastDay !== $day) {
$this->nextHeader()->add(
$this::th($day, [
'colspan' => 2,
'class' => 'table-header-day'
])
);
$this->lastDay = $day;
$this->nextBody();
}
}
protected function getTime(int $timeStamp)
{
$timeFormatter = $this->getDateFormatter();
$timeFormatter->setPattern(
in_array(Locale::getDefault(), ['en_US', 'en_US.UTF-8']) ? 'h:mm:ss a': 'H:mm:ss'
);
return $timeFormatter->format((new DateTime())->setTimestamp($timeStamp));
}
}

View File

@ -4,9 +4,8 @@ namespace Icinga\Module\Director\Web\Table;
use Icinga\Module\Director\Objects\SyncRule;
use gipfl\IcingaWeb2\Link;
use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
class SyncRunTable extends ZfQueryBasedTable
class SyncRunTable extends IntlZfQueryBasedTable
{
/** @var SyncRule */
protected $rule;
@ -28,7 +27,7 @@ class SyncRunTable extends ZfQueryBasedTable
return $this::tr([
$this::td($this->makeSummary($row)),
$this::td(new Link(
strftime('%H:%M:%S', $time),
$this->getTime($time),
'director/syncrule/history',
[
'id' => $row->rule_id,

View File

@ -1,6 +1,6 @@
Name: Icinga Director
Version: 1.8.0
Depends: reactbundle (>=0.7.0), ipl (>=0.3.0), incubator (>=0.6.0)
Version: 1.8.2
Depends: reactbundle (>=0.7.0), ipl (>=0.3.0), incubator (>=0.21.0)
Description: Director - Config tool for Icinga 2
Icinga Director is a configuration tool that has been designed to make
Icinga 2 configuration easy and understandable.

View File

@ -453,6 +453,9 @@ form dl {
text-decoration: line-through;
}
}
.strike-links span.ro-service {
text-decoration: line-through;
}
// TODO: figure out whether form.editor and filter-related CSS is still required
div.filter > form.search, div.filter > a {

17
run.php
View File

@ -1,6 +1,7 @@
<?php
use Icinga\Application\Modules\Module;
use Icinga\Module\Director\Application\DependencyChecker;
if (version_compare(PHP_VERSION, '5.6.0') < 0) {
include __DIR__ . '/run-php5.3.php';
@ -8,20 +9,8 @@ if (version_compare(PHP_VERSION, '5.6.0') < 0) {
}
/** @var Module $this */
$modules = $this->app->getModuleManager();
foreach ($this->getDependencies() as $module => $required) {
if ($modules->hasEnabled($module)) {
$installed = $modules->getModule($module, false)->getVersion();
$installed = ltrim($installed, 'v'); // v0.6.0 VS 0.6.0
if (preg_match('/^([<>=]+)\s*v?(\d+\.\d+\.\d+)$/', $required, $match)) {
$operator = $match[1];
$vRequired = $match[2];
if (version_compare($installed, $vRequired, $operator)) {
continue;
}
}
}
$checker = new DependencyChecker($this->app);
if (! $checker->satisfiesDependencies($this)) {
include __DIR__ . '/run-missingdeps.php';
return;
}

View File

@ -0,0 +1,72 @@
<?php
namespace Tests\Icinga\Module\Director\Application;
use Icinga\Module\Director\Application\Dependency;
use Icinga\Module\Director\Test\BaseTestCase;
class DependencyTest extends BaseTestCase
{
public function testIsNotInstalled()
{
$dependency = new Dependency('something', '>=0.3.0');
$this->assertFalse($dependency->isInstalled());
}
public function testNotSatisfiedWhenNotInstalled()
{
$dependency = new Dependency('something', '>=0.3.0');
$this->assertFalse($dependency->isSatisfied());
}
public function testIsInstalled()
{
$dependency = new Dependency('something', '>=0.3.0');
$dependency->setInstalledVersion('1.10.0');
$this->assertTrue($dependency->isInstalled());
}
public function testNotEnabled()
{
$dependency = new Dependency('something', '>=0.3.0');
$this->assertFalse($dependency->isEnabled());
}
public function testIsEnabled()
{
$dependency = new Dependency('something', '>=0.3.0');
$dependency->setEnabled();
$this->assertTrue($dependency->isEnabled());
}
public function testNotSatisfiedWhenNotEnabled()
{
$dependency = new Dependency('something', '>=0.3.0');
$dependency->setInstalledVersion('1.10.0');
$this->assertFalse($dependency->isSatisfied());
}
public function testSatisfiedWhenEqual()
{
$dependency = new Dependency('something', '>=0.3.0');
$dependency->setInstalledVersion('0.3.0');
$dependency->setEnabled();
$this->assertTrue($dependency->isSatisfied());
}
public function testSatisfiedWhenGreater()
{
$dependency = new Dependency('something', '>=0.3.0');
$dependency->setInstalledVersion('0.10.0');
$dependency->setEnabled();
$this->assertTrue($dependency->isSatisfied());
}
public function testNotSatisfiedWhenSmaller()
{
$dependency = new Dependency('something', '>=20.3.0');
$dependency->setInstalledVersion('4.999.999');
$dependency->setEnabled();
$this->assertFalse($dependency->isSatisfied());
}
}