diff --git a/synchro/synchro_exec.php b/synchro/synchro_exec.php new file mode 100644 index 0000000000..64bc069feb --- /dev/null +++ b/synchro/synchro_exec.php @@ -0,0 +1,143 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @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= auth_pwd= 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(); +?> diff --git a/synchro/synchrodatasource.class.inc.php b/synchro/synchrodatasource.class.inc.php index f85b916d30..25c5cfc23f 100644 --- a/synchro/synchrodatasource.class.inc.php +++ b/synchro/synchrodatasource.class.inc.php @@ -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 "

Update object ".$oDestObj->GetName()."

"; + foreach($aAttributes as $sAttCode) + { + $value = $this->GetValueFromExtData($sAttCode); + $oDestObj->Set($sAttCode, $value); + echo "

   Setting $sAttCode to $value

"; + } + 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 "

Creating new $sClass

"; + $oDestObj = MetaModel::NewObject($sClass); + foreach($aAttributes as $sAttCode) + { + $value = $this->GetValueFromExtData($sAttCode); + $oDestObj->Set($sAttCode, $value); + echo "

   Setting $sAttCode to $value

"; + } + 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]; + } } ?>