synchrodatasource.class.inc.php 57 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219
  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'), "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. $this->DisplayStatusTab($oPage);
  130. }
  131. parent::DisplayBareRelations($oPage, $bEditMode);
  132. }
  133. /**
  134. * Displays the status (SynchroLog) of the datasource in a graphical manner
  135. * @param $oPage WebPage
  136. * @return void
  137. */
  138. protected function DisplayStatusTab(WebPage $oPage)
  139. {
  140. $oPage->SetCurrentTab(Dict::S('Core:SynchroStatus'));
  141. $sSelectSynchroLog = 'SELECT SynchroLog WHERE sync_source_id = :source_id';
  142. $oSetSynchroLog = new CMDBObjectSet(DBObjectSearch::FromOQL($sSelectSynchroLog), array('start_date' => false) /* order by*/, array('source_id' => $this->GetKey()));
  143. if ($oSetSynchroLog->Count() > 0)
  144. {
  145. $oLastLog = $oSetSynchroLog->Fetch();
  146. $sStartDate = $oLastLog->Get('start_date');
  147. $oLastLog->Get('stats_nb_replica_seen');
  148. if ($oLastLog->Get('status') == 'running')
  149. {
  150. // Still running !
  151. $oPage->p('<h2>'.Dict::Format('Core:Synchro:SynchroRunningStartedOn_Date', $sStartDate).'</h2>');
  152. }
  153. else
  154. {
  155. $sEndDate = $oLastLog->Get('end_date');
  156. $oPage->p('<h2>'.Dict::Format('Core:Synchro:SynchroEndedOn_Date', $sEndDate).'</h2>');
  157. }
  158. $oPage->add('<table class="synoptics"><tr><td style="color:#333;vertical-align:top">');
  159. $oPage->add('<h2>'.Dict::S('Core:Synchro:History').'</h2>');
  160. $oSetSynchroLog->Rewind();
  161. $oPage->add('<select size="30">');
  162. $sSelected = ' selected'; // First log is selected by default
  163. while($oLog = $oSetSynchroLog->Fetch())
  164. {
  165. $sLogTitle = Dict::Format('Core:SynchroLogTitle', $oLog->Get('status'), $oLog->Get('start_date'));
  166. $oPage->add('<option value="'.$oLog->GetKey().'"'.$sSelected.'>'.$sLogTitle.'</option>');
  167. $sSelected = ''; // only first log is selected by default
  168. }
  169. $oPage->add('</select>');
  170. $oPage->add('</td><td style="vertical-align:top;">');
  171. $iDeleted = $oLastLog->Get('stats_nb_obj_deleted');
  172. $iObsoleted = $oLastLog->Get('stats_nb_obj_obsoleted');
  173. $iDisappearedErrors = $oLastLog->Get('stats_nb_obj_obsoleted_errors') + $oLastLog->Get('stats_nb_obj_deleted_errors');
  174. $iUpdated = $oLastLog->Get('stats_nb_obj_updated');
  175. $iUpdatedErrors = $oLastLog->Get('stats_nb_obj_updated_errors');
  176. $iNewUpdated = $oLastLog->Get('stats_nb_obj_new_updated');
  177. $iNewUnchanged = $oLastLog->Get('stats_nb_obj_new_unchanged');
  178. $iReconciledErrors = $oLastLog->Get('stats_nb_replica_reconciled_errors');
  179. $iCreated = $oLastLog->Get('stats_nb_obj_created');
  180. $iCreatedErrors = $oLastLog->Get('stats_nb_obj_created_errors');
  181. $iDisappeared = $iDisappearedErrors + $iObsoleted + $iDeleted;
  182. $iNewErrors = $iCreatedErrors + $iReconciledErrors;
  183. $iNew = $iCreated + $iCreatedErrors + $iNewUpdated + $iNewUnchanged + $iReconciledErrors;
  184. $iExisting = $oLastLog->Get('stats_nb_replica_seen') - $iNew;
  185. $iUnchanged = $iExisting - $iUpdated - $iUpdatedErrors;
  186. $iIgnored = $oLastLog->Get('stats_nb_replica_total') - $iNew - $iExisting - $iDisappeared;
  187. $iNbObjects = $iNew + $iExisting + $iDisappeared;
  188. $iReplicas = $iNbObjects + $iIgnored;
  189. $sNbReplica = Dict::Format('Core:Synchro:Nb_Replica', "<span id=\"nb_replica_total\">$iReplicas</span>");
  190. $sNbObjects = Dict::Format('Core:Synchro:Nb_Objects', "<span id=\"nb_obj_total\">$iNbObjects</span>");
  191. $oPage->add(
  192. <<<EOF
  193. <table class="synoptics">
  194. <tr class="synoptics_header">
  195. <td>$sNbReplica</td><td>&nbsp;</td><td>$sNbObjects</td>
  196. </tr>
  197. <tr>
  198. EOF
  199. );
  200. $oPage->add($this->HtmlBox('repl_ignored', $iIgnored, '#999').'<td colspan="2">&nbsp;</td>');
  201. $oPage->add("</tr>\n<tr>");
  202. $oPage->add($this->HtmlBox('repl_disappeared', $iDisappeared, '#630', 'rowspan="3"').'<td rowspan="3" class="arrow">=&gt;</td>'.$this->HtmlBox('obj_deleted', $iDeleted, '#000'));
  203. $oPage->add("</tr>\n<tr>");
  204. $oPage->add($this->HtmlBox('obj_obsoleted', $iDisappeared, '#630'));
  205. $oPage->add("</tr>\n<tr>");
  206. $oPage->add($this->HtmlBox('obj_disappeared_errors', $iDisappearedErrors, '#C00'));
  207. $oPage->add("</tr>\n<tr>");
  208. $oPage->add($this->HtmlBox('repl_existing', $iExisting, '#093', 'rowspan="3"').'<td rowspan="3" class="arrow">=&gt;</td>'.$this->HtmlBox('obj_unchanged', $iUnchanged, '#393'));
  209. $oPage->add("</tr>\n<tr>");
  210. $oPage->add($this->HtmlBox('obj_updated', $iUpdated, '#3C3'));
  211. $oPage->add("</tr>\n<tr>");
  212. $oPage->add($this->HtmlBox('obj_updated_errors', $iUpdatedErrors, '#C00'));
  213. $oPage->add("</tr>\n<tr>");
  214. $oPage->add($this->HtmlBox('repl_new', $iNew, '#339', 'rowspan="4"').'<td rowspan="4" class="arrow">=&gt;</td>'.$this->HtmlBox('obj_new_unchanged', $iNewUnchanged, '#393'));
  215. $oPage->add("</tr>\n<tr>");
  216. $oPage->add($this->HtmlBox('obj_new_updated', $iNewUpdated, '#3C3'));
  217. $oPage->add("</tr>\n<tr>");
  218. $oPage->add($this->HtmlBox('obj_created', $iCreated, '#339'));
  219. $oPage->add("</tr>\n<tr>");
  220. $oPage->add($this->HtmlBox('obj_new_errors', $iNewErrors, '#C00'));
  221. $oPage->add("</tr>\n</table>\n");
  222. $oPage->add('</td></tr></table>');
  223. }
  224. else
  225. {
  226. $oPage->p('<h2>'.Dict::S('Core:Synchro:NeverRun').'</h2>');
  227. }
  228. }
  229. public function HtmlBox($sId, $iCount, $sColor, $sHTMLAttribs = '')
  230. {
  231. $sCount = "<span id=\"c_{$sId}\">$iCount</span>";
  232. $sLabel = Dict::Format('Core:Synchro:label_'.$sId, $iCount);
  233. $sOpacity = ($iCount==0) ? "opacity:0.3;" : "";
  234. return "<td id=\"$sId\" style=\"background-color:$sColor;$sOpacity;\" {$sHTMLAttribs}>$sLabel</td>";
  235. }
  236. public function GetAttributeFlags($sAttCode)
  237. {
  238. if (($sAttCode == 'scope_class') && (!$this->IsNew()))
  239. {
  240. return OPT_ATT_READONLY;
  241. }
  242. return parent::GetAttributeFlags($sAttCode);
  243. }
  244. public function UpdateObject($sFormPrefix = '')
  245. {
  246. parent::UpdateObject($sFormPrefix);
  247. // And now read the other post parameters...
  248. $oAttributeSet = $this->Get('attribute_list');
  249. $aAttributes = array();
  250. while($oAttribute = $oAttributeSet->Fetch())
  251. {
  252. $aAttributes[$oAttribute->Get('attcode')] = $oAttribute;
  253. }
  254. $aReconcile = utils::ReadPostedParam('reconciliation', array());
  255. $aUpdate = utils::ReadPostedParam('update', array());
  256. $aUpdatePolicy = utils::ReadPostedParam('attr_update_policy', array());
  257. // update_policy cannot be empty, so there is one entry per attribute, use this to iterate
  258. // through all the writable attributes
  259. foreach($aUpdatePolicy as $sAttCode => $sValue)
  260. {
  261. if(!isset($aAttributes[$sAttCode]))
  262. {
  263. $oAttribute = new SynchroAttribute();
  264. $oAttribute->Set('sync_source_id', $this->GetKey());
  265. $oAttribute->Set('attcode', $sAttCode);
  266. }
  267. else
  268. {
  269. $oAttribute = $aAttributes[$sAttCode];
  270. }
  271. $bReconcile = 0;
  272. if (isset($aReconcile[$sAttCode]))
  273. {
  274. $bReconcile = $aReconcile[$sAttCode] == 'on' ? 1 : 0;
  275. }
  276. $bUpdate = 0 ; // Default / initial value
  277. if (isset($aUpdate[$sAttCode]))
  278. {
  279. $bUpdate = $aUpdate[$sAttCode] == 'on' ? 1 : 0;
  280. }
  281. $oAttribute->Set('reconcile', $bReconcile);
  282. $oAttribute->Set('update', $bUpdate);
  283. $oAttribute->Set('update_policy', $sValue);
  284. $oAttributeSet->AddObject($oAttribute);
  285. }
  286. $this->Set('attribute_list', $oAttributeSet);
  287. }
  288. public function GetTargetClass()
  289. {
  290. return $this->Get('scope_class');
  291. }
  292. public function GetDataTable()
  293. {
  294. $sName = strtolower($this->GetTargetClass());
  295. $sName = str_replace('\'"&@|\\/ ', '_', $sName); // Remove forbidden characters from the table name
  296. $sName .= '_'.$this->GetKey(); // Add a suffix for unicity
  297. $sTable = MetaModel::GetConfig()->GetDBSubName()."synchro_data_$sName"; // Add the prefix if any
  298. return $sTable;
  299. }
  300. /**
  301. * When inserting a new datasource object, also create the SynchroAttribute objects
  302. * for each field of the target class
  303. */
  304. protected function OnInsert()
  305. {
  306. // Create all the SynchroAttribute records
  307. $oAttributeSet = $this->Get('attribute_list');
  308. foreach(MetaModel::ListAttributeDefs($this->GetTargetClass()) as $sAttCode=>$oAttDef)
  309. {
  310. if ($oAttDef->IsScalar() && $oAttDef->IsWritable())
  311. {
  312. $oAttribute = new SynchroAttribute();
  313. $oAttribute->Set('sync_source_id', $this->GetKey());
  314. $oAttribute->Set('attcode', $sAttCode);
  315. $oAttribute->Set('reconcile', MetaModel::IsReconcKey($this->GetTargetClass(), $sAttCode) ? 1 : 0);
  316. $oAttribute->Set('update', 1);
  317. $oAttribute->Set('update_policy', 'master_locked');
  318. $oAttributeSet->AddObject($oAttribute);
  319. }
  320. }
  321. $this->Set('attribute_list', $oAttributeSet);
  322. }
  323. /**
  324. * When the new datasource has been created, let's create the synchro_data table
  325. * that will hold the data records and the correspoding triggers which will maintain
  326. * both tables in sync
  327. */
  328. protected function AfterInsert()
  329. {
  330. parent::AfterInsert();
  331. $sTable = $this->GetDataTable();
  332. $aColumns = $this->GetSQLColumns();
  333. $aFieldDefs = array();
  334. // Allow '0', otherwise mysql will render an error when the id is not given
  335. // (the trigger is expected to set the value, but it is not executed soon enough)
  336. $aFieldDefs[] = "id INTEGER(11) NOT NULL DEFAULT 0 ";
  337. $aFieldDefs[] = "`primary_key` VARCHAR(255) NULL DEFAULT NULL";
  338. foreach($aColumns as $sColumn => $ColSpec)
  339. {
  340. $aFieldDefs[] = "`$sColumn` $ColSpec NULL DEFAULT NULL";
  341. }
  342. $aFieldDefs[] = "INDEX (id)";
  343. $aFieldDefs[] = "INDEX (primary_key)";
  344. $sFieldDefs = implode(', ', $aFieldDefs);
  345. $sCreateTable = "CREATE TABLE `$sTable` ($sFieldDefs) ENGINE = innodb;";
  346. CMDBSource::Query($sCreateTable);
  347. $sTriggerInsert = "CREATE TRIGGER `{$sTable}_bi` BEFORE INSERT ON $sTable";
  348. $sTriggerInsert .= " FOR EACH ROW";
  349. $sTriggerInsert .= " BEGIN";
  350. $sTriggerInsert .= " INSERT INTO priv_sync_replica (sync_source_id, status_last_seen, `status`) VALUES ({$this->GetKey()}, NOW(), 'new');";
  351. $sTriggerInsert .= " SET NEW.id = LAST_INSERT_ID();";
  352. $sTriggerInsert .= " END;";
  353. CMDBSource::Query($sTriggerInsert);
  354. $aModified = array();
  355. foreach($aColumns as $sColumn => $ColSpec)
  356. {
  357. // <=> is a null-safe 'EQUALS' operator (there is no equivalent for "DIFFERS FROM")
  358. $aModified[] = "NOT(NEW.`$sColumn` <=> OLD.`$sColumn`)";
  359. }
  360. $sIsModified = '('.implode(') OR (', $aModified).')';
  361. // Update the replica
  362. //
  363. // status is forced to "new" if the replica was obsoleted directly from the state "new" (dest_id = null)
  364. // otherwise, if status was either 'obsolete' or 'synchronized' it is turned into 'modified' or 'synchronized' depending on the changes
  365. // otherwise, the status is left as is
  366. $sTriggerUpdate = "CREATE TRIGGER `{$sTable}_bu` BEFORE UPDATE ON $sTable";
  367. $sTriggerUpdate .= " FOR EACH ROW";
  368. $sTriggerUpdate .= " BEGIN";
  369. $sTriggerUpdate .= " IF @itopuser is null THEN";
  370. $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;";
  371. $sTriggerUpdate .= " SET NEW.id = OLD.id;"; // make sure this id won't change
  372. $sTriggerUpdate .= " END IF;";
  373. $sTriggerUpdate .= " END;";
  374. CMDBSource::Query($sTriggerUpdate);
  375. $sTriggerInsert = "CREATE TRIGGER `{$sTable}_ad` AFTER DELETE ON $sTable";
  376. $sTriggerInsert .= " FOR EACH ROW";
  377. $sTriggerInsert .= " BEGIN";
  378. $sTriggerInsert .= " DELETE FROM priv_sync_replica WHERE id = OLD.id;";
  379. $sTriggerInsert .= " END;";
  380. CMDBSource::Query($sTriggerInsert);
  381. }
  382. protected function AfterDelete()
  383. {
  384. parent::AfterDelete();
  385. $sTable = $this->GetDataTable();
  386. $sDropTable = "DROP TABLE `$sTable`";
  387. CMDBSource::Query($sDropTable);
  388. // TO DO - check that triggers get dropped with the table
  389. }
  390. /**
  391. * Perform a synchronization between the data stored in the replicas (&synchro_data_xxx_xx table)
  392. * and the iTop objects. If the lastFullLoadStartDate is NOT specified then the full_load_periodicity
  393. * is used to determine which records are obsolete.
  394. * @param Hash $aTraces Debugs/Trace information, one or more entries per replica
  395. * @param DateTime $oLastFullLoadStartDate Date of the last full load (start date/time), if known
  396. * @return void
  397. */
  398. public function Synchronize(&$aTraces, $oLastFullLoadStartDate = null)
  399. {
  400. // Create a change used for logging all the modifications/creations happening during the synchro
  401. $oMyChange = MetaModel::NewObject("CMDBChange");
  402. $oMyChange->Set("date", time());
  403. $sUserString = CMDBChange::GetCurrentUserName();
  404. $oMyChange->Set("userinfo", $sUserString);
  405. $iChangeId = $oMyChange->DBInsert();
  406. // Start logging this execution (stats + protection against reentrance)
  407. //
  408. $oStatLog = new SynchroLog();
  409. $oStatLog->Set('sync_source_id', $this->GetKey());
  410. $oStatLog->Set('start_date', time());
  411. $oStatLog->Set('status', 'running');
  412. $oStatLog->Set('stats_nb_replica_seen', 0);
  413. $oStatLog->Set('stats_nb_replica_total', 0);
  414. $oStatLog->Set('stats_nb_obj_deleted', 0);
  415. $oStatLog->Set('stats_nb_obj_deleted_errors', 0);
  416. $oStatLog->Set('stats_nb_obj_obsoleted', 0);
  417. $oStatLog->Set('stats_nb_obj_obsoleted_errors', 0);
  418. $oStatLog->Set('stats_nb_obj_created', 0);
  419. $oStatLog->Set('stats_nb_obj_created_errors', 0);
  420. $oStatLog->Set('stats_nb_obj_updated', 0);
  421. $oStatLog->Set('stats_nb_obj_updated_errors', 0);
  422. // $oStatLog->Set('stats_nb_replica_reconciled', 0);
  423. $oStatLog->Set('stats_nb_replica_reconciled_errors', 0);
  424. $oStatLog->Set('stats_nb_obj_new_updated', 0);
  425. $oStatLog->Set('stats_nb_obj_new_unchanged',0);
  426. $sSelectTotal = "SELECT SynchroReplica WHERE sync_source_id = :source_id";
  427. $oSetTotal = new DBObjectSet(DBObjectSearch::FromOQL($sSelectTotal), array() /* order by*/, array('source_id' => $this->GetKey()));
  428. $oStatLog->Set('stats_nb_replica_total', $oSetTotal->Count());
  429. $oStatLog->DBInsertTracked($oMyChange);
  430. try
  431. {
  432. $this->DoSynchronize($oLastFullLoadStartDate, $oMyChange, $oStatLog, $aTraces);
  433. $oStatLog->Set('end_date', time());
  434. $oStatLog->Set('status', 'completed');
  435. $oStatLog->DBUpdateTracked($oMyChange);
  436. }
  437. catch (Exception $e)
  438. {
  439. $oStatLog->Set('end_date', time());
  440. $oStatLog->Set('status', 'completed');
  441. $oStatLog->DBUpdateTracked($oMyChange);
  442. }
  443. return $oStatLog;
  444. }
  445. protected function DoSynchronize($oLastFullLoadStartDate, $oMyChange, &$oStatLog, &$aTraces)
  446. {
  447. // Get all the replicas that were not seen in the last import and mark them as obsolete
  448. if ($oLastFullLoadStartDate == null)
  449. {
  450. // No previous import known, use the full_load_periodicity value... and the current date
  451. $oLastFullLoadStartDate = new DateTime(); // Now
  452. // TO DO: how do we support localization here ??
  453. $sLoadPeriodicity = trim($this->Get('full_load_periodicity'));
  454. if (strlen($sLoadPeriodicity) > 0)
  455. {
  456. $sInterval = '-'.$sLoadPeriodicity;
  457. // Note: the PHP doc states that Modify return FALSE in case of error
  458. // but, this is actually NOT the case
  459. // Therefore, I do compare before and after, considering that the
  460. // format is incorrect when the datetime remains unchanged
  461. $sBefore = $oLastFullLoadStartDate->Format('Y-m-d H:i:s');
  462. $oLastFullLoadStartDate->Modify($sInterval);
  463. $sAfter = $oLastFullLoadStartDate->Format('Y-m-d H:i:s');
  464. if ($sBefore == $sAfter)
  465. {
  466. throw new CoreException("Data exchange: Wrong interval specification", array('interval' => $sInterval, 'source_id' => $this->GetKey()));
  467. }
  468. }
  469. }
  470. $sLimitDate = $oLastFullLoadStartDate->Format('Y-m-d H:i:s');
  471. $aTraces[] = "Limit Date: $sLimitDate";
  472. $sSelectToObsolete = "SELECT SynchroReplica WHERE sync_source_id = :source_id AND status IN ('new', 'synchronized', 'modified', 'orphan') AND status_last_seen < :last_import";
  473. $oSetToObsolete = new DBObjectSet(DBObjectSearch::FromOQL($sSelectToObsolete), array() /* order by*/, array('source_id' => $this->GetKey(), 'last_import' => $sLimitDate));
  474. while($oReplica = $oSetToObsolete->Fetch())
  475. {
  476. // TO DO: take the appropriate action based on the 'delete_policy' field
  477. $sUpdateOnObsolete = $this->Get('delete_policy');
  478. if ( ($sUpdateOnObsolete == 'update') || ($sUpdateOnObsolete == 'update_then_delete') )
  479. {
  480. $aTraces[] = "Destination object: (dest_id:".$oReplica->Get('dest_id').") to be updated";
  481. $aToUpdate = array();
  482. $aToUpdate = explode(';', $this->Get('delete_policy_update')); //ex: 'status:obsolete;description:stopped',
  483. foreach($aToUpdate as $sUpdateSpec)
  484. {
  485. $aUpdateSpec = explode(':', $sUpdateSpec);
  486. if (count($aUpdateSpec) == 2)
  487. {
  488. $sAttCode = $aUpdateSpec[0];
  489. $sValue = $aUpdateSpec[1];
  490. $aToUpdate[$sAttCode] = $sValue;
  491. }
  492. }
  493. $oReplica->UpdateDestObject($aToUpdate, $oMyChange, $oStatLog, $aTraces, 'stats_nb_obj_obsoleted');
  494. }
  495. $oReplica->Set('status', 'obsolete');
  496. $oReplica->DBUpdateTracked($oMyChange);
  497. }
  498. //Count "seen" objects
  499. $sSelectSeen = "SELECT SynchroReplica WHERE sync_source_id = :source_id AND status IN ('new', 'synchronized', 'modified', 'orphan') AND status_last_seen >= :last_import";
  500. $oSetSeen = new DBObjectSet(DBObjectSearch::FromOQL($sSelectSeen), array() /* order by*/, array('source_id' => $this->GetKey(), 'last_import' => $sLimitDate));
  501. $oStatLog->Set('stats_nb_replica_seen', $oSetSeen->Count());
  502. // Get all the replicas that are 'new' or modified
  503. //
  504. // Get the list of SQL columns
  505. $sClass = $this->GetTargetClass();
  506. $aAttCodesExpected = array();
  507. $aAttCodesToReconcile = array();
  508. $aAttCodesToUpdate = array();
  509. $sSelectAtt = "SELECT SynchroAttribute WHERE sync_source_id = :source_id AND (update = 1 OR reconcile = 1)";
  510. $oSetAtt = new DBObjectSet(DBObjectSearch::FromOQL($sSelectAtt), array() /* order by*/, array('source_id' => $this->GetKey()) /* aArgs */);
  511. while ($oSyncAtt = $oSetAtt->Fetch())
  512. {
  513. if ($oSyncAtt->Get('update'))
  514. {
  515. $aAttCodesToUpdate[] = $oSyncAtt->Get('attcode');
  516. }
  517. if ($oSyncAtt->Get('reconcile'))
  518. {
  519. $aAttCodesToReconcile[] = $oSyncAtt->Get('attcode');
  520. }
  521. $aAttCodesExpected[] = $oSyncAtt->Get('attcode');
  522. }
  523. $aColumns = $this->GetSQLColumns($aAttCodesExpected);
  524. $aExtDataFields = array_keys($aColumns);
  525. $aExtDataFields[] = 'primary_key';
  526. $aExtDataSpec = array(
  527. 'table' => $this->GetDataTable(),
  528. 'join_key' => 'id',
  529. 'fields' => $aExtDataFields
  530. );
  531. // Get the list of reconciliation keys
  532. if ($this->Get('reconciliation_policy') == 'use_attributes')
  533. {
  534. $aReconciliationKeys = $aAttCodesToReconcile;
  535. }
  536. elseif ($this->Get('reconciliation_policy') == 'use_primary_key')
  537. {
  538. // Override the setings made at the attribute level !
  539. $aReconciliationKeys = array("primary_key");
  540. }
  541. $aTraces[] = "Reconciliation on: {".implode(', ', $aReconciliationKeys)."}";
  542. $aAttributes = array();
  543. foreach($aAttCodesToUpdate as $sAttCode)
  544. {
  545. $oAttDef = MetaModel::GetAttributeDef($this->GetTargetClass(), $sAttCode);
  546. if ($oAttDef->IsWritable() && $oAttDef->IsScalar())
  547. {
  548. $aAttributes[] = $sAttCode;
  549. }
  550. }
  551. $sSelectToSync = "SELECT SynchroReplica WHERE (status = 'new' OR status = 'modified') AND sync_source_id = :source_id";
  552. $oSetToSync = new DBObjectSet(DBObjectSearch::FromOQL($sSelectToSync), array() /* order by*/, array('source_id' => $this->GetKey()) /* aArgs */, $aExtDataSpec, 0 /* limitCount */, 0 /* limitStart */);
  553. while($oReplica = $oSetToSync->Fetch())
  554. {
  555. $oReplica->Synchro($this, $aReconciliationKeys, $aAttributes, $oMyChange, $oStatLog, $aTraces);
  556. }
  557. // Get all the replicas that are to be deleted
  558. //
  559. $oDeletionDate = $oLastFullLoadStartDate;
  560. $sDeleteRetention = trim($this->Get('delete_policy_retention'));
  561. if (strlen($sDeleteRetention) > 0)
  562. {
  563. $sInterval = '-'.$sDeleteRetention;
  564. // Note: the PHP doc states that Modify return FALSE in case of error
  565. // but, this is actually NOT the case
  566. // Therefore, I do compare before and after, considering that the
  567. // format is incorrect when the datetime remains unchanged
  568. $sBefore = $oDeletionDate->Format('Y-m-d H:i:s');
  569. $oDeletionDate->Modify($sInterval);
  570. $sAfter = $oDeletionDate->Format('Y-m-d H:i:s');
  571. if ($sBefore == $sAfter)
  572. {
  573. throw new CoreException("Data exchange: Wrong interval specification", array('interval' => $sInterval, 'source_id' => $this->GetKey()));
  574. }
  575. }
  576. $sDeletionDate = $oDeletionDate->Format('Y-m-d H:i:s');
  577. $aTraces[] = "sDeletionDate: $sDeletionDate";
  578. $sSelectToDelete = "SELECT SynchroReplica WHERE sync_source_id = :source_id AND status IN ('obsolete') AND status_last_seen < :last_import";
  579. $oSetToDelete = new DBObjectSet(DBObjectSearch::FromOQL($sSelectToDelete), array() /* order by*/, array('source_id' => $this->GetKey(), 'last_import' => $sDeletionDate));
  580. while($oReplica = $oSetToDelete->Fetch())
  581. {
  582. $sUpdateOnObsolete = $this->Get('delete_policy');
  583. if ( ($sUpdateOnObsolete == 'delete') || ($sUpdateOnObsolete == 'update_then_delete') )
  584. {
  585. $aTraces[] = "Destination object: (dest_id:".$oReplica->Get('dest_id').") to be DELETED";
  586. $oReplica->DeleteDestObject($oMyChange, $oStatLog, $aTraces);
  587. }
  588. $aTraces[] = "Replica id:".$oReplica->GetKey()." (dest_id:".$oReplica->Get('dest_id').") to be deleted";
  589. $oReplica->DBDeleteTracked($oMyChange);
  590. }
  591. }
  592. /**
  593. * Get the list of SQL columns corresponding to a particular list of attribute codes
  594. * Defaults to the whole list of columns for the current class
  595. */
  596. public function GetSQLColumns($aAttributeCodes = null)
  597. {
  598. $aColumns = array();
  599. $sClass = $this->GetTargetClass();
  600. if (is_null($aAttributeCodes))
  601. {
  602. $aAttributeCodes = array();
  603. foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
  604. {
  605. if ($sAttCode == 'finalclass') continue;
  606. $aAttributeCodes[] = $sAttCode;
  607. }
  608. }
  609. foreach($aAttributeCodes as $sAttCode)
  610. {
  611. $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
  612. foreach($oAttDef->GetSQLColumns() as $sField => $sDBFieldType)
  613. {
  614. $aColumns[$sField] = $sDBFieldType;
  615. }
  616. }
  617. return $aColumns;
  618. }
  619. public function IsRunning()
  620. {
  621. $sOQL = "SELECT SynchroLog WHERE sync_source_id = :source_id AND status='running'";
  622. $oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array('start_date' => false) /* order by*/, array('source_id' => $this->GetKey()) /* aArgs */, array(), 1 /* limitCount */, 0 /* limitStart */);
  623. if ($oSet->Count() < 1)
  624. {
  625. $bRet = false;
  626. }
  627. else
  628. {
  629. $bRet = true;
  630. }
  631. return $bRet;
  632. }
  633. public function GetLatestLog()
  634. {
  635. $oLog = null;
  636. $sOQL = "SELECT SynchroLog WHERE sync_source_id = :source_id";
  637. $oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array('start_date' => false) /* order by*/, array('source_id' => $this->GetKey()) /* aArgs */, array(), 1 /* limitCount */, 0 /* limitStart */);
  638. if ($oSet->Count() >= 1)
  639. {
  640. $oLog = $oSet->Fetch();
  641. }
  642. return $oLog;
  643. }
  644. // TO DO: remove if still unused
  645. /**
  646. * Retrieve from the log, the date of the last completed import
  647. * @return DateTime
  648. */
  649. public function GetLastCompletedImportDate()
  650. {
  651. $date = null;
  652. $sOQL = "SELECT SynchroLog WHERE sync_source_id = :source_id AND status='completed'";
  653. $oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array('end_date' => false) /* order by*/, array('source_id' => $this->GetKey()) /* aArgs */, array(), 0 /* limitCount */, 0 /* limitStart */);
  654. if ($oSet->Count() >= 1)
  655. {
  656. $oLog = $oSet->Fetch();
  657. $date = $oLog->Get('end_date');
  658. }
  659. else
  660. {
  661. // TO DO: remove trace
  662. echo "<p>No completed log found</p>\n";
  663. }
  664. return $date;
  665. }
  666. }
  667. class SynchroAttribute extends cmdbAbstractObject
  668. {
  669. public static function Init()
  670. {
  671. $aParams = array
  672. (
  673. "category" => "core/cmdb,view_in_gui",
  674. "key_type" => "autoincrement",
  675. "name_attcode" => "",
  676. "state_attcode" => "",
  677. "reconc_keys" => array(),
  678. "db_table" => "priv_sync_att",
  679. "db_key_field" => "id",
  680. "db_finalclass_field" => "",
  681. "display_template" => "",
  682. );
  683. MetaModel::Init_Params($aParams);
  684. MetaModel::Init_InheritAttributes();
  685. 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())));
  686. MetaModel::Init_AddAttribute(new AttributeString("attcode", array("allowed_values"=>null, "sql"=>"attcode", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
  687. MetaModel::Init_AddAttribute(new AttributeBoolean("update", array("allowed_values"=>null, "sql"=>"update", "default_value"=>true, "is_null_allowed"=>false, "depends_on"=>array())));
  688. MetaModel::Init_AddAttribute(new AttributeBoolean("reconcile", array("allowed_values"=>null, "sql"=>"reconcile", "default_value"=>false, "is_null_allowed"=>false, "depends_on"=>array())));
  689. MetaModel::Init_AddAttribute(new AttributeEnum("update_policy", array("allowed_values"=>new ValueSetEnum('master_locked,master_unlocked,write_if_empty'), "sql"=>"update_policy", "default_value"=>"master_locked", "is_null_allowed"=>false, "depends_on"=>array())));
  690. // Display lists
  691. MetaModel::Init_SetZListItems('details', array('sync_source_id', 'attcode', 'update', 'reconcile', 'update_policy')); // Attributes to be displayed for the complete details
  692. MetaModel::Init_SetZListItems('list', array('sync_source_id', 'attcode', 'update', 'reconcile', 'update_policy')); // Attributes to be displayed for a list
  693. // Search criteria
  694. // MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form
  695. // MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form
  696. }
  697. }
  698. class SynchroAttExtKey extends SynchroAttribute
  699. {
  700. public static function Init()
  701. {
  702. $aParams = array
  703. (
  704. "category" => "core/cmdb,view_in_gui",
  705. "key_type" => "autoincrement",
  706. "name_attcode" => "",
  707. "state_attcode" => "",
  708. "reconc_keys" => array(),
  709. "db_table" => "priv_sync_att_extkey",
  710. "db_key_field" => "id",
  711. "db_finalclass_field" => "",
  712. "display_template" => "",
  713. );
  714. MetaModel::Init_Params($aParams);
  715. MetaModel::Init_InheritAttributes();
  716. MetaModel::Init_AddAttribute(new AttributeString("reconciliation_attcode", array("allowed_values"=>null, "sql"=>"reconciliation_attcode", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
  717. // Display lists
  718. MetaModel::Init_SetZListItems('details', array('sync_source_id', 'attcode', 'update', 'reconcile', 'update_policy', 'reconciliation_attcode')); // Attributes to be displayed for the complete details
  719. MetaModel::Init_SetZListItems('list', array('sync_source_id', 'attcode', 'update', 'reconcile', 'update_policy')); // Attributes to be displayed for a list
  720. // Search criteria
  721. // MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form
  722. // MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form
  723. }
  724. }
  725. class SynchroAttLinkSet extends SynchroAttribute
  726. {
  727. public static function Init()
  728. {
  729. $aParams = array
  730. (
  731. "category" => "core/cmdb,view_in_gui",
  732. "key_type" => "autoincrement",
  733. "name_attcode" => "",
  734. "state_attcode" => "",
  735. "reconc_keys" => array(),
  736. "db_table" => "priv_sync_att_linkset",
  737. "db_key_field" => "id",
  738. "db_finalclass_field" => "",
  739. "display_template" => "",
  740. );
  741. MetaModel::Init_Params($aParams);
  742. MetaModel::Init_InheritAttributes();
  743. MetaModel::Init_AddAttribute(new AttributeString("row_separator", array("allowed_values"=>null, "sql"=>"row_separator", "default_value"=>'|', "is_null_allowed"=>true, "depends_on"=>array())));
  744. MetaModel::Init_AddAttribute(new AttributeString("attribute_separator", array("allowed_values"=>null, "sql"=>"attribute_separator", "default_value"=>';', "is_null_allowed"=>true, "depends_on"=>array())));
  745. // Display lists
  746. 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
  747. MetaModel::Init_SetZListItems('list', array('sync_source_id', 'attcode', 'update', 'reconcile', 'update_policy')); // Attributes to be displayed for a list
  748. // Search criteria
  749. // MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form
  750. // MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form
  751. }
  752. }
  753. //class SynchroLog extends Event
  754. class SynchroLog extends cmdbAbstractObject
  755. {
  756. public static function Init()
  757. {
  758. $aParams = array
  759. (
  760. "category" => "core/cmdb,view_in_gui",
  761. "key_type" => "autoincrement",
  762. "name_attcode" => "",
  763. "state_attcode" => "",
  764. "reconc_keys" => array(),
  765. "db_table" => "priv_sync_log",
  766. "db_key_field" => "id",
  767. "db_finalclass_field" => "",
  768. "display_template" => "",
  769. );
  770. MetaModel::Init_Params($aParams);
  771. MetaModel::Init_InheritAttributes();
  772. // MetaModel::Init_AddAttribute(new AttributeString("userinfo", array("allowed_values"=>null, "sql"=>"userinfo", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
  773. 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())));
  774. MetaModel::Init_AddAttribute(new AttributeDateTime("start_date", array("allowed_values"=>null, "sql"=>"start_date", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array())));
  775. MetaModel::Init_AddAttribute(new AttributeDateTime("end_date", array("allowed_values"=>null, "sql"=>"end_date", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array())));
  776. 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())));
  777. MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_replica_seen", array("allowed_values"=>null, "sql"=>"stats_nb_replica_seen", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array())));
  778. MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_replica_total", array("allowed_values"=>null, "sql"=>"stats_nb_replica_total", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array())));
  779. MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_obj_deleted", array("allowed_values"=>null, "sql"=>"stats_nb_obj_deleted", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array())));
  780. MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_obj_deleted_errors", array("allowed_values"=>null, "sql"=>"stats_deleted_errors", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array())));
  781. MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_obj_obsoleted", array("allowed_values"=>null, "sql"=>"stats_nb_obj_obsoleted", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array())));
  782. MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_obj_obsoleted_errors", array("allowed_values"=>null, "sql"=>"stats_nb_obj_obsoleted_errors", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array())));
  783. MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_obj_created", array("allowed_values"=>null, "sql"=>"stats_nb_obj_created", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array())));
  784. MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_obj_created_errors", array("allowed_values"=>null, "sql"=>"stats_nb_obj_created_errors", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array())));
  785. MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_obj_updated", array("allowed_values"=>null, "sql"=>"stats_nb_obj_updated", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array())));
  786. MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_obj_updated_errors", array("allowed_values"=>null, "sql"=>"stats_nb_obj_updated_errors", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array())));
  787. // MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_replica_reconciled", array("allowed_values"=>null, "sql"=>"stats_nb_replica_reconciled", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array())));
  788. MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_replica_reconciled_errors", array("allowed_values"=>null, "sql"=>"stats_nb_replica_reconciled_errors", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array())));
  789. MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_obj_new_updated", array("allowed_values"=>null, "sql"=>"stats_nb_obj_new_updated", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array())));
  790. MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_obj_new_unchanged", array("allowed_values"=>null, "sql"=>"stats_nb_obj_new_unchanged", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array())));
  791. // Display lists
  792. MetaModel::Init_SetZListItems('details', array('sync_source_id', 'start_date', 'end_date', 'status', 'stats_nb_replica_total', 'stats_nb_replica_seen', 'stats_nb_obj_created', /*'stats_nb_replica_reconciled',*/ 'stats_nb_obj_updated', 'stats_nb_obj_obsoleted', 'stats_nb_obj_deleted',
  793. 'stats_nb_obj_created_errors', 'stats_nb_replica_reconciled_errors', 'stats_nb_obj_updated_errors', 'stats_nb_obj_obsoleted_errors', 'stats_nb_obj_deleted_errors', 'stats_nb_obj_new_unchanged', 'stats_nb_obj_new_updated')); // Attributes to be displayed for the complete details
  794. MetaModel::Init_SetZListItems('list', array('sync_source_id', 'start_date', 'end_date', 'status', 'stats_nb_replica_seen')); // Attributes to be displayed for a list
  795. // Search criteria
  796. // MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form
  797. // MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form
  798. }
  799. /**
  800. * Increments a statistics counter
  801. */
  802. function Inc($sCode)
  803. {
  804. $this->Set($sCode, 1+$this->Get($sCode));
  805. }
  806. }
  807. class SynchroReplica extends DBObject
  808. {
  809. static $aSearches = array(); // Cache of OQL queries used for reconciliation (per data source)
  810. public static function Init()
  811. {
  812. $aParams = array
  813. (
  814. "category" => "core/cmdb,view_in_gui",
  815. "key_type" => "autoincrement",
  816. "name_attcode" => "",
  817. "state_attcode" => "",
  818. "reconc_keys" => array(),
  819. "db_table" => "priv_sync_replica",
  820. "db_key_field" => "id",
  821. "db_finalclass_field" => "",
  822. "display_template" => "",
  823. );
  824. MetaModel::Init_Params($aParams);
  825. MetaModel::Init_InheritAttributes();
  826. 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())));
  827. MetaModel::Init_AddAttribute(new AttributeInteger("dest_id", array("allowed_values"=>null, "sql"=>"dest_id", "default_value"=>0, "is_null_allowed"=>true, "depends_on"=>array())));
  828. 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())));
  829. 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())));
  830. 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())));
  831. 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())));
  832. 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())));
  833. 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())));
  834. 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())));
  835. 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())));
  836. // Display lists
  837. 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
  838. 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
  839. // Search criteria
  840. 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
  841. // MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form
  842. }
  843. public function DBInsert()
  844. {
  845. throw new CoreException('A synchronization replica must be created only by the mean of triggers');
  846. }
  847. // Overload the deletion -> the replica has been created by the mean of a trigger,
  848. // it will be deleted by the mean of a trigger too
  849. public function DBDelete()
  850. {
  851. $oDataSource = MetaModel::GetObject('SynchroDataSource', $this->Get('sync_source_id'));
  852. $sTable = $oDataSource->GetDataTable();
  853. $sSQL = "DELETE FROM `$sTable` WHERE id = '{$this->GetKey()}'";
  854. CMDBSource::Query($sSQL);
  855. $this->m_bIsInDB = false;
  856. $this->m_iKey = null;
  857. }
  858. public function SetLastError($sMessage, $oException = null)
  859. {
  860. if ($oException)
  861. {
  862. $sText = $sMessage.$oException->getMessage();
  863. }
  864. else
  865. {
  866. $sText = $sMessage;
  867. }
  868. if (strlen($sText) > 255)
  869. {
  870. $sText = substr($sText, 0, 200).'...('.strlen($sText).' chars)...';
  871. }
  872. $this->Set('status_last_error', $sText);
  873. }
  874. public function Synchro($oDataSource, $aReconciliationKeys, $aAttributes, $oChange, &$oStatLog, &$aTraces)
  875. {
  876. switch($this->Get('status'))
  877. {
  878. case 'new':
  879. // If needed, construct the query used for the reconciliation
  880. if (!isset(self::$aSearches[$oDataSource->GetKey()]))
  881. {
  882. foreach($aReconciliationKeys as $sFilterCode)
  883. {
  884. $aCriterias[] = ($sFilterCode == 'primary_key' ? 'id' : $sFilterCode).' = :'.$sFilterCode;
  885. }
  886. $sOQL = "SELECT ".$oDataSource->GetTargetClass()." WHERE ".implode(' AND ', $aCriterias);
  887. self::$aSearches[$oDataSource->GetKey()] = DBObjectSearch::FromOQL($sOQL);
  888. }
  889. // Get the criterias for the search
  890. $aFilterValues = array();
  891. foreach($aReconciliationKeys as $sFilterCode)
  892. {
  893. $value = $this->GetValueFromExtData($sFilterCode);
  894. if (!is_null($value))
  895. {
  896. $aFilterValues[$sFilterCode] = $value;
  897. }
  898. else
  899. {
  900. // Reconciliation could not be performed - log and EXIT
  901. $aTraces[] = "Could not reconcile on null value: ".$sFilterCode;
  902. $this->SetLastError('Could not reconcile on null value: '.$sFilterCode);
  903. $oStatLog->Inc('stats_nb_replica_reconciled_errors');
  904. return;
  905. }
  906. }
  907. $oDestSet = new DBObjectSet(self::$aSearches[$oDataSource->GetKey()], array(), $aFilterValues);
  908. $iCount = $oDestSet->Count();
  909. $aConditions = array();
  910. foreach($aFilterValues as $sCode => $sValue)
  911. {
  912. $aConditions[] = $sCode.'='.$sValue;
  913. }
  914. $sConditionDesc = implode(' AND ', $aConditions);
  915. // How many objects match the reconciliation criterias
  916. switch($iCount)
  917. {
  918. case 0:
  919. $aTraces[] = "Nothing found on: $sConditionDesc";
  920. if ($oDataSource->Get('action_on_zero') == 'create')
  921. {
  922. $this->CreateObjectFromReplica($oDataSource->GetTargetClass(), $aAttributes, $oChange, $oStatLog, $aTraces);
  923. }
  924. else // assumed to be 'error'
  925. {
  926. $aTraces[] = "Failed to reconcile (no match)";
  927. $this->SetLastError('Could not find a match for reconciliation');
  928. $oStatLog->Inc('stats_nb_replica_reconciled_errors');
  929. }
  930. break;
  931. case 1:
  932. $aTraces[] = "Found 1 object on: $sConditionDesc";
  933. if ($oDataSource->Get('action_on_one') == 'update')
  934. {
  935. $oDestObj = $oDestSet->Fetch();
  936. $this->UpdateObjectFromReplica($oDestObj, $aAttributes, $oChange, $oStatLog, $aTraces, 'stats_nb_obj_new', 'stats_nb_replica_reconciled_errors');
  937. $this->Set('dest_id', $oDestObj->GetKey());
  938. $this->Set('status_dest_creator', false);
  939. $this->Set('dest_class', get_class($oDestObj));
  940. $oStatLog->Inc('stats_nb_replica_reconciled'); //@@@
  941. }
  942. else
  943. {
  944. // assumed to be 'error'
  945. $aTraces[] = "Failed to reconcile (1 match)";
  946. $this->SetLastError('Found a match while expecting several');
  947. $oStatLog->Inc('stats_nb_replica_reconciled_errors');
  948. }
  949. break;
  950. default:
  951. $aTraces[] = "Found $iCount objects on: $sConditionDesc";
  952. if ($oDataSource->Get('action_on_multiple') == 'error')
  953. {
  954. $aTraces[] = "Failed to reconcile (N>1 matches)";
  955. $this->SetLastError($iCount.' destination objects match the reconciliation criterias: '.$sConditionDesc);
  956. $oStatLog->Inc('stats_nb_replica_reconciled_errors');
  957. }
  958. elseif ($oDataSource->Get('action_on_multiple') == 'create')
  959. {
  960. $this->CreateObjectFromReplica($oDataSource->GetTargetClass(), $aAttributes, $oChange, $oStatLog, $aTraces);
  961. }
  962. else
  963. {
  964. // assumed to be 'take_first'
  965. $oDestObj = $oDestSet->Fetch();
  966. $this->UpdateObjectFromReplica($oDestObj, $aAttributes, $oChange, $oStatLog, $aTraces, 'stats_nb_obj_new', 'stats_nb_replica_reconciled_errors');
  967. $this->Set('dest_id', $oDestObj->GetKey());
  968. $this->Set('status_dest_creator', false);
  969. $this->Set('dest_class', get_class($oDestObj));
  970. }
  971. }
  972. break;
  973. case 'modified':
  974. $oDestObj = MetaModel::GetObject($oDataSource->GetTargetClass(), $this->Get('dest_id'));
  975. if ($oDestObj == null)
  976. {
  977. $this->Set('status', 'orphan'); // The destination object has been deleted !
  978. $this->SetLastError('Destination object deleted unexpectedly');
  979. $oStatLog->Inc('stats_nb_obj_updated_errors');
  980. }
  981. else
  982. {
  983. $this->UpdateObjectFromReplica($oDestObj, $aAttributes, $oChange, $oStatLog, $aTraces, 'stats_nb_obj', 'stats_nb_obj_updated_errors');
  984. }
  985. break;
  986. default: // Do nothing in all other cases
  987. }
  988. $this->DBUpdateTracked($oChange);
  989. }
  990. /**
  991. * Updates the destination object with the Extended data found in the synchro_data_XXXX table
  992. */
  993. protected function UpdateObjectFromReplica($oDestObj, $aAttributes, $oChange, &$oStatLog, &$aTraces, $sStatsCode, $sStatsCodeError)
  994. {
  995. $aValueTrace = array();
  996. foreach($aAttributes as $sAttCode)
  997. {
  998. $value = $this->GetValueFromExtData($sAttCode);
  999. if (!is_null($value))
  1000. {
  1001. $oDestObj->Set($sAttCode, $value);
  1002. $aValueTrace[] = "$sAttCode: $value";
  1003. }
  1004. }
  1005. try
  1006. {
  1007. // Really modified ?
  1008. if ($oDestObj->IsModified())
  1009. {
  1010. $oDestObj->DBUpdateTracked($oChange);
  1011. $aTraces[] = "Updated object ".$oDestObj->GetHyperLink()." (".implode(', ', $aValueTrace).")";
  1012. $oStatLog->Inc($sStatsCode.'_updated');
  1013. }
  1014. else
  1015. {
  1016. $aTraces[] = "Unchanged object ".$oDestObj->GetHyperLink()." (".implode(', ', $aValueTrace).")";
  1017. $oStatLog->Inc($sStatsCode.'_unchanged');
  1018. }
  1019. $this->Set('status_last_error', '');
  1020. $this->Set('status', 'synchronized');
  1021. }
  1022. catch(Exception $e)
  1023. {
  1024. $aTraces[] = "Failed to update destination object: {$e->getMessage()}";
  1025. $this->SetLastError('Unable to update destination object: ', $e);
  1026. $oStatLog->Inc($sStatsCodeError);
  1027. }
  1028. }
  1029. /**
  1030. * Creates the destination object populating it with the Extended data found in the synchro_data_XXXX table
  1031. */
  1032. protected function CreateObjectFromReplica($sClass, $aAttributes, $oChange, &$oStatLog, &$aTraces)
  1033. {
  1034. $oDestObj = MetaModel::NewObject($sClass);
  1035. try
  1036. {
  1037. $aValueTrace = array();
  1038. foreach($aAttributes as $sAttCode)
  1039. {
  1040. $value = $this->GetValueFromExtData($sAttCode);
  1041. if (!is_null($value))
  1042. {
  1043. $oDestObj->Set($sAttCode, $value);
  1044. $aValueTrace[] = "$sAttCode: $value";
  1045. }
  1046. }
  1047. $iNew = $oDestObj->DBInsertTracked($oChange);
  1048. $aTraces[] = "Created $sClass::$iNew (".implode(', ', $aValueTrace).")";
  1049. $this->Set('dest_id', $oDestObj->GetKey());
  1050. $this->Set('dest_class', get_class($oDestObj));
  1051. $this->Set('status_dest_creator', true);
  1052. $this->Set('status_last_error', '');
  1053. $this->Set('status', 'synchronized');
  1054. $oStatLog->Inc('stats_nb_obj_created');
  1055. }
  1056. catch(Exception $e)
  1057. {
  1058. $aTraces[] = "Failed to create $sClass ({$e->getMessage()})";
  1059. $this->SetLastError('Unable to create destination object: ', $e);
  1060. $oStatLog->Inc('stats_nb_obj_created_errors');
  1061. }
  1062. }
  1063. /**
  1064. * Update the destination object with given values
  1065. */
  1066. public function UpdateDestObject($aValues, $oChange, &$oStatLog, &$aTraces, $sStatCode)
  1067. {
  1068. try
  1069. {
  1070. $oDestObj = MetaModel::GetObject($this->Get('dest_class'), $this->Get('dest_id'));
  1071. foreach($aValues as $sAttCode => $value)
  1072. {
  1073. $oDestObj->Set($sAttCode, $value);
  1074. }
  1075. $oDestObj->DBUpdateTracked($oChange);
  1076. $aTraces[] = "Replica id:".$this->GetKey()." (dest_id:".$this->Get('dest_id').") marked as obsolete";
  1077. $oStatLog->Inc($sStatCode);
  1078. }
  1079. catch(Exception $e)
  1080. {
  1081. $this->SetLastError('Unable to update the destination object: ', $e);
  1082. $oStatLog->Inc($sStatCode.'_errors');
  1083. }
  1084. }
  1085. /**
  1086. * Delete the destination object
  1087. */
  1088. public function DeleteDestObject($oChange, &$oStatLog, &$aTraces)
  1089. {
  1090. if($this->Get('status_dest_creator'))
  1091. {
  1092. $oDestObj = MetaModel::GetObject($this->Get('dest_class'), $this->Get('dest_id'));
  1093. try
  1094. {
  1095. $oDestObj->DBDeleteTracked($oChange);
  1096. $oStatLog->Inc('stats_nb_obj_deleted');
  1097. }
  1098. catch(Exception $e)
  1099. {
  1100. $this->SetLastError('Unable to delete the destination object: ', $e);
  1101. $oStatLog->Inc('stats_nb_obj_deleted_errors');
  1102. }
  1103. }
  1104. }
  1105. /**
  1106. * Get the value from the 'Extended Data' located in the synchro_data_xxx table for this replica
  1107. */
  1108. protected function GetValueFromExtData($sColumnName)
  1109. {
  1110. // $aData should contain attributes defined either for reconciliation or update
  1111. $aData = $this->GetExtendedData();
  1112. return $aData[$sColumnName];
  1113. }
  1114. }
  1115. // TO DO: finalize.... admins only ? which options ? troubleshoot WebPageMenuNode::__construct(.... sEnableClass...) ?
  1116. //if (UserRights::IsAdministrator())
  1117. {
  1118. $oAdminMenu = new MenuGroup('AdminTools', 80 /* fRank */);
  1119. new OQLMenuNode('DataSources', 'SELECT SynchroDataSource', $oAdminMenu->GetIndex(), 12 /* fRank */, true, 'SynchroDataSource', UR_ACTION_MODIFY, UR_ALLOWED_YES);
  1120. new WebPageMenuNode('Test:RunSynchro', '../synchro/synchro_exec.php', $oAdminMenu->GetIndex(), 13 /* fRank */, 'SynchroDataSource');
  1121. }
  1122. ?>