BacketCommand: allow to purge objects on restore

fixes #2201
This commit is contained in:
Thomas Gelf 2021-04-15 08:30:32 +02:00
parent c709c00fbd
commit 93222099ad
4 changed files with 209 additions and 19 deletions

View File

@ -4,8 +4,10 @@ namespace Icinga\Module\Director\Clicommands;
use Icinga\Date\DateFormatter;
use Icinga\Module\Director\Cli\Command;
use Icinga\Module\Director\Core\Json;
use Icinga\Module\Director\DirectorObject\Automation\Basket;
use Icinga\Module\Director\DirectorObject\Automation\BasketSnapshot;
use Icinga\Module\Director\DirectorObject\ObjectPurgeHelper;
/**
* Export Director Config Objects
@ -79,14 +81,43 @@ class BasketCommand extends Command
* icingacli director basket restore < basket-dump.json
*
* OPTIONS
* --purge <ObjectType>[,<ObjectType] Purge objects of the
* Given types. WARNING: this removes ALL objects that are
* not shipped with the given basket
* --force Purge refuses to purge Objects in case there are
* no Objects of a given ObjectType in the provided basket
* unless forced to do so
*/
public function restoreAction()
{
if ($purge = $this->params->get('purge')) {
$purge = explode(',', $purge);
ObjectPurgeHelper::assertObjectTypesAreEligibleForPurge($purge);
}
$json = file_get_contents('php://stdin');
BasketSnapshot::restoreJson($json, $this->db());
if ($purge) {
$this->purgeObjectTypes(Json::decode($json), $purge, $this->params->get('force'));
}
echo "Objects from Basket Snapshot have been restored\n";
}
protected function purgeObjectTypes($objects, array $types, $force = false)
{
$helper = new ObjectPurgeHelper($this->db());
if ($force) {
$helper->force();
}
foreach ($types as $type) {
list($className, $typeFilter) = BasketSnapshot::getClassAndObjectTypeForType($type);
$helper->purge(
isset($objects->$type) ? (array) $objects->$type : [],
$className,
$typeFilter
);
}
}
/**
*/
protected function requireBasket()

View File

@ -14,6 +14,9 @@ next (will be 1.9.0)
### Import and Sync
* FEATURE: introduce 'disable' as your purge action on Sync (#2285)
### Configuration Baskets
* FEATURE: it's now possible to purge objects of specific types (#2201)
### Permissions and Restrictions
* FEATURE: allow using monitoring module permissions (#2304)

View File

@ -111,9 +111,18 @@ class BasketSnapshot extends DbObject
if (is_array(self::$typeClasses[$type])) {
return self::$typeClasses[$type][0];
} else {
}
return self::$typeClasses[$type];
}
public static function getClassAndObjectTypeForType($type)
{
if (is_array(self::$typeClasses[$type])) {
return self::$typeClasses[$type];
}
return [self::$typeClasses[$type], null];
}
/**
@ -259,7 +268,7 @@ class BasketSnapshot extends DbObject
* @throws \Icinga\Module\Director\Exception\DuplicateKeyException
* @throws \Zend_Db_Adapter_Exception
*/
protected function restoreType(
public function restoreType(
&$all,
$typeName,
BasketSnapshotFieldResolver $fieldResolver,
@ -357,9 +366,9 @@ class BasketSnapshot extends DbObject
{
if ($this->hasBeenLoadedFromDb()) {
return $this->getContent()->get('summary');
} else {
return Json::encode($this->getSummary(), JSON_PRETTY_PRINT);
}
return Json::encode($this->getSummary(), JSON_PRETTY_PRINT);
}
/**
@ -370,14 +379,14 @@ class BasketSnapshot extends DbObject
{
if ($this->hasBeenLoadedFromDb()) {
return Json::decode($this->getContent()->get('summary'));
} else {
$summary = [];
foreach (array_keys($this->objects) as $key) {
$summary[$key] = count($this->objects[$key]);
}
return $summary;
}
$summary = [];
foreach (array_keys($this->objects) as $key) {
$summary[$key] = count($this->objects[$key]);
}
return $summary;
}
/**
@ -388,19 +397,15 @@ class BasketSnapshot extends DbObject
{
if ($this->hasBeenLoadedFromDb()) {
return $this->getContent()->get('content');
} else {
return Json::encode($this->objects, JSON_PRETTY_PRINT);
}
return Json::encode($this->objects, JSON_PRETTY_PRINT);
}
protected function addAll($typeName)
{
$class = static::getClassForType($typeName);
if (is_array(self::$typeClasses[$typeName])) {
$filter = self::$typeClasses[$typeName][1];
} else {
$filter = null;
}
list($class, $filter) = static::getClassAndObjectTypeForType($typeName);
/** @var IcingaObject $dummy */
$dummy = $class::create();
/** @var ExportInterface $object */

View File

@ -0,0 +1,151 @@
<?php
namespace Icinga\Module\Director\DirectorObject;
use Icinga\Module\Director\Db;
use Icinga\Module\Director\Objects\IcingaObject;
use InvalidArgumentException;
class ObjectPurgeHelper
{
protected $db;
protected $force = false;
public function __construct(Db $db)
{
$this->db = $db;
}
public function force($force = true)
{
$this->force = $force;
return $this;
}
public function purge(array $keep, $class, $objectType = null)
{
if (empty($keep) && ! $this->force) {
throw new InvalidArgumentException('I will NOT purge all object unless being forced to do so');
}
$db = $this->db->getDbAdapter();
/** @var IcingaObject $class cheating, it's a class name, not an object */
$dummy = $class::create();
assert($dummy instanceof IcingaObject);
$keyCols = (array) $dummy->getKeyName();
if ($objectType !== null) {
$keyCols[] = 'object_type';
}
$keepKeys = [];
foreach ($keep as $object) {
if ($object instanceof \stdClass) {
$properties = (array) $object;
// TODO: this is object-specific and to be found in the ::import() function!
unset($properties['fields']);
$object = $class::fromPlainObject($properties);
} elseif (\get_class($object) !== $class) {
throw new InvalidArgumentException(
'Can keep only matching objects, expected "%s", got "%s',
$class,
\get_class($keep)
);
}
$key = [];
foreach ($keyCols as $col) {
$key[$col] = $object->get($col);
}
$keepKeys[$this->makeRowKey($key)] = true;
}
$query = $db->select()->from(['o' => $dummy->getTableName()], $keyCols);
if ($objectType !== null) {
$query->where('object_type = ?', $objectType);
}
$allExisting = [];
foreach ($db->fetchAll($query) as $row) {
$allExisting[$this->makeRowKey($row)] = $row;
}
$remove = [];
foreach ($allExisting as $key => $keyProperties) {
if (! isset($keepKeys[$key])) {
$remove[] = $keyProperties;
}
}
$db->beginTransaction();
foreach ($remove as $keyProperties) {
$keyColumn = $class::getKeyColumnName();
if (is_array($keyColumn)) {
$object = $class::load((array) $keyProperties, $this->db);
} else {
$object = $class::load($keyProperties->$keyColumn, $this->db);
}
// $object->markForRemoval();
$object->delete();
}
$db->commit();
// DirectorActivityLog::logRemoval($this, $this->connection);
}
public static function listObjectTypesAvailableForPurge()
{
return [
'Basket',
'Command',
'CommandTemplate',
'Dependency',
'DirectorJob',
'ExternalCommand',
'HostGroup',
'HostTemplate',
'IcingaTemplateChoiceHost',
'IcingaTemplateChoiceService',
'ImportSource',
'Notification',
'NotificationTemplate',
'ServiceGroup',
'ServiceSet',
'ServiceTemplate',
'SyncRule',
'TimePeriod',
];
}
public static function objectTypeIsEligibleForPurge($type)
{
return in_array($type, static::listObjectTypesAvailableForPurge(), true);
}
public static function assertObjectTypesAreEligibleForPurge($types)
{
$invalid = [];
foreach ($types as $type) {
if (! static::objectTypeIsEligibleForPurge($type)) {
$invalid[] = $type;
}
}
if (empty($invalid)) {
return;
}
if (count($invalid) === 1) {
$message = sprintf('"%s" is not eligible for purge', $invalid[0]);
} else {
$message = 'The following types are not eligible for purge: '
. implode(', ', $invalid);
}
throw new InvalidArgumentException(
"$message. Valid types: "
. implode(', ', static::listObjectTypesAvailableForPurge())
);
}
protected function makeRowKey($row)
{
$row = (array) $row;
ksort($row);
return json_encode($row, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}
}