浏览代码

DataExchange: implementation in progress.

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@1062 a333f486-631f-4898-b8df-5754b55c2be0
dflaven 14 年之前
父节点
当前提交
70056ed6f7
共有 2 个文件被更改,包括 374 次插入11 次删除
  1. 143 0
      synchro/synchro_exec.php
  2. 231 11
      synchro/synchrodatasource.class.inc.php

+ 143 - 0
synchro/synchro_exec.php

@@ -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();
+?>

+ 231 - 11
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 "<p>Update object ".$oDestObj->GetName()."</p>";
+		foreach($aAttributes as $sAttCode)
+		{
+			$value = $this->GetValueFromExtData($sAttCode);
+			$oDestObj->Set($sAttCode, $value);
+			echo "<p>&nbsp;&nbsp;&nbsp;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>&nbsp;&nbsp;&nbsp;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];
+	 }
 }
 
 ?>