synchrodatasource.class.inc.php 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110
  1. <?php
  2. // Copyright (C) 2010 Combodo SARL
  3. //
  4. // This program is free software; you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation; version 3 of the License.
  7. //
  8. // This program is distributed in the hope that it will be useful,
  9. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. // GNU General Public License for more details.
  12. //
  13. // You should have received a copy of the GNU General Public License
  14. // along with this program; if not, write to the Free Software
  15. // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  16. /**
  17. * Data Exchange - synchronization with external applications (incoming data)
  18. *
  19. * @author Erwan Taloc <erwan.taloc@combodo.com>
  20. * @author Romain Quetiez <romain.quetiez@combodo.com>
  21. * @author Denis Flaven <denis.flaven@combodo.com>
  22. * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL
  23. */
  24. class SynchroDataSource extends cmdbAbstractObject
  25. {
  26. public static function Init()
  27. {
  28. $aParams = array
  29. (
  30. "category" => "core/cmdb,view_in_gui",
  31. "key_type" => "autoincrement",
  32. "name_attcode" => array('name'),
  33. "state_attcode" => "",
  34. "reconc_keys" => array(),
  35. "db_table" => "priv_sync_datasource",
  36. "db_key_field" => "id",
  37. "db_finalclass_field" => "realclass",
  38. "display_template" => "",
  39. "icon" => "../images/synchro.png",
  40. );
  41. MetaModel::Init_Params($aParams);
  42. //MetaModel::Init_InheritAttributes();
  43. MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
  44. MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
  45. MetaModel::Init_AddAttribute(new AttributeEnum("status", array("allowed_values"=>new ValueSetEnum('implementation,production,obsolete'), "sql"=>"status", "default_value"=>"implementation", "is_null_allowed"=>false, "depends_on"=>array())));
  46. MetaModel::Init_AddAttribute(new AttributeExternalKey("user_id", array("targetclass"=>"User", "jointype"=>null, "allowed_values"=>null, "sql"=>"user_id", "is_null_allowed"=>true, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array())));
  47. MetaModel::Init_AddAttribute(new AttributeClass("scope_class", array("class_category"=>"bizmodel", "more_values"=>"", "sql"=>"scope_class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
  48. MetaModel::Init_AddAttribute(new AttributeString("scope_restriction", array("allowed_values"=>null, "sql"=>"scope_restriction", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
  49. //MetaModel::Init_AddAttribute(new AttributeDateTime("last_synchro_date", array("allowed_values"=>null, "sql"=>"last_synchro_date", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array())));
  50. // Format: '1 hour', '2 weeks', '3 hoursABCDEF'... Cf DateTime->Modify()
  51. MetaModel::Init_AddAttribute(new AttributeString("full_load_periodicity", array("allowed_values"=>null, "sql"=>"full_load_periodicity", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array())));
  52. // MetaModel::Init_AddAttribute(new AttributeString("reconciliation_list", array("allowed_values"=>null, "sql"=>"reconciliation_list", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
  53. MetaModel::Init_AddAttribute(new AttributeEnum("reconciliation_policy", array("allowed_values"=>new ValueSetEnum('use_primary_key,use_attributes'), "sql"=>"reconciliation_policy", "default_value"=>"use_attributes", "is_null_allowed"=>false, "depends_on"=>array())));
  54. MetaModel::Init_AddAttribute(new AttributeEnum("action_on_zero", array("allowed_values"=>new ValueSetEnum('create,error'), "sql"=>"action_on_zero", "default_value"=>"create", "is_null_allowed"=>false, "depends_on"=>array())));
  55. MetaModel::Init_AddAttribute(new AttributeEnum("action_on_one", array("allowed_values"=>new ValueSetEnum('update,error,delete'), "sql"=>"action_on_one", "default_value"=>"update", "is_null_allowed"=>false, "depends_on"=>array())));
  56. MetaModel::Init_AddAttribute(new AttributeEnum("action_on_multiple", array("allowed_values"=>new ValueSetEnum('take_first,create,error'), "sql"=>"action_on_multiple", "default_value"=>"error", "is_null_allowed"=>false, "depends_on"=>array())));
  57. MetaModel::Init_AddAttribute(new AttributeEnum("delete_policy", array("allowed_values"=>new ValueSetEnum('ignore,delete,update,update_then_delete'), "sql"=>"delete_policy", "default_value"=>"ignore", "is_null_allowed"=>false, "depends_on"=>array())));
  58. MetaModel::Init_AddAttribute(new AttributeString("delete_policy_update", array("allowed_values"=>null, "sql"=>"delete_policy_update", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
  59. // Format: '1 hour', '2 weeks', '3 hoursABCDEF'... Cf DateTime->Modify()
  60. MetaModel::Init_AddAttribute(new AttributeString("delete_policy_retention", array("allowed_values"=>null, "sql"=>"delete_policy_retention", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
  61. MetaModel::Init_AddAttribute(new AttributeLinkedSet("attribute_list", array("linked_class"=>"SynchroAttribute", "ext_key_to_me"=>"sync_source_id", "allowed_values"=>null, "count_min"=>0, "count_max"=>0, "depends_on"=>array())));
  62. MetaModel::Init_AddAttribute(new AttributeLinkedSet("status_list", array("linked_class"=>"SynchroLog", "ext_key_to_me"=>"sync_source_id", "allowed_values"=>null, "count_min"=>0, "count_max"=>0, "depends_on"=>array())));
  63. // Display lists
  64. MetaModel::Init_SetZListItems('details', array('name', 'description', 'scope_class', 'scope_restriction', 'status', 'user_id', 'full_load_periodicity', 'reconciliation_policy', 'action_on_zero', 'action_on_one', 'action_on_multiple', 'delete_policy', 'delete_policy_update', 'delete_policy_retention' /*'attribute_list'*/, 'status_list')); // Attributes to be displayed for the complete details
  65. MetaModel::Init_SetZListItems('list', array('scope_class', 'status', 'user_id', 'full_load_periodicity')); // Attributes to be displayed for a list
  66. // Search criteria
  67. MetaModel::Init_SetZListItems('standard_search', array('name', 'status', 'scope_class', 'user_id')); // Criteria of the std search form
  68. // MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form
  69. }
  70. public function DisplayBareRelations(WebPage $oPage, $bEditMode = false)
  71. {
  72. if (!$this->IsNew())
  73. {
  74. $oPage->SetCurrentTab(Dict::S('Core:SynchroAttributes'));
  75. $oAttributeSet = $this->Get('attribute_list');
  76. $aAttributes = array();
  77. while($oAttribute = $oAttributeSet->Fetch())
  78. {
  79. $aAttributes[$oAttribute->Get('attcode')] = $oAttribute;
  80. }
  81. $aAttribs = array(
  82. 'attcode' => array('label'=>'Attribute', 'description' => 'Field of the object'),
  83. 'reconciliation' => array('label'=>'Reconciliation ?', 'description' => 'Used for searching'),
  84. 'update' => array('label'=>'Update ?', 'description' => 'Used to update the object'),
  85. 'update_policy' => array('label'=>'Update Policy', 'description' => 'Behavior of the updated field'),
  86. );
  87. $aValues = array();
  88. foreach(MetaModel::ListAttributeDefs($this->GetTargetClass()) as $sAttCode=>$oAttDef)
  89. {
  90. if ($oAttDef->IsScalar() && $oAttDef->IsWritable())
  91. {
  92. if (isset($aAttributes[$sAttCode]))
  93. {
  94. $oAttribute = $aAttributes[$sAttCode];
  95. }
  96. else
  97. {
  98. $oAttribute = new SynchroAttribute();
  99. $oAttribute->Set('sync_source_id', $this->GetKey());
  100. $oAttribute->Set('attcode', $sAttCode);
  101. $oAttribute->Set('reconcile', MetaModel::IsReconcKey($this->GetTargetClass(), $sAttCode) ? 1 : 0);
  102. $oAttribute->Set('update', 1);
  103. $oAttribute->Set('update_policy', 'master_locked');
  104. }
  105. if (!$bEditMode)
  106. {
  107. // Read-only mode
  108. $aRow['reconciliation'] = $oAttribute->Get('reconcile') == 1 ? Dict::S('Core:SynchroReconcile:Yes') : Dict::S('Core:SynchroReconcile:No');
  109. $aRow['update'] = $oAttribute->Get('update') == 1 ? Dict::S('Core:SynchroUpdate:Yes') : Dict::S('Core:SynchroUpdate:No');
  110. $aRow['attcode'] = MetaModel::GetLabel($this->GetTargetClass(), $oAttribute->Get('attcode'));
  111. $aRow['update_policy'] = $oAttribute->GetAsHTML('update_policy');
  112. }
  113. else
  114. {
  115. // Read-only mode
  116. $sAttCode = $oAttribute->Get('attcode');
  117. $sChecked = $oAttribute->Get('reconcile') == 1 ? 'checked' : '';
  118. $aRow['reconciliation'] = "<input type=\"checkbox\" name=\"reconciliation[$sAttCode]\" $sChecked/>";
  119. $sChecked = $oAttribute->Get('update') == 1 ? 'checked' : '';
  120. $aRow['update'] = "<input type=\"checkbox\" name=\"update[$sAttCode]\" $sChecked/>";
  121. $aRow['attcode'] = MetaModel::GetLabel($this->GetTargetClass(), $oAttribute->Get('attcode'));
  122. $oAttDef = MetaModel::GetAttributeDef(get_class($oAttribute), 'update_policy');
  123. $aRow['update_policy'] = cmdbAbstractObject::GetFormElementForField($oPage, get_class($oAttribute), 'update_policy', $oAttDef, $oAttribute->Get('update_policy'), '', 'update_policy_'.$sAttCode, "[$sAttCode]");
  124. }
  125. $aValues[] = $aRow;
  126. }
  127. }
  128. $oPage->Table($aAttribs, $aValues);
  129. $oPage->SetCurrentTab(Dict::S('Core:SynchroStatus'));
  130. $sSelectSynchroLog = 'SELECT SynchroLog WHERE sync_source_id = :source_id';
  131. $oSetSynchroLog = new CMDBObjectSet(DBObjectSearch::FromOQL($sSelectSynchroLog), array('start_date' => false) /* order by*/, array('source_id' => $this->GetKey()));
  132. if ($oSetSynchroLog->Count() > 0)
  133. {
  134. $oLastLog = $oSetSynchroLog->Fetch();
  135. $sStartDate = $oLastLog->Get('start_date');
  136. $oLastLog->Get('stats_nb_seen');
  137. $iModified = $oLastLog->Get('stats_nb_modified');
  138. $iErrors = $oLastLog->Get('stats_nb_errors');
  139. $iCreated = $oLastLog->Get('stats_nb_created');
  140. $iDeleted = $oLastLog->Get('stats_nb_deleted');
  141. $oLastLog->Get('stats_nb_reconciled');
  142. if ($oLastLog->Get('status') == 'running')
  143. {
  144. // Still running !
  145. $oPage->p('<h2>'.Dict::Format('Core:Synchro:SynchroRunningStartedOn_Date', $sStartDate).'</h2>');
  146. }
  147. else
  148. {
  149. $sEndDate = $oLastLog->Get('end_date');
  150. $oPage->p('<h2>'.Dict::Format('Core:Synchro:SynchroEndedOn_Date', $sEndDate).'</h2>');
  151. }
  152. $iIgnored = 0;
  153. $iDisappeared = 0;
  154. $iDeleted = 0;
  155. $iObsoleted = 0;
  156. $iDisappearedErrors = 0;
  157. $iExisting = 0;
  158. $iUnchanged = 0;
  159. $iUpdated = 0;
  160. $iUpdatedErrors = 0;
  161. $iNew = 0;
  162. $iNewErrors = 0;
  163. $iReconciled = 0;
  164. $iCreated = 0;
  165. $oPage->add(
  166. <<<EOF
  167. <style>
  168. .synoptics, .synoptics tr td { background: transparent; padding:10px; font-size:1em; vertical-align:middle; color:#fff; text-align:center;}
  169. .synoptics tr td.arrow { color:#333; border-top: 1px dashed #333; width:100px; }
  170. </style>
  171. <table class="synoptics">
  172. <tr>
  173. <td style="background-color:#999;">Ignored ($iIgnored)</td><td colspan="2">&nbsp;</td>
  174. </tr>
  175. <tr>
  176. <td style="background-color:#630;" rowspan="3">Disappeared ($iDisappeared)</td><td rowspan="3" class="arrow">=&gt;</td><td style="background-color:#300;">Deleted ($iDeleted)</td>
  177. </tr>
  178. <tr>
  179. <td style="background-color:#630;">Obsoleted ($iObsoleted)</td>
  180. </tr>
  181. <tr>
  182. <td style="background-color:#C00;">Errors ($iDisappearedErrors)</td>
  183. </tr>
  184. <tr>
  185. <td style="background-color:#093;" rowspan="3">Existing ($iExisting)</td><td rowspan="3" class="arrow">=&gt;</td><td style="background-color:#393;">Unchanged ($iUnchanged)</td>
  186. </tr>
  187. <tr>
  188. <td style="background-color:#3C3;">Updated ($iUpdated)</td>
  189. </tr>
  190. <tr>
  191. <td style="background-color:#C00;">Errors ($iUpdatedErrors)</td>
  192. </tr>
  193. <tr>
  194. <td style="background-color:#039;"rowspan="3">New ($iNew)</td><td rowspan="3" class="arrow">=&gt;</td><td style="background-color:#C00;">Errors ($iNewErrors)</td>
  195. </tr>
  196. <tr>
  197. <td style="background-color:#33F;">Reconciled ($iReconciled)</td>
  198. </tr>
  199. <tr>
  200. <td style="background-color:#339;">Created ($iCreated)</td>
  201. </tr>
  202. </table>
  203. EOF
  204. );
  205. }
  206. else
  207. {
  208. $oPage->p('<h2>'.Dict::S('Core:Synchro:NeverRun').'</h2>');
  209. }
  210. }
  211. parent::DisplayBareRelations($oPage, $bEditMode);
  212. }
  213. public function GetAttributeFlags($sAttCode)
  214. {
  215. if (($sAttCode == 'scope_class') && (!$this->IsNew()))
  216. {
  217. return OPT_ATT_READONLY;
  218. }
  219. return parent::GetAttributeFlags($sAttCode);
  220. }
  221. public function UpdateObject($sFormPrefix = '')
  222. {
  223. parent::UpdateObject($sFormPrefix);
  224. // And now read the other post parameters...
  225. $oAttributeSet = $this->Get('attribute_list');
  226. $aAttributes = array();
  227. while($oAttribute = $oAttributeSet->Fetch())
  228. {
  229. $aAttributes[$oAttribute->Get('attcode')] = $oAttribute;
  230. }
  231. $aReconcile = utils::ReadPostedParam('reconciliation', array());
  232. $aUpdate = utils::ReadPostedParam('update', array());
  233. $aUpdatePolicy = utils::ReadPostedParam('attr_update_policy', array());
  234. // update_policy cannot be empty, so there is one entry per attribute, use this to iterate
  235. // through all the writable attributes
  236. foreach($aUpdatePolicy as $sAttCode => $sValue)
  237. {
  238. if(!isset($aAttributes[$sAttCode]))
  239. {
  240. $oAttribute = new SynchroAttribute();
  241. $oAttribute->Set('sync_source_id', $this->GetKey());
  242. $oAttribute->Set('attcode', $sAttCode);
  243. }
  244. else
  245. {
  246. $oAttribute = $aAttributes[$sAttCode];
  247. }
  248. $bReconcile = 0;
  249. if (isset($aReconcile[$sAttCode]))
  250. {
  251. $bReconcile = $aReconcile[$sAttCode] == 'on' ? 1 : 0;
  252. }
  253. $bUpdate = 0 ; // Default / initial value
  254. if (isset($aUpdate[$sAttCode]))
  255. {
  256. $bUpdate = $aUpdate[$sAttCode] == 'on' ? 1 : 0;
  257. }
  258. $oAttribute->Set('reconcile', $bReconcile);
  259. $oAttribute->Set('update', $bUpdate);
  260. $oAttribute->Set('update_policy', $sValue);
  261. $oAttributeSet->AddObject($oAttribute);
  262. }
  263. $this->Set('attribute_list', $oAttributeSet);
  264. }
  265. public function GetTargetClass()
  266. {
  267. return $this->Get('scope_class');
  268. }
  269. public function GetDataTable()
  270. {
  271. $sName = strtolower($this->GetTargetClass());
  272. $sName = str_replace('\'"&@|\\/ ', '_', $sName); // Remove forbidden characters from the table name
  273. $sName .= '_'.$this->GetKey(); // Add a suffix for unicity
  274. $sTable = MetaModel::GetConfig()->GetDBSubName()."synchro_data_$sName"; // Add the prefix if any
  275. return $sTable;
  276. }
  277. /**
  278. * When inserting a new datasource object, also create the SynchroAttribute objects
  279. * for each field of the target class
  280. */
  281. protected function OnInsert()
  282. {
  283. // Create all the SynchroAttribute records
  284. $oAttributeSet = $this->Get('attribute_list');
  285. foreach(MetaModel::ListAttributeDefs($this->GetTargetClass()) as $sAttCode=>$oAttDef)
  286. {
  287. if ($oAttDef->IsScalar() && $oAttDef->IsWritable())
  288. {
  289. $oAttribute = new SynchroAttribute();
  290. $oAttribute->Set('sync_source_id', $this->GetKey());
  291. $oAttribute->Set('attcode', $sAttCode);
  292. $oAttribute->Set('reconcile', MetaModel::IsReconcKey($this->GetTargetClass(), $sAttCode) ? 1 : 0);
  293. $oAttribute->Set('update', 1);
  294. $oAttribute->Set('update_policy', 'master_locked');
  295. $oAttributeSet->AddObject($oAttribute);
  296. }
  297. }
  298. $this->Set('attribute_list', $oAttributeSet);
  299. }
  300. /**
  301. * When the new datasource has been created, let's create the synchro_data table
  302. * that will hold the data records and the correspoding triggers which will maintain
  303. * both tables in sync
  304. */
  305. protected function AfterInsert()
  306. {
  307. parent::AfterInsert();
  308. $sTable = $this->GetDataTable();
  309. $aColumns = $this->GetSQLColumns();
  310. $aFieldDefs = array();
  311. // Allow '0', otherwise mysql will render an error when the id is not given
  312. // (the trigger is expected to set the value, but it is not executed soon enough)
  313. $aFieldDefs[] = "id INTEGER(11) NOT NULL DEFAULT 0 ";
  314. $aFieldDefs[] = "`primary_key` VARCHAR(255) NULL DEFAULT NULL";
  315. foreach($aColumns as $sColumn => $ColSpec)
  316. {
  317. $aFieldDefs[] = "`$sColumn` $ColSpec NULL DEFAULT NULL";
  318. }
  319. $aFieldDefs[] = "INDEX (id)";
  320. $aFieldDefs[] = "INDEX (primary_key)";
  321. $sFieldDefs = implode(', ', $aFieldDefs);
  322. $sCreateTable = "CREATE TABLE `$sTable` ($sFieldDefs) ENGINE = innodb;";
  323. CMDBSource::Query($sCreateTable);
  324. $sTriggerInsert = "CREATE TRIGGER `{$sTable}_bi` BEFORE INSERT ON $sTable";
  325. $sTriggerInsert .= " FOR EACH ROW";
  326. $sTriggerInsert .= " BEGIN";
  327. $sTriggerInsert .= " INSERT INTO priv_sync_replica (sync_source_id, status_last_seen, `status`) VALUES ({$this->GetKey()}, NOW(), 'new');";
  328. $sTriggerInsert .= " SET NEW.id = LAST_INSERT_ID();";
  329. $sTriggerInsert .= " END;";
  330. CMDBSource::Query($sTriggerInsert);
  331. $aModified = array();
  332. foreach($aColumns as $sColumn => $ColSpec)
  333. {
  334. // <=> is a null-safe 'EQUALS' operator (there is no equivalent for "DIFFERS FROM")
  335. $aModified[] = "NOT(NEW.`$sColumn` <=> OLD.`$sColumn`)";
  336. }
  337. $sIsModified = '('.implode(') OR (', $aModified).')';
  338. // Update the replica
  339. //
  340. // status is forced to "new" if the replica was obsoleted directly from the state "new" (dest_id = null)
  341. // otherwise, if status was either 'obsolete' or 'synchronized' it is turned into 'modified' or 'synchronized' depending on the changes
  342. // otherwise, the status is left as is
  343. $sTriggerUpdate = "CREATE TRIGGER `{$sTable}_bu` BEFORE UPDATE ON $sTable";
  344. $sTriggerUpdate .= " FOR EACH ROW";
  345. $sTriggerUpdate .= " BEGIN";
  346. $sTriggerUpdate .= " IF @itopuser is null THEN";
  347. $sTriggerUpdate .= " UPDATE priv_sync_replica SET status_last_seen = NOW(), `status` = IF(`status` = 'obsolete', IF(`dest_id` IS NULL, 'new', 'modified'), IF(`status` IN ('synchronized') AND ($sIsModified), 'modified', `status`)) WHERE sync_source_id = {$this->GetKey()} AND id = OLD.id;";
  348. $sTriggerUpdate .= " SET NEW.id = OLD.id;"; // make sure this id won't change
  349. $sTriggerUpdate .= " END IF;";
  350. $sTriggerUpdate .= " END;";
  351. CMDBSource::Query($sTriggerUpdate);
  352. $sTriggerInsert = "CREATE TRIGGER `{$sTable}_ad` AFTER DELETE ON $sTable";
  353. $sTriggerInsert .= " FOR EACH ROW";
  354. $sTriggerInsert .= " BEGIN";
  355. $sTriggerInsert .= " DELETE FROM priv_sync_replica WHERE id = OLD.id;";
  356. $sTriggerInsert .= " END;";
  357. CMDBSource::Query($sTriggerInsert);
  358. }
  359. protected function AfterDelete()
  360. {
  361. parent::AfterDelete();
  362. $sTable = $this->GetDataTable();
  363. $sDropTable = "DROP TABLE `$sTable`";
  364. CMDBSource::Query($sDropTable);
  365. // TO DO - check that triggers get dropped with the table
  366. }
  367. /**
  368. * Perform a synchronization between the data stored in the replicas (&synchro_data_xxx_xx table)
  369. * and the iTop objects. If the lastFullLoadStartDate is NOT specified then the full_load_periodicity
  370. * is used to determine which records are obsolete.
  371. * @param Hash $aDataToReplica Debugs/Trace information, one entry per replica
  372. * @param DateTime $oLastFullLoadStartDate Date of the last full load (start date/time), if known
  373. * @return void
  374. */
  375. public function Synchronize(&$aDataToReplica, $oLastFullLoadStartDate = null)
  376. {
  377. // Create a change used for logging all the modifications/creations happening during the synchro
  378. $oMyChange = MetaModel::NewObject("CMDBChange");
  379. $oMyChange->Set("date", time());
  380. $sUserString = CMDBChange::GetCurrentUserName();
  381. $oMyChange->Set("userinfo", $sUserString);
  382. $iChangeId = $oMyChange->DBInsert();
  383. // Start logging this execution (stats + protection against reentrance)
  384. //
  385. $oStatLog = new SynchroLog();
  386. $oStatLog->Set('sync_source_id', $this->GetKey());
  387. $oStatLog->Set('start_date', time());
  388. $oStatLog->Set('status', 'running');
  389. $oStatLog->Set('stats_nb_seen', 0);
  390. $oStatLog->Set('stats_nb_modified', 0);
  391. $oStatLog->Set('stats_nb_errors', 0);
  392. $oStatLog->Set('stats_nb_created', 0);
  393. $oStatLog->Set('stats_nb_deleted', 0);
  394. $oStatLog->Set('stats_nb_reconciled', 0);
  395. $oStatLog->DBInsertTracked($oMyChange);
  396. try
  397. {
  398. $this->DoSynchronize($aDataToReplica, $oLastFullLoadStartDate, $oMyChange, $oStatLog);
  399. $oStatLog->Set('end_date', time());
  400. $oStatLog->Set('status', 'completed');
  401. $oStatLog->DBUpdateTracked($oMyChange);
  402. }
  403. catch (Exception $e)
  404. {
  405. $oStatLog->Set('end_date', time());
  406. $oStatLog->Set('status', 'completed');
  407. $oStatLog->DBUpdateTracked($oMyChange);
  408. }
  409. }
  410. protected function DoSynchronize(&$aDataToReplica, $oLastFullLoadStartDate, $oMyChange, &$oStatLog)
  411. {
  412. // Get all the replicas that were not seen in the last import and mark them as obsolete
  413. if ($oLastFullLoadStartDate == null)
  414. {
  415. // No previous import known, use the full_load_periodicity value... and the current date
  416. $oLastFullLoadStartDate = new DateTime(); // Now
  417. // TO DO: how do we support localization here ??
  418. $sLoadPeriodicity = trim($this->Get('full_load_periodicity'));
  419. if (strlen($sLoadPeriodicity) > 0)
  420. {
  421. $sInterval = '-'.$sLoadPeriodicity;
  422. // Note: the PHP doc states that Modify return FALSE in case of error
  423. // but, this is actually NOT the case
  424. // Therefore, I do compare before and after, considering that the
  425. // format is incorrect when the datetime remains unchanged
  426. $sBefore = $oLastFullLoadStartDate->Format('Y-m-d H:i:s');
  427. $oLastFullLoadStartDate->Modify($sInterval);
  428. $sAfter = $oLastFullLoadStartDate->Format('Y-m-d H:i:s');
  429. if ($sBefore == $sAfter)
  430. {
  431. throw new CoreException("Data exchange: Wrong interval specification", array('interval' => $sInterval, 'source_id' => $this->GetKey()));
  432. }
  433. }
  434. }
  435. $sLimitDate = $oLastFullLoadStartDate->Format('Y-m-d H:i:s');
  436. // TO DO: remove trace
  437. echo "<p>sLimitDate: $sLimitDate</p>\n";
  438. $sSelectToObsolete = "SELECT SynchroReplica WHERE sync_source_id = :source_id AND status IN ('new', 'synchronized', 'modified', 'orphan') AND status_last_seen < :last_import";
  439. $oSetToObsolete = new DBObjectSet(DBObjectSearch::FromOQL($sSelectToObsolete), array() /* order by*/, array('source_id' => $this->GetKey(), 'last_import' => $sLimitDate));
  440. while($oReplica = $oSetToObsolete->Fetch())
  441. {
  442. // TO DO: take the appropriate action based on the 'delete_policy' field
  443. $sUpdateOnObsolete = $this->Get('delete_policy');
  444. if ( ($sUpdateOnObsolete == 'update') || ($sUpdateOnObsolete == 'update_then_delete') )
  445. {
  446. // TO DO: remove trace
  447. echo "<p>Destination object: (dest_id:".$oReplica->Get('dest_id').") to be updated.</p>";
  448. $aToUpdate = array();
  449. $aToUpdate = explode(';', $this->Get('delete_policy_update')); //ex: 'status:obsolete;description:stopped',
  450. foreach($aToUpdate as $sUpdateSpec)
  451. {
  452. $aUpdateSpec = explode(':', $sUpdateSpec);
  453. if (count($aUpdateSpec) == 2)
  454. {
  455. $sAttCode = $aUpdateSpec[0];
  456. $sValue = $aUpdateSpec[1];
  457. $aToUpdate[$sAttCode] = $sValue;
  458. }
  459. }
  460. $oReplica->UpdateDestObject($aToUpdate, $oMyChange, $oStatLog);
  461. }
  462. // TO DO: remove trace
  463. echo "<p>Replica id:".$oReplica->GetKey()." (dest_id:".$oReplica->Get('dest_id').") marked as obsolete</p>";
  464. $oReplica->Set('status', 'obsolete');
  465. $oReplica->DBUpdateTracked($oMyChange);
  466. }
  467. //Count "seen" objects
  468. $sSelectSeen = "SELECT SynchroReplica WHERE sync_source_id = :source_id AND status IN ('new', 'synchronized', 'modified', 'orphan') AND status_last_seen >= :last_import";
  469. $oSetSeen = new DBObjectSet(DBObjectSearch::FromOQL($sSelectSeen), array() /* order by*/, array('source_id' => $this->GetKey(), 'last_import' => $sLimitDate));
  470. $oStatLog->Set('stats_nb_seen', $oSetSeen->Count());
  471. // Get all the replicas that are 'new' or modified
  472. //
  473. // Get the list of SQL columns
  474. $sClass = $this->GetTargetClass();
  475. // TO DO: remove trace
  476. echo "<p>TargetClass: $sClass</p>";
  477. $aAttCodes = array();
  478. $sSelectAtt = "SELECT SynchroAttribute WHERE sync_source_id = :source_id AND update = 1";
  479. $oSetAtt = new DBObjectSet(DBObjectSearch::FromOQL($sSelectAtt), array() /* order by*/, array('source_id' => $this->GetKey()) /* aArgs */);
  480. while ($oSyncAtt = $oSetAtt->Fetch())
  481. {
  482. $aAttCodes[] = $oSyncAtt->Get('attcode');
  483. }
  484. $aColumns = $this->GetSQLColumns($aAttCodes);
  485. $aExtDataFields = array_keys($aColumns);
  486. $aExtDataFields[] = 'primary_key';
  487. $aExtDataSpec = array(
  488. 'table' => $this->GetDataTable(),
  489. 'join_key' => 'id',
  490. 'fields' => $aExtDataFields
  491. );
  492. // Get the list of reconciliation keys
  493. $aReconciliationKeys = array();
  494. if ($this->Get('reconciliation_policy') == 'use_attributes')
  495. {
  496. $sSelectAtt = "SELECT SynchroAttribute WHERE sync_source_id = :source_id AND reconcile = 1";
  497. $oAttSet = new DBObjectSet(DBObjectSearch::FromOQL($sSelectAtt), array() /* order by*/, array('source_id' => $this->GetKey()) /* aArgs */);
  498. while ($oSyncAtt = $oAttSet->Fetch())
  499. {
  500. $aReconciliationKeys[] = $oSyncAtt->Get('attcode');
  501. }
  502. }
  503. elseif ($this->Get('reconciliation_policy') == 'use_primary_key')
  504. {
  505. $aReconciliationKeys[] = "primary_key";
  506. }
  507. // TO DO: remove trace
  508. echo "Reconciliation on: {".implode(', ', $aReconciliationKeys)."}<br/>\n";
  509. $aAttributes = array();
  510. foreach($aAttCodes as $sAttCode)
  511. {
  512. $oAttDef = MetaModel::GetAttributeDef($this->GetTargetClass(), $sAttCode);
  513. if ($oAttDef->IsWritable() && $oAttDef->IsScalar())
  514. {
  515. $aAttributes[] = $sAttCode;
  516. }
  517. }
  518. $sSelectToSync = "SELECT SynchroReplica WHERE (status = 'new' OR status = 'modified') AND sync_source_id = :source_id";
  519. $oSetToSync = new DBObjectSet(DBObjectSearch::FromOQL($sSelectToSync), array() /* order by*/, array('source_id' => $this->GetKey()) /* aArgs */, $aExtDataSpec, 0 /* limitCount */, 0 /* limitStart */);
  520. while($oReplica = $oSetToSync->Fetch())
  521. {
  522. if ($oReplica->Get('status') == 'modified')
  523. {
  524. $oStatLog->Set('stats_nb_modified', $oStatLog->Get('stats_nb_modified') + 1);
  525. }
  526. $oReplica->Synchro($this, $aReconciliationKeys, $aAttributes, $oMyChange, $oStatLog);
  527. }
  528. // Get all the replicas that are to be deleted
  529. //
  530. $oDeletionDate = $oLastFullLoadStartDate;
  531. $sDeleteRetention = trim($this->Get('delete_policy_retention'));
  532. if (strlen($sDeleteRetention) > 0)
  533. {
  534. $sInterval = '-'.$sDeleteRetention;
  535. // Note: the PHP doc states that Modify return FALSE in case of error
  536. // but, this is actually NOT the case
  537. // Therefore, I do compare before and after, considering that the
  538. // format is incorrect when the datetime remains unchanged
  539. $sBefore = $oDeletionDate->Format('Y-m-d H:i:s');
  540. $oDeletionDate->Modify($sInterval);
  541. $sAfter = $oDeletionDate->Format('Y-m-d H:i:s');
  542. if ($sBefore == $sAfter)
  543. {
  544. throw new CoreException("Data exchange: Wrong interval specification", array('interval' => $sInterval, 'source_id' => $this->GetKey()));
  545. }
  546. }
  547. $sDeletionDate = $oDeletionDate->Format('Y-m-d H:i:s');
  548. // TO DO: remove trace
  549. echo "<p>sDeletionDate: $sDeletionDate</p>\n";
  550. $sSelectToDelete = "SELECT SynchroReplica WHERE sync_source_id = :source_id AND status IN ('obsolete') AND status_last_seen < :last_import";
  551. $oSetToDelete = new DBObjectSet(DBObjectSearch::FromOQL($sSelectToDelete), array() /* order by*/, array('source_id' => $this->GetKey(), 'last_import' => $sDeletionDate));
  552. while($oReplica = $oSetToDelete->Fetch())
  553. {
  554. $oStatLog->Set('stats_nb_deleted', $oStatLog->Get('stats_nb_deleted') + 1);
  555. $sUpdateOnObsolete = $this->Get('delete_policy');
  556. if ( ($sUpdateOnObsolete == 'delete') || ($sUpdateOnObsolete == 'update_then_delete') )
  557. {
  558. // TO DO: remove trace
  559. echo "<p>Destination object: (dest_id:".$oReplica->Get('dest_id').") to be DELETED.</p>";
  560. // TO DO: delete the dest object for real...
  561. $oReplica->DeleteDestObject($oMyChange, $oStatLog);
  562. }
  563. // TO DO: remove trace
  564. echo "<p>Replica id:".$oReplica->GetKey()." (dest_id:".$oReplica->Get('dest_id').") to be deleted</p>";
  565. $oReplica->DBDeleteTracked($oMyChange);
  566. }
  567. }
  568. /**
  569. * Get the list of SQL columns corresponding to a particular list of attribute codes
  570. * Defaults to the whole list of columns for the current class
  571. */
  572. public function GetSQLColumns($aAttributeCodes = null)
  573. {
  574. $aColumns = array();
  575. $sClass = $this->GetTargetClass();
  576. if (is_null($aAttributeCodes))
  577. {
  578. $aAttributeCodes = array();
  579. foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
  580. {
  581. if ($sAttCode == 'finalclass') continue;
  582. $aAttributeCodes[] = $sAttCode;
  583. }
  584. }
  585. foreach($aAttributeCodes as $sAttCode)
  586. {
  587. $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
  588. foreach($oAttDef->GetSQLColumns() as $sField => $sDBFieldType)
  589. {
  590. $aColumns[$sField] = $sDBFieldType;
  591. }
  592. }
  593. return $aColumns;
  594. }
  595. public function IsRunning()
  596. {
  597. $sOQL = "SELECT SynchroLog WHERE sync_source_id = :source_id AND status='running'";
  598. $oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array('start_date' => false) /* order by*/, array('source_id' => $this->GetKey()) /* aArgs */, array(), 1 /* limitCount */, 0 /* limitStart */);
  599. if ($oSet->Count() < 1)
  600. {
  601. $bRet = false;
  602. }
  603. else
  604. {
  605. $bRet = true;
  606. }
  607. return $bRet;
  608. }
  609. public function GetLatestLog()
  610. {
  611. $oLog = null;
  612. $sOQL = "SELECT SynchroLog WHERE sync_source_id = :source_id";
  613. $oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array('start_date' => false) /* order by*/, array('source_id' => $this->GetKey()) /* aArgs */, array(), 1 /* limitCount */, 0 /* limitStart */);
  614. if ($oSet->Count() >= 1)
  615. {
  616. $oLog = $oSet->Fetch();
  617. }
  618. return $oLog;
  619. }
  620. // TO DO: remove if still unused
  621. /**
  622. * Retrieve from the log, the date of the last completed import
  623. * @return DateTime
  624. */
  625. public function GetLastCompletedImportDate()
  626. {
  627. $date = null;
  628. $sOQL = "SELECT SynchroLog WHERE sync_source_id = :source_id AND status='completed'";
  629. $oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array('end_date' => false) /* order by*/, array('source_id' => $this->GetKey()) /* aArgs */, array(), 0 /* limitCount */, 0 /* limitStart */);
  630. if ($oSet->Count() >= 1)
  631. {
  632. $oLog = $oSet->Fetch();
  633. $date = $oLog->Get('end_date');
  634. }
  635. else
  636. {
  637. // TO DO: remove trace
  638. echo "<p>No completed log found</p>\n";
  639. }
  640. return $date;
  641. }
  642. }
  643. class SynchroAttribute extends cmdbAbstractObject
  644. {
  645. public static function Init()
  646. {
  647. $aParams = array
  648. (
  649. "category" => "core/cmdb,view_in_gui",
  650. "key_type" => "autoincrement",
  651. "name_attcode" => "",
  652. "state_attcode" => "",
  653. "reconc_keys" => array(),
  654. "db_table" => "priv_sync_att",
  655. "db_key_field" => "id",
  656. "db_finalclass_field" => "",
  657. "display_template" => "",
  658. );
  659. MetaModel::Init_Params($aParams);
  660. MetaModel::Init_InheritAttributes();
  661. MetaModel::Init_AddAttribute(new AttributeExternalKey("sync_source_id", array("targetclass"=>"SynchroDataSource", "jointype"=> "", "allowed_values"=>null, "sql"=>"sync_source_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array())));
  662. MetaModel::Init_AddAttribute(new AttributeString("attcode", array("allowed_values"=>null, "sql"=>"attcode", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
  663. MetaModel::Init_AddAttribute(new AttributeBoolean("update", array("allowed_values"=>null, "sql"=>"update", "default_value"=>true, "is_null_allowed"=>false, "depends_on"=>array())));
  664. MetaModel::Init_AddAttribute(new AttributeBoolean("reconcile", array("allowed_values"=>null, "sql"=>"reconcile", "default_value"=>false, "is_null_allowed"=>false, "depends_on"=>array())));
  665. MetaModel::Init_AddAttribute(new AttributeEnum("update_policy", array("allowed_values"=>new ValueSetEnum('master_locked,master_unlocked,write_once'), "sql"=>"update_policy", "default_value"=>"master_locked", "is_null_allowed"=>false, "depends_on"=>array())));
  666. // Display lists
  667. MetaModel::Init_SetZListItems('details', array('sync_source_id', 'attcode', 'update', 'reconcile', 'update_policy')); // Attributes to be displayed for the complete details
  668. MetaModel::Init_SetZListItems('list', array('sync_source_id', 'attcode', 'update', 'reconcile', 'update_policy')); // Attributes to be displayed for a list
  669. // Search criteria
  670. // MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form
  671. // MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form
  672. }
  673. }
  674. class SynchroAttExtKey extends SynchroAttribute
  675. {
  676. public static function Init()
  677. {
  678. $aParams = array
  679. (
  680. "category" => "core/cmdb,view_in_gui",
  681. "key_type" => "autoincrement",
  682. "name_attcode" => "",
  683. "state_attcode" => "",
  684. "reconc_keys" => array(),
  685. "db_table" => "priv_sync_att_extkey",
  686. "db_key_field" => "id",
  687. "db_finalclass_field" => "",
  688. "display_template" => "",
  689. );
  690. MetaModel::Init_Params($aParams);
  691. MetaModel::Init_InheritAttributes();
  692. MetaModel::Init_AddAttribute(new AttributeString("reconciliation_attcode", array("allowed_values"=>null, "sql"=>"reconciliation_attcode", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
  693. // Display lists
  694. MetaModel::Init_SetZListItems('details', array('sync_source_id', 'attcode', 'update', 'reconcile', 'update_policy', 'reconciliation_attcode')); // Attributes to be displayed for the complete details
  695. MetaModel::Init_SetZListItems('list', array('sync_source_id', 'attcode', 'update', 'reconcile', 'update_policy')); // Attributes to be displayed for a list
  696. // Search criteria
  697. // MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form
  698. // MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form
  699. }
  700. }
  701. class SynchroAttLinkSet extends SynchroAttribute
  702. {
  703. public static function Init()
  704. {
  705. $aParams = array
  706. (
  707. "category" => "core/cmdb,view_in_gui",
  708. "key_type" => "autoincrement",
  709. "name_attcode" => "",
  710. "state_attcode" => "",
  711. "reconc_keys" => array(),
  712. "db_table" => "priv_sync_att_linkset",
  713. "db_key_field" => "id",
  714. "db_finalclass_field" => "",
  715. "display_template" => "",
  716. );
  717. MetaModel::Init_Params($aParams);
  718. MetaModel::Init_InheritAttributes();
  719. MetaModel::Init_AddAttribute(new AttributeString("row_separator", array("allowed_values"=>null, "sql"=>"row_separator", "default_value"=>'|', "is_null_allowed"=>true, "depends_on"=>array())));
  720. MetaModel::Init_AddAttribute(new AttributeString("attribute_separator", array("allowed_values"=>null, "sql"=>"attribute_separator", "default_value"=>';', "is_null_allowed"=>true, "depends_on"=>array())));
  721. // Display lists
  722. MetaModel::Init_SetZListItems('details', array('sync_source_id', 'attcode', 'update', 'reconcile', 'update_policy', 'row_separator', 'attribute_separator')); // Attributes to be displayed for the complete details
  723. MetaModel::Init_SetZListItems('list', array('sync_source_id', 'attcode', 'update', 'reconcile', 'update_policy')); // Attributes to be displayed for a list
  724. // Search criteria
  725. // MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form
  726. // MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form
  727. }
  728. }
  729. //class SynchroLog extends Event
  730. class SynchroLog extends cmdbAbstractObject
  731. {
  732. public static function Init()
  733. {
  734. $aParams = array
  735. (
  736. "category" => "core/cmdb,view_in_gui",
  737. "key_type" => "autoincrement",
  738. "name_attcode" => "",
  739. "state_attcode" => "",
  740. "reconc_keys" => array(),
  741. "db_table" => "priv_sync_log",
  742. "db_key_field" => "id",
  743. "db_finalclass_field" => "",
  744. "display_template" => "",
  745. );
  746. MetaModel::Init_Params($aParams);
  747. MetaModel::Init_InheritAttributes();
  748. // MetaModel::Init_AddAttribute(new AttributeString("userinfo", array("allowed_values"=>null, "sql"=>"userinfo", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
  749. MetaModel::Init_AddAttribute(new AttributeExternalKey("sync_source_id", array("targetclass"=>"SynchroDataSource", "jointype"=> "", "allowed_values"=>null, "sql"=>"sync_source_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array())));
  750. MetaModel::Init_AddAttribute(new AttributeDateTime("start_date", array("allowed_values"=>null, "sql"=>"start_date", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array())));
  751. MetaModel::Init_AddAttribute(new AttributeDateTime("end_date", array("allowed_values"=>null, "sql"=>"end_date", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array())));
  752. MetaModel::Init_AddAttribute(new AttributeEnum("status", array("allowed_values"=>new ValueSetEnum('running,completed'), "sql"=>"status", "default_value"=>"running", "is_null_allowed"=>false, "depends_on"=>array())));
  753. MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_seen", array("allowed_values"=>null, "sql"=>"stats_nb_seen", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array())));
  754. MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_modified", array("allowed_values"=>null, "sql"=>"stats_nb_modified", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array())));
  755. MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_errors", array("allowed_values"=>null, "sql"=>"stats_nb_errors", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array())));
  756. MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_created", array("allowed_values"=>null, "sql"=>"stats_nb_created", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array())));
  757. MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_deleted", array("allowed_values"=>null, "sql"=>"stats_nb_deleted", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array())));
  758. MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_reconciled", array("allowed_values"=>null, "sql"=>"stats_nb_reconciled", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array())));
  759. // Display lists
  760. MetaModel::Init_SetZListItems('details', array('sync_source_id', 'start_date', 'end_date', 'status', 'stats_nb_seen', 'stats_nb_modified', 'stats_nb_errors', 'stats_nb_created', 'stats_nb_deleted', 'stats_nb_reconciled')); // Attributes to be displayed for the complete details
  761. MetaModel::Init_SetZListItems('list', array('sync_source_id', 'start_date', 'end_date', 'status', 'stats_nb_seen', 'stats_nb_modified', 'stats_nb_errors')); // Attributes to be displayed for a list
  762. MetaModel::Init_SetZListItems('preview', array('start_date', 'end_date', 'status', 'stats_nb_seen', 'stats_nb_errors')); // Attributes to be displayed for a list
  763. // Search criteria
  764. // MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form
  765. // MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form
  766. }
  767. }
  768. class SynchroReplica extends DBObject
  769. {
  770. static $aSearches = array(); // Cache of OQL queries used for reconciliation (per data source)
  771. public static function Init()
  772. {
  773. $aParams = array
  774. (
  775. "category" => "core/cmdb,view_in_gui",
  776. "key_type" => "autoincrement",
  777. "name_attcode" => "",
  778. "state_attcode" => "",
  779. "reconc_keys" => array(),
  780. "db_table" => "priv_sync_replica",
  781. "db_key_field" => "id",
  782. "db_finalclass_field" => "",
  783. "display_template" => "",
  784. );
  785. MetaModel::Init_Params($aParams);
  786. MetaModel::Init_InheritAttributes();
  787. MetaModel::Init_AddAttribute(new AttributeExternalKey("sync_source_id", array("targetclass"=>"SynchroDataSource", "jointype"=> "", "allowed_values"=>null, "sql"=>"sync_source_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array())));
  788. MetaModel::Init_AddAttribute(new AttributeInteger("dest_id", array("allowed_values"=>null, "sql"=>"dest_id", "default_value"=>0, "is_null_allowed"=>true, "depends_on"=>array())));
  789. MetaModel::Init_AddAttribute(new AttributeClass("dest_class", array("class_category"=>"bizmodel", "more_values"=>"", "sql"=>"dest_class", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
  790. MetaModel::Init_AddAttribute(new AttributeDateTime("status_last_seen", array("allowed_values"=>null, "sql"=>"status_last_seen", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array())));
  791. MetaModel::Init_AddAttribute(new AttributeEnum("status", array("allowed_values"=>new ValueSetEnum('new,synchronized,modified,orphan,obsolete'), "sql"=>"status", "default_value"=>"new", "is_null_allowed"=>false, "depends_on"=>array())));
  792. MetaModel::Init_AddAttribute(new AttributeBoolean("status_dest_creator", array("allowed_values"=>null, "sql"=>"status_dest_creator", "default_value"=>0, "is_null_allowed"=>true, "depends_on"=>array())));
  793. MetaModel::Init_AddAttribute(new AttributeString("status_last_error", array("allowed_values"=>null, "sql"=>"status_last_error", "default_value"=>'', "is_null_allowed"=>true, "depends_on"=>array())));
  794. MetaModel::Init_AddAttribute(new AttributeDateTime("info_creation_date", array("allowed_values"=>null, "sql"=>"info_creation_date", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array())));
  795. MetaModel::Init_AddAttribute(new AttributeDateTime("info_last_modified", array("allowed_values"=>null, "sql"=>"info_last_modified", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array())));
  796. MetaModel::Init_AddAttribute(new AttributeDateTime("info_last_synchro", array("allowed_values"=>null, "sql"=>"info_last_synchro", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array())));
  797. // Display lists
  798. MetaModel::Init_SetZListItems('details', array('sync_source_id', 'dest_id', 'dest_class', '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
  799. MetaModel::Init_SetZListItems('list', array('sync_source_id', 'dest_id', 'dest_class', 'status_last_seen', 'status', 'status_dest_creator', 'status_last_error')); // Attributes to be displayed for a list
  800. // Search criteria
  801. MetaModel::Init_SetZListItems('standard_search', array('sync_source_id', 'status_last_seen', 'status', 'status_dest_creator', 'dest_class', 'dest_id', 'status_last_error')); // Criteria of the std search form
  802. // MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form
  803. }
  804. public function DBInsert()
  805. {
  806. throw new CoreException('A synchronization replica must be created only by the mean of triggers');
  807. }
  808. // Overload the deletion -> the replica has been created by the mean of a trigger,
  809. // it will be deleted by the mean of a trigger too
  810. public function DBDelete()
  811. {
  812. $oDataSource = MetaModel::GetObject('SynchroDataSource', $this->Get('sync_source_id'));
  813. $sTable = $oDataSource->GetDataTable();
  814. $sSQL = "DELETE FROM `$sTable` WHERE id = '{$this->GetKey()}'";
  815. CMDBSource::Query($sSQL);
  816. $this->m_bIsInDB = false;
  817. $this->m_iKey = null;
  818. }
  819. public function SetLastError($sMessage, $oException = null)
  820. {
  821. if ($oException)
  822. {
  823. $sText = $sMessage.$oException->getMessage();
  824. }
  825. else
  826. {
  827. $sText = $sMessage;
  828. }
  829. if (strlen($sText) > 255)
  830. {
  831. $sText = substr($sText, 0, 200).'...('.strlen($sText).' chars)...';
  832. }
  833. $this->Set('status_last_error', $sText);
  834. }
  835. public function Synchro($oDataSource, $aReconciliationKeys, $aAttributes, $oChange, $oStatLog)
  836. {
  837. switch($this->Get('status'))
  838. {
  839. case 'new':
  840. // If needed, construct the query used for the reconciliation
  841. if (!isset(self::$aSearches[$oDataSource->GetKey()]))
  842. {
  843. foreach($aReconciliationKeys as $sFilterCode)
  844. {
  845. $aCriterias[] = ($sFilterCode == 'primary_key' ? 'id' : $sFilterCode).' = :'.$sFilterCode;
  846. }
  847. $sOQL = "SELECT ".$oDataSource->GetTargetClass()." WHERE ".implode(' AND ', $aCriterias);
  848. self::$aSearches[$oDataSource->GetKey()] = DBObjectSearch::FromOQL($sOQL);
  849. }
  850. // Get the criterias for the search
  851. $aFilterValues = array();
  852. foreach($aReconciliationKeys as $sFilterCode)
  853. {
  854. $aFilterValues[$sFilterCode] = $this->GetValueFromExtData($sFilterCode);
  855. }
  856. $oDestSet = new DBObjectSet(self::$aSearches[$oDataSource->GetKey()], array(), $aFilterValues);
  857. $iCount = $oDestSet->Count();
  858. // How many objects match the reconciliation criterias
  859. switch($iCount)
  860. {
  861. case 0:
  862. //echo "<p>Nothing found for: ".self::$aSearches[$oDataSource->GetKey()]->ToOQL(true, $aFilterValues)."</p>";
  863. $this->CreateObjectFromReplica($oDataSource->GetTargetClass(), $aAttributes, $oChange, $oStatLog);
  864. break;
  865. case 1:
  866. //echo "<p>Found 1 for: ".self::$aSearches[$oDataSource->GetKey()]->ToOQL(true, $aFilterValues)."</p>";
  867. $oDestObj = $oDestSet->Fetch();
  868. $this->UpdateObjectFromReplica($oDestObj, $aAttributes, $oChange, $oStatLog);
  869. $this->Set('dest_id', $oDestObj->GetKey());
  870. $this->Set('status_dest_creator', false);
  871. $this->Set('dest_class', get_class($oDestObj));
  872. $oStatLog->Set('stats_nb_reconciled', $oStatLog->Get('stats_nb_reconciled') + 1);
  873. break;
  874. default:
  875. $aConditions = array();
  876. foreach($aFilterValues as $sCode => $sValue)
  877. {
  878. $aConditions[] = $sCode.'='.$sValue;
  879. }
  880. $sCondition = implode(' AND ', $aConditions);
  881. //echo "<p>Found N for: ".self::$aSearches[$oDataSource->GetKey()]->ToOQL(true, $aFilterValues)."</p>";
  882. $this->SetLastError($iCount.' destination objects match the reconciliation criterias: '.$sCondition);
  883. $oStatLog->Set('stats_nb_errors', $oStatLog->Get('stats_nb_errors') + 1);
  884. }
  885. break;
  886. case 'modified':
  887. $oDestObj = MetaModel::GetObject($oDataSource->GetTargetClass(), $this->Get('dest_id'));
  888. if ($oDestObj == null)
  889. {
  890. $this->Set('status', 'orphan'); // The destination object has been deleted !
  891. $this->SetLastError('Destination object deleted unexpectedly');
  892. $oStatLog->Set('stats_nb_errors', $oStatLog->Get('stats_nb_errors') + 1);
  893. }
  894. else
  895. {
  896. $this->UpdateObjectFromReplica($oDestObj, $aAttributes, $oChange, $oStatLog);
  897. }
  898. break;
  899. default: // Do nothing in all other cases
  900. }
  901. $this->DBUpdateTracked($oChange);
  902. }
  903. /**
  904. * Updates the destination object with the Extended data found in the synchro_data_XXXX table
  905. */
  906. protected function UpdateObjectFromReplica($oDestObj, $aAttributes, $oChange, &$oStatLog)
  907. {
  908. // TO DO: remove trace
  909. echo "<p>Update object ".$oDestObj->GetHyperLink()."</p>";
  910. foreach($aAttributes as $sAttCode)
  911. {
  912. $value = $this->GetValueFromExtData($sAttCode);
  913. $oDestObj->Set($sAttCode, $value);
  914. // TO DO: remove trace
  915. echo "<p>&nbsp;&nbsp;&nbsp;Setting $sAttCode to $value</p>";
  916. }
  917. try
  918. {
  919. $oDestObj->DBUpdateTracked($oChange);
  920. $this->Set('status_last_error', '');
  921. $this->Set('status', 'synchronized');
  922. }
  923. catch(Exception $e)
  924. {
  925. $this->SetLastError('Unable to update destination object: ', $e);
  926. $oStatLog->Set('stats_nb_errors', $oStatLog->Get('stats_nb_errors') + 1);
  927. }
  928. }
  929. /**
  930. * Creates the destination object populating it with the Extended data found in the synchro_data_XXXX table
  931. */
  932. protected function CreateObjectFromReplica($sClass, $aAttributes, $oChange, $oStatLog)
  933. {
  934. // TO DO: remove trace
  935. echo "<p>Creating new $sClass</p>";
  936. $oDestObj = MetaModel::NewObject($sClass);
  937. foreach($aAttributes as $sAttCode)
  938. {
  939. $value = $this->GetValueFromExtData($sAttCode);
  940. $oDestObj->Set($sAttCode, $value);
  941. // TO DO: remove trace
  942. echo "<p>&nbsp;&nbsp;&nbsp;Setting $sAttCode to $value</p>";
  943. }
  944. try
  945. {
  946. $iNew = $oDestObj->DBInsertTracked($oChange);
  947. // TO DO: remove trace
  948. echo "<p>Created: $iNew</p>";
  949. $this->Set('dest_id', $oDestObj->GetKey());
  950. $this->Set('dest_class', get_class($oDestObj));
  951. $this->Set('status_dest_creator', true);
  952. $this->Set('status_last_error', '');
  953. $this->Set('status', 'synchronized');
  954. $oStatLog->Set('stats_nb_created', $oStatLog->Get('stats_nb_created') + 1);
  955. }
  956. catch(Exception $e)
  957. {
  958. $this->SetLastError('Unable to create destination object: ', $e);
  959. $oStatLog->Set('stats_nb_errors', $oStatLog->Get('stats_nb_errors') + 1);
  960. }
  961. }
  962. /**
  963. * Update the destination object with given values
  964. */
  965. public function UpdateDestObject($aValues, $oChange, &$oStatLog)
  966. {
  967. try
  968. {
  969. $oDestObj = MetaModel::GetObject($this->Get('dest_class'), $this->Get('dest_id'));
  970. foreach($aValues as $sAttCode => $value)
  971. {
  972. $oDestObj->Set($sAttCode, $value);
  973. }
  974. $oDestObj->DBUpdateTracked($oChange);
  975. }
  976. catch(Exception $e)
  977. {
  978. $this->SetLastError('Unable to update the destination object: ', $e);
  979. $oStatLog->Set('stats_nb_errors', $oStatLog->Get('stats_nb_errors') + 1);
  980. }
  981. }
  982. /**
  983. * Delete the destination object
  984. */
  985. public function DeleteDestObject($oChange, &$oStatLog)
  986. {
  987. if($this->Get('status_dest_creator'))
  988. {
  989. $oDestObj = MetaModel::GetObject($this->Get('dest_class'), $this->Get('dest_id'));
  990. try
  991. {
  992. $oDestObj->DBDeleteTracked($oChange);
  993. }
  994. catch(Exception $e)
  995. {
  996. $this->SetLastError('Unable to delete the destination object: ', $e);
  997. $oStatLog->Set('stats_nb_errors', $oStatLog->Get('stats_nb_errors') + 1);
  998. }
  999. }
  1000. }
  1001. /**
  1002. * Get the value from the 'Extended Data' located in the synchro_data_xxx table for this replica
  1003. */
  1004. protected function GetValueFromExtData($sColumnName)
  1005. {
  1006. $aData = $this->GetExtendedData();
  1007. return $aData[$sColumnName];
  1008. }
  1009. }
  1010. // TO DO: finalize.... admins only ? which options ? troubleshoot WebPageMenuNode::__construct(.... sEnableClass...) ?
  1011. //if (UserRights::IsAdministrator())
  1012. {
  1013. $oAdminMenu = new MenuGroup('AdminTools', 80 /* fRank */);
  1014. new OQLMenuNode('DataSources', 'SELECT SynchroDataSource', $oAdminMenu->GetIndex(), 12 /* fRank */, true, 'SynchroDataSource', UR_ACTION_MODIFY, UR_ALLOWED_YES);
  1015. new WebPageMenuNode('Test:RunSynchro', '../synchro/synchro_exec.php', $oAdminMenu->GetIndex(), 13 /* fRank */, 'SynchroDataSource');
  1016. }
  1017. ?>