Compare commits

...

8 Commits

Author SHA1 Message Date
odain
6e52a0fa4d asynchronous cleanup 2026-03-13 17:24:27 +01:00
Eric Espie
c5ad3553de Add background operation 2026-03-13 15:23:13 +01:00
odain
787587454e deletion plan test coverage 2026-03-13 12:34:31 +01:00
odain
e157427d7a WIP 2026-03-13 11:57:45 +01:00
odain
4853a4992a WIP - prepare cron cleanup + first tests 2026-03-13 11:04:33 +01:00
odain
b7b00b1685 report Exception method coming from develop before merge 2026-03-13 11:04:10 +01:00
Eric Espie
266b917551 Add delta 2026-03-13 10:42:08 +01:00
odain
55d606f110 N°9165 - Allow direct suppression or delayed suppression depending on the amount of data to remove 2026-03-13 10:40:55 +01:00
19 changed files with 1027 additions and 22 deletions

View File

@@ -691,6 +691,28 @@ abstract class LogAPI
static::$m_oMockMetaModelConfig = $oMetaModelConfig;
}
public static function Exception(string $sMessage, throwable $oException, string $sChannel = null, array $aContext = []): void
{
$aErrorLogs = [];
$aErrorLogs[] = static::PrepareErrorLog($sMessage, $oException, $aContext);
$oException = $oException->getPrevious();
while ($oException !== null) {
$aErrorLogs[] = static::PrepareErrorLog($oException->getMessage(), $oException, $aContext, true);
$oException = $oException->getPrevious();
}
$aErrorLogs = array_reverse($aErrorLogs);
foreach ($aErrorLogs as $aErrorLog) {
static::Error($aErrorLog['message'], $sChannel, $aErrorLog['context']);
}
}
private static function PrepareErrorLog(string $sMessage, throwable $oException, array $aContext, bool $isPrevious = false): array
{
$aContext['Error Message'] = $oException->getMessage();
$aContext['Stack Trace'] = $oException->getTraceAsString();
return ['message' => ($isPrevious ? "Previous " : '')."Exception: $sMessage", 'context' => $aContext];
}
public static function Error($sMessage, $sChannel = null, $aContext = [])
{
static::Log(self::LEVEL_ERROR, $sMessage, $sChannel, $aContext);

View File

@@ -1,5 +1,121 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.3">
<classes>
<class id="DataFeatureRemovalBackgroundOperation" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>datafeatureremovalbackgroundoperation</db_table>
<style>
<icon>
<fileref ref="icons8-electricity_643cf7fd7a024968679dc0c35a710a03"/>
</icon>
<main_color>#0000ff</main_color>
</style>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes/>
</reconciliation>
<uniqueness_rules>
<rule id="list_of_classes">
<attributes>
<attribute id="classes"/>
</attributes>
<filter><![CDATA[]]></filter>
<disabled>false</disabled>
<is_blocking>true</is_blocking>
</rule>
</uniqueness_rules>
</properties>
<fields>
<field id="creation_date" xsi:type="AttributeDateTime">
<sql>creation_date</sql>
<default_value/>
<is_null_allowed>false</is_null_allowed>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
<field id="features_code" xsi:type="AttributeText">
<sql>features_code</sql>
<default_value/>
<is_null_allowed>false</is_null_allowed>
<validation_pattern/>
<width/>
<height/>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
<field id="classes" xsi:type="AttributeText">
<sql>classes</sql>
<default_value/>
<is_null_allowed>false</is_null_allowed>
<validation_pattern/>
<width/>
<height/>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
</fields>
<methods/>
<presentation>
<list>
<items>
<item id="creation_date">
<rank>10</rank>
</item>
<item id="features_code">
<rank>20</rank>
</item>
<item id="classes">
<rank>30</rank>
</item>
</items>
</list>
<search>
<items/>
</search>
<details>
<items>
<item id="creation_date">
<rank>10</rank>
</item>
<item id="features_code">
<rank>20</rank>
</item>
<item id="classes">
<rank>30</rank>
</item>
</items>
</details>
</presentation>
<parent>cmdbAbstractObject</parent>
</class>
</classes>
<dictionaries>
<dictionary id="EN US">
<entries>
<entry id="Class:DataFeatureRemovalBackgroundOperation/Name" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation/ComplementaryName" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation" _delta="define"><![CDATA[DataFeatureRemovalBackgroundOperation]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation/Attribute:creation_date" _delta="define"><![CDATA[Creation date]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation/Attribute:creation_date+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation/Attribute:features_code" _delta="define"><![CDATA[Features code]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation/Attribute:features_code+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation/Attribute:classes" _delta="define"><![CDATA[Classes]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation/Attribute:classes+" _delta="define"><![CDATA[]]></entry>
</entries>
</dictionary>
</dictionaries>
<files>
<file id="icons8-electricity_643cf7fd7a024968679dc0c35a710a03" xsi:type="File" _delta="define_if_not_exists">
<name>images/icons/icons8-electricity.svg</name>
<mime_type>image/svg+xml</mime_type>
<data>PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciICB2aWV3Qm94PSIwIDAgNDggNDgiIHdpZHRoPSIyNDBweCIgaGVpZ2h0PSIyNDBweCI+PHBhdGggZmlsbD0iIzM3YzZkMCIgZD0iTTI2LjQ1MTIsMzdsNy43NzY0Ni0xNS41NTI5MWExLDEsMCwwLDAtLjg5NDM1LTEuNDQ3MjJMMjIuOTUxMiwxOS45OTkyNWw2LjMzNi0xMy41NzYzNUExLDEsMCwwLDAsMjguMzgxLDVIMjIuNTk2MzlhMSwxLDAsMCwwLS45MTExMS41ODc4M2wtOC41OTUyNCwxOUExLDEsMCwwLDAsMTQuMDAxMTUsMjZoMTAuNDVsLTUuNSwxMVoiLz48cGF0aCBmaWxsPSIjMzdjNmQwIiBkPSJNMTYuMjIyODYsMzdIMjkuODY0NzFhLjU1NzM1LjU1NzM1LDAsMCwxLC4zNTgxNSwxTDE5Ljk2LDQ0LjM2N2ExLjAzMzY0LDEuMDMzNjQsMCwwLDEtMS40OTEyMS0uNDI1ODZMMTUuNzIyODYsMzcuNzVBLjUwNDUuNTA0NSwwLDAsMSwxNi4yMjI4NiwzN1oiLz48L3N2Zz4=</data>
</file>
</files>
<menus>
<menu id="DataFeatureRemovalMenu" xsi:type="WebPageMenuNode" _delta="define">
<rank>30</rank>

View File

@@ -23,6 +23,7 @@ Dict::Add('EN US', 'English', 'English', [
'DataFeatureRemoval:Features:Title' => 'Features',
'DataFeatureRemoval:Analysis:Title' => 'Analysis result',
'DataFeatureRemoval:Analysis:SubTitle' => '%1$s element(s) to clean before continuing',
'DataFeatureRemoval:Analysis:Ok' => "No data to cleanup",
'DataFeatureRemoval:DeletionPlan:Title' => 'Deletion plan',
'DataFeatureRemoval:DeletionPlan:SubTitle' => 'Database tables to clean before continuing',
@@ -39,6 +40,7 @@ Dict::Add('EN US', 'English', 'English', [
'UI:Button:AnalyzeAndSetup' => 'Analyze and go to setup',
'UI:Button:PlanDeletion' => 'Prepare deletion plan',
'UI:Button:DoDeletion' => 'Delete data',
'UI:Button:DoAsyncDeletion' => 'Do asynchronous deletion',
'UI:Button:BackToMain' => 'Back to Feature Removal',
'UI:Button:Setup' => 'Back to setup',

View File

@@ -23,6 +23,7 @@ Dict::Add('FR FR', 'French', 'Français', [
'DataFeatureRemoval:Features:Title' => 'Features',
'DataFeatureRemoval:Analysis:Title' => 'Analysis result',
'DataFeatureRemoval:Analysis:SubTitle' => '%1$s element(s) to clean before continuing',
'DataFeatureRemoval:Analysis:Ok' => "No data to cleanup",
'DataFeatureRemoval:DeletionPlan:Title' => 'Deletion plan',
'DataFeatureRemoval:DeletionPlan:SubTitle' => 'Database tables to clean before continuing',
@@ -38,6 +39,7 @@ Dict::Add('FR FR', 'French', 'Français', [
'UI:Button:ModifyChoices' => 'Modify Choices',
'UI:Button:AnalyzeAndSetup' => 'Analyze and go to setup',
'UI:Button:PlanDeletion' => 'Prepare deletion plan',
'UI:Button:DoAsyncDeletion' => 'Do asynchronous deletion',
'UI:Button:DoDeletion' => 'Delete data',
'UI:Button:BackToMain' => 'Back to Feature Removal',
'UI:Button:Setup' => 'Back to setup',

View File

@@ -31,6 +31,7 @@ SetupWebPage::AddModule(
'datamodel' => [
'vendor/autoload.php',
'model.combodo-data-feature-removal.php', // Contains the PHP code generated by the "compilation" of datamodel.combodo-data-feature-removal.xml
'src/Hook/DataFeatureRemovalBackgroundTask.php',
],
'webservice' => [],
'data.struct' => [

View File

@@ -13,6 +13,7 @@ require_once APPROOT.'setup/feature_removal/DryRemovalRuntimeEnvironment.php';
use Combodo\iTop\Application\TwigBase\Controller\Controller;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalHelper;
use Combodo\iTop\DataFeatureRemoval\Service\BackgroundOperationService;
use Combodo\iTop\DataFeatureRemoval\Service\DataFeatureRemoverExtensionService;
use Combodo\iTop\DataFeatureRemoval\Service\DeletionPlanService;
use Combodo\iTop\Setup\FeatureRemoval\DryRemovalRuntimeEnvironment;
@@ -38,15 +39,19 @@ class DataFeatureRemovalController extends Controller
$this->AddAnalyzeParams();
$aParams['sTransactionId'] = utils::GetNewTransactionId();
$aParams['aExtensions'] = $this->GetExtensionsTable();
$aParams['aExtensionsCode'] = $this->aSelectedExtensionsForCheck;
$aParams['aAnalysisDataTable'] = $this->aAnalysisDataTable;
$aParams['aClasses'] = array_keys($this->aCountClassesToCleanup);
$aParams['DataFeatureRemovalErrorMessage'] = $sErrorMessage;
$aParams['bHasData'] = $this->iCount > 0;
$aParams['sSetupUrl'] = utils::GetAbsoluteUrlAppRoot().'setup';
$aParams['iCount'] = $this->iCount;
$aParams['DataFeatureRemovalErrorMessage'] = $sErrorMessage;
$aParams['bAnalysisOk'] = (count($this->aCountClassesToCleanup) > 0) && ($this->iCount === 0);
$this->AddLinkedStylesheet(utils::GetAbsoluteUrlModulesRoot().DataFeatureRemovalHelper::MODULE_NAME.'/assets/css/DataFeatureRemoval.css');
$this->AddLinkedScript(utils::GetAbsoluteUrlModulesRoot().DataFeatureRemovalHelper::MODULE_NAME.'/assets/js/DataFeatureRemoval.js');
$this->m_sOperation = "Main";
$this->DisplayPage($aParams);
}
@@ -119,6 +124,7 @@ class DataFeatureRemovalController extends Controller
$aParams['sTransactionId'] = utils::GetNewTransactionId();
$aParams['aDeletionPlanSummary'] = $this->GetTableData('Extensions', $aColumns, $aRows);
$aParams['aClasses'] = $aClasses;
$aParams['aExtensionsCode'] = utils::ReadPostedParam('aExtensionsCode', []);
$this->DisplayPage($aParams);
}
@@ -129,6 +135,14 @@ class DataFeatureRemovalController extends Controller
$this->ValidateTransactionId();
$aClasses = utils::ReadPostedParam('classes', null, utils::ENUM_SANITIZATION_FILTER_CLASS);
$sDeletionButtonValue = utils::ReadPostedParam('btn_deletion', null);
$bAsynchronous = ('async_deletion' === $sDeletionButtonValue);
if ($bAsynchronous) {
BackgroundOperationService::GetInstance()->CreateOperation(utils::ReadPostedParam('aExtensionsCode', []), $aClasses);
$this->OperationMain();
return;
}
$aDeletionExecutionSummary = DeletionPlanService::GetInstance()->ExecuteDeletionPlan($aClasses);
$aColumns = ['Class', 'DeletedCount' , 'UpdatedCount'];
@@ -159,16 +173,24 @@ class DataFeatureRemovalController extends Controller
foreach (DataFeatureRemoverExtensionService::GetInstance()->ReadItopExtensions() as $sCode => $oExtension) {
/** @var \iTopExtension $oExtension */
$bCleanupOngoing = BackgroundOperationService::GetInstance()->IsExtensionBeingCleaned($sCode);
$sChecked = '';
$sDisabledHtml = '';
if ($oExtension->bRemovedFromDisk) {
$sLabel = $oExtension->sLabel;
if ($bCleanupOngoing) {
$sDisabledHtml = 'disabled=""';
$sLabel .= <<<HTML
&nbsp; <span class="ibo-spinner ibo-is-inline ibo-spinner ibo-block" data-role="ibo-spinner"><i class="ibo-spinner--icon fas fa-sync-alt fa-spin" aria-hidden="true"/></span>
HTML;
;
} elseif ($oExtension->bRemovedFromDisk) {
$sDisabledHtml = 'disabled=""';
$sChecked = 'checked';
} elseif (in_array($sCode, $this->aSelectedExtensionsForCheck)) {
$sChecked = 'checked';
}
$sLabel = $oExtension->sLabel;
$sVersion = $oExtension->sVersion;
$sIdEnable = "aExtensions[$sCode][enable]";
@@ -226,12 +248,12 @@ HTML,
}
/**
* @return void
* @return array
*/
public function ReadRemovedExtensions(): void
public function ReadRemovedExtensions(): array
{
if (count($this->aSelectedExtensionsForCheck) > 0) {
return;
return $this->aSelectedExtensionsForCheck;
}
$aSelectedExtensionsFromUI = utils::ReadPostedParam('aExtensions', []);
@@ -248,5 +270,7 @@ HTML,
$this->aSelectedExtensionsForCheck[] = $sCode;
}
}
return $this->aSelectedExtensionsForCheck;
}
}

View File

@@ -0,0 +1,71 @@
<?php
/**
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\DataFeatureRemoval\Helper;
use Config;
use MetaModel;
use utils;
class DataFeatureRemovalConfig
{
private static DataFeatureRemovalConfig $oInstance;
protected function __construct()
{
}
final public static function GetInstance(): DataFeatureRemovalConfig
{
if (!isset(static::$oInstance)) {
static::$oInstance = new DataFeatureRemovalConfig();
}
return static::$oInstance;
}
public function Get(string $sParamName, $default = null)
{
return MetaModel::GetModuleSetting(DataFeatureRemovalHelper::MODULE_NAME, $sParamName, $default);
}
public function GetBoolean(string $sParamName, $default = null): bool
{
$res = $this->Get($sParamName, $default);
return boolval($res);
}
public function IsEnabled(): bool
{
return $this->GetBoolean('enable', false);
}
public function Set(string $sParamName, $value)
{
$oConfig = utils::GetConfig();
$oConfig->SetModuleSetting(DataFeatureRemovalHelper::MODULE_NAME, $sParamName, $value);
}
/**
* @param \Config|null $oConfig
*
* @return void
* @throws \ConfigException
* @throws \CoreException
*/
public function SaveItopConfiguration(Config $oConfig = null)
{
if (is_null($oConfig)) {
$oConfig = utils::GetConfig();
}
$sConfigFile = APPROOT.'conf/'.utils::GetCurrentEnvironment().'/config-itop.php';
@chmod($sConfigFile, 0770); // Allow overwriting the file
$oConfig->WriteToFile($sConfigFile);
@chmod($sConfigFile, 0444); // Read-only
}
}

View File

@@ -10,4 +10,14 @@ namespace Combodo\iTop\DataFeatureRemoval\Helper;
class DataFeatureRemovalHelper
{
public const MODULE_NAME = 'combodo-data-feature-removal';
public static function IsTimeLimitExceeded(int $iUnixTimeLimit): bool
{
if ($iUnixTimeLimit === 0) {
//no time limit
return false;
}
return (time() > $iUnixTimeLimit);
}
}

View File

@@ -0,0 +1,44 @@
<?php
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalConfig;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalHelper;
use Combodo\iTop\DataFeatureRemoval\Service\BackgroundOperationService;
use Combodo\iTop\DataFeatureRemoval\Service\DeletionPlanService;
class DataFeatureRemovalBackgroundTask implements iBackgroundProcess
{
/**
* @inheritDoc
*/
public function GetPeriodicity()
{
return DataFeatureRemovalConfig::GetInstance()->Get('cron_periodicity_in_s', 10);
}
/**
* @inheritDoc
*/
public function Process($iUnixTimeLimit)
{
while ($oBackgroundOperation = BackgroundOperationService::GetInstance()->GetNext()) {
$aClasses = BackgroundOperationService::GetInstance()->GetClasses($oBackgroundOperation);
$aRes = DeletionPlanService::GetInstance()->ExecuteDeletionPlan($aClasses, $iUnixTimeLimit);
IssueLog::Info(__METHOD__, null, $aRes);
IssueLog::Info(__METHOD__, null, [
'$iUnixTimeLimit' => $iUnixTimeLimit,
'time' => time(),
'timeout reached' => DataFeatureRemovalHelper::IsTimeLimitExceeded($iUnixTimeLimit),
]);
if (DataFeatureRemovalHelper::IsTimeLimitExceeded($iUnixTimeLimit)) {
//timeout reached
return;
}
//execution finished before timeout: nothing left to remove
$oBackgroundOperation->DBDelete();
}
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace Combodo\iTop\DataFeatureRemoval\Service;
use DataFeatureRemovalBackgroundOperation;
use DBObjectSet;
use DBSearch;
use MetaModel;
class BackgroundOperationService
{
private static BackgroundOperationService $oInstance;
protected function __construct()
{
}
final public static function GetInstance(): BackgroundOperationService
{
if (!isset(self::$oInstance)) {
self::$oInstance = new BackgroundOperationService();
}
return self::$oInstance;
}
final public static function SetInstance(?BackgroundOperationService $oInstance): void
{
static::$oInstance = $oInstance;
}
/**
* @return \DataFeatureRemovalBackgroundOperation
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
* @throws \OQLException
*/
public function GetNext(): ?DataFeatureRemovalBackgroundOperation
{
$sOQL = <<<OQL
SELECT DataFeatureRemovalBackgroundOperation
OQL;
$oSet = new DBObjectSet(DBSearch::FromOQL($sOQL), ['creation_date' => true]);
/** @var DataFeatureRemovalBackgroundOperation $oDBObject */
$oDBObject = $oSet->Fetch();
return $oDBObject;
}
/**
* @param array $aExtensionsCodes
* @param array $aClasses
* @return void
* @throws \ArchivedObjectException
* @throws \CoreCannotSaveObjectException
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \CoreWarning
* @throws \MySQLException
* @throws \OQLException
*/
public function CreateOperation(array $aExtensionsCodes, array $aClasses): void
{
sort($aExtensionsCodes);
sort($aClasses);
$aValues = [
'creation_date' => time(),
'features_code' => '|'.implode('|', $aExtensionsCodes).'|',
'classes' => implode(',', $aClasses),
];
$oObj = MetaModel::NewObject('DataFeatureRemovalBackgroundOperation', $aValues);
$oObj->DBWrite();
}
/**
* @param string $sExtensionCode
* @return bool
* @throws \CoreException
* @throws \MissingQueryArgument
* @throws \MySQLException
* @throws \MySQLHasGoneAwayException
* @throws \OQLException
*/
public function IsExtensionBeingCleaned(string $sExtensionCode): bool
{
$sOQL = <<<OQL
SELECT DataFeatureRemovalBackgroundOperation WHERE features_code LIKE '%|$sExtensionCode|%'
OQL;
$oSet = new DBObjectSet(DBSearch::FromOQL($sOQL));
return $oSet->Count() > 0;
}
/**
* @param \DataFeatureRemovalBackgroundOperation $oBackgroundOperation
* @return array
*/
public function GetClasses(DataFeatureRemovalBackgroundOperation $oBackgroundOperation): array
{
return explode(',', $oBackgroundOperation->Get('classes'));
}
}

View File

@@ -5,6 +5,7 @@ namespace Combodo\iTop\DataFeatureRemoval\Service;
use CMDBSource;
use Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanSummaryEntity;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalHelper;
use DBObjectSearch;
use DeletionPlan;
use MetaModel;
@@ -13,6 +14,8 @@ class DeletionPlanService
{
private static DeletionPlanService $oInstance;
public int $iExecutionCount = 0;
protected function __construct()
{
}
@@ -89,7 +92,10 @@ class DeletionPlanService
}
/**
* @since 3.3.0
* @param array $aClasses
* @param int $iUnixTimeLimit : max execution time in seconds since Epoch before stopping deletion. by default: no limit (ie remove all without stop)
* @param int $iMaxExecutionCount : max execution count before stopping deletion. by default: no limit (ie remove all without stop)
*
* @return array<\Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanSummaryEntity>
* @throws \Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException
@@ -97,7 +103,7 @@ class DeletionPlanService
* @throws \CoreUnexpectedValue
* @throws \MySQLException
*/
public function ExecuteDeletionPlan(array $aClasses): array
public function ExecuteDeletionPlan(array $aClasses, int $iUnixTimeLimit = 0, int $iMaxExecutionCount = -1): array
{
$oDeletionPlan = $this->GetDeletionPlan($aClasses);
@@ -105,11 +111,19 @@ class DeletionPlanService
throw new DataFeatureRemovalException("Deletion Plan cannot be executed due to issues");
}
$this->iExecutionCount = 0;
$aSummary = [];
foreach ($oDeletionPlan->ListUpdates() as $sClass => $aToUpdate) {
$oDeletionPlanSummaryEntity = $aSummary[$sClass] ?? new DeletionPlanSummaryEntity($sClass);
foreach ($aToUpdate as $aData) {
if ($this->IsTimeLimitExceeded($iUnixTimeLimit, $iMaxExecutionCount)) {
$aSummary[$sClass] = $oDeletionPlanSummaryEntity;
return $aSummary;
}
$this->iExecutionCount++;
$oToUpdate = $aData['to_reset'];
/** @var \DBObject $oToUpdate */
foreach ($aData['attributes'] as $sRemoteExtKey => $aRemoteAttDef) {
@@ -126,20 +140,35 @@ class DeletionPlanService
$oDeletionPlanSummaryEntity = $aSummary[$sClass] ?? new DeletionPlanSummaryEntity($sClass);
foreach ($aDeletes as $sId => $aDelete) {
// Delete any existing change tracking about the current object
$oFilter = new DBObjectSearch('CMDBChangeOp');
$oFilter->AddCondition('objclass', $sClass, '=');
$oFilter->AddCondition('objkey', $sId, '=');
MetaModel::PurgeData($oFilter);
// Delete the entry
$aClassesToRemove = array_merge(MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL), MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_EXCLUDELEAF, false));
foreach ($aClassesToRemove as $sParentClass) {
$oFilter = DBObjectSearch::FromOQL_AllData("SELECT $sParentClass WHERE id=:id");
$sQuery = $oFilter->MakeDeleteQuery(['id' => $sId]);
CMDBSource::DeleteFrom($sQuery);
if ($this->IsTimeLimitExceeded($iUnixTimeLimit, $iMaxExecutionCount)) {
$aSummary[$sClass] = $oDeletionPlanSummaryEntity;
return $aSummary;
}
$this->iExecutionCount++;
try {
CMDBSource::Query('START TRANSACTION');
// Delete any existing change tracking about the current object
$oFilter = new DBObjectSearch('CMDBChangeOp');
$oFilter->AddCondition('objclass', $sClass, '=');
$oFilter->AddCondition('objkey', $sId, '=');
MetaModel::PurgeData($oFilter);
// Delete the entry
$aClassesToRemove = array_merge(MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL), MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_EXCLUDELEAF, false));
foreach ($aClassesToRemove as $sParentClass) {
$oFilter = DBObjectSearch::FromOQL_AllData("SELECT $sParentClass WHERE id=:id");
$sQuery = $oFilter->MakeDeleteQuery(['id' => $sId]);
CMDBSource::DeleteFrom($sQuery);
}
CMDBSource::Query('COMMIT');
} catch (\Exception $e) {
\IssueLog::Exception(__METHOD__.": Cleanup failed", $e);
CMDBSource::Query('ROLLBACK');
throw $e;
}
$oDeletionPlanSummaryEntity->iDeleteCount++;
}
@@ -171,4 +200,13 @@ class DeletionPlanService
return $oDeletionPlan;
}
public function IsTimeLimitExceeded(int $iUnixTimeLimit, int $iMaxExecutionCount = -1): bool
{
if (($iMaxExecutionCount !== -1) && ($iMaxExecutionCount <= $this->iExecutionCount)) {
return true;
}
return DataFeatureRemovalHelper::IsTimeLimitExceeded($iUnixTimeLimit);
}
}

View File

@@ -11,8 +11,14 @@
{% for sKey, sClass in aClasses %}
{% UIInput ForHidden { sName:"classes[" ~ sKey ~ "]", sValue:sClass } %}
{% endfor %}
{% for sKey, sCode in aExtensionsCode %}
{% UIInput ForHidden { sName:"aExtensionsCode[" ~ sKey ~ "]", sValue:sCode } %}
{% endfor %}
{% UIToolbar ForButton {} %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:DoDeletion'|dict_s, sName:'btn_deletion', sId:'btn_deletion', bIsSubmit:true} %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:DoDeletion'|dict_s, sName:'btn_deletion', sId:'btn_deletion', sValue: 'immediate_deletion', bIsSubmit:true} %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:DoAsyncDeletion'|dict_s, sName:'btn_deletion', sId:'btn_async_deletion', sValue: 'async_deletion', bIsSubmit:true} %}
{% EndUIToolbar %}
{% EndUIForm %}

View File

@@ -12,8 +12,13 @@
{% for sKey, sClass in aClasses %}
{% UIInput ForHidden { sName:"classes[" ~ sKey ~ "]", sValue:sClass } %}
{% endfor %}
{% for sKey, sCode in aExtensionsCode %}
{% UIInput ForHidden { sName:"aExtensionsCode[" ~ sKey ~ "]", sValue:sCode } %}
{% endfor %}
{% UIToolbar ForButton {} %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:PlanDeletion'|dict_s, sName:'btn_plandeletion', sId:'btn_plandeletion', bIsSubmit:true} %}
{% EndUIToolbar %}
{% EndUIForm %}
{% endif %}

View File

@@ -25,7 +25,9 @@
{% include 'Features.html.twig' %}
{% include 'ExtensionRemovalData.html.twig' %}
{% if not bHasData %}
{% if bAnalysisOk %}
{{ "DataFeatureRemoval:Analysis:Ok"|dict_s }}
{% UIToolbar ForButton {} %}
<a href="{{ sSetupUrl }}">
{% UIButton ForPrimaryAction {sLabel:'UI:Button:Setup'|dict_s, sName:'btn_setup', sId:'btn_setup', bIsSubmit:false} %}

View File

@@ -14,7 +14,10 @@ if (PHP_VERSION_ID < 50600) {
echo $err;
}
}
throw new RuntimeException($err);
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';

View File

@@ -8,9 +8,11 @@ $baseDir = dirname($vendorDir);
return array(
'Combodo\\iTop\\DataFeatureRemoval\\Controller\\DataFeatureRemovalController' => $baseDir . '/src/Controller/DataFeatureRemovalController.php',
'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DeletionPlanSummaryEntity' => $baseDir . '/src/Entity/DeletionPlanSummaryEntity.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalConfig' => $baseDir . '/src/Helper/DataFeatureRemovalConfig.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalException' => $baseDir . '/src/Helper/DataFeatureRemovalException.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalHelper' => $baseDir . '/src/Helper/DataFeatureRemovalHelper.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalLog' => $baseDir . '/src/Helper/DataFeatureRemovalLog.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\BackgroundOperationService' => $baseDir . '/src/Service/BackgroundOperationService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DataFeatureRemoverExtensionService' => $baseDir . '/src/Service/DataFeatureRemoverExtensionService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DeletionPlanService' => $baseDir . '/src/Service/DeletionPlanService.php',
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',

View File

@@ -27,9 +27,11 @@ class ComposerStaticInit4f96a7199e2c0d90e547333758b26464
public static $classMap = array (
'Combodo\\iTop\\DataFeatureRemoval\\Controller\\DataFeatureRemovalController' => __DIR__ . '/../..' . '/src/Controller/DataFeatureRemovalController.php',
'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DeletionPlanSummaryEntity' => __DIR__ . '/../..' . '/src/Entity/DeletionPlanSummaryEntity.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalConfig' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalConfig.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalException' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalException.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalHelper' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalHelper.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalLog' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalLog.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\BackgroundOperationService' => __DIR__ . '/../..' . '/src/Service/BackgroundOperationService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DataFeatureRemoverExtensionService' => __DIR__ . '/../..' . '/src/Service/DataFeatureRemoverExtensionService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DeletionPlanService' => __DIR__ . '/../..' . '/src/Service/DeletionPlanService.php',
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',

View File

@@ -0,0 +1,204 @@
<?php
namespace Combodo\iTop\DataFeatureRemoval\Service\Test;
use Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanSummaryEntity;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException;
use Combodo\iTop\DataFeatureRemoval\Service\DeletionPlanService;
use Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase;
class DeletionPlanServiceTest extends ItopCustomDatamodelTestCase
{
public function testExecuteDeletionPlan_DeleteOneObjPerClassWithoutLimit()
{
$this->GivenDFRTreeInDB(<<<EOF
DFRToRemoveLeaf_1 <- DFRToUpdate_1
DFRToRemoveLeaf_1 <- DFRRemovedCollateral_1
DFRRemovedCollateral_1 <- DFRRemovedCollateralCascade_1
EOF);
$aClasses = [ 'DFRToRemoveLeaf' ];
$aRes = DeletionPlanService::GetInstance()->ExecuteDeletionPlan($aClasses);
$aExpected = [
['DFRToUpdate', 1, 0 ],
['DFRToRemoveLeaf', 0, 1 ],
['DFRRemovedCollateral', 0, 1 ],
['DFRRemovedCollateralCascade', 0, 1 ],
];
$this->AssertSummaryEquals($aExpected, $aRes);
}
public function testExecuteDeletionPlan_DeleteManyObjPerClassWithoutLimit()
{
$this->GivenDFRTreeInDB(<<<EOF
DFRToRemoveLeaf_1 <- DFRToUpdate_1
DFRToRemoveLeaf_1 <- DFRRemovedCollateral_1
DFRRemovedCollateral_1 <- DFRRemovedCollateralCascade_1
DFRToRemoveLeaf_2 <- DFRToUpdate_2
DFRToRemoveLeaf_2 <- DFRRemovedCollateral_2
DFRRemovedCollateral_2 <- DFRRemovedCollateralCascade_2
DFRToRemoveLeaf_3 <- DFRToUpdate_3
DFRToRemoveLeaf_3 <- DFRRemovedCollateral_3
DFRRemovedCollateral_3 <- DFRRemovedCollateralCascade_3
EOF);
$aClasses = [ 'DFRToRemoveLeaf' ];
$aRes = DeletionPlanService::GetInstance()->ExecuteDeletionPlan($aClasses);
$aExpected = [
['DFRToUpdate', 3, 0 ],
['DFRToRemoveLeaf', 0, 3 ],
['DFRRemovedCollateral', 0, 3 ],
['DFRRemovedCollateralCascade', 0, 3 ],
];
$this->AssertSummaryEquals($aExpected, $aRes);
}
public function testExecuteDeletionPlan_ManualDeleteShouldFail()
{
$this->GivenDFRTreeInDB(<<<EOF
DFRToRemoveLeaf_1 <- DFRManual_1
EOF);
$aClasses = [ 'DFRToRemoveLeaf' ];
$this->expectException(DataFeatureRemovalException::class);
$this->expectExceptionMessage('Deletion Plan cannot be executed due to issues');
DeletionPlanService::GetInstance()->ExecuteDeletionPlan($aClasses);
}
private function AssertSummaryEquals(array $expected, $actual, $sMessage = '')
{
$aExpected = [];
foreach ($expected as $line) {
$sClass = $line[0];
$iUpdate = $line[1];
$iDelete = $line[2];
$oDeletionPlanSummaryEntity = new DeletionPlanSummaryEntity($sClass);
$oDeletionPlanSummaryEntity->iUpdateCount = $iUpdate;
$oDeletionPlanSummaryEntity->iDeleteCount = $iDelete;
$aExpected[$sClass] = $oDeletionPlanSummaryEntity;
}
$this->assertEquals($aExpected, $actual, $sMessage);
}
public function testExecuteDeletionPlan_StopInUpdates()
{
$this->GivenDFRTreeInDB(<<<EOF
DFRToRemoveLeaf_1 <- DFRToUpdate_1
DFRToRemoveLeaf_1 <- DFRRemovedCollateral_1
DFRRemovedCollateral_1 <- DFRRemovedCollateralCascade_1
DFRToRemoveLeaf_2 <- DFRToUpdate_2
DFRToRemoveLeaf_2 <- DFRRemovedCollateral_2
DFRRemovedCollateral_2 <- DFRRemovedCollateralCascade_2
DFRToRemoveLeaf_3 <- DFRToUpdate_3
DFRToRemoveLeaf_3 <- DFRRemovedCollateral_3
DFRRemovedCollateral_3 <- DFRRemovedCollateralCascade_3
EOF);
$aClasses = [ 'DFRToRemoveLeaf' ];
$aRes = DeletionPlanService::GetInstance()->ExecuteDeletionPlan($aClasses, 0, 3);
$aExpected = [
['DFRToUpdate', 3, 0 ],
['DFRToRemoveLeaf', 0, 0 ],
];
$this->AssertSummaryEquals($aExpected, $aRes);
}
public function testExecuteDeletionPlan_StopInDeletes()
{
$this->GivenDFRTreeInDB(<<<EOF
DFRToRemoveLeaf_1 <- DFRToUpdate_1
DFRToRemoveLeaf_1 <- DFRRemovedCollateral_1
DFRRemovedCollateral_1 <- DFRRemovedCollateralCascade_1
DFRToRemoveLeaf_2 <- DFRToUpdate_2
DFRToRemoveLeaf_2 <- DFRRemovedCollateral_2
DFRRemovedCollateral_2 <- DFRRemovedCollateralCascade_2
DFRToRemoveLeaf_3 <- DFRToUpdate_3
DFRToRemoveLeaf_3 <- DFRRemovedCollateral_3
DFRRemovedCollateral_3 <- DFRRemovedCollateralCascade_3
EOF);
$aClasses = [ 'DFRToRemoveLeaf' ];
$aRes = DeletionPlanService::GetInstance()->ExecuteDeletionPlan($aClasses, 0, 8);
$aExpected = [
['DFRToUpdate', 3, 0 ],
['DFRToRemoveLeaf', 0, 3 ],
['DFRRemovedCollateral', 0, 2 ],
];
$this->AssertSummaryEquals($aExpected, $aRes);
}
public function testExecuteDeletionPlan_WrongOrderDeletion()
{
$this->GivenDFRTreeInDB(<<<EOF
DFRToRemoveLeaf_1 <- DFRRemovedCollateral_1
DFRRemovedCollateral_1 <- DFRRemovedCollateralCascade_1
DFRToRemoveLeaf_2 <- DFRRemovedCollateral_2
DFRRemovedCollateral_2 <- DFRRemovedCollateralCascade_2
DFRToRemoveLeaf_3 <- DFRRemovedCollateral_3
DFRRemovedCollateral_3 <- DFRRemovedCollateralCascade_3
EOF);
$aClasses = [ 'DFRToRemoveLeaf' ];
$oSet = new \DBObjectSet(\DBObjectSearch::FromOQL("SELECT DFRRemovedCollateral WHERE name='DFRRemovedCollateral_3'"));
$oExpectedObj = $oSet->Fetch();
self::assertNotNull($oExpectedObj);
$aRes = DeletionPlanService::GetInstance()->ExecuteDeletionPlan($aClasses, 0, 5);
$aExpected = [
['DFRToRemoveLeaf', 0, 3 ],
['DFRRemovedCollateral', 0, 2 ],
];
$this->AssertSummaryEquals($aExpected, $aRes);
$oSet = new \DBObjectSet(\DBObjectSearch::FromOQL("SELECT DFRRemovedCollateral WHERE name='DFRRemovedCollateral_3'"));
$oActualObj = $oSet->Fetch();
self::assertNotNull($oActualObj, "Deletion plan executed in wrong order: DFRRemovedCollateralCascade/DFRRemovedCollateral are not valid anymore");
self::assertEquals($oExpectedObj->GetKey(), $oActualObj->GetKey());
}
public function GetDatamodelDeltaAbsPath(): string
{
return __DIR__.'/deletionplan_delta.xml';
}
private function GivenDFRTreeInDB(string $sTree)
{
$aTree = explode("\n", $sTree);
foreach ($aTree as $sLine) {
if (trim($sLine) === "") {
continue;
}
$this->GivenDFRTreeLineInDB($sLine);
}
}
private array $aIdByObjectName = [];
private function GivenDFRTreeLineInDB(string $sLine)
{
list($sLeft, $sRight) = explode('<-', $sLine);
$sLeft = trim($sLeft);
$iLeftId = $this->aIdByObjectName[$sLeft] ?? 0;
if ($iLeftId === 0) {
list($sChildClass, ) = explode('_', $sLeft, 2);
$iLeftId = $this->GivenObjectInDB($sChildClass, ['name' => $sLeft]);
$this->aIdByObjectName[$sLeft] = $iLeftId;
}
$sRight = trim($sRight);
list($sChildClass, ) = explode('_', $sRight, 2);
$iRightId = $this->GivenObjectInDB($sChildClass, ['name' => $sRight, 'extkey_id' => $iLeftId]);
$this->aIdByObjectName[$sRight] = $iRightId;
}
}

View File

@@ -0,0 +1,346 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.2">
<classes>
<class id="DFRToRemove" _created_in="itop-structure" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>true</abstract>
<db_table>dfrtoremove</db_table>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes/>
</reconciliation>
</properties>
<fields>
<field id="name" xsi:type="AttributeString">
<sql>name</sql>
<default_value/>
<is_null_allowed>true</is_null_allowed>
<validation_pattern/>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
</fields>
<methods/>
<presentation>
<list>
<items/>
</list>
<search>
<items/>
</search>
<details>
<items>
<item id="name">
<rank>10</rank>
</item>
</items>
</details>
</presentation>
<parent>cmdbAbstractObject</parent>
</class>
<class id="DFRToUpdate" _created_in="itop-structure" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>dfrtoupdate</db_table>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes/>
</reconciliation>
</properties>
<fields>
<field id="name" xsi:type="AttributeString">
<sql>name</sql>
<default_value/>
<is_null_allowed>true</is_null_allowed>
<validation_pattern/>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
<field id="extkey_id" xsi:type="AttributeExternalKey">
<sql>extkey_id</sql>
<filter/>
<dependencies/>
<is_null_allowed>true</is_null_allowed>
<target_class>DFRToRemove</target_class>
<on_target_delete>DEL_AUTO</on_target_delete>
<tracking_level>all</tracking_level>
</field>
</fields>
<methods/>
<presentation>
<list>
<items/>
</list>
<search>
<items/>
</search>
<details>
<items>
<item id="name">
<rank>10</rank>
</item>
<item id="extkey_id">
<rank>20</rank>
</item>
</items>
</details>
</presentation>
<parent>cmdbAbstractObject</parent>
</class>
<class id="DFRRemovedCollateral" _created_in="itop-structure" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>dfrremovedcollateral</db_table>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes/>
</reconciliation>
</properties>
<fields>
<field id="name" xsi:type="AttributeString">
<sql>name</sql>
<default_value/>
<is_null_allowed>true</is_null_allowed>
<validation_pattern/>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
<field id="extkey_id" xsi:type="AttributeExternalKey">
<sql>extkey_id</sql>
<filter/>
<dependencies/>
<is_null_allowed>false</is_null_allowed>
<target_class>DFRToRemove</target_class>
<on_target_delete>DEL_AUTO</on_target_delete>
<tracking_level>all</tracking_level>
</field>
</fields>
<methods/>
<presentation>
<list>
<items/>
</list>
<search>
<items/>
</search>
<details>
<items>
<item id="name">
<rank>10</rank>
</item>
<item id="extkey_id">
<rank>20</rank>
</item>
</items>
</details>
</presentation>
<parent>cmdbAbstractObject</parent>
</class>
<class id="DFRToRemoveLeaf" _created_in="itop-structure" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>dfrtoremoveleaf</db_table>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes/>
</reconciliation>
</properties>
<fields>
<field id="desc" xsi:type="AttributeString">
<sql>desc</sql>
<default_value/>
<is_null_allowed>true</is_null_allowed>
<validation_pattern/>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
</fields>
<methods/>
<presentation>
<list>
<items/>
</list>
<search>
<items/>
</search>
<details>
<items>
<item id="name">
<rank>10</rank>
</item>
<item id="desc">
<rank>20</rank>
</item>
</items>
</details>
</presentation>
<parent>DFRToRemove</parent>
</class>
<class id="DFRRemovedCollateralCascade" _created_in="itop-structure" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>dfrremovedcollateralcascade</db_table>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes/>
</reconciliation>
</properties>
<fields>
<field id="name" xsi:type="AttributeString">
<sql>name</sql>
<default_value/>
<is_null_allowed>true</is_null_allowed>
<validation_pattern/>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
<field id="extkey_id" xsi:type="AttributeExternalKey">
<sql>extkey_id</sql>
<filter/>
<dependencies/>
<is_null_allowed>false</is_null_allowed>
<target_class>DFRRemovedCollateral</target_class>
<on_target_delete>DEL_AUTO</on_target_delete>
<tracking_level>all</tracking_level>
</field>
</fields>
<methods/>
<presentation>
<list>
<items/>
</list>
<search>
<items/>
</search>
<details>
<items>
<item id="name">
<rank>10</rank>
</item>
<item id="extkey_id">
<rank>20</rank>
</item>
</items>
</details>
</presentation>
<parent>cmdbAbstractObject</parent>
</class>
<class id="DFRManual" _created_in="itop-structure" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>dfrmanual</db_table>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes/>
</reconciliation>
</properties>
<fields>
<field id="name" xsi:type="AttributeString">
<sql>name</sql>
<default_value/>
<is_null_allowed>true</is_null_allowed>
<validation_pattern/>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
<field id="extkey_id" xsi:type="AttributeExternalKey">
<sql>extkey_id</sql>
<filter/>
<dependencies/>
<is_null_allowed>false</is_null_allowed>
<target_class>DFRToRemove</target_class>
<on_target_delete>DEL_MANUAL</on_target_delete>
<tracking_level>all</tracking_level>
</field>
</fields>
<methods/>
<presentation>
<list>
<items/>
</list>
<search>
<items/>
</search>
<details>
<items>
<item id="name">
<rank>10</rank>
</item>
<item id="extkey_id">
<rank>20</rank>
</item>
</items>
</details>
</presentation>
<parent>cmdbAbstractObject</parent>
</class>
</classes>
<dictionaries>
<dictionary id="EN US">
<entries>
<entry id="Class:DFRToRemove/Name" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRToRemove/ComplementaryName" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRToRemove" _delta="define"><![CDATA[DFRToRemove]]></entry>
<entry id="Class:DFRToRemove+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRToRemove/Attribute:name" _delta="define"><![CDATA[Name]]></entry>
<entry id="Class:DFRToRemove/Attribute:name+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRToUpdate/Name" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRToUpdate/ComplementaryName" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRToUpdate" _delta="define"><![CDATA[DFRToUpdate]]></entry>
<entry id="Class:DFRToUpdate+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRToUpdate/Attribute:name" _delta="define"><![CDATA[Name]]></entry>
<entry id="Class:DFRToUpdate/Attribute:name+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRToUpdate/Attribute:extkey_id" _delta="define"><![CDATA[Dfrtoremove id]]></entry>
<entry id="Class:DFRToUpdate/Attribute:extkey_id+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRRemovedCollateral/Name" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRRemovedCollateral/ComplementaryName" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRRemovedCollateral" _delta="define"><![CDATA[DFRRemovedCollateral]]></entry>
<entry id="Class:DFRRemovedCollateral+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRRemovedCollateral/Attribute:name" _delta="define"><![CDATA[Name]]></entry>
<entry id="Class:DFRRemovedCollateral/Attribute:name+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRRemovedCollateral/Attribute:extkey_id" _delta="define"><![CDATA[Dfrtoremove id]]></entry>
<entry id="Class:DFRRemovedCollateral/Attribute:extkey_id+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRToRemoveLeaf/Name" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRToRemoveLeaf/ComplementaryName" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRToRemoveLeaf" _delta="define"><![CDATA[DFRToRemoveLeaf]]></entry>
<entry id="Class:DFRToRemoveLeaf+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRToRemoveLeaf/Attribute:desc" _delta="define"><![CDATA[Desc]]></entry>
<entry id="Class:DFRToRemoveLeaf/Attribute:desc+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRRemovedCollateralCascade/Name" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRRemovedCollateralCascade/ComplementaryName" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRRemovedCollateralCascade" _delta="define"><![CDATA[DFRRemovedCollateralCascade]]></entry>
<entry id="Class:DFRRemovedCollateralCascade+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRRemovedCollateralCascade/Attribute:name" _delta="define"><![CDATA[Name]]></entry>
<entry id="Class:DFRRemovedCollateralCascade/Attribute:name+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRRemovedCollateralCascade/Attribute:extkey_id" _delta="define"><![CDATA[Dfrremovedcollateral id]]></entry>
<entry id="Class:DFRRemovedCollateralCascade/Attribute:extkey_id+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRManual/Name" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRManual/ComplementaryName" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRManual" _delta="define"><![CDATA[DFRManual]]></entry>
<entry id="Class:DFRManual+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRManual/Attribute:name" _delta="define"><![CDATA[Name]]></entry>
<entry id="Class:DFRManual/Attribute:name+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRManual/Attribute:extkey_id" _delta="define"><![CDATA[Dfrtoremove id]]></entry>
<entry id="Class:DFRManual/Attribute:extkey_id+" _delta="define"><![CDATA[]]></entry>
</entries>
</dictionary>
</dictionaries>
</itop_design>