Clean orphaned (missing) nodes (#355)

resolves #222 

## Blocked by:
- https://github.com/Icinga/icingaweb2-module-businessprocess/pull/358
This commit is contained in:
Johannes Meyer 2023-08-03 10:51:09 +02:00 committed by GitHub
commit 7298134f2f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 271 additions and 9 deletions

View file

@ -0,0 +1,106 @@
<?php
namespace Icinga\Module\Businessprocess\Clicommands;
use Exception;
use Icinga\Application\Logger;
use Icinga\Application\Modules\Module;
use Icinga\Cli\Command;
use Icinga\Module\Businessprocess\Modification\NodeRemoveAction;
use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport;
use Icinga\Module\Businessprocess\State\IcingaDbState;
use Icinga\Module\Businessprocess\State\MonitoringState;
use Icinga\Module\Businessprocess\Storage\LegacyStorage;
class CleanupCommand extends Command
{
/**
* @var LegacyStorage
*/
protected $storage;
protected $defaultActionName = 'cleanup';
public function init()
{
$this->storage = LegacyStorage::getInstance();
}
/**
* Cleanup all missing monitoring nodes from the specified config name
* If no config name is specified, the missing nodes are cleaned from all available configs.
* Invalid config files and file names are ignored
*
* USAGE
*
* icingacli businessprocess cleanup [<config-name>]
*
* OPTIONS
*
* <config-name>
*/
public function cleanupAction(): void
{
$configNames = (array) $this->params->shift() ?: $this->storage->listAllProcessNames();
$foundMissingNode = false;
foreach ($configNames as $configName) {
if (! $this->storage->hasProcess($configName)) {
continue;
}
try {
$bp = $this->storage->loadProcess($configName);
} catch (Exception $e) {
Logger::error(
'Failed to scan the %s.conf file for missing nodes. Invalid config found.',
$configName
);
continue;
}
if (Module::exists('icingadb')
&& (! $bp->hasBackendName() && IcingadbSupport::useIcingaDbAsBackend())
) {
IcingaDbState::apply($bp);
} else {
MonitoringState::apply($bp);
}
$removedNodes = [];
foreach (array_keys($bp->getMissingChildren()) as $missingNode) {
$node = $bp->getNode($missingNode);
$remove = new NodeRemoveAction($node);
try {
if ($remove->appliesTo($bp)) {
$remove->applyTo($bp);
$removedNodes[] = $node->getName();
$this->storage->storeProcess($bp);
$bp->clearAppliedChanges();
$foundMissingNode = true;
}
} catch (Exception $e) {
Logger::error(sprintf('(%s.conf) %s', $configName, $e->getMessage()));
continue;
}
}
if (! empty($removedNodes)) {
echo sprintf(
'Removed following %d missing node(s) from %s.conf successfully:',
count($removedNodes),
$configName
);
echo "\n" . implode("\n", $removedNodes) . "\n\n";
}
}
if (! $foundMissingNode) {
echo "No missing node found.\n";
}
}
}

View file

@ -27,7 +27,10 @@ use Icinga\Web\Url;
use Icinga\Web\Widget\Tabextension\DashboardAction;
use Icinga\Web\Widget\Tabextension\OutputFormat;
use ipl\Html\Html;
use ipl\Html\HtmlElement;
use ipl\Html\HtmlString;
use ipl\Html\TemplateString;
use ipl\Web\Widget\Link;
class ProcessController extends Controller
{
@ -114,14 +117,6 @@ class ProcessController extends Controller
$this->tabs()->extend(new OutputFormat());
$missing = $bp->getMissingChildren();
if (! empty($missing)) {
if (($count = count($missing)) > 10) {
$missing = array_slice($missing, 0, 10);
$missing[] = '...';
}
$bp->addError('There are %d missing nodes: %s', $count, implode(', ', $missing));
}
$this->content()->add($this->showHints($bp));
$this->content()->add($this->showWarnings($bp));
$this->content()->add($this->showErrors($bp));
@ -232,6 +227,12 @@ class ProcessController extends Controller
->setParentNode($node)
->setSession($this->session())
->handleRequest();
} elseif ($action === 'cleanup' && $canEdit) {
$form = $this->loadForm('CleanupNode')
->setSuccessUrl(Url::fromRequest()->without('action'))
->setProcess($bp)
->setSession($this->session())
->handleRequest();
} elseif ($action === 'editmonitored' && $canEdit) {
$form = $this->loadForm('EditNode')
->setSuccessUrl(Url::fromRequest()->without('action'))
@ -323,9 +324,11 @@ class ProcessController extends Controller
protected function showHints(BpConfig $bp)
{
$ul = Html::tag('ul', ['class' => 'error']);
$this->prepareMissingNodeLinks($ul);
foreach ($bp->getErrors() as $error) {
$ul->add(Html::tag('li')->setContent($error));
$ul->addHtml(Html::tag('li', $error));
}
if ($bp->hasChanges()) {
$li = Html::tag('li')->setSeparator(' ');
$li->add(sprintf(
@ -366,6 +369,66 @@ class ProcessController extends Controller
}
}
protected function prepareMissingNodeLinks(HtmlElement $ul): void
{
$missing = $this->bp->getMissingChildren();
if (! empty($missing)) {
$missingLinkedNodes = null;
foreach ($this->bp->getImportedNodes() as $process) {
if ($process->hasMissingChildren()) {
$missingLinkedNodes = array_keys($process->getMissingChildren());
$link = Url::fromPath('businessprocess/process/show')
->addParams(['config' => $process->getConfigName()]);
$ul->addHtml(Html::tag(
'li',
[
TemplateString::create(
tp(
'Linked node %s has one missing child node: {{#link}}Show{{/link}}',
'Linked node %s has %d missing child nodes: {{#link}}Show{{/link}}',
count($missingLinkedNodes)
),
$process->getAlias(),
count($missingLinkedNodes),
['link' => new Link(null, (string) $link)]
)
]
));
}
}
if (! empty($missingLinkedNodes)) {
return;
}
$count = count($missing);
if ($count > 10) {
$missing = array_slice($missing, 0, 10);
$missing[] = '...';
}
$link = Url::fromPath('businessprocess/process/show')
->addParams(['config' => $this->bp->getName(), 'action' => 'cleanup']);
$ul->addHtml(Html::tag(
'li',
[
TemplateString::create(
tp(
'{{#link}}Cleanup{{/link}} one missing node: %2$s',
'{{#link}}Cleanup{{/link}} %d missing nodes: %s',
count($missing)
),
['link' => new Link(null, (string) $link)],
$count,
implode(', ', $missing)
)
]
));
}
}
/**
* Show the source code for a process
*/

View file

@ -0,0 +1,93 @@
<?php
namespace Icinga\Module\Businessprocess\Forms;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\Modification\ProcessChanges;
use Icinga\Module\Businessprocess\Web\Form\QuickForm;
use Icinga\Module\Monitoring\Backend\MonitoringBackend;
use Icinga\Web\Session\SessionNamespace;
use ipl\Html\FormattedString;
use ipl\Html\Html;
use ipl\Sql\Connection as IcingaDbConnection;
class CleanupNodeForm extends QuickForm
{
/** @var MonitoringBackend|IcingaDbConnection */
protected $backend;
/** @var BpConfig */
protected $bp;
/** @var SessionNamespace */
protected $session;
public function setup()
{
$this->addHtml(Html::tag('h2', $this->translate('Cleanup missing nodes')));
$this->addElement('checkbox', 'cleanup_all', [
'class' => 'autosubmit',
'label' => $this->translate('Cleanup all missing nodes'),
'description' => $this->translate('Remove all missing nodes from config')
]);
if ($this->getSentValue('cleanup_all') !== '1') {
$this->addElement('multiselect', 'nodes', [
'label' => $this->translate('Select nodes to cleanup'),
'required' => true,
'size' => 8,
'multiOptions' => $this->bp->getMissingChildren()
]);
}
}
/**
* @param MonitoringBackend|IcingaDbConnection $backend
* @return $this
*/
public function setBackend($backend)
{
$this->backend = $backend;
return $this;
}
/**
* @param BpConfig $process
* @return $this
*/
public function setProcess(BpConfig $process)
{
$this->bp = $process;
$this->setBackend($process->getBackend());
return $this;
}
/**
* @param SessionNamespace $session
* @return $this
*/
public function setSession(SessionNamespace $session)
{
$this->session = $session;
return $this;
}
public function onSuccess()
{
$changes = ProcessChanges::construct($this->bp, $this->session);
$nodesToCleanup = $this->getValue('cleanup_all') === '1'
? array_keys($this->bp->getMissingChildren())
: $this->getValue('nodes');
foreach ($nodesToCleanup as $nodeName) {
$node = $this->bp->getNode($nodeName);
$changes->deleteNode($node);
}
unset($changes);
parent::onSuccess();
}
}