mirror of
https://github.com/Combodo/iTop.git
synced 2026-04-19 16:48:42 +02:00
DataExchange: implementation in progress.
SVN:trunk[1062]
This commit is contained in:
143
synchro/synchro_exec.php
Normal file
143
synchro/synchro_exec.php
Normal file
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
// Copyright (C) 2010 Combodo SARL
|
||||
//
|
||||
// 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, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
/**
|
||||
* Import web service
|
||||
*
|
||||
* @author Erwan Taloc <erwan.taloc@combodo.com>
|
||||
* @author Romain Quetiez <romain.quetiez@combodo.com>
|
||||
* @author Denis Flaven <denis.flaven@combodo.com>
|
||||
* @license http://www.opensource.org/licenses/gpl-3.0.html LGPL
|
||||
*/
|
||||
|
||||
//
|
||||
// Known limitations
|
||||
// - reconciliation is made on the first column
|
||||
//
|
||||
// Known issues
|
||||
// - ALMOST impossible to troubleshoot when an externl key has a wrong value
|
||||
// - no character escaping in the xml output (yes !?!?!)
|
||||
// - not outputing xml when a wrong input is given (class, attribute names)
|
||||
//
|
||||
|
||||
require_once('../approot.inc.php');
|
||||
require_once(APPROOT.'/application/application.inc.php');
|
||||
require_once(APPROOT.'/application/webpage.class.inc.php');
|
||||
require_once(APPROOT.'/application/csvpage.class.inc.php');
|
||||
require_once(APPROOT.'/application/clipage.class.inc.php');
|
||||
|
||||
require_once(APPROOT.'/application/startup.inc.php');
|
||||
|
||||
|
||||
function UsageAndExit($oP)
|
||||
{
|
||||
global $aPageParams;
|
||||
$bModeCLI = utils::IsModeCLI();
|
||||
|
||||
if ($bModeCLI)
|
||||
{
|
||||
$oP->p("USAGE:\n");
|
||||
$oP->p("php -q synchro_exec.php auth_user=<login> auth_pwd=<password> data_sources=<comma_separated_list_of_data_sources>\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
$oP->p("The parameter 'data_sources' is mandatory, and must contain a comma separated list of data sources\n");
|
||||
}
|
||||
$oP->output();
|
||||
exit;
|
||||
}
|
||||
|
||||
function ReadMandatoryParam($oP, $sParam)
|
||||
{
|
||||
global $aPageParams;
|
||||
assert(isset($aPageParams[$sParam]));
|
||||
assert($aPageParams[$sParam]['mandatory']);
|
||||
|
||||
$sValue = utils::ReadParam($sParam, null, true /* Allow CLI */);
|
||||
if (is_null($sValue))
|
||||
{
|
||||
$oP->p("ERROR: Missing argument '$sParam'\n");
|
||||
UsageAndExit($oP);
|
||||
}
|
||||
return trim($sValue);
|
||||
}
|
||||
|
||||
/////////////////////////////////
|
||||
// Main program
|
||||
|
||||
if (utils::IsModeCLI())
|
||||
{
|
||||
$oP = new CLIPage(Dict::S("TitleSynchroExecution"));
|
||||
|
||||
// Next steps:
|
||||
// specific arguments: 'csvfile'
|
||||
//
|
||||
$sAuthUser = ReadMandatoryParam($oP, 'auth_user');
|
||||
$sAuthPwd = ReadMandatoryParam($oP, 'auth_pwd');
|
||||
$sCsvFile = ReadMandatoryParam($oP, 'data_sources');
|
||||
if (UserRights::CheckCredentials($sAuthUser, $sAuthPwd))
|
||||
{
|
||||
UserRights::Login($sAuthUser); // Login & set the user's language
|
||||
}
|
||||
else
|
||||
{
|
||||
$oP->p("Access restricted or wrong credentials ('$sAuthUser')");
|
||||
exit;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
require_once(APPROOT.'/application/loginwebpage.class.inc.php');
|
||||
LoginWebPage::DoLogin(); // Check user rights and prompt if needed
|
||||
|
||||
$oP = new WebPage(Dict::S("TitleSynchroExecution"));
|
||||
$sDataSourcesList = utils::ReadParam('data_sources', null);
|
||||
if ($sDataSourcesList == null)
|
||||
{
|
||||
UsageAndExit($oP);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
//////////////////////////////////////////////////
|
||||
//
|
||||
// Security
|
||||
//
|
||||
if (!UserRights::IsAdministrator())
|
||||
{
|
||||
throw new SecurityException(Dict::Format('UI:Error:ActionNotAllowed', $sClass));
|
||||
}
|
||||
|
||||
foreach(explode(',', $sDataSourcesList) as $iSDS)
|
||||
{
|
||||
$oSynchroDataSource = MetaModel::GetObject('SynchroDataSource', $iSDS, true);
|
||||
$aResults = array();
|
||||
$oSynchroDataSource->Synchronize($aResults);
|
||||
}
|
||||
}
|
||||
catch(SecurityException $e)
|
||||
{
|
||||
$oP->add($e->getMessage());
|
||||
}
|
||||
catch(Exception $e)
|
||||
{
|
||||
$oP->add((string)$e);
|
||||
}
|
||||
|
||||
$oP->output();
|
||||
?>
|
||||
@@ -31,7 +31,7 @@ class SynchroDataSource extends cmdbAbstractObject
|
||||
(
|
||||
"category" => "core/cmdb,view_in_gui",
|
||||
"key_type" => "autoincrement",
|
||||
"name_attcode" => "",
|
||||
"name_attcode" => array('name'),
|
||||
"state_attcode" => "",
|
||||
"reconc_keys" => array(),
|
||||
"db_table" => "priv_sync_datasource",
|
||||
@@ -60,9 +60,9 @@ class SynchroDataSource extends cmdbAbstractObject
|
||||
|
||||
// Display lists
|
||||
MetaModel::Init_SetZListItems('details', array('name', 'description', 'status', 'user_id', 'scope', 'last_synchro_date', 'reconciliation_list', 'action_on_zero', 'action_on_one', 'action_on_multiple', 'delete_policy', 'delete_policy_update', 'delete_policy_retention')); // Attributes to be displayed for the complete details
|
||||
MetaModel::Init_SetZListItems('list', array('name', 'status', 'scope')); // Attributes to be displayed for a list
|
||||
MetaModel::Init_SetZListItems('list', array('name', 'status', 'scope', 'user_id')); // Attributes to be displayed for a list
|
||||
// Search criteria
|
||||
// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form
|
||||
MetaModel::Init_SetZListItems('standard_search', array('name', 'status', 'scope', 'user_id')); // Criteria of the std search form
|
||||
// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form
|
||||
}
|
||||
|
||||
@@ -85,8 +85,8 @@ class SynchroDataSource extends cmdbAbstractObject
|
||||
public function GetDataTable()
|
||||
{
|
||||
$sName = trim(strtolower($this->Get('name')));
|
||||
$sName = str_replace(' ', '_', $sName);
|
||||
$sTable = "synchro_data_$sName";
|
||||
$sName = str_replace('\'"&@|\\/ ', '_', $sName); // Remove forbidden characters from the table name
|
||||
$sTable = MetaModel::GetConfig()->GetDBSubName()."synchro_data_$sName"; // Add the prefix if any
|
||||
return $sTable;
|
||||
}
|
||||
|
||||
@@ -96,16 +96,15 @@ class SynchroDataSource extends cmdbAbstractObject
|
||||
|
||||
$sTable = $this->GetDataTable();
|
||||
|
||||
$sClass = $this->GetTargetClass();
|
||||
$sClass = $this->GetTargetClass();
|
||||
$aAttCodes = array();
|
||||
foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
|
||||
{
|
||||
if ($sAttCode == 'finalclass') continue;
|
||||
|
||||
foreach($oAttDef->GetSQLColumns() as $sField => $sDBFieldType)
|
||||
{
|
||||
$aColumns[$sField] = $sDBFieldType;
|
||||
}
|
||||
$aAttCodes[] = $sAttCode;
|
||||
}
|
||||
$aColumns = $this->GetSQLColumns($aAttCodes);
|
||||
|
||||
$aFieldDefs = array();
|
||||
// Allow '0', otherwise mysql will render an error when the id is not given
|
||||
@@ -149,6 +148,102 @@ class SynchroDataSource extends cmdbAbstractObject
|
||||
$sTriggerUpdate .= " END;";
|
||||
CMDBSource::Query($sTriggerUpdate);
|
||||
}
|
||||
|
||||
public function Synchronize(&$aDataToReplica)
|
||||
{
|
||||
// Get all the replicas that were not seen in the last import
|
||||
// TO DO: mark them as obsolete... depending on the delete policy
|
||||
|
||||
// Get all the replicas that are 'new' or modified
|
||||
// Get the list of SQL columns: TO DO: retrieve this list from the SynchroAttributes
|
||||
$sClass = $this->GetTargetClass();
|
||||
$aAttCodes = array();
|
||||
foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
|
||||
{
|
||||
if ($sAttCode == 'finalclass') continue;
|
||||
|
||||
$aAttCodes[] = $sAttCode;
|
||||
}
|
||||
$aColumns = $this->GetSQLColumns($aAttCodes);
|
||||
$aExtDataSpec = array(
|
||||
'table' => $this->GetDataTable(),
|
||||
'join_key' => 'id',
|
||||
'fields' => array_keys($aColumns));
|
||||
|
||||
$sOQL = "SELECT SynchroReplica WHERE (status = 'new' OR status = 'modified') AND sync_source_id = :source_id";
|
||||
$oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array() /* order by*/, array('source_id' => $this->GetKey()) /* aArgs */, $aExtDataSpec, 0 /* limitCount */, 0 /* limitStart */);
|
||||
|
||||
// Get the list of reconciliation keys, make sure they are valid
|
||||
$aReconciliationKeys = array();
|
||||
foreach( explode(',', $this->Get('reconciliation_list')) as $sKey)
|
||||
{
|
||||
$sFilterCode = trim($sKey);
|
||||
if (MetaModel::IsValidFilterCode($this->GetTargetClass(), $sFilterCode))
|
||||
{
|
||||
$aReconciliationKeys[] = $sFilterCode;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw(new Exception('Invalid reconciliation criteria: '.$sFilterCode));
|
||||
}
|
||||
}
|
||||
|
||||
// TO DO: Get the "real" list of enabled attributes ! Not all of them !
|
||||
// for now get all scalar & writable attributes
|
||||
$aAttributes = array();
|
||||
foreach($aAttCodes as $sAttCode)
|
||||
{
|
||||
$oAttDef = MetaModel::GetAttributeDef($this->GetTargetClass(), $sAttCode);
|
||||
if ($oAttDef->IsWritable() && $oAttDef->IsScalar())
|
||||
{
|
||||
$aAttributes[] = $sAttCode;
|
||||
}
|
||||
}
|
||||
// Create a change used for logging all the modifications/creations happening during the synchro
|
||||
$oMyChange = MetaModel::NewObject("CMDBChange");
|
||||
$oMyChange->Set("date", time());
|
||||
$sUserString = CMDBChange::GetCurrentUserName();
|
||||
$oMyChange->Set("userinfo", $sUserString);
|
||||
$iChangeId = $oMyChange->DBInsert();
|
||||
|
||||
while($oReplica = $oSet->Fetch())
|
||||
{
|
||||
$oReplica->Synchro($this, $aReconciliationKeys, $aAttributes, $oMyChange);
|
||||
}
|
||||
|
||||
// Get all the replicas that are obsolete / to be deleted
|
||||
// TO DO: update or delete them based on the delete_policy and retention period defined in the data source
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of SQL columns corresponding to a particular list of attribute codes
|
||||
*/
|
||||
protected function GetSQLColumns($aAttributeCodes)
|
||||
{
|
||||
$aColumns = array();
|
||||
$sClass = $this->GetTargetClass();
|
||||
foreach($aAttributeCodes as $sAttCode)
|
||||
{
|
||||
$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
|
||||
|
||||
foreach($oAttDef->GetSQLColumns() as $sField => $sDBFieldType)
|
||||
{
|
||||
$aColumns[$sField] = $sDBFieldType;
|
||||
}
|
||||
}
|
||||
return $aColumns;
|
||||
}
|
||||
|
||||
public function IsRunning()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function GetLatestLog()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class SynchroAttribute extends cmdbAbstractObject
|
||||
@@ -287,6 +382,8 @@ class SynchroLog extends cmdbAbstractObject
|
||||
|
||||
class SynchroReplica extends cmdbAbstractObject
|
||||
{
|
||||
static $aSearches = array(); // Cache of OQL queries used for reconciliation (per data source)
|
||||
|
||||
public static function Init()
|
||||
{
|
||||
$aParams = array
|
||||
@@ -320,9 +417,132 @@ class SynchroReplica extends cmdbAbstractObject
|
||||
MetaModel::Init_SetZListItems('details', array('sync_source_id', 'dest_id', 'status_last_seen', 'status', 'status_dest_creator', 'status_last_error', 'info_creation_date', 'info_last_modified', 'info_last_synchro')); // Attributes to be displayed for the complete details
|
||||
MetaModel::Init_SetZListItems('list', array('sync_source_id', 'dest_id', 'status_last_seen', 'status', 'status_dest_creator', 'status_last_error')); // Attributes to be displayed for a list
|
||||
// Search criteria
|
||||
// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form
|
||||
MetaModel::Init_SetZListItems('standard_search', array('sync_source_id', 'status_last_seen', 'status', 'status_dest_creator', 'dest_id', 'status_last_error')); // Criteria of the std search form
|
||||
// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form
|
||||
}
|
||||
|
||||
public function Synchro($oDataSource, $aReconciliationKeys, $aAttributes, $oChange)
|
||||
{
|
||||
switch($this->Get('status'))
|
||||
{
|
||||
case 'new':
|
||||
// If needed, construct the query used for the reconciliation
|
||||
if (!isset(self::$aSearches[$oDataSource->GetKey()]))
|
||||
{
|
||||
foreach($aReconciliationKeys as $sFilterCode)
|
||||
{
|
||||
$aCriterias[] = ($sFilterCode == 'primary_key' ? 'id' : $sFilterCode).' = :'.$sFilterCode;
|
||||
}
|
||||
$sOQL = "SELECT ".$oDataSource->GetTargetClass()." WHERE ".implode(' AND ', $aCriterias);
|
||||
self::$aSearches[$oDataSource->GetKey()] = DBObjectSearch::FromOQL($sOQL);
|
||||
}
|
||||
// Get the criterias for the search
|
||||
$aFilterValues = array();
|
||||
foreach($aReconciliationKeys as $sFilterCode)
|
||||
{
|
||||
$aFilterValues[$sFilterCode] = $this->GetValueFromExtData($sFilterCode);
|
||||
}
|
||||
$oDestSet = new DBObjectSet(self::$aSearches[$oDataSource->GetKey()], array(), $aFilterValues);
|
||||
$iCount = $oDestSet->Count();
|
||||
// How many objects match the reconciliation criterias
|
||||
switch($iCount)
|
||||
{
|
||||
case 0:
|
||||
$this->CreateObjectFromReplica($oDataSource->GetTargetClass(), $aAttributes, $oChange);
|
||||
break;
|
||||
|
||||
case 1:
|
||||
$oDestObj = $oDestSet->Fetch();
|
||||
$this->UpdateObjectFromReplica($oDestObj, $aAttributes, $oChange);
|
||||
break;
|
||||
|
||||
default:
|
||||
$aConditions = array();
|
||||
foreach($aFilterValues as $sCode => $sValue)
|
||||
{
|
||||
$aConditions[] = $sCode.'='.$sValue;
|
||||
}
|
||||
$sCondition = implode(' AND ', $aConditions);
|
||||
$this->Set('status_last_error', $iCount.' destination objects match the reconciliation criterias: '.$sCondition);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'modified':
|
||||
$oDestObj = MetaModel::GetObject($oDataSource->GetTargetClass(), $this->Get('dest_id'));
|
||||
if ($oDestObj == null)
|
||||
{
|
||||
$this->Set('status', 'orphan'); // The destination object has been deleted !
|
||||
$this->Set('status_last_error', 'Destination object deleted unexpectedly');
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->UpdateObjectFromReplica($oDestObj, $aAttributes, $oChange);
|
||||
}
|
||||
break;
|
||||
|
||||
default: // Do nothing in all other cases
|
||||
}
|
||||
$this->DBUpdateTracked($oChange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the destination object with the Extended data found in the synchro_data_XXXX table
|
||||
*/
|
||||
protected function UpdateObjectFromReplica($oDestObj, $aAttributes, $oChange)
|
||||
{
|
||||
echo "<p>Update object ".$oDestObj->GetName()."</p>";
|
||||
foreach($aAttributes as $sAttCode)
|
||||
{
|
||||
$value = $this->GetValueFromExtData($sAttCode);
|
||||
$oDestObj->Set($sAttCode, $value);
|
||||
echo "<p> Setting $sAttCode to $value</p>";
|
||||
}
|
||||
try
|
||||
{
|
||||
$oDestObj->DBUpdateTracked($oChange);
|
||||
$this->Set('status_last_error', '');
|
||||
$this->Set('status', 'synchronized');
|
||||
}
|
||||
catch(Exception $e)
|
||||
{
|
||||
$this->Set('status_last_error', 'Unable to update destination object');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the destination object populating it with the Extended data found in the synchro_data_XXXX table
|
||||
*/
|
||||
protected function CreateObjectFromReplica($sClass, $aAttributes, $oChange)
|
||||
{
|
||||
echo "<p>Creating new $sClass</p>";
|
||||
$oDestObj = MetaModel::NewObject($sClass);
|
||||
foreach($aAttributes as $sAttCode)
|
||||
{
|
||||
$value = $this->GetValueFromExtData($sAttCode);
|
||||
$oDestObj->Set($sAttCode, $value);
|
||||
echo "<p> Setting $sAttCode to $value</p>";
|
||||
}
|
||||
try
|
||||
{
|
||||
$oDestObj->DBInsertTracked($oChange);
|
||||
$this->Set('dest_id', $oDestObj->GetKey());
|
||||
$this->Set('status_last_error', '');
|
||||
$this->Set('status', 'synchronized');
|
||||
}
|
||||
catch(Exception $e)
|
||||
{
|
||||
$this->Set('status_last_error', 'Unable to update destination object');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value from the 'Extended Data' located in the synchro_data_xxx table for this replica
|
||||
*/
|
||||
protected function GetValueFromExtData($sColumnName)
|
||||
{
|
||||
$aData = $this->GetExtendedData();
|
||||
return $aData[$sColumnName];
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
Reference in New Issue
Block a user