Skip to content

Commit

Permalink
Merge pull request it-novum#1529 from it-novum/ITC-3049
Browse files Browse the repository at this point in the history
ITC-3049 Corrupt containers after multiple operations are executed in parallel / concurrent
  • Loading branch information
ibering authored Jul 3, 2023
2 parents 882bb53 + be3096f commit f1f2409
Show file tree
Hide file tree
Showing 47 changed files with 936 additions and 105 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"ext-sockets": "*",
"ext-xmlrpc": "*",
"ext-zip": "*",
"ext-sysvsem": "*",
"azuyalabs/yasumi": "^2.2",
"cakephp/authentication": "^2.0",
"cakephp/authorization": "^2.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -365,4 +365,16 @@ public function getGrafanaUserdashboardsWithPanelsAndMetricsById($id) {

return $query->first();
}

/**
* @param int $containerId
* @return array
*/
public function getOrphanedGrafanaUserdashboardsByContainerId(int $containerId){
$query = $this->find()
->where(['container_id' => $containerId]);
$result = $query->all();

return $result->toArray();
}
}
31 changes: 31 additions & 0 deletions plugins/MapModule/src/Model/Table/MapsTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -2274,4 +2274,35 @@ public function getMapeditorSettings($config) {
}
return json_decode($config, true);
}

/**
* @param int $containerId
* @return array
*/
public function getOrphanedMapsByContainerId(int $containerId) {
$query = $this->find()
->innerJoinWith('Containers')
->contain([
'Containers' => function (Query $query) use ($containerId) {
return $query->select([
'Containers.id',
])->whereNotInList('Containers.id', [$containerId]);
}
])
->where(['Containers.id' => $containerId]);

$result = $query->all();
$maps = $result->toArray();

// Check each map, if it as more than one container.
// If the map has more than 1 container, we can keep this map because is not orphaned
$orphanedMaps = [];
foreach ($maps as $map) {
if (empty($map->containers)) {
$orphanedMaps[] = $map;
}
}

return $orphanedMaps;
}
}
210 changes: 210 additions & 0 deletions src/Command/ContainerRecoverCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
<?php
// Copyright (C) <2015-present> <it-novum GmbH>
//
// This file is dual licensed
//
// 1.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3 of the License.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//

// 2.
// If you purchased an openITCOCKPIT Enterprise Edition you can use this file
// under the terms of the openITCOCKPIT Enterprise Edition license agreement.
// License agreement and license key will be shipped with the order
// confirmation.

declare(strict_types=1);

namespace App\Command;

use App\itnovum\openITCOCKPIT\Database\Backup;
use App\Model\Table\ContainersTable;
use Cake\Command\Command;
use Cake\Console\Arguments;
use Cake\Console\ConsoleIo;
use Cake\Console\ConsoleOptionParser;
use Cake\Core\Configure;
use Cake\ORM\TableRegistry;

/**
* ContainerRecover command.
*
* Usage:
* oitc container_recover --check
* oitc container_recover --recover
*/
class ContainerRecoverCommand extends Command {
/**
* Hook method for defining this command's option parser.
*
* @see https://book.cakephp.org/4/en/console-commands/commands.html#defining-arguments-and-options
* @param \Cake\Console\ConsoleOptionParser $parser The parser to be defined
* @return \Cake\Console\ConsoleOptionParser The built parser.
*/
public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser {
$parser = parent::buildOptionParser($parser);

$parser->addOptions([
'check' => ['help' => 'Will check if any any errors are within the container tree', 'boolean' => true, 'default' => false],
'recover' => ['help' => 'Will recover the container tree using the parent_id field', 'boolean' => true, 'default' => false],
]);

return $parser;
}

/**
* Implement this method with your command's logic.
*
* @param \Cake\Console\Arguments $args The command arguments.
* @param \Cake\Console\ConsoleIo $io The console io
* @return null|void|int The exit code or null for success
*/
public function execute(Arguments $args, ConsoleIo $io) {

/** @var ContainersTable $ContainersTable */
$ContainersTable = TableRegistry::getTableLocator()->get('Containers');

// Run --check option
if ($args->getOption('check') === true) {

$orphanedContainers = $ContainersTable->getOrphanedContainers();
if (empty($orphanedContainers)) {
$io->success('No orphaned containers found.');
$io->success('Nothing to do.');

// Exit command
return 0;
}


$io->error('Orphaned containers found! Checking if an automatically repair is possible...');
$io->info('This is a read-only operation. No data is being changed.');
$autoRepairPossible = true;
foreach ($orphanedContainers as $orphanedContainer) {
// This container has a parent_id that does not exist.
// Before it is safe to delete this container, we need to make sure that it is not in used by any object
// Such as hosts, host groups, service templates, service template groups etc...
$io->info(sprintf(
'Found orphaned Container "%s" (ID: %s, Type: %s)',
$orphanedContainer['name'],
$orphanedContainer['id'],
$orphanedContainer['containertype_id']
));

if ($ContainersTable->isOrphanedContainerInUse($orphanedContainer['id'])) {
$autoRepairPossible = false;
$io->error(sprintf(
'The container "%s" (ID: %s, Type: %s) is in use by other objects. Please see the file /opt/openitc/frontend/logs/cli-error.log for more information',
$orphanedContainer['name'],
$orphanedContainer['id'],
$orphanedContainer['containertype_id']
));
}
}

if ($autoRepairPossible === false) {
$io->hr();
$io->out('Containers tree has operphand children.');
$io->out('An automatically recovery is not possible due to the containers are used by other elements.');
$io->out('Please contact commercial support or join our community Discord https://discord.gg/G8KhxKuQ9G');
$io->hr();
} else {
$io->hr();
$io->success('Automatically repair is possible!');
$io->out('Run "oitc container_recover --recover" to remove orphaned containers.');
$io->out('A backup of your database will be created BEFORE any modifications are done.');
$io->hr();
}

// Exit command with bad exit code due to errors
return 1;
}

// Run --recover option
if ($args->getOption('recover') === true) {
// Create backup first
$io->info('Start backup of MySQL database...');
Configure::load('nagios');
$MysqlBackup = new Backup();
$filename = Configure::read('nagios.export.backupTarget') . '/' . 'container_recover_' . date('d.m.y_H_i_s') . '.sql';
$return = $MysqlBackup->createMysqlDump($filename);
if ($return['returncode'] === 0) {
$io->success('MySQL Backup created at: ' . $filename);

$autoRepairPossible = true;
$orphanedContainers = $ContainersTable->getOrphanedContainers();

if(empty($orphanedContainers)){
$io->success('No orphaned containers found.');
$io->success('Nothing to do.');
return 0;
}

foreach ($orphanedContainers as $orphanedContainer)
if ($ContainersTable->isOrphanedContainerInUse($orphanedContainer['id'])) {
$autoRepairPossible = false;
}

if ($autoRepairPossible === false) {
$io->error('Container is in use! Run the command "oitc container_recover --check"');
$io->error('ABORTING - No data has ben changed.');
return 1;
}

// We have orphaned containers - by all are safe to delete
$io->info(sprintf('Found %s orphaned containers, but all are safe to delete', sizeof($orphanedContainers)));
$ContainersTable->removeBehavior('Tree');

$noErrors = true;
foreach ($orphanedContainers as $orphanedContainer) {
$entity = $ContainersTable->get($orphanedContainer['id']);
if ($ContainersTable->delete($entity)) {
$io->success(sprintf(
'Orphaned Container "%s" (ID: %s, Type: %s) deleted successfully',
$orphanedContainer['name'],
$orphanedContainer['id'],
$orphanedContainer['containertype_id']
));
} else {
$noErrors = false;
$io->error(sprintf('Could not delete orphaned container "%s" (ID: %s, Type: %s)!!',
$orphanedContainer['name'],
$orphanedContainer['id'],
$orphanedContainer['containertype_id']
));
}
}

if ($noErrors === true) {
$ContainersTable->addBehavior('Tree');
$ContainersTable->recover();
$io->hr();
$io->success('Recovery successful');
return 0;
}

$io->hr();
$io->error('Errors occurred - tree not recovered!');
return 1;
}

$io->error('Error while creating MySQL dump. EXIT NOW!');
return 1;
}

// Print help if no option is passed
// Valid options are --check or --recover
$this->displayHelp($this->getOptionParser(), $args, $io);
return 0;
}
}
17 changes: 15 additions & 2 deletions src/Controller/ContactgroupsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ public function add() {
$contactgroup->set('uuid', UUID::v4());
$contactgroup->get('container')->set('containertype_id', CT_CONTACTGROUP);

/** @var ContainersTable $ContainersTable */
$ContainersTable = TableRegistry::getTableLocator()->get('Containers');

$ContainersTable->acquireLock();

$ContactgroupsTable->save($contactgroup);
if ($contactgroup->hasErrors()) {
$this->response = $this->response->withStatus(400);
Expand Down Expand Up @@ -197,6 +202,11 @@ public function edit($id = null) {
//Update contact data
$User = new User($this->getUser());

/** @var ContainersTable $ContainersTable */
$ContainersTable = TableRegistry::getTableLocator()->get('Containers');

$ContainersTable->acquireLock();

$contactgroupEntity = $ContactgroupsTable->get($id, [
'contain' => [
'Containers'
Expand Down Expand Up @@ -254,10 +264,15 @@ public function delete($id) {
/** @var $ContactgroupsTable ContactgroupsTable */
$ContactgroupsTable = TableRegistry::getTableLocator()->get('Contactgroups');

/** @var ContainersTable $ContainersTable */
$ContainersTable = TableRegistry::getTableLocator()->get('Containers');

if (!$ContactgroupsTable->existsById($id)) {
throw new NotFoundException(__('Contact group not found'));
}

$ContainersTable->acquireLock();

$contactgroupEntity = $ContactgroupsTable->get($id, [
'contain' => [
'Containers'
Expand Down Expand Up @@ -288,8 +303,6 @@ public function delete($id) {
return;
}

/** @var $ContainersTable ContainersTable */
$ContainersTable = TableRegistry::getTableLocator()->get('Containers');
$container = $ContainersTable->get($contactgroupEntity->get('container')->get('id'), [
'contain' => [
'Contactgroups'
Expand Down
11 changes: 10 additions & 1 deletion src/Controller/ContainersController.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ public function add() {
return;
}

$ContainersTable->acquireLock();

$ContainersTable->save($container);
if ($container->hasErrors()) {
$this->response = $this->response->withStatus(400);
Expand Down Expand Up @@ -154,6 +156,9 @@ public function edit() {
if (!$ContainersTable->existsById($containerId)) {
throw new NotFoundException(__('Invalid container'));
}

$ContainersTable->acquireLock();

$container = $ContainersTable->get($containerId);
$containerForChangelog = $container->toArray();

Expand Down Expand Up @@ -416,6 +421,9 @@ public function delete($id = null) {

/** @var $ContainersTable ContainersTable */
$ContainersTable = TableRegistry::getTableLocator()->get('Containers');

$ContainersTable->acquireLock();

$container = $ContainersTable->find()
->where([
'Containers.id' => $id,
Expand All @@ -424,7 +432,8 @@ public function delete($id = null) {
->first();

if (empty($container)) {
return $this->render403();
$this->render403();
return;
}
$containerForChangelog = $container->toArray();

Expand Down
Loading

0 comments on commit f1f2409

Please sign in to comment.