Explorar o código

Rework of the relation diagrams: implemented MetaModel::GetRelatedObjectsUp, and took the redundancy into account (still misses a GUI)

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@3545 a333f486-631f-4898-b8df-5754b55c2be0
romainq %!s(int64=10) %!d(string=hai) anos
pai
achega
2151413df9

+ 2 - 1
core/cmdbobject.class.inc.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2015 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -63,6 +63,7 @@ require_once('dbobjectset.class.php');
 require_once('backgroundprocess.inc.php');
 require_once('asynctask.class.inc.php');
 require_once('dbproperty.class.inc.php');
+require_once('redundancysettings.class.inc.php');
 
 // db change tracking data model
 require_once('cmdbchange.class.inc.php');

+ 22 - 6
core/metamodel.class.php

@@ -1390,7 +1390,7 @@ abstract class MetaModel
 	}
 
 	/**
-	 * Compute the "RelatedObjects" (for the given relation, as defined by MetaModel::GetRelatedObjects) for a whole set of DBObjects
+	 * Compute the "RelatedObjects" for a whole set of DBObjects
 	 * 
 	 * @param string $sRelCode The code of the relation to use for the computation
 	 * @param array $asourceObjects The objects to start with
@@ -1404,14 +1404,30 @@ abstract class MetaModel
 		$oGraph = new RelationGraph();
 		foreach ($aSourceObjects as $oObject)
 		{
-			$oSourceNode = new RelationObjectNode($oGraph, $oObject);
-			$oSourceNode->SetProperty('source', true);
+			$oGraph->AddSourceObject($oObject);
 		}
-		$aSourceNodes = $oGraph->_GetNodes();
-		foreach ($aSourceNodes as $oSourceNode)
+		$oGraph->ComputeRelatedObjectsDown($sRelCode, $iMaxDepth, $bEnableRedundancy);
+		return $oGraph;
+	}
+
+	/**
+	 * Compute the "RelatedObjects" in the reverse way
+	 * 
+	 * @param string $sRelCode The code of the relation to use for the computation
+	 * @param array $asourceObjects The objects to start with
+	 * @param int $iMaxDepth
+	 * @param boolean $bEnableReduncancy
+	 * 
+	 * @return RelationGraph The graph of all the related objects
+	 */
+	static public function GetRelatedObjectsUp($sRelCode, $aSourceObjects, $iMaxDepth = 99, $bEnableRedundancy = true)
+	{
+		$oGraph = new RelationGraph();
+		foreach ($aSourceObjects as $oObject)
 		{
-			$oGraph->AddRelatedObjectsDown($sRelCode, $oSourceNode, $iMaxDepth, $bEnableRedundancy);
+			$oGraph->AddSinkObject($oObject);
 		}
+		$oGraph->ComputeRelatedObjectsUp($sRelCode, $iMaxDepth, $bEnableRedundancy);
 		return $oGraph;
 	}
 

+ 98 - 0
core/redundancysettings.class.inc.php

@@ -0,0 +1,98 @@
+<?php
+// Copyright (C) 2015 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify	
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
+/**
+ * Persistent classes (internal): user settings for the redundancy
+ *
+ * @copyright   Copyright (C) 2015 Combodo SARL
+ * @license     http://opensource.org/licenses/AGPL-3.0
+ */
+
+
+/**
+ * Redundancy settings
+ *
+ * @package     iTopORM
+ */
+class RedundancySettings extends DBObject
+{
+	public static function Init()
+	{
+		$aParams = array
+		(
+			"category" => "core/cmdb",
+			"key_type" => "autoincrement",
+			"name_attcode" => array('relation_code','from_class','neighbour','objkey'),
+			"state_attcode" => "",
+			"reconc_keys" => array(),
+			"db_table" => "priv_redundancy_settings",
+			"db_key_field" => "id",
+			"db_finalclass_field" => "finalclass",
+			"display_template" => "",
+			'indexes' => array(
+				array('relation_code', 'from_class', 'neighbour', 'objclass', 'objkey'),
+			)
+		);
+		MetaModel::Init_Params($aParams);
+		MetaModel::Init_AddAttribute(new AttributeString("relation_code", array("allowed_values"=>null, "sql"=>"relation_code", "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array())));
+		MetaModel::Init_AddAttribute(new AttributeString("from_class", array("allowed_values"=>null, "sql"=>"from_class", "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array())));
+		MetaModel::Init_AddAttribute(new AttributeString("neighbour", array("allowed_values"=>null, "sql"=>"neighbour", "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array())));
+
+		MetaModel::Init_AddAttribute(new AttributeString("objclass", array("allowed_values"=>null, "sql"=>"objclass", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array())));
+		MetaModel::Init_AddAttribute(new AttributeObjectKey("objkey", array("allowed_values"=>null, "class_attcode"=>"objclass", "sql"=>"objkey", "is_null_allowed"=>false, "depends_on"=>array())));
+
+		MetaModel::Init_AddAttribute(new AttributeBoolean("enabled", array("allowed_values"=>null, "sql"=>"enabled", "default_value"=>false, "is_null_allowed"=>false, "depends_on"=>array())));
+
+		MetaModel::Init_AddAttribute(new AttributeEnum("min_up_type", array("allowed_values"=>new ValueSetEnum('count,percent'), "sql"=>"min_up_type", "default_value"=>"count", "is_null_allowed"=>false, "depends_on"=>array())));
+		MetaModel::Init_AddAttribute(new AttributeInteger("min_up_count", array("allowed_values"=>null, "sql"=>"min_up_count", "default_value"=>1, "is_null_allowed"=>true, "depends_on"=>array())));
+		MetaModel::Init_AddAttribute(new AttributeInteger("min_up_percent", array("allowed_values"=>null, "sql"=>"min_up_percent", "default_value"=>50, "is_null_allowed"=>true, "depends_on"=>array())));
+	}
+
+	public static function MakeDefault($sRelCode, $aQueryInfo, $oToObject)
+	{
+		$oRet = MetaModel::NewObject('RedundancySettings');
+		$oRet->Set('relation_code', $sRelCode);
+		$oRet->Set('from_class', $aQueryInfo['sFromClass']);
+		$oRet->Set('neighbour', $aQueryInfo['sNeighbour']);
+		$oRet->Set('objclass', get_class($oToObject));
+		$oRet->Set('objkey', $oToObject->GetKey());
+		$oRet->Set('enabled', $aQueryInfo['bRedundancyEnabledValue']);
+		$oRet->Set('min_up_type', $aQueryInfo['sRedundancyMinUpType']);
+		$oRet->Set('min_up_count', ($aQueryInfo['sRedundancyMinUpType'] == 'count') ? $aQueryInfo['iRedundancyMinUpValue'] : 1);
+		$oRet->Set('min_up_percent', ($aQueryInfo['sRedundancyMinUpType'] == 'percent') ? $aQueryInfo['iRedundancyMinUpValue'] : 50);
+		return $oRet;
+	}
+
+	public static function GetSettings($sRelCode, $aQueryInfo, $oToObject)
+	{
+		$oSearch = new DBObjectSearch('RedundancySettings');
+		$oSearch->AddCondition('relation_code', $sRelCode, '=');
+		$oSearch->AddCondition('from_class', $aQueryInfo['sFromClass'], '=');
+		$oSearch->AddCondition('neighbour', $aQueryInfo['sNeighbour'], '=');
+		$oSearch->AddCondition('objclass', get_class($oToObject), '=');
+		$oSearch->AddCondition('objkey', $oToObject->GetKey(), '=');
+
+		$oSet = new DBObjectSet($oSearch);
+		$oRet = $oSet->Fetch();
+		if (!$oRet)
+		{
+			$oRet = self::MakeDefault($sRelCode, $aQueryInfo, $oToObject);
+		}
+		return $oRet;
+	}
+}

+ 363 - 22
core/relationgraph.class.inc.php

@@ -37,11 +37,58 @@ class RelationObjectNode extends GraphNode
 		$this->SetProperty('label', get_class($oObject).'::'.$oObject->GetKey().' ('.$oObject->Get('friendlyname').')');
 	}
 
+	/**
+	 * Make a normalized ID to ensure the uniqueness of such a node	
+	 */	
 	public static function MakeId($oObject)
 	{
 		return get_class($oObject).'::'.$oObject->GetKey();
 	}
 
+	/**
+	 * Formatting for GraphViz
+	 */	 	
+	public function GetDotAttributes()
+	{
+		$sDot = parent::GetDotAttributes();
+		if ($this->GetProperty('developped', false))
+		{
+			$sDot .= ',fontcolor=black';
+		}
+		else
+		{
+			$sDot .= ',fontcolor=lightgrey';
+		}
+		if ($this->GetProperty('source', false) || $this->GetProperty('sink', false))
+		{
+			$sDot .= ',shape=rectangle';
+		}
+		if ($this->GetProperty('is_reached', false))
+		{
+			$sDot .= ',fillcolor="#ffdddd"';
+		}
+		else
+		{
+			$sDot .= ',fillcolor=white';
+		}
+		return $sDot;
+	}
+
+	/**
+	 * Recursively mark the objects nodes as reached, unless we get stopped by a redundancy node
+	 */	 	
+	public function ReachDown()
+	{
+		if (!$this->GetProperty('is_reached', false))
+		{
+			$this->SetProperty('is_reached', true);
+			foreach ($this->GetOutgoingEdges() as $oOutgoingEdge)
+			{
+				// Recurse
+				$oOutgoingEdge->GetSinkNode()->ReachDown();
+			}
+		}
+	}
 }
 
 /**
@@ -49,63 +96,357 @@ class RelationObjectNode extends GraphNode
  */
 class RelationRedundancyNode extends GraphNode
 {
+	public function __construct($oGraph, $sId, $fThreshold)
+	{
+		parent::__construct($oGraph, $sId);
+		$this->SetProperty('threshold', $fThreshold);
+		$this->SetProperty('reach_count', 0);
+	}
+
+	/**
+	 * Make a normalized ID to ensure the uniqueness of such a node	
+	 */	
+	public static function MakeId($sRelCode, $sNeighbourId, $oSinkObject)
+	{
+		return 'redundancy-'.$sRelCode.'-'.$sNeighbourId.'-'.get_class($oSinkObject).'::'.$oSinkObject->GetKey();
+	}
+
+	/**
+	 * Formatting for GraphViz
+	 */	 	
 	public function GetDotAttributes()
 	{
-		$sDot = 'shape=point,label="'.$this->GetProperty('threshold').'"';
+		$sDisplayThreshold = sprintf('%.1f', $this->GetProperty('threshold'));
+		$sDot = 'shape=doublecircle,fillcolor=indianred,fontcolor=papayawhip,label="'.$sDisplayThreshold.'"';
 		return $sDot;
-// shape=point
+	}
+
+	/**
+	 * Recursively mark the objects nodes as reached, unless we get stopped by a redundancy node
+	 */	 	
+	public function ReachDown()
+	{
+		$this->SetProperty('reach_count', $this->GetProperty('reach_count', 0) + 1);
+		if ($this->GetProperty('reach_count') > $this->GetProperty('threshold'))
+		{
+			// Looping... though there should be only ONE SINGLE outgoing edge
+			foreach ($this->GetOutgoingEdges() as $oOutgoingEdge)
+			{
+				// Recurse
+				$oOutgoingEdge->GetSinkNode()->ReachDown();
+			}
+		}
 	}
 }
 
 
+/**
+ * Helper to name the edges in a unique way
+ */ 
+class RelationEdge extends GraphEdge
+{
+	public function __construct(SimpleGraph $oGraph, GraphNode $oSourceNode, GraphNode $oSinkNode)
+	{
+		$sId = $oSourceNode->GetId().'-to-'.$oSinkNode->GetId();
+		parent::__construct($oGraph, $sId, $oSourceNode, $oSinkNode);
+	}
+}
+
+/**
+ * A graph representing the relations between objects
+ * The graph is made of two types of nodes. Here is a list of the meaningful node properties
+ * 1) RelationObjectNode
+ *    source: boolean, that node was added as a source node
+ *    sink: boolean, that node was added as a sink node
+ *    reached: boolean, that node has been marked as reached (impacted by the source nodes)
+ *    developped: boolean, that node has been visited to search for related objects    
+ * 1) RelationRedundancyNode
+ *    reached_count: int, the number of source nodes having reached=true
+ *    threshold: float, if reached_count > threshold, the sink nodes become reachable    
+ */ 
 class RelationGraph extends SimpleGraph
 {
+	protected $aSourceNodes; // Index of source nodes (for a quicker access)
+	protected $aSinkNodes; // Index of sink nodes (for a quicker access)
+	protected $aRedundancySettings; // Cache of user settings
+
+	public function __construct()
+	{
+		parent::__construct();
+		$this->aSourceNodes = array();
+		$this->aSinkNodes = array();
+		$this->aRedundancySettings = array();
+	}
+
+	/**
+	 * Add an object that will be the starting point for building the relations downstream
+	 */	 	
+	public function AddSourceObject(DBObject $oObject)
+	{
+		$oSourceNode = new RelationObjectNode($this, $oObject);
+		$oSourceNode->SetProperty('source', true);
+		$this->aSourceNodes[$oSourceNode->GetId()] = $oSourceNode;
+	}
+
+	/**
+	 * Add an object that will be the starting point for building the relations uptream
+	 */	 	
+	public function AddSinkObject(DBObject$oObject)
+	{
+		$oSinkNode = new RelationObjectNode($this, $oObject);
+		$oSinkNode->SetProperty('sink', true);
+		$this->aSinkNodes[$oSinkNode->GetId()] = $oSinkNode;
+	}
+
+	/**
+	 * Build the graph downstream, and mark the nodes that can be reached from the source node
+	 */	 	
+	public function ComputeRelatedObjectsDown($sRelCode, $iMaxDepth, $bEnableRedundancy)
+	{
+		//echo "<h5>Sources only...</h5>\n".$this->DumpAsHtmlImage()."<br/>\n";
+		// Build the graph out of the sources
+		foreach ($this->aSourceNodes as $oSourceNode)
+		{
+			$this->AddRelatedObjects($sRelCode, true, $oSourceNode, $iMaxDepth, $bEnableRedundancy);
+			//echo "<h5>After processing of {$oSourceNode->GetId()}</h5>\n".$this->DumpAsHtmlImage()."<br/>\n";
+		}
+		// Determine the reached nodes
+		foreach ($this->aSourceNodes as $oSourceNode)
+		{
+			$oSourceNode->ReachDown();
+			//echo "<h5>After reaching from {$oSourceNode->GetId()}</h5>\n".$this->DumpAsHtmlImage()."<br/>\n";
+		}
+	}
+
+	/**
+	 * Build the graph upstream
+	 */	 	
+	public function ComputeRelatedObjectsUp($sRelCode, $iMaxDepth, $bEnableRedundancy)
+	{
+		//echo "<h5>Sinks only...</h5>\n".$this->DumpAsHtmlImage()."<br/>\n";
+		// Build the graph out of the sinks
+		foreach ($this->aSinkNodes as $oSinkNode)
+		{
+			$this->AddRelatedObjects($sRelCode, false, $oSinkNode, $iMaxDepth, $bEnableRedundancy);
+			//echo "<h5>After processing of {$oSinkNode->GetId()}</h5>\n".$this->DumpAsHtmlImage()."<br/>\n";
+		}
+	}
+
+
 	/**
 	 * Recursively find related objects, and add them into the graph
 	 * 
 	 * @param string $sRelCode The code of the relation to use for the computation
+	 * @param boolean $bDown The direction: downstream or upstream
 	 * @param array $oObjectNode The node from which to compute the neighbours
 	 * @param int $iMaxDepth
 	 * @param boolean $bEnableReduncancy
 	 * 
 	 * @return void
 	 */
-	public function AddRelatedObjectsDown($sRelCode, $oObjectNode, $iMaxDepth, $bEnableRedundancy)
+	protected function AddRelatedObjects($sRelCode, $bDown, $oObjectNode, $iMaxDepth, $bEnableRedundancy)
 	{
 		if ($iMaxDepth > 0)
 		{
-			$oObject = $oObjectNode->GetProperty('object');
-			foreach (MetaModel::EnumRelationQueries(get_class($oObject), $sRelCode, true) as $sDummy => $aQueryInfo)
+			if ($oObjectNode instanceof RelationRedundancyNode)
+			{
+				// Note: this happens when recursing on an existing part of the graph 
+				// Skip that redundancy node
+				$aRelatedEdges = $bDown ? $oObjectNode->GetOutgoingEdges() : $oObjectNode->GetIncomingEdges();
+				foreach ($aRelatedEdges as $oRelatedEdge)
+				{
+					$oRelatedNode = $bDown ? $oRelatedEdge->GetSinkNode() : $oRelatedEdge->GetSourceNode();
+					// Recurse (same depth)
+					$this->AddRelatedObjects($sRelCode, $bDown, $oRelatedNode, $iMaxDepth, $bEnableRedundancy);
+				}
+			}
+			elseif ($oObjectNode->GetProperty('developped', false))
 			{
-				$sQuery = $aQueryInfo['sQueryDown'];
+				// No need to execute the queries again... just dig into the nodes down/up to iMaxDepth
+				//
+				$aRelatedEdges = $bDown ? $oObjectNode->GetOutgoingEdges() : $oObjectNode->GetIncomingEdges();
+				foreach ($aRelatedEdges as $oRelatedEdge)
+				{
+					$oRelatedNode = $bDown ? $oRelatedEdge->GetSinkNode() : $oRelatedEdge->GetSourceNode();
+					// Recurse (decrement the depth)
+					$this->AddRelatedObjects($sRelCode, $bDown, $oRelatedNode, $iMaxDepth - 1, $bEnableRedundancy);
+				}
+			}
+			else
+			{
+				$oObjectNode->SetProperty('developped', true);
+	
+				$oObject = $oObjectNode->GetProperty('object');
+				foreach (MetaModel::EnumRelationQueries(get_class($oObject), $sRelCode, $bDown) as $sDummy => $aQueryInfo)
+				{
+	 				$sQuery = $bDown ? $aQueryInfo['sQueryDown'] : $aQueryInfo['sQueryUp'];
+					try
+					{
+						$oFlt = DBObjectSearch::FromOQL($sQuery);
+						$oObjSet = new DBObjectSet($oFlt, array(), $oObject->ToArgsForQuery());
+						$oRelatedObj = $oObjSet->Fetch();
+					}
+					catch (Exception $e)
+					{
+						$sDirection = $bDown ? 'downstream' : 'upstream';
+						throw new Exception("Wrong query ($sDirection) for the relation $sRelCode/{$aQueryInfo['sDefinedInClass']}/{$aQueryInfo['sNeighbour']}: ".$e->getMessage());
+					}
+					if ($oRelatedObj)
+					{
+						do
+						{
+							$sObjectRef = 	RelationObjectNode::MakeId($oRelatedObj);
+							$oRelatedNode = $this->GetNode($sObjectRef);
+							if (is_null($oRelatedNode))
+							{	
+								$oRelatedNode = new RelationObjectNode($this, $oRelatedObj);
+							}
+							$oSourceNode = $bDown ? $oObjectNode : $oRelatedNode;
+							$oSinkNode = $bDown ? $oRelatedNode : $oObjectNode;
+							if ($bEnableRedundancy)
+							{
+								$oRedundancyNode = $this->ComputeRedundancy($sRelCode, $aQueryInfo, $oSourceNode, $oSinkNode);
+							}
+							else
+							{
+								$oRedundancyNode = null;
+							}
+							if (!$oRedundancyNode)
+							{
+								// Direct link (otherwise handled by ComputeRedundancy)
+								$oEdge = new RelationEdge($this, $oSourceNode, $oSinkNode);
+							}
+							// Recurse
+							$this->AddRelatedObjects($sRelCode, $bDown, $oRelatedNode, $iMaxDepth - 1, $bEnableRedundancy);
+	
+						}
+						while ($oRelatedObj = $oObjSet->Fetch());
+					}
+				}
+			}
+		}
+	}
+
+	/**
+	 * Determine if there is a redundancy (or use the existing one) and add the corresponding nodes/edges	
+	 */	
+	protected function ComputeRedundancy($sRelCode, $aQueryInfo, $oFromNode, $oToNode)
+	{
+		$oRedundancyNode = null;
+		$oObject = $oToNode->GetProperty('object');
+		if ($this->IsRedundancyEnabled($sRelCode, $aQueryInfo, $oToNode))
+		{
+
+			$sId = RelationRedundancyNode::MakeId($sRelCode, $aQueryInfo['sNeighbour'], $oToNode->GetProperty('object'));
+
+			$oRedundancyNode = $this->GetNode($sId);
+			if (is_null($oRedundancyNode))
+			{
+				// Get the upper neighbours
+				$sQuery = $aQueryInfo['sQueryUp'];
 				try
 				{
 					$oFlt = DBObjectSearch::FromOQL($sQuery);
 					$oObjSet = new DBObjectSet($oFlt, array(), $oObject->ToArgsForQuery());
-					$oRelatedObj = $oObjSet->Fetch();
+					$iCount = $oObjSet->Count();
 				}
 				catch (Exception $e)
 				{
-					throw new Exception("Wrong query (downstream) for the relation $sRelCode/{$aQueryInfo['sDefinedInClass']}/{$aQueryInfo['sNeighbour']}: ".$e->getMessage());
+					throw new Exception("Wrong query (upstream) for the relation $sRelCode/{$aQueryInfo['sDefinedInClass']}/{$aQueryInfo['sNeighbour']}: ".$e->getMessage());
 				}
-				if ($oRelatedObj)
-				{
-					do
-					{
-						$sObjectRef = 	RelationObjectNode::MakeId($oRelatedObj);
-						$oRelatedNode = $this->GetNode($sObjectRef);
-						if (is_null($oRelatedNode))
-						{	
-							$oRelatedNode = new RelationObjectNode($this, $oRelatedObj);
 	
-							// Recurse
-							$this->AddRelatedObjectsDown($sRelCode, $oRelatedNode, $iMaxDepth - 1, $bEnableRedundancy);
-						}
-						$oEdge = new GraphEdge($this, $oObjectNode->GetId().' to '.$oRelatedNode->GetId(), $oObjectNode, $oRelatedNode);
+				$fThreshold = $this->GetRedundancyThreshold($sRelCode, $aQueryInfo, $oToNode, $iCount);
+				$oRedundancyNode = new RelationRedundancyNode($this, $sId, $fThreshold);
+				new RelationEdge($this, $oRedundancyNode, $oToNode);
+	
+				while ($oUpperObj = $oObjSet->Fetch())
+				{
+					$sObjectRef = 	RelationObjectNode::MakeId($oUpperObj);
+					$oUpperNode = $this->GetNode($sObjectRef);
+					if (is_null($oUpperNode))
+					{	
+						$oUpperNode = new RelationObjectNode($this, $oUpperObj);
 					}
-					while ($oRelatedObj = $oObjSet->Fetch());
+					new RelationEdge($this, $oUpperNode, $oRedundancyNode);
 				}
 			}
 		}
+		return $oRedundancyNode;
+	}
+
+	/**
+	 * Helper to determine the redundancy setting on a given relation	
+	 */	
+	protected function IsRedundancyEnabled($sRelCode, $aQueryInfo, $oToNode)
+	{
+		$bRet = false;
+		if (isset($aQueryInfo['sRedundancyEnabledMode']))
+		{
+			if ($aQueryInfo['sRedundancyEnabledMode'] == 'fixed')
+			{
+				$bRet = $aQueryInfo['bRedundancyEnabledValue'];
+			}
+			elseif ($aQueryInfo['sRedundancyEnabledMode'] == 'user')
+			{
+				$oUserSettings = $this->FindRedundancyUserSettings($sRelCode, $aQueryInfo, $oToNode);
+				$bRet = $oUserSettings->Get('enabled');
+			}
+		}
+		return $bRet;
+	}
+
+	/**
+	 * Helper to determine the redundancy threshold, given the count of objects upstream 	
+	 */	
+	protected function GetRedundancyThreshold($sRelCode, $aQueryInfo, $oToNode, $iUpstreamObjects)
+	{
+		$fThreshold = $iUpstreamObjects;
+		if (isset($aQueryInfo['sRedundancyMinUpMode']))
+		{
+			if ($aQueryInfo['sRedundancyMinUpMode'] == 'fixed')
+			{
+				if ($aQueryInfo['sRedundancyMinUpType'] == 'count')
+				{
+					$fThreshold = $iUpstreamObjects - $aQueryInfo['iRedundancyMinUpValue'];
+				}
+				else // 'percent' assumed
+				{
+					$fThreshold = $iUpstreamObjects - ($iUpstreamObjects * $aQueryInfo['iRedundancyMinUpValue'] / 100);
+				}
+			}
+			elseif ($aQueryInfo['sRedundancyMinUpMode'] == 'user')
+			{
+				$oUserSettings = $this->FindRedundancyUserSettings($sRelCode, $aQueryInfo, $oToNode);
+				if ($oUserSettings->Get('min_up_type') == 'count')
+				{
+					$fThreshold = $iUpstreamObjects - $oUserSettings->Get('min_up_count');
+				}
+				else
+				{
+					$fThreshold = $iUpstreamObjects - ($iUpstreamObjects * $oUserSettings->Get('min_up_percent') / 100);
+				}
+			}
+		}
+		return max(0, $fThreshold);
+	}
+
+	/**
+	 * Helper to search for and cache the reduncancy user settings (could be an object NOT recorded in the DB)	
+	 */	
+	protected function FindRedundancyUserSettings($sRelCode, $aQueryInfo, $oToNode)
+	{
+		$sNeighbourKey = $sRelCode.'/'.$aQueryInfo['sFromClass'].'/'.$aQueryInfo['sNeighbour'];
+		if (isset($this->aRedundancySettings[$sNeighbourKey][$oToNode->GetId()]))
+		{
+			// Cache hit
+			$oUserSettings = $this->aRedundancySettings[$sNeighbourKey][$oToNode->GetId()];
+		}
+		else
+		{
+			// Cache miss: build the entry
+			$oUserSettings = RedundancySettings::GetSettings($sRelCode, $aQueryInfo, $oToNode->GetProperty('object'));
+			$this->aRedundancySettings[$sNeighbourKey][$oToNode->GetId()] = $oUserSettings;
+		}
+		return $oUserSettings;
 	}
 }

+ 11 - 0
datamodels/2.x/itop-config-mgmt/datamodel.itop-config-mgmt.xml

@@ -1472,6 +1472,17 @@
             </neighbour>
             <neighbour id="applicationsolution">
               <attribute>applicationsolution_list</attribute>
+              <redundancy>
+                <enabled>
+                  <value>false</value>
+                  <mode>user</mode>
+                </enabled>
+                <min_up>
+                  <type>count</type>
+                  <value>1</value>
+                  <mode>user</mode>
+                </min_up>
+              </redundancy>
             </neighbour>
             <neighbour id="softwareinstance">
               <attribute>softwares_list</attribute>

+ 11 - 0
datamodels/2.x/itop-datacenter-mgmt/datamodel.itop-datacenter-mgmt.xml

@@ -533,6 +533,17 @@
             <neighbour id="datacenterdevice">
               <query_down>SELECT DatacenterDevice WHERE powerA_id = :this-&gt;id OR powerB_id = :this-&gt;id</query_down>
               <query_up>SELECT PowerConnection WHERE id = :this-&gt;powerA_id OR id = :this-&gt;powerB_id</query_up>
+              <redundancy>
+                <enabled>
+                  <value>true</value>
+                  <mode>fixed</mode>
+                </enabled>
+                <min_up>
+                  <type>count</type>
+                  <value>1</value>
+                  <mode>fixed</mode>
+                </min_up>
+              </redundancy>
             </neighbour>
             <neighbour id="pdu">
               <query_down>SELECT PDU WHERE powerstart_id = :this-&gt;id</query_down>

+ 11 - 0
datamodels/2.x/itop-virtualization-mgmt/datamodel.itop-virtualization-mgmt.xml

@@ -407,6 +407,17 @@
           <neighbours>
             <neighbour id="farm">
               <attribute>farm_id</attribute>
+              <redundancy>
+                <enabled>
+                  <value>true</value>
+                  <mode>user</mode>
+                </enabled>
+                <min_up>
+                  <type>count</type>
+                  <value>1</value>
+                  <mode>user</mode>
+                </min_up>
+              </redundancy>
             </neighbour>
           </neighbours>
         </relation>

+ 6 - 5
setup/compiler.class.inc.php

@@ -1449,11 +1449,12 @@ EOF;
 					if ($oRedundancy)
 					{
 						$oEnabled = $oRedundancy->GetUniqueElement('enabled');
-						$aData['bEnabledValue'] = ($oEnabled->GetChildText('value', 'false') == 'true');
-						$aData['sEnabledMode'] = $oEnabled->GetChildText('mode', 'fixed');
-						$oThreshold = $oRedundancy->GetUniqueElement('threshold');
-						$aData['iThresholdValue'] = (int) $oThreshold->GetChildText('value', 1);
-						$aData['sThresholdMode'] = $oThreshold->GetChildText('mode', 'fixed');
+						$aData['bRedundancyEnabledValue'] = ($oEnabled->GetChildText('value', 'false') == 'true');
+						$aData['sRedundancyEnabledMode'] = $oEnabled->GetChildText('mode', 'fixed');
+						$oMinUp = $oRedundancy->GetUniqueElement('min_up');
+						$aData['sRedundancyMinUpType'] = $oMinUp->GetChildText('type', 'count');
+						$aData['iRedundancyMinUpValue'] = (int) $oMinUp->GetChildText('value', 1);
+						$aData['sRedundancyMinUpMode'] = $oMinUp->GetChildText('mode', 'fixed');
 					}
 
 					$aRelations[$sRelationId][$sNeighbourId] = $aData;