View Issue Details

IDProjectCategoryView StatusLast Update
691RackTablesdefaultpublic2013-04-16 16:24
ReporterMWilkinson Assigned Toandriyanov  
PrioritynormalSeverityfeatureReproducibilityN/A
Status assignedResolutionopen 
Product Version0.20.1 
Summary691: Patch to allow plugins to add Portlets to Object default page
DescriptionThe patch re-works the object default page rendering mechanism to introduce a feature similar to the tabs functionality allowing plugins to insert information within the rendered page rather than just before or after.

Included is a demo-plugin which as well as demonstrating this feature provides a new tab and update operation.
Steps To ReproduceApply patch against RackTables version 0.20.1
Additional InformationThis could probably be extended to other default pages, but as the plugin I was working on dealt only with the objects, that is all I currently looked at implementing.
Hopefully you will find this useful.

Regards
Mark
TagsNo tags attached.
Attached Files
racktables-0.20.1.patch (24,843 bytes)   
diff -urN RackTables-0.20.1-orig/plugins/demo-plugin.php RackTables-0.20.1/plugins/demo-plugin.php
--- RackTables-0.20.1-orig/plugins/demo-plugin.php	1970-01-01 01:00:00.000000000 +0100
+++ RackTables-0.20.1/plugins/demo-plugin.php	2012-12-07 12:44:22.219569142 +0000
@@ -0,0 +1,53 @@
+<?php
+
+$tab['object']['demo'] = 'Demo';
+registerTabHandler('object', 'demo', 'myRenderDemo');
+registerOpHandler('object', 'demo', 'update', 'updateDemo');
+registerPortletHandler( 'object', 'default', 'left', 'Demo', 'myRenderObPortletDemo', 'after', 'Comment');
+
+
+function myRenderObPortletDemo ( $info )
+{
+	startPortlet('Demo Plugin Portlet');
+	echo "This text was produced by the Demo Plugin\n";
+	finishPortlet();
+}
+
+$msgcode['updateDemo']['OK'] = 43;
+function updateDemo()
+{
+        assertUIntArg ('object_id');
+//
+// Update something here 
+//
+
+        showFuncMessage (__FUNCTION__, 'OK');
+        return buildRedirectURL (NULL, NULL, NULL);
+}
+
+function myRenderDemo ()
+{
+        global $pageno, $virtual_obj_types;
+        $object_id = getBypassValue();
+        $info = spotEntity ('object', $object_id);
+	amplifyCell ($info);
+
+        startPortlet ();
+        printOpFormIntro ('update');
+
+        echo '<table border=0 cellspacing=0 cellpadding=3 align=center>';
+        echo "<tr>";
+        echo "<td>&nbsp;</td>";
+        echo "<td>Insert form here</td>";
+        echo "<td>&nbsp;</td>";
+        echo "</tr>\n";
+        echo "<tr><th class=submit colspan=3>";
+        printImageHREF ('SAVE', 'Save changes', TRUE);
+
+        echo "</tr></table>";
+        
+
+        echo "</form></th></tr></table>\n";
+        finishPortlet();
+
+}
diff -urN RackTables-0.20.1-orig/wwwroot/inc/functions.php RackTables-0.20.1/wwwroot/inc/functions.php
--- RackTables-0.20.1-orig/wwwroot/inc/functions.php	2012-10-04 13:06:14.000000000 +0100
+++ RackTables-0.20.1/wwwroot/inc/functions.php	2012-12-07 12:44:16.039569296 +0000
@@ -5656,4 +5656,51 @@
 	return arePortTypesCompatible ($portinfo_a['oif_id'], $portinfo_b['oif_id']);
 }
 
+// Registers additional portleyhandler on page-tab-column triplet.
+// Valid $column values are 'left', 'right'
+//   'left' puts you portlet in the left hand column on a page (default)
+//   'right' puts you portlet in the left hand column on a page
+// $name is used as an identifer when inserting between current portlets
+// Valid $method values are 'before', 'after'.
+//   'before' puts your tabhandler in the beginning of the list (and thus before the default)
+//   'after' puts your tabhandler to the end of the list (and thus after the default)
+// $name2 modified the behaviour of $method to insert the new portlet between currently definded portlets on
+// the page.
+function registerPortletHandler ($page, $tab, $column = 'left', $name, $callback, $method = 'after', $name2 = '')
+{
+	global $portlethandler;
+
+	if ($name2 == '')
+		if ($method == 'before')
+			array_unshift ($portlethandler[$page][$tab][$column], array( 'name' => $name, 'callback' => $callback));
+		elseif ($method == 'after')
+			array_push ($portlethandler[$page][$tab][$column], array( 'name' => $name, 'callback' => $callback));
+		else
+			throw new RacktablesError ("unknown portlethandler injection method '$method'", RackTablesError::INTERNAL);
+	else
+	{
+		$done = 0;
+		foreach ($portlethandler[$page][$tab][$column] as $idx => $portlet)
+			if ($portlet['name'] == $name2)
+			{
+				if ($method == 'before')
+				{
+					array_splice($portlethandler[$page][$tab][$column], $idx, 0, array(array('name'=> $name, 'callback' => $callback)));
+					$done = 1;
+				}
+				elseif ($method == 'after')
+				{
+					array_splice($portlethandler[$page][$tab][$column], ($idx+1), 0, array(array('name'=> $name, 'callback' => $callback)));
+					$done = 1;
+				}
+				else
+					throw new RacktablesError ("unknown portlethandler injection method '$method'", RackTablesError::INTERNAL);
+				break;
+			}
+		if ( ! $done )
+			throw new RacktablesError ("unknown portlet ($name2) to insert $method", RackTablesError::MISCONFIGURED);
+	}
+
+}
+
 ?>
diff -urN RackTables-0.20.1-orig/wwwroot/inc/interface.php RackTables-0.20.1/wwwroot/inc/interface.php
--- RackTables-0.20.1-orig/wwwroot/inc/interface.php	2012-10-04 13:06:14.000000000 +0100
+++ RackTables-0.20.1/wwwroot/inc/interface.php	2012-12-07 11:57:17.109569551 +0000
@@ -12,6 +12,7 @@
 
 require_once 'ajax-interface.php';
 require_once 'slb-interface.php';
+require_once 'portlets.php';
 
 // Interface function's special.
 $nextorder['odd'] = 'even';
@@ -1101,237 +1102,26 @@
 function renderObject ($object_id)
 {
 	global $nextorder, $virtual_obj_types;
+        global $portlethandler;
 	$info = spotEntity ('object', $object_id);
 	amplifyCell ($info);
 	// Main layout starts.
 	echo "<table border=0 class=objectview cellspacing=0 cellpadding=0>";
 	echo "<tr><td colspan=2 align=center><h1>${info['dname']}</h1></td></tr>\n";
 	// left column with uknown number of portlets
-	echo "<tr><td class=pcleft>";
-
-	// display summary portlet
-	$summary  = array();
-	if (strlen ($info['name']))
-		$summary['Common name'] = $info['name'];
-	elseif (considerConfiguredConstraint ($info, 'NAMEWARN_LISTSRC'))
-		$summary[] = array ('<tr><td colspan=2 class=msg_error>Common name is missing.</td></tr>');
-	$summary['Object type'] = '<a href="' . makeHref (array (
-		'page' => 'depot',
-		'tab' => 'default',
-		'cfe' => '{$typeid_' . $info['objtype_id'] . '}'
-	)) . '">' .  decodeObjectType ($info['objtype_id'], 'o') . '</a>';
-	if (strlen ($info['label']))
-		$summary['Visible label'] = $info['label'];
-	if (strlen ($info['asset_no']))
-		$summary['Asset tag'] = $info['asset_no'];
-	elseif (considerConfiguredConstraint ($info, 'ASSETWARN_LISTSRC'))
-		$summary[] = array ('<tr><td colspan=2 class=msg_error>Asset tag is missing.</td></tr>');
-	$parents = getEntityRelatives ('parents', 'object', $object_id);
-	if (count ($parents))
-	{
-		$fmt_parents = array();
-		foreach ($parents as $parent)
-			$fmt_parents[] =  "<a href='".makeHref(array('page'=>$parent['page'], $parent['id_name'] => $parent['entity_id']))."'>${parent['name']}</a>";
-		$summary[count($parents) > 1 ? 'Containers' : 'Container'] = implode ('<br>', $fmt_parents);
-	}
-	$children = getEntityRelatives ('children', 'object', $object_id);
-	if (count ($children))
-	{
-		$fmt_children = array();
-		foreach ($children as $child)
-			$fmt_children[] = "<a href='".makeHref(array('page'=>$child['page'], $child['id_name']=>$child['entity_id']))."'>${child['name']}</a>";
-		$summary['Contains'] = implode ('<br>', $fmt_children);
-	}
-	if ($info['has_problems'] == 'yes')
-		$summary[] = array ('<tr><td colspan=2 class=msg_error>Has problems</td></tr>');
-	foreach (getAttrValues ($object_id) as $record)
-		if
-		(
-			strlen ($record['value']) and 
-			permitted (NULL, NULL, NULL, array (array ('tag' => '$attr_' . $record['id'])))
-		)
-			$summary['{sticker}' . $record['name']] = formatAttributeValue ($record);
-	$summary[] = array (getOutputOf ('printTagTRs',
-		$info,
-		makeHref
-		(
-			array
-			(
-				'page'=>'depot',
-				'tab'=>'default',
-				'andor' => 'and',
-				'cfe' => '{$typeid_' . $info['objtype_id'] . '}',
-			)
-		)."&"
-	));
-	renderEntitySummary ($info, 'summary', $summary);
-
-	if (strlen ($info['comment']))
-	{
-		startPortlet ('Comment');
-		echo '<div class=commentblock>' . string_insert_hrefs ($info['comment']) . '</div>';
-		finishPortlet ();
-	}
-
-	$logrecords = getLogRecordsForObject ($_REQUEST['object_id']);
-	if (count ($logrecords))
-	{
-		startPortlet ('log records');
-		echo "<table cellspacing=0 cellpadding=5 align=center class=widetable width='100%'>";
-		$order = 'odd';
-		foreach ($logrecords as $row)
-		{
-			echo "<tr class=row_${order} valign=top>";
-			echo '<td class=tdleft>' . $row['date'] . '<br>' . $row['user'] . '</td>';
-			echo '<td class="logentry">' . string_insert_hrefs (htmlspecialchars ($row['content'], ENT_NOQUOTES)) . '</td>';
-			echo '</tr>';
-			$order = $nextorder[$order];
-		}
-		echo '</table>';
-		finishPortlet();
-	}
-
-	switchportInfoJS ($object_id); // load JS code to make portnames interactive
-	renderFilesPortlet ('object', $object_id);
-
-	if (count ($info['ports']))
-	{
-		startPortlet ('ports and links');
-		$hl_port_id = 0;
-		if (isset ($_REQUEST['hl_port_id']))
-		{
-			assertUIntArg ('hl_port_id');
-			$hl_port_id = $_REQUEST['hl_port_id'];
-			addAutoScrollScript ("port-$hl_port_id");
-		}
-		echo "<table cellspacing=0 cellpadding='5' align='center' class='widetable'>";
-		echo '<tr><th class=tdleft>Local name</th><th class=tdleft>Visible label</th>';
-		echo '<th class=tdleft>Interface</th><th class=tdleft>L2 address</th>';
-		echo '<th class=tdcenter colspan=2>Remote object and port</th>';
-		echo '<th class=tdleft>Cable ID</th></tr>';
-		foreach ($info['ports'] as $port)
-			callHook ('renderObjectPortRow', $port, ($hl_port_id == $port['id']));
-		if (permitted (NULL, 'ports', 'set_reserve_comment'))
-			addJS ('js/inplace-edit.js');
-		echo "</table><br>";
-		finishPortlet();
-	}
-
-	if (count ($info['ipv4']) + count ($info['ipv6']))
-	{
-		startPortlet ('IP addresses');
-		echo "<table cellspacing=0 cellpadding='5' align='center' class='widetable'>\n";
-		if (getConfigVar ('EXT_IPV4_VIEW') == 'yes')
-			echo "<tr><th>OS interface</th><th>IP address</th><th>network</th><th>routed by</th><th>peers</th></tr>\n";
-		else
-			echo "<tr><th>OS interface</th><th>IP address</th><th>peers</th></tr>\n";
-
-		// group IP allocations by interface name instead of address family
-		$allocs_by_iface = array();
-		foreach (array ('ipv4', 'ipv6') as $ip_v)
-			foreach ($info[$ip_v] as $ip_bin => $alloc)
-				$allocs_by_iface[$alloc['osif']][$ip_bin] = $alloc;
-				
-		// sort allocs array by portnames
-		foreach (sortPortList ($allocs_by_iface) as $iface_name => $alloclist)
-		{
-			$is_first_row = TRUE;
-			foreach ($alloclist as $alloc)
-			{
-				$rendered_alloc = callHook ('getRenderedAlloc', $object_id, $alloc);
-				echo "<tr class='${rendered_alloc['tr_class']}' valign=top>";
-
-				// display iface name, same values are grouped into single cell
-				if ($is_first_row)
-				{
-					$rowspan = count ($alloclist) > 1 ? 'rowspan="' . count ($alloclist) . '"' : '';
-					echo "<td class=tdleft $rowspan>" . $iface_name . $rendered_alloc['td_name_suffix'] . "</td>";
-					$is_first_row = FALSE;
-				}
-				echo $rendered_alloc['td_ip'];
-				if (getConfigVar ('EXT_IPV4_VIEW') == 'yes')
-				{
-					echo $rendered_alloc['td_network'];
-					echo $rendered_alloc['td_routed_by'];
-				}
-				echo $rendered_alloc['td_peers'];
-
-				echo "</tr>\n";
-			}
-		}
-		echo "</table><br>\n";
-		finishPortlet();
-	}
-
-	$forwards = $info['nat4'];
-	if (count($forwards['in']) or count($forwards['out']))
+	echo "<tr>";
+	foreach (array('left','right') as $side)
 	{
-		startPortlet('NATv4');
+		echo "<td class=pc$side>";
 
-		if (count($forwards['out']))
+		foreach ($portlethandler['object']['default'][$side] as $idx => $portlet)
 		{
-
-			echo "<h3>locally performed NAT</h3>";
-
-			echo "<table class='widetable' cellpadding=5 cellspacing=0 border=0 align='center'>\n";
-			echo "<tr><th>Proto</th><th>Match endpoint</th><th>Translate to</th><th>Target object</th><th>Rule comment</th></tr>\n";
-
-			foreach ($forwards['out'] as $pf)
-			{
-				$class = 'trerror';
-				$osif = '';
-				if (isset ($alloclist [$pf['localip']]))
-				{
-					$class = $alloclist [$pf['localip']]['addrinfo']['class'];
-					$osif = $alloclist [$pf['localip']]['osif'] . ': ';
-				}
-				echo "<tr class='$class'>";
-				echo "<td>${pf['proto']}</td><td class=tdleft>${osif}" . getRenderedIPPortPair ($pf['localip'], $pf['localport']) . "</td>";
-				echo "<td class=tdleft>" . getRenderedIPPortPair ($pf['remoteip'], $pf['remoteport']) . "</td>";
-				$address = getIPAddress (ip4_parse ($pf['remoteip']));
-				echo "<td class='description'>";
-				if (count ($address['allocs']))
-					foreach($address['allocs'] as $bond)
-						echo "<a href='".makeHref(array('page'=>'object', 'tab'=>'default', 'object_id'=>$bond['object_id']))."'>${bond['object_name']}(${bond['name']})</a> ";
-				elseif (strlen ($pf['remote_addr_name']))
-					echo '(' . $pf['remote_addr_name'] . ')';
-				echo "</td><td class='description'>${pf['description']}</td></tr>";
-			}
-			echo "</table><br><br>";
-		}
-		if (count($forwards['in']))
-		{
-			echo "<h3>arriving NAT connections</h3>";
-			echo "<table class='widetable' cellpadding=5 cellspacing=0 border=0 align='center'>\n";
-			echo "<tr><th>Matched endpoint</th><th>Source object</th><th>Translated to</th><th>Rule comment</th></tr>\n";
-			foreach ($forwards['in'] as $pf)
-			{
-				echo "<tr>";
-				echo "<td>${pf['proto']}/" . getRenderedIPPortPair ($pf['localip'], $pf['localport']) . "</td>";
-				echo "<td class='description'><a href='".makeHref(array('page'=>'object', 'tab'=>'default', 'object_id'=>$pf['object_id']))."'>${pf['object_name']}</a>";
-				echo "</td><td>" . getRenderedIPPortPair ($pf['remoteip'], $pf['remoteport']) . "</td>";
-				echo "<td class='description'>${pf['description']}</td></tr>";
-			}
-			echo "</table><br><br>";
+			echo "<!-- $idx -->\n";
+			call_user_func($portlet['callback'], $info );
 		}
-		finishPortlet();
+		echo "</td>\n";
 	}
 
-	renderSLBTriplets ($info);
-	echo "</td>\n";
-
-	// After left column we have (surprise!) right column with rackspace portlet only.
-	echo "<td class=pcright>";
-	if (!in_array($info['objtype_id'], $virtual_obj_types))
-	{
-		// rackspace portlet
-		startPortlet ('rackspace allocation');
-		foreach (getResidentRacksData ($object_id, FALSE) as $rack_id)
-			renderRack ($rack_id, $object_id);
-		echo '<br>';
-		finishPortlet();
-	}
-	echo "</td></tr>";
 	echo "</table>\n";
 }
 
diff -urN RackTables-0.20.1-orig/wwwroot/inc/navigation.php RackTables-0.20.1/wwwroot/inc/navigation.php
--- RackTables-0.20.1-orig/wwwroot/inc/navigation.php	2012-10-04 13:06:14.000000000 +0100
+++ RackTables-0.20.1/wwwroot/inc/navigation.php	2012-12-07 11:56:17.869570284 +0000
@@ -35,6 +35,8 @@
 $svghandler = array();
 $ajaxhandler = array();
 
+$portlethandler = array();
+
 $indexlayout = array
 (
 	array ('rackspace', 'depot', 'ipv4space', 'ipv6space'),
@@ -254,6 +256,15 @@
 $ophandler['object']['cacti']['del'] = 'tableHandler';
 $ophandler['object']['ucs']['autoPopulateUCS'] = 'autoPopulateUCS';
 $ophandler['object']['ucs']['cleanupUCS'] = 'cleanupUCS';
+$portlethandler['object']['default']['left'][] = array( 'name' => 'Summary', 'callback' => 'renderObPortletSummary' );
+$portlethandler['object']['default']['left'][] = array( 'name' => 'Comment', 'callback' => 'renderObPortletComment' );
+$portlethandler['object']['default']['left'][] = array( 'name' => 'Log', 'callback' => 'renderObPortletLog' );
+$portlethandler['object']['default']['left'][] = array( 'name' => 'Files', 'callback' => 'renderObPortletFiles' );
+$portlethandler['object']['default']['left'][] = array( 'name' => 'Ports', 'callback' => 'renderObPortletPorts' );
+$portlethandler['object']['default']['left'][] = array( 'name' => 'IPAddresses', 'callback' => 'renderObPortletIPAddresses' );
+$portlethandler['object']['default']['left'][] = array( 'name' => 'NATv4', 'callback' => 'renderObPortletNATv4' );
+$portlethandler['object']['default']['left'][] = array( 'name' => 'SLBTriplets', 'callback' => 'renderObPortletSLBTriplets' );
+$portlethandler['object']['default']['right'][] = array( 'name' => 'Rackspace', 'callback' => 'renderObPortletRackspace' );
 $delayauth['object-8021qports-save8021QConfig'] = TRUE;
 $delayauth['object-livevlans-setPortVLAN'] = TRUE;
 $delayauth['object-8021qorder-add'] = TRUE;
diff -urN RackTables-0.20.1-orig/wwwroot/inc/portlets.php RackTables-0.20.1/wwwroot/inc/portlets.php
--- RackTables-0.20.1-orig/wwwroot/inc/portlets.php	1970-01-01 01:00:00.000000000 +0100
+++ RackTables-0.20.1/wwwroot/inc/portlets.php	2012-12-07 12:15:22.959570886 +0000
@@ -0,0 +1,248 @@
+<?php
+
+function renderObPortletSummary ( $info )
+{
+	$object_id = $info['id'];
+	$summary  = array();
+	if (strlen ($info['name']))
+		$summary['Common name'] = $info['name'];
+	elseif (considerConfiguredConstraint ($info, 'NAMEWARN_LISTSRC'))
+		$summary[] = array ('<tr><td colspan=2 class=msg_error>Common name is missing.</td></tr>');
+	$summary['Object type'] = '<a href="' . makeHref (array (
+		'page' => 'depot',
+		'tab' => 'default',
+		'cfe' => '{$typeid_' . $info['objtype_id'] . '}'
+	)) . '">' .  decodeObjectType ($info['objtype_id'], 'o') . '</a>';
+	if (strlen ($info['label']))
+		$summary['Visible label'] = $info['label'];
+	if (strlen ($info['asset_no']))
+		$summary['Asset tag'] = $info['asset_no'];
+	elseif (considerConfiguredConstraint ($info, 'ASSETWARN_LISTSRC'))
+		$summary[] = array ('<tr><td colspan=2 class=msg_error>Asset tag is missing.</td></tr>');
+	$parents = getEntityRelatives ('parents', 'object', $object_id);
+	if (count ($parents))
+	{
+		$fmt_parents = array();
+		foreach ($parents as $parent)
+			$fmt_parents[] =  "<a href='".makeHref(array('page'=>$parent['page'], $parent['id_name'] => $parent['entity_id']))."'>${parent['name']}</a>";
+		$summary[count($parents) > 1 ? 'Containers' : 'Container'] = implode ('<br>', $fmt_parents);
+	}
+	$children = getEntityRelatives ('children', 'object', $object_id);
+	if (count ($children))
+	{
+		$fmt_children = array();
+		foreach ($children as $child)
+			$fmt_children[] = "<a href='".makeHref(array('page'=>$child['page'], $child['id_name']=>$child['entity_id']))."'>${child['name']}</a>";
+		$summary['Contains'] = implode ('<br>', $fmt_children);
+	}
+	if ($info['has_problems'] == 'yes')
+		$summary[] = array ('<tr><td colspan=2 class=msg_error>Has problems</td></tr>');
+	foreach (getAttrValues ($object_id) as $record)
+		if
+		(
+			strlen ($record['value']) and 
+			permitted (NULL, NULL, NULL, array (array ('tag' => '$attr_' . $record['id'])))
+		)
+			$summary['{sticker}' . $record['name']] = formatAttributeValue ($record);
+	$summary[] = array (getOutputOf ('printTagTRs',
+		$info,
+		makeHref
+		(
+			array
+			(
+				'page'=>'depot',
+				'tab'=>'default',
+				'andor' => 'and',
+				'cfe' => '{$typeid_' . $info['objtype_id'] . '}',
+			)
+		)."&"
+	));
+	renderEntitySummary ($info, 'summary', $summary);
+}
+
+function renderObPortletComment ( $info )
+{
+
+	if (strlen ($info['comment']))
+	{
+		startPortlet ('Comment');
+		echo '<div class=commentblock>' . string_insert_hrefs ($info['comment']) . '</div>';
+		finishPortlet ();
+	}
+}
+
+function renderObPortletLog ( $info )
+{
+	$logrecords = getLogRecordsForObject ($_REQUEST['object_id']);
+	if (count ($logrecords))
+	{
+		startPortlet ('log records');
+		echo "<table cellspacing=0 cellpadding=5 align=center class=widetable width='100%'>";
+		$order = 'odd';
+		foreach ($logrecords as $row)
+		{
+			echo "<tr class=row_${order} valign=top>";
+			echo '<td class=tdleft>' . $row['date'] . '<br>' . $row['user'] . '</td>';
+			echo '<td class="logentry">' . string_insert_hrefs (htmlspecialchars ($row['content'], ENT_NOQUOTES)) . '</td>';
+			echo '</tr>';
+			$order = $nextorder[$order];
+		}
+		echo '</table>';
+		finishPortlet();
+	}
+}
+
+function renderObPortletFiles ( $info )
+{
+	$object_id = $info['id'];
+	switchportInfoJS ($object_id); // load JS code to make portnames interactive
+	renderFilesPortlet ('object', $object_id);
+}
+
+function renderObPortletPorts ( $info )
+{
+	if (count ($info['ports']))
+	{
+		startPortlet ('Ports / Links');
+		$hl_port_id = 0;
+		if (isset ($_REQUEST['hl_port_id']))
+		{
+			assertUIntArg ('hl_port_id');
+			$hl_port_id = $_REQUEST['hl_port_id'];
+			addAutoScrollScript ("port-$hl_port_id");
+		}
+		echo "<table cellspacing=0 cellpadding='5' align='center' class='widetable'>";
+		echo '<tr><th class=tdleft>Local name</th><th class=tdleft>Visible label</th>';
+		echo '<th class=tdleft>Interface</th><th class=tdleft>L2 address</th>';
+		echo '<th class=tdcenter colspan=2>Remote object and port</th>';
+		echo '<th class=tdleft>Cable ID</th></tr>';
+		foreach ($info['ports'] as $port)
+			callHook ('renderObjectPortRow', $port, ($hl_port_id == $port['id']));
+		if (permitted (NULL, 'ports', 'set_reserve_comment'))
+			addJS ('js/inplace-edit.js');
+		echo "</table><br>";
+		finishPortlet();
+	}
+}
+
+function renderObPortletIPAddresses ( $info )
+{
+	if (count ($info['ipv4']) + count ($info['ipv6']))
+	{
+		startPortlet ('IP Addresses');
+		echo "<table cellspacing=0 cellpadding='5' align='center' class='widetable'>\n";
+		if (getConfigVar ('EXT_IPV4_VIEW') == 'yes')
+			echo "<tr><th>OS interface</th><th>IP address</th><th>network</th><th>routed by</th><th>peers</th></tr>\n";
+		else
+			echo "<tr><th>OS interface</th><th>IP address</th><th>peers</th></tr>\n";
+
+		// group IP allocations by interface name instead of address family
+		$allocs_by_iface = array();
+		foreach (array ('ipv4', 'ipv6') as $ip_v)
+			foreach ($info[$ip_v] as $ip_bin => $alloc)
+				$allocs_by_iface[$alloc['osif']][$ip_bin] = $alloc;
+				
+		// sort allocs array by portnames
+		foreach (sortPortList ($allocs_by_iface) as $iface_name => $alloclist)
+		{
+			$is_first_row = TRUE;
+			foreach ($alloclist as $alloc)
+			{
+				$rendered_alloc = callHook ('getRenderedAlloc', $object_id, $alloc);
+				echo "<tr class='${rendered_alloc['tr_class']}' valign=top>";
+
+				// display iface name, same values are grouped into single cell
+				if ($is_first_row)
+				{
+					$rowspan = count ($alloclist) > 1 ? 'rowspan="' . count ($alloclist) . '"' : '';
+					echo "<td class=tdleft $rowspan>" . $iface_name . $rendered_alloc['td_name_suffix'] . "</td>";
+					$is_first_row = FALSE;
+				}
+				echo $rendered_alloc['td_ip'];
+				if (getConfigVar ('EXT_IPV4_VIEW') == 'yes')
+				{
+					echo $rendered_alloc['td_network'];
+					echo $rendered_alloc['td_routed_by'];
+				}
+				echo $rendered_alloc['td_peers'];
+				echo "</tr>\n";
+			}
+		}
+		echo "</table><br>\n";
+		finishPortlet();
+	}
+}
+
+function renderObPortletNATv4 ( $info )
+{
+	$forwards = $info['nat4'];
+	if (count($forwards['in']) or count($forwards['out']))
+	{
+		startPortlet('NATv4');
+
+		if (count($forwards['out']))
+		{
+
+			echo "<h3>locally performed NAT</h3>";
+
+			echo "<table class='widetable' cellpadding=5 cellspacing=0 border=0 align='center'>\n";
+			echo "<tr><th>Proto</th><th>Match endpoint</th><th>Translate to</th><th>Target object</th><th>Rule comment</th></tr>\n";
+
+			foreach ($forwards['out'] as $pf)
+			{
+				$class = 'trerror';
+				$osif = '';
+				if (isset ($alloclist [$pf['localip']]))
+				{
+					$class = $alloclist [$pf['localip']]['addrinfo']['class'];
+					$osif = $alloclist [$pf['localip']]['osif'] . ': ';
+				}
+				echo "<tr class='$class'>";
+				echo "<td>${pf['proto']}</td><td class=tdleft>${osif}" . getRenderedIPPortPair ($pf['localip'], $pf['localport']) . "</td>";
+				echo "<td class=tdleft>" . getRenderedIPPortPair ($pf['remoteip'], $pf['remoteport']) . "</td>";
+				$address = getIPAddress (ip4_parse ($pf['remoteip']));
+				echo "<td class='description'>";
+				if (count ($address['allocs']))
+					foreach($address['allocs'] as $bond)
+						echo "<a href='".makeHref(array('page'=>'object', 'tab'=>'default', 'object_id'=>$bond['object_id']))."'>${bond['object_name']}(${bond['name']})</a> ";
+				elseif (strlen ($pf['remote_addr_name']))
+					echo '(' . $pf['remote_addr_name'] . ')';
+				echo "</td><td class='description'>${pf['description']}</td></tr>";
+			}
+			echo "</table><br><br>";
+		}
+		if (count($forwards['in']))
+		{
+			echo "<h3>arriving NAT connections</h3>";
+			echo "<table class='widetable' cellpadding=5 cellspacing=0 border=0 align='center'>\n";
+			echo "<tr><th>Matched endpoint</th><th>Source object</th><th>Translated to</th><th>Rule comment</th></tr>\n";
+			foreach ($forwards['in'] as $pf)
+			{
+				echo "<tr>";
+				echo "<td>${pf['proto']}/" . getRenderedIPPortPair ($pf['localip'], $pf['localport']) . "</td>";
+				echo "<td class='description'><a href='".makeHref(array('page'=>'object', 'tab'=>'default', 'object_id'=>$pf['object_id']))."'>${pf['object_name']}</a>";
+				echo "</td><td>" . getRenderedIPPortPair ($pf['remoteip'], $pf['remoteport']) . "</td>";
+				echo "<td class='description'>${pf['description']}</td></tr>";
+			}
+			echo "</table><br><br>";
+		}
+		finishPortlet();
+	}
+}
+
+function renderObPortletSLBTriplets ( $info )
+{
+	renderSLBTriplets ($info);
+}
+
+function renderObPortletRackspace ( $info )
+{	if (!in_array($info['objtype_id'], $virtual_obj_types))
+	{
+		// rackspace portlet
+		startPortlet ('rackspace allocation');
+		foreach (getResidentRacksData ($object_id, FALSE) as $rack_id)
+			renderRack ($rack_id, $object_id);
+		echo '<br>';
+		finishPortlet();
+	}
+}
racktables-0.20.1.patch (24,843 bytes)   
racktables-0-20-3.patch (25,512 bytes)   
diff -uNr RackTables-0.20.3-orig/plugins/demo-plugin.php RackTables-0.20.3-dev/plugins/demo-plugin.php
--- RackTables-0.20.3-orig/plugins/demo-plugin.php	1970-01-01 01:00:00.000000000 +0100
+++ RackTables-0.20.3-dev/plugins/demo-plugin.php	2012-12-24 10:23:11.999568017 +0000
@@ -0,0 +1,65 @@
+<?php
+
+$tab['object']['demo'] = 'Demo';
+registerTabHandler('object', 'demo', 'myRenderDemo');
+registerOpHandler('object', 'demo', 'update', 'updateDemo');
+registerPortletHandler( 'object', 'default', 'left', 'Demo', 'myRenderObPortletDemo', 'after', 'Comment');
+
+$tab['reports']['demo'] = 'Demo Report';
+registerTabHandler('reports', 'demo', 'myRenderDemoReport');
+
+
+function myRenderObPortletDemo ( $info )
+{
+	startPortlet('Demo Plugin Portlet');
+	echo "This text was produced by the Demo Plugin\n";
+	finishPortlet();
+}
+
+$msgcode['updateDemo']['OK'] = 43;
+function updateDemo()
+{
+        assertUIntArg ('object_id');
+//
+// Update something here 
+//
+
+        showFuncMessage (__FUNCTION__, 'OK');
+        return buildRedirectURL (NULL, NULL, NULL);
+}
+
+function myRenderDemo ()
+{
+        global $pageno, $virtual_obj_types;
+        $object_id = getBypassValue();
+        $info = spotEntity ('object', $object_id);
+	amplifyCell ($info);
+
+        startPortlet ();
+        printOpFormIntro ('update');
+
+        echo '<table border=0 cellspacing=0 cellpadding=3 align=center>';
+        echo "<tr>";
+        echo "<td>&nbsp;</td>";
+        echo "<td>Insert form here</td>";
+        echo "<td>&nbsp;</td>";
+        echo "</tr>\n";
+        echo "<tr><th class=submit colspan=3>";
+        printImageHREF ('SAVE', 'Save changes', TRUE);
+
+        echo "</tr></table>";
+        
+
+        echo "</form></th></tr></table>\n";
+        finishPortlet();
+
+}
+
+function myRenderDemoReport ()
+{
+        echo '<table border=0 cellspacing=0 cellpadding=3 align=center>';
+	echo "<tr><th><h3>Demo Report</h3></th></tr>\n";
+        echo "<td>Insert form here</td>";
+        echo "</tr>\n";
+        echo "</tr></table>";
+}
diff -uNr RackTables-0.20.3-orig/wwwroot/inc/functions.php RackTables-0.20.3-dev/wwwroot/inc/functions.php
--- RackTables-0.20.3-orig/wwwroot/inc/functions.php	2012-12-19 16:30:47.000000000 +0000
+++ RackTables-0.20.3-dev/wwwroot/inc/functions.php	2012-12-24 09:19:41.299570063 +0000
@@ -5705,4 +5705,51 @@
 	return $ret;
 }
 
+// Registers additional portleyhandler on page-tab-column triplet.
+// Valid $column values are 'left', 'right'
+//   'left' puts you portlet in the left hand column on a page (default)
+//   'right' puts you portlet in the left hand column on a page
+// $name is used as an identifer when inserting between current portlets
+// Valid $method values are 'before', 'after'.
+//   'before' puts your tabhandler in the beginning of the list (and thus before the default)
+//   'after' puts your tabhandler to the end of the list (and thus after the default)
+// $name2 modified the behaviour of $method to insert the new portlet between currently definded portlets on
+// the page.
+function registerPortletHandler ($page, $tab, $column = 'left', $name, $callback, $method = 'after', $name2 = '')
+{
+	global $portlethandler;
+
+	if ($name2 == '')
+		if ($method == 'before')
+			array_unshift ($portlethandler[$page][$tab][$column], array( 'name' => $name, 'callback' => $callback));
+		elseif ($method == 'after')
+			array_push ($portlethandler[$page][$tab][$column], array( 'name' => $name, 'callback' => $callback));
+		else
+			throw new RacktablesError ("unknown portlethandler injection method '$method'", RackTablesError::INTERNAL);
+	else
+	{
+		$done = 0;
+		foreach ($portlethandler[$page][$tab][$column] as $idx => $portlet)
+			if ($portlet['name'] == $name2)
+			{
+				if ($method == 'before')
+				{
+					array_splice($portlethandler[$page][$tab][$column], $idx, 0, array(array('name'=> $name, 'callback' => $callback)));
+					$done = 1;
+				}
+				elseif ($method == 'after')
+				{
+					array_splice($portlethandler[$page][$tab][$column], ($idx+1), 0, array(array('name'=> $name, 'callback' => $callback)));
+					$done = 1;
+				}
+				else
+					throw new RacktablesError ("unknown portlethandler injection method '$method'", RackTablesError::INTERNAL);
+				break;
+			}
+		if ( ! $done )
+			throw new RacktablesError ("unknown portlet ($name2) to insert $method", RackTablesError::MISCONFIGURED);
+	}
+
+}
+
 ?>
diff -uNr RackTables-0.20.3-orig/wwwroot/inc/interface.php RackTables-0.20.3-dev/wwwroot/inc/interface.php
--- RackTables-0.20.3-orig/wwwroot/inc/interface.php	2012-12-19 16:30:47.000000000 +0000
+++ RackTables-0.20.3-dev/wwwroot/inc/interface.php	2012-12-24 09:53:52.859568602 +0000
@@ -12,6 +12,7 @@
 
 require_once 'ajax-interface.php';
 require_once 'slb-interface.php';
+require_once 'portlets.php';
 
 // Interface function's special.
 $nextorder['odd'] = 'even';
@@ -1098,243 +1099,32 @@
 	echo "</tr>";
 }
 
+
 function renderObject ($object_id)
 {
 	global $nextorder, $virtual_obj_types;
+	global $portlethandler;
 	$info = spotEntity ('object', $object_id);
 	amplifyCell ($info);
 	// Main layout starts.
 	echo "<table border=0 class=objectview cellspacing=0 cellpadding=0>";
 	echo "<tr><td colspan=2 align=center><h1>${info['dname']}</h1></td></tr>\n";
 	// left column with uknown number of portlets
-	echo "<tr><td class=pcleft>";
-
-	// display summary portlet
-	$summary  = array();
-	if (strlen ($info['name']))
-		$summary['Common name'] = $info['name'];
-	elseif (considerConfiguredConstraint ($info, 'NAMEWARN_LISTSRC'))
-		$summary[] = array ('<tr><td colspan=2 class=msg_error>Common name is missing.</td></tr>');
-	$summary['Object type'] = '<a href="' . makeHref (array (
-		'page' => 'depot',
-		'tab' => 'default',
-		'cfe' => '{$typeid_' . $info['objtype_id'] . '}'
-	)) . '">' .  decodeObjectType ($info['objtype_id'], 'o') . '</a>';
-	if (strlen ($info['label']))
-		$summary['Visible label'] = $info['label'];
-	if (strlen ($info['asset_no']))
-		$summary['Asset tag'] = $info['asset_no'];
-	elseif (considerConfiguredConstraint ($info, 'ASSETWARN_LISTSRC'))
-		$summary[] = array ('<tr><td colspan=2 class=msg_error>Asset tag is missing.</td></tr>');
-	$parents = getEntityRelatives ('parents', 'object', $object_id);
-	if (count ($parents))
-	{
-		$fmt_parents = array();
-		foreach ($parents as $parent)
-			$fmt_parents[] =  "<a href='".makeHref(array('page'=>$parent['page'], $parent['id_name'] => $parent['entity_id']))."'>${parent['name']}</a>";
-		$summary[count($parents) > 1 ? 'Containers' : 'Container'] = implode ('<br>', $fmt_parents);
-	}
-	$children = getEntityRelatives ('children', 'object', $object_id);
-	if (count ($children))
-	{
-		$fmt_children = array();
-		foreach ($children as $child)
-			$fmt_children[] = "<a href='".makeHref(array('page'=>$child['page'], $child['id_name']=>$child['entity_id']))."'>${child['name']}</a>";
-		$summary['Contains'] = implode ('<br>', $fmt_children);
-	}
-	if ($info['has_problems'] == 'yes')
-		$summary[] = array ('<tr><td colspan=2 class=msg_error>Has problems</td></tr>');
-	foreach (getAttrValues ($object_id) as $record)
-		if
-		(
-			strlen ($record['value']) and
-			permitted (NULL, NULL, NULL, array (array ('tag' => '$attr_' . $record['id'])))
-		)
-			$summary['{sticker}' . $record['name']] = formatAttributeValue ($record);
-	$summary[] = array (getOutputOf ('printTagTRs',
-		$info,
-		makeHref
-		(
-			array
-			(
-				'page'=>'depot',
-				'tab'=>'default',
-				'andor' => 'and',
-				'cfe' => '{$typeid_' . $info['objtype_id'] . '}',
-			)
-		)."&"
-	));
-	renderEntitySummary ($info, 'summary', $summary);
-
-	if (strlen ($info['comment']))
-	{
-		startPortlet ('Comment');
-		echo '<div class=commentblock>' . string_insert_hrefs ($info['comment']) . '</div>';
-		finishPortlet ();
-	}
-
-	$logrecords = getLogRecordsForObject ($_REQUEST['object_id']);
-	if (count ($logrecords))
-	{
-		startPortlet ('log records');
-		echo "<table cellspacing=0 cellpadding=5 align=center class=widetable width='100%'>";
-		$order = 'odd';
-		foreach ($logrecords as $row)
-		{
-			echo "<tr class=row_${order} valign=top>";
-			echo '<td class=tdleft>' . $row['date'] . '<br>' . $row['user'] . '</td>';
-			echo '<td class="logentry">' . string_insert_hrefs (htmlspecialchars ($row['content'], ENT_NOQUOTES)) . '</td>';
-			echo '</tr>';
-			$order = $nextorder[$order];
-		}
-		echo '</table>';
-		finishPortlet();
-	}
-
-	switchportInfoJS ($object_id); // load JS code to make portnames interactive
-	renderFilesPortlet ('object', $object_id);
-
-	if (count ($info['ports']))
-	{
-		startPortlet ('ports and links');
-		$hl_port_id = 0;
-		if (isset ($_REQUEST['hl_port_id']))
-		{
-			assertUIntArg ('hl_port_id');
-			$hl_port_id = $_REQUEST['hl_port_id'];
-			addAutoScrollScript ("port-$hl_port_id");
-		}
-		echo "<table cellspacing=0 cellpadding='5' align='center' class='widetable'>";
-		echo '<tr><th class=tdleft>Local name</th><th class=tdleft>Visible label</th>';
-		echo '<th class=tdleft>Interface</th><th class=tdleft>L2 address</th>';
-		echo '<th class=tdcenter colspan=2>Remote object and port</th>';
-		echo '<th class=tdleft>Cable ID</th></tr>';
-		foreach ($info['ports'] as $port)
-			callHook ('renderObjectPortRow', $port, ($hl_port_id == $port['id']));
-		if (permitted (NULL, 'ports', 'set_reserve_comment'))
-			addJS ('js/inplace-edit.js');
-		echo "</table><br>";
-		finishPortlet();
-	}
-
-	if (count ($info['ipv4']) + count ($info['ipv6']))
-	{
-		startPortlet ('IP addresses');
-		echo "<table cellspacing=0 cellpadding='5' align='center' class='widetable'>\n";
-		if (getConfigVar ('EXT_IPV4_VIEW') == 'yes')
-			echo "<tr><th>OS interface</th><th>IP address</th><th>network</th><th>routed by</th><th>peers</th></tr>\n";
-		else
-			echo "<tr><th>OS interface</th><th>IP address</th><th>peers</th></tr>\n";
-
-		// group IP allocations by interface name instead of address family
-		$allocs_by_iface = array();
-		foreach (array ('ipv4', 'ipv6') as $ip_v)
-			foreach ($info[$ip_v] as $ip_bin => $alloc)
-				$allocs_by_iface[$alloc['osif']][$ip_bin] = $alloc;
-
-		// sort allocs array by portnames
-		foreach (sortPortList ($allocs_by_iface) as $iface_name => $alloclist)
-		{
-			$is_first_row = TRUE;
-			foreach ($alloclist as $alloc)
-			{
-				$rendered_alloc = callHook ('getRenderedAlloc', $object_id, $alloc);
-				echo "<tr class='${rendered_alloc['tr_class']}' valign=top>";
-
-				// display iface name, same values are grouped into single cell
-				if ($is_first_row)
-				{
-					$rowspan = count ($alloclist) > 1 ? 'rowspan="' . count ($alloclist) . '"' : '';
-					echo "<td class=tdleft $rowspan>" . $iface_name . $rendered_alloc['td_name_suffix'] . "</td>";
-					$is_first_row = FALSE;
-				}
-				echo $rendered_alloc['td_ip'];
-				if (getConfigVar ('EXT_IPV4_VIEW') == 'yes')
-				{
-					echo $rendered_alloc['td_network'];
-					echo $rendered_alloc['td_routed_by'];
-				}
-				echo $rendered_alloc['td_peers'];
-
-				echo "</tr>\n";
-			}
-		}
-		echo "</table><br>\n";
-		finishPortlet();
-	}
-
-	$forwards = $info['nat4'];
-	if (count($forwards['in']) or count($forwards['out']))
+	echo "<tr>";
+	foreach (array('left','right') as $side)
 	{
-		startPortlet('NATv4');
+		echo "<td class=pc$side>";
 
-		if (count($forwards['out']))
+		foreach ($portlethandler['object']['default'][$side] as $idx => $portlet)
 		{
-
-			echo "<h3>locally performed NAT</h3>";
-
-			echo "<table class='widetable' cellpadding=5 cellspacing=0 border=0 align='center'>\n";
-			echo "<tr><th>Proto</th><th>Match endpoint</th><th>Translate to</th><th>Target object</th><th>Rule comment</th></tr>\n";
-
-			foreach ($forwards['out'] as $pf)
-			{
-				$class = 'trerror';
-				$osif = '';
-				if (isset ($alloclist [$pf['localip']]))
-				{
-					$class = $alloclist [$pf['localip']]['addrinfo']['class'];
-					$osif = $alloclist [$pf['localip']]['osif'] . ': ';
-				}
-				echo "<tr class='$class'>";
-				echo "<td>${pf['proto']}</td><td class=tdleft>${osif}" . getRenderedIPPortPair ($pf['localip'], $pf['localport']) . "</td>";
-				echo "<td class=tdleft>" . getRenderedIPPortPair ($pf['remoteip'], $pf['remoteport']) . "</td>";
-				$address = getIPAddress (ip4_parse ($pf['remoteip']));
-				echo "<td class='description'>";
-				if (count ($address['allocs']))
-					foreach($address['allocs'] as $bond)
-						echo "<a href='".makeHref(array('page'=>'object', 'tab'=>'default', 'object_id'=>$bond['object_id']))."'>${bond['object_name']}(${bond['name']})</a> ";
-				elseif (strlen ($pf['remote_addr_name']))
-					echo '(' . $pf['remote_addr_name'] . ')';
-				echo "</td><td class='description'>${pf['description']}</td></tr>";
-			}
-			echo "</table><br><br>";
-		}
-		if (count($forwards['in']))
-		{
-			echo "<h3>arriving NAT connections</h3>";
-			echo "<table class='widetable' cellpadding=5 cellspacing=0 border=0 align='center'>\n";
-			echo "<tr><th>Matched endpoint</th><th>Source object</th><th>Translated to</th><th>Rule comment</th></tr>\n";
-			foreach ($forwards['in'] as $pf)
-			{
-				echo "<tr>";
-				echo "<td>${pf['proto']}/" . getRenderedIPPortPair ($pf['localip'], $pf['localport']) . "</td>";
-				echo "<td class='description'><a href='".makeHref(array('page'=>'object', 'tab'=>'default', 'object_id'=>$pf['object_id']))."'>${pf['object_name']}</a>";
-				echo "</td><td>" . getRenderedIPPortPair ($pf['remoteip'], $pf['remoteport']) . "</td>";
-				echo "<td class='description'>${pf['description']}</td></tr>";
-			}
-			echo "</table><br><br>";
+			call_user_func($portlet['callback'], $info );
 		}
-		finishPortlet();
+		echo "</td>\n";
 	}
 
-	renderSLBTriplets ($info);
-	echo "</td>\n";
-
-	// After left column we have (surprise!) right column with rackspace portlet only.
-	echo "<td class=pcright>";
-	if (!in_array($info['objtype_id'], $virtual_obj_types))
-	{
-		// rackspace portlet
-		startPortlet ('rackspace allocation');
-		foreach (getResidentRacksData ($object_id, FALSE) as $rack_id)
-			renderRack ($rack_id, $object_id);
-		echo '<br>';
-		finishPortlet();
-	}
-	echo "</td></tr>";
 	echo "</table>\n";
 }
-
+ 
 function renderRackMultiSelect ($sname, $racks, $selected)
 {
 	// Transform the given flat list into a list of groups, each representing a rack row.
diff -uNr RackTables-0.20.3-orig/wwwroot/inc/navigation.php RackTables-0.20.3-dev/wwwroot/inc/navigation.php
--- RackTables-0.20.3-orig/wwwroot/inc/navigation.php	2012-12-19 16:30:47.000000000 +0000
+++ RackTables-0.20.3-dev/wwwroot/inc/navigation.php	2012-12-24 09:19:41.299570063 +0000
@@ -35,6 +35,8 @@
 $svghandler = array();
 $ajaxhandler = array();
 
+$portlethandler = array();
+
 $indexlayout = array
 (
 	array ('rackspace', 'depot', 'ipv4space', 'ipv6space'),
@@ -254,6 +256,15 @@
 $ophandler['object']['munin']['del'] = 'tableHandler';
 $ophandler['object']['ucs']['autoPopulateUCS'] = 'autoPopulateUCS';
 $ophandler['object']['ucs']['cleanupUCS'] = 'cleanupUCS';
+$portlethandler['object']['default']['left'][] = array( 'name' => 'Summary', 'callback' => 'renderObPortletSummary' );
+$portlethandler['object']['default']['left'][] = array( 'name' => 'Comment', 'callback' => 'renderObPortletComment' );
+$portlethandler['object']['default']['left'][] = array( 'name' => 'Log', 'callback' => 'renderObPortletLog' );
+$portlethandler['object']['default']['left'][] = array( 'name' => 'Files', 'callback' => 'renderObPortletFiles' );
+$portlethandler['object']['default']['left'][] = array( 'name' => 'Ports', 'callback' => 'renderObPortletPorts' );
+$portlethandler['object']['default']['left'][] = array( 'name' => 'IPAddresses', 'callback' => 'renderObPortletIPAddresses' );
+$portlethandler['object']['default']['left'][] = array( 'name' => 'NATv4', 'callback' => 'renderObPortletNATv4' );
+$portlethandler['object']['default']['left'][] = array( 'name' => 'SLBTriplets', 'callback' => 'renderObPortletSLBTriplets' );
+$portlethandler['object']['default']['right'][] = array( 'name' => 'Rackspace', 'callback' => 'renderObPortletRackspace' );
 $delayauth['object-8021qports-save8021QConfig'] = TRUE;
 $delayauth['object-8021qorder-add'] = TRUE;
 $delayauth['object-8021qorder-del'] = TRUE;
diff -uNr RackTables-0.20.3-orig/wwwroot/inc/portlets.php RackTables-0.20.3-dev/wwwroot/inc/portlets.php
--- RackTables-0.20.3-orig/wwwroot/inc/portlets.php	1970-01-01 01:00:00.000000000 +0100
+++ RackTables-0.20.3-dev/wwwroot/inc/portlets.php	2012-12-24 10:05:22.559572928 +0000
@@ -0,0 +1,261 @@
+<?php
+
+function renderObPortletSummary ( $info )
+{
+        $object_id = $info['id'];
+	$summary  = array();
+	if (strlen ($info['name']))
+		$summary['Common name'] = $info['name'];
+	elseif (considerConfiguredConstraint ($info, 'NAMEWARN_LISTSRC'))
+		$summary[] = array ('<tr><td colspan=2 class=msg_error>Common name is missing.</td></tr>');
+	$summary['Object type'] = '<a href="' . makeHref (array (
+		'page' => 'depot',
+		'tab' => 'default',
+		'cfe' => '{$typeid_' . $info['objtype_id'] . '}'
+	)) . '">' .  decodeObjectType ($info['objtype_id'], 'o') . '</a>';
+	if (strlen ($info['label']))
+		$summary['Visible label'] = $info['label'];
+	if (strlen ($info['asset_no']))
+		$summary['Asset tag'] = $info['asset_no'];
+	elseif (considerConfiguredConstraint ($info, 'ASSETWARN_LISTSRC'))
+		$summary[] = array ('<tr><td colspan=2 class=msg_error>Asset tag is missing.</td></tr>');
+	$parents = getEntityRelatives ('parents', 'object', $object_id);
+	if (count ($parents))
+	{
+		$fmt_parents = array();
+		foreach ($parents as $parent)
+			$fmt_parents[] =  "<a href='".makeHref(array('page'=>$parent['page'], $parent['id_name'] => $parent['entity_id']))."'>${parent['name']}</a>";
+		$summary[count($parents) > 1 ? 'Containers' : 'Container'] = implode ('<br>', $fmt_parents);
+	}
+	$children = getEntityRelatives ('children', 'object', $object_id);
+	if (count ($children))
+	{
+		$fmt_children = array();
+		foreach ($children as $child)
+			$fmt_children[] = "<a href='".makeHref(array('page'=>$child['page'], $child['id_name']=>$child['entity_id']))."'>${child['name']}</a>";
+		$summary['Contains'] = implode ('<br>', $fmt_children);
+	}
+	if ($info['has_problems'] == 'yes')
+		$summary[] = array ('<tr><td colspan=2 class=msg_error>Has problems</td></tr>');
+	foreach (getAttrValues ($object_id) as $record)
+		if
+		(
+			strlen ($record['value']) and
+			permitted (NULL, NULL, NULL, array (array ('tag' => '$attr_' . $record['id'])))
+		)
+			$summary['{sticker}' . $record['name']] = formatAttributeValue ($record);
+	$summary[] = array (getOutputOf ('printTagTRs',
+		$info,
+		makeHref
+		(
+			array
+			(
+				'page'=>'depot',
+				'tab'=>'default',
+				'andor' => 'and',
+				'cfe' => '{$typeid_' . $info['objtype_id'] . '}',
+			)
+		)."&"
+	));
+	renderEntitySummary ($info, 'summary', $summary);
+}
+
+function renderObPortletComments ( $info )
+{
+        $object_id = $info['id'];
+
+	if (strlen ($info['comment']))
+	{
+		startPortlet ('Comment');
+		echo '<div class=commentblock>' . string_insert_hrefs ($info['comment']) . '</div>';
+		finishPortlet ();
+	}
+}
+
+function renderObPortletLog ( $info )
+{
+        $object_id = $info['id'];
+
+	$logrecords = getLogRecordsForObject ($object_id);
+	if (count ($logrecords))
+	{
+		startPortlet ('log records');
+		echo "<table cellspacing=0 cellpadding=5 align=center class=widetable width='100%'>";
+		$order = 'odd';
+		foreach ($logrecords as $row)
+		{
+			echo "<tr class=row_${order} valign=top>";
+			echo '<td class=tdleft>' . $row['date'] . '<br>' . $row['user'] . '</td>';
+			echo '<td class="logentry">' . string_insert_hrefs (htmlspecialchars ($row['content'], ENT_NOQUOTES)) . '</td>';
+			echo '</tr>';
+			$order = $nextorder[$order];
+		}
+		echo '</table>';
+		finishPortlet();
+	}
+}
+
+function renderObPortletFiles ( $info )
+{
+        $object_id = $info['id'];
+	renderFilesPortlet ('object', $object_id);
+}
+
+function renderObPortletPorts ( $info )
+{
+        $object_id = $info['id'];
+
+	switchportInfoJS ($object_id); // load JS code to make portnames interactive
+	if (count ($info['ports']))
+	{
+		startPortlet ('ports and links');
+		$hl_port_id = 0;
+		if (isset ($_REQUEST['hl_port_id']))
+		{
+			assertUIntArg ('hl_port_id');
+			$hl_port_id = $_REQUEST['hl_port_id'];
+			addAutoScrollScript ("port-$hl_port_id");
+		}
+		echo "<table cellspacing=0 cellpadding='5' align='center' class='widetable'>";
+		echo '<tr><th class=tdleft>Local name</th><th class=tdleft>Visible label</th>';
+		echo '<th class=tdleft>Interface</th><th class=tdleft>L2 address</th>';
+		echo '<th class=tdcenter colspan=2>Remote object and port</th>';
+		echo '<th class=tdleft>Cable ID</th></tr>';
+		foreach ($info['ports'] as $port)
+			callHook ('renderObjectPortRow', $port, ($hl_port_id == $port['id']));
+		if (permitted (NULL, 'ports', 'set_reserve_comment'))
+			addJS ('js/inplace-edit.js');
+		echo "</table><br>";
+		finishPortlet();
+	}
+}
+
+function renderObPortletIPAddresses ( $info )
+{
+        $object_id = $info['id'];
+
+	if (count ($info['ipv4']) + count ($info['ipv6']))
+	{
+		startPortlet ('IP addresses');
+		echo "<table cellspacing=0 cellpadding='5' align='center' class='widetable'>\n";
+		if (getConfigVar ('EXT_IPV4_VIEW') == 'yes')
+			echo "<tr><th>OS interface</th><th>IP address</th><th>network</th><th>routed by</th><th>peers</th></tr>\n";
+		else
+			echo "<tr><th>OS interface</th><th>IP address</th><th>peers</th></tr>\n";
+
+		// group IP allocations by interface name instead of address family
+		$allocs_by_iface = array();
+		foreach (array ('ipv4', 'ipv6') as $ip_v)
+			foreach ($info[$ip_v] as $ip_bin => $alloc)
+				$allocs_by_iface[$alloc['osif']][$ip_bin] = $alloc;
+
+		// sort allocs array by portnames
+		foreach (sortPortList ($allocs_by_iface) as $iface_name => $alloclist)
+		{
+			$is_first_row = TRUE;
+			foreach ($alloclist as $alloc)
+			{
+				$rendered_alloc = callHook ('getRenderedAlloc', $object_id, $alloc);
+				echo "<tr class='${rendered_alloc['tr_class']}' valign=top>";
+
+				// display iface name, same values are grouped into single cell
+				if ($is_first_row)
+				{
+					$rowspan = count ($alloclist) > 1 ? 'rowspan="' . count ($alloclist) . '"' : '';
+					echo "<td class=tdleft $rowspan>" . $iface_name . $rendered_alloc['td_name_suffix'] . "</td>";
+					$is_first_row = FALSE;
+				}
+				echo $rendered_alloc['td_ip'];
+				if (getConfigVar ('EXT_IPV4_VIEW') == 'yes')
+				{
+					echo $rendered_alloc['td_network'];
+					echo $rendered_alloc['td_routed_by'];
+				}
+				echo $rendered_alloc['td_peers'];
+
+				echo "</tr>\n";
+			}
+		}
+		echo "</table><br>\n";
+		finishPortlet();
+	}
+}
+
+function renderObPortletNATv4 ( $info )
+{
+	$forwards = $info['nat4'];
+	if (count($forwards['in']) or count($forwards['out']))
+	{
+		startPortlet('NATv4');
+
+		if (count($forwards['out']))
+		{
+
+			echo "<h3>locally performed NAT</h3>";
+
+			echo "<table class='widetable' cellpadding=5 cellspacing=0 border=0 align='center'>\n";
+			echo "<tr><th>Proto</th><th>Match endpoint</th><th>Translate to</th><th>Target object</th><th>Rule comment</th></tr>\n";
+
+			foreach ($forwards['out'] as $pf)
+			{
+				$class = 'trerror';
+				$osif = '';
+				if (isset ($alloclist [$pf['localip']]))
+				{
+					$class = $alloclist [$pf['localip']]['addrinfo']['class'];
+					$osif = $alloclist [$pf['localip']]['osif'] . ': ';
+				}
+				echo "<tr class='$class'>";
+				echo "<td>${pf['proto']}</td><td class=tdleft>${osif}" . getRenderedIPPortPair ($pf['localip'], $pf['localport']) . "</td>";
+				echo "<td class=tdleft>" . getRenderedIPPortPair ($pf['remoteip'], $pf['remoteport']) . "</td>";
+				$address = getIPAddress (ip4_parse ($pf['remoteip']));
+				echo "<td class='description'>";
+				if (count ($address['allocs']))
+					foreach($address['allocs'] as $bond)
+						echo "<a href='".makeHref(array('page'=>'object', 'tab'=>'default', 'object_id'=>$bond['object_id']))."'>${bond['object_name']}(${bond['name']})</a> ";
+				elseif (strlen ($pf['remote_addr_name']))
+					echo '(' . $pf['remote_addr_name'] . ')';
+				echo "</td><td class='description'>${pf['description']}</td></tr>";
+			}
+			echo "</table><br><br>";
+		}
+		if (count($forwards['in']))
+		{
+			echo "<h3>arriving NAT connections</h3>";
+			echo "<table class='widetable' cellpadding=5 cellspacing=0 border=0 align='center'>\n";
+			echo "<tr><th>Matched endpoint</th><th>Source object</th><th>Translated to</th><th>Rule comment</th></tr>\n";
+			foreach ($forwards['in'] as $pf)
+			{
+				echo "<tr>";
+				echo "<td>${pf['proto']}/" . getRenderedIPPortPair ($pf['localip'], $pf['localport']) . "</td>";
+				echo "<td class='description'><a href='".makeHref(array('page'=>'object', 'tab'=>'default', 'object_id'=>$pf['object_id']))."'>${pf['object_name']}</a>";
+				echo "</td><td>" . getRenderedIPPortPair ($pf['remoteip'], $pf['remoteport']) . "</td>";
+				echo "<td class='description'>${pf['description']}</td></tr>";
+			}
+			echo "</table><br><br>";
+		}
+		finishPortlet();
+	}
+}
+
+function renderObPortletSLBTriplets ( $info )
+{
+	renderSLBTriplets ($info);
+}
+
+function renderObPortletRackspace ( $info )
+{
+        $object_id = $info['id'];
+
+	if (!in_array($info['objtype_id'], $virtual_obj_types))
+	{
+		// rackspace portlet
+		startPortlet ('rackspace allocation');
+		foreach (getResidentRacksData ($object_id, FALSE) as $rack_id)
+			renderRack ($rack_id, $object_id);
+		echo '<br>';
+		finishPortlet();
+	}
+}
+
+?>
racktables-0-20-3.patch (25,512 bytes)   
portlet-0.20.4.patch (27,799 bytes)   
diff -uNr RackTables-0.20.4-orig/plugins/demo-plugin.php RackTables-0.20.4-dev/plugins/demo-plugin.php
--- RackTables-0.20.4-orig/plugins/demo-plugin.php	1970-01-01 01:00:00.000000000 +0100
+++ RackTables-0.20.4-dev/plugins/demo-plugin.php	2013-04-16 15:16:16.211660164 +0100
@@ -0,0 +1,75 @@
+<?php
+
+$tab['object']['demo'] = 'Demo';
+registerTabHandler('object', 'demo', 'myRenderDemo');
+registerOpHandler('object', 'demo', 'update', 'updateDemo');
+if ( function_exists('registerPortletHandler') )
+{
+	registerPortletHandler( 'object', 'default', 'left', 'Demo', 'myRenderObPortletDemo', 'after', 'Comment');
+	registerPortletHandler( 'object', 'default', 'right', 'Demo', 'myRenderObPortletDemo2');
+}
+
+$tab['reports']['demo'] = 'Demo Report';
+registerTabHandler('reports', 'demo', 'myRenderDemoReport');
+
+
+function myRenderObPortletDemo ( $info )
+{
+	startPortlet('Demo Plugin Portlet');
+	echo "This text was produced by the Demo Plugin\n";
+	finishPortlet();
+}
+function myRenderObPortletDemo2 ( $info )
+{
+	startPortlet('Demo Plugin Portlet');
+	echo "This text was also produced by the Demo Plugin\n";
+	finishPortlet();
+}
+
+$msgcode['updateDemo']['OK'] = 43;
+function updateDemo()
+{
+        assertUIntArg ('object_id');
+//
+// Update something here 
+//
+
+        showFuncMessage (__FUNCTION__, 'OK');
+        return buildRedirectURL (NULL, NULL, NULL);
+}
+
+function myRenderDemo ()
+{
+        global $pageno, $virtual_obj_types;
+        $object_id = getBypassValue();
+        $info = spotEntity ('object', $object_id);
+	amplifyCell ($info);
+
+        startPortlet ();
+        printOpFormIntro ('update');
+
+        echo '<table border=0 cellspacing=0 cellpadding=3 align=center>';
+        echo "<tr>";
+        echo "<td>&nbsp;</td>";
+        echo "<td>Insert form here</td>";
+        echo "<td>&nbsp;</td>";
+        echo "</tr>\n";
+        echo "<tr><th class=submit colspan=3>";
+        printImageHREF ('SAVE', 'Save changes', TRUE);
+
+        echo "</tr></table>";
+        
+
+        echo "</form></th></tr></table>\n";
+        finishPortlet();
+
+}
+
+function myRenderDemoReport ()
+{
+        echo '<table border=0 cellspacing=0 cellpadding=3 align=center>';
+	echo "<tr><th><h3>Demo Report</h3></th></tr>\n";
+        echo "<td>Insert form here</td>";
+        echo "</tr>\n";
+        echo "</tr></table>";
+}
diff -uNr RackTables-0.20.4-orig/wwwroot/inc/functions.php RackTables-0.20.4-dev/wwwroot/inc/functions.php
--- RackTables-0.20.4-orig/wwwroot/inc/functions.php	2013-04-14 21:27:19.000000000 +0100
+++ RackTables-0.20.4-dev/wwwroot/inc/functions.php	2013-04-16 11:30:30.561661364 +0100
@@ -6045,4 +6045,51 @@
 	return FALSE;
 }
 
+// Registers additional portleyhandler on page-tab-column triplet.
+// Valid $column values are 'left', 'right'
+//   'left' puts you portlet in the left hand column on a page (default)
+//   'right' puts you portlet in the left hand column on a page
+// $name is used as an identifer when inserting between current portlets
+// Valid $method values are 'before', 'after'.
+//   'before' puts your tabhandler in the beginning of the list (and thus before the default)
+//   'after' puts your tabhandler to the end of the list (and thus after the default)
+// $name2 modified the behaviour of $method to insert the new portlet between currently definded portlets on
+// the page.
+function registerPortletHandler ($page, $tab, $column = 'left', $name, $callback, $method = 'after', $name2 = '')
+{
+	global $portlethandler;
+
+	if ($name2 == '')
+		if ($method == 'before')
+			array_unshift ($portlethandler[$page][$tab][$column], array( 'name' => $name, 'callback' => $callback));
+		elseif ($method == 'after')
+			array_push ($portlethandler[$page][$tab][$column], array( 'name' => $name, 'callback' => $callback));
+		else
+			throw new RacktablesError ("unknown portlethandler injection method '$method'", RackTablesError::INTERNAL);
+	else
+	{
+		$done = 0;
+		foreach ($portlethandler[$page][$tab][$column] as $idx => $portlet)
+			if ($portlet['name'] == $name2)
+			{
+				if ($method == 'before')
+				{
+					array_splice($portlethandler[$page][$tab][$column], $idx, 0, array(array('name'=> $name, 'callback' => $callback)));
+					$done = 1;
+				}
+				elseif ($method == 'after')
+				{
+					array_splice($portlethandler[$page][$tab][$column], ($idx+1), 0, array(array('name'=> $name, 'callback' => $callback)));
+					$done = 1;
+				}
+				else
+					throw new RacktablesError ("unknown portlethandler injection method '$method'", RackTablesError::INTERNAL);
+				break;
+			}
+		if ( ! $done )
+			throw new RacktablesError ("unknown portlet ($name2) to insert $method", RackTablesError::MISCONFIGURED);
+	}
+
+}
+
 ?>
diff -uNr RackTables-0.20.4-orig/wwwroot/inc/interface.php RackTables-0.20.4-dev/wwwroot/inc/interface.php
--- RackTables-0.20.4-orig/wwwroot/inc/interface.php	2013-04-14 21:27:19.000000000 +0100
+++ RackTables-0.20.4-dev/wwwroot/inc/interface.php	2013-04-16 15:03:03.241660771 +0100
@@ -12,6 +12,7 @@
 
 require_once 'ajax-interface.php';
 require_once 'slb-interface.php';
+require_once 'portlets.php';
 
 // Interface function's special.
 $nextorder['odd'] = 'even';
@@ -1084,238 +1085,24 @@
 
 function renderObject ($object_id)
 {
-	global $nextorder, $virtual_obj_types;
+	global $nextorder, $virtual_obj_types, $portlethandler;
 	$info = spotEntity ('object', $object_id);
 	amplifyCell ($info);
 	// Main layout starts.
 	echo "<table border=0 class=objectview cellspacing=0 cellpadding=0>";
 	echo "<tr><td colspan=2 align=center><h1>${info['dname']}</h1></td></tr>\n";
 	// left column with uknown number of portlets
-	echo "<tr><td class=pcleft>";
-
-	// display summary portlet
-	$summary  = array();
-	if (strlen ($info['name']))
-		$summary['Common name'] = $info['name'];
-	elseif (considerConfiguredConstraint ($info, 'NAMEWARN_LISTSRC'))
-		$summary[] = array ('<tr><td colspan=2 class=msg_error>Common name is missing.</td></tr>');
-	$summary['Object type'] = '<a href="' . makeHref (array (
-		'page' => 'depot',
-		'tab' => 'default',
-		'cfe' => '{$typeid_' . $info['objtype_id'] . '}'
-	)) . '">' .  decodeObjectType ($info['objtype_id'], 'o') . '</a>';
-	if (strlen ($info['label']))
-		$summary['Visible label'] = $info['label'];
-	if (strlen ($info['asset_no']))
-		$summary['Asset tag'] = $info['asset_no'];
-	elseif (considerConfiguredConstraint ($info, 'ASSETWARN_LISTSRC'))
-		$summary[] = array ('<tr><td colspan=2 class=msg_error>Asset tag is missing.</td></tr>');
-	$parents = getEntityRelatives ('parents', 'object', $object_id);
-	if (count ($parents))
-	{
-		$fmt_parents = array();
-		foreach ($parents as $parent)
-			$fmt_parents[] =  "<a href='".makeHref(array('page'=>$parent['page'], $parent['id_name'] => $parent['entity_id']))."'>${parent['name']}</a>";
-		$summary[count($parents) > 1 ? 'Containers' : 'Container'] = implode ('<br>', $fmt_parents);
-	}
-	$children = getEntityRelatives ('children', 'object', $object_id);
-	if (count ($children))
-	{
-		$fmt_children = array();
-		foreach ($children as $child)
-			$fmt_children[] = "<a href='".makeHref(array('page'=>$child['page'], $child['id_name']=>$child['entity_id']))."'>${child['name']}</a>";
-		$summary['Contains'] = implode ('<br>', $fmt_children);
-	}
-	if ($info['has_problems'] == 'yes')
-		$summary[] = array ('<tr><td colspan=2 class=msg_error>Has problems</td></tr>');
-	foreach (getAttrValues ($object_id) as $record)
-		if
-		(
-			strlen ($record['value']) and
-			permitted (NULL, NULL, NULL, array (array ('tag' => '$attr_' . $record['id'])))
-		)
-			$summary['{sticker}' . $record['name']] = formatAttributeValue ($record);
-	$summary[] = array (getOutputOf ('printTagTRs',
-		$info,
-		makeHref
-		(
-			array
-			(
-				'page'=>'depot',
-				'tab'=>'default',
-				'andor' => 'and',
-				'cfe' => '{$typeid_' . $info['objtype_id'] . '}',
-			)
-		)."&"
-	));
-	renderEntitySummary ($info, 'summary', $summary);
-
-	if (strlen ($info['comment']))
-	{
-		startPortlet ('Comment');
-		echo '<div class=commentblock>' . string_insert_hrefs ($info['comment']) . '</div>';
-		finishPortlet ();
-	}
-
-	$logrecords = getLogRecordsForObject ($_REQUEST['object_id']);
-	if (count ($logrecords))
-	{
-		startPortlet ('log records');
-		echo "<table cellspacing=0 cellpadding=5 align=center class=widetable width='100%'>";
-		$order = 'odd';
-		foreach ($logrecords as $row)
-		{
-			echo "<tr class=row_${order} valign=top>";
-			echo '<td class=tdleft>' . $row['date'] . '<br>' . $row['user'] . '</td>';
-			echo '<td class="logentry">' . string_insert_hrefs (htmlspecialchars ($row['content'], ENT_NOQUOTES)) . '</td>';
-			echo '</tr>';
-			$order = $nextorder[$order];
-		}
-		echo '</table>';
-		finishPortlet();
-	}
-
-	switchportInfoJS ($object_id); // load JS code to make portnames interactive
-	renderFilesPortlet ('object', $object_id);
-
-	if (count ($info['ports']))
-	{
-		startPortlet ('ports and links');
-		$hl_port_id = 0;
-		if (isset ($_REQUEST['hl_port_id']))
-		{
-			assertUIntArg ('hl_port_id');
-			$hl_port_id = $_REQUEST['hl_port_id'];
-			addAutoScrollScript ("port-$hl_port_id");
-		}
-		echo "<table cellspacing=0 cellpadding='5' align='center' class='widetable'>";
-		echo '<tr><th class=tdleft>Local name</th><th class=tdleft>Visible label</th>';
-		echo '<th class=tdleft>Interface</th><th class=tdleft>L2 address</th>';
-		echo '<th class=tdcenter colspan=2>Remote object and port</th>';
-		echo '<th class=tdleft>Cable ID</th></tr>';
-		foreach ($info['ports'] as $port)
-			callHook ('renderObjectPortRow', $port, ($hl_port_id == $port['id']));
-		if (permitted (NULL, 'ports', 'set_reserve_comment'))
-			addJS ('js/inplace-edit.js');
-		echo "</table><br>";
-		finishPortlet();
-	}
-
-	if (count ($info['ipv4']) + count ($info['ipv6']))
-	{
-		startPortlet ('IP addresses');
-		echo "<table cellspacing=0 cellpadding='5' align='center' class='widetable'>\n";
-		if (getConfigVar ('EXT_IPV4_VIEW') == 'yes')
-			echo "<tr><th>OS interface</th><th>IP address</th><th>network</th><th>routed by</th><th>peers</th></tr>\n";
-		else
-			echo "<tr><th>OS interface</th><th>IP address</th><th>peers</th></tr>\n";
-
-		// group IP allocations by interface name instead of address family
-		$allocs_by_iface = array();
-		foreach (array ('ipv4', 'ipv6') as $ip_v)
-			foreach ($info[$ip_v] as $ip_bin => $alloc)
-				$allocs_by_iface[$alloc['osif']][$ip_bin] = $alloc;
-
-		// sort allocs array by portnames
-		foreach (sortPortList ($allocs_by_iface) as $iface_name => $alloclist)
-		{
-			$is_first_row = TRUE;
-			foreach ($alloclist as $alloc)
-			{
-				$rendered_alloc = callHook ('getRenderedAlloc', $object_id, $alloc);
-				echo "<tr class='${rendered_alloc['tr_class']}' valign=top>";
-
-				// display iface name, same values are grouped into single cell
-				if ($is_first_row)
-				{
-					$rowspan = count ($alloclist) > 1 ? 'rowspan="' . count ($alloclist) . '"' : '';
-					echo "<td class=tdleft $rowspan>" . $iface_name . $rendered_alloc['td_name_suffix'] . "</td>";
-					$is_first_row = FALSE;
-				}
-				echo $rendered_alloc['td_ip'];
-				if (getConfigVar ('EXT_IPV4_VIEW') == 'yes')
-				{
-					echo $rendered_alloc['td_network'];
-					echo $rendered_alloc['td_routed_by'];
-				}
-				echo $rendered_alloc['td_peers'];
-
-				echo "</tr>\n";
-			}
-		}
-		echo "</table><br>\n";
-		finishPortlet();
-	}
-
-	$forwards = $info['nat4'];
-	if (count($forwards['in']) or count($forwards['out']))
+	echo "<tr>";
+	foreach (array('left','right') as $side)
 	{
-		startPortlet('NATv4');
-
-		if (count($forwards['out']))
+		echo "<td class=pc$side>";
+		foreach ($portlethandler['object']['default'][$side] as $idx => $portlet)
 		{
-
-			echo "<h3>locally performed NAT</h3>";
-
-			echo "<table class='widetable' cellpadding=5 cellspacing=0 border=0 align='center'>\n";
-			echo "<tr><th>Proto</th><th>Match endpoint</th><th>Translate to</th><th>Target object</th><th>Rule comment</th></tr>\n";
-
-			foreach ($forwards['out'] as $pf)
-			{
-				$class = 'trerror';
-				$osif = '';
-				if (isset ($alloclist [$pf['localip']]))
-				{
-					$class = $alloclist [$pf['localip']]['addrinfo']['class'];
-					$osif = $alloclist [$pf['localip']]['osif'] . ': ';
-				}
-				echo "<tr class='$class'>";
-				echo "<td>${pf['proto']}</td><td class=tdleft>${osif}" . getRenderedIPPortPair ($pf['localip'], $pf['localport']) . "</td>";
-				echo "<td class=tdleft>" . getRenderedIPPortPair ($pf['remoteip'], $pf['remoteport']) . "</td>";
-				$address = getIPAddress (ip4_parse ($pf['remoteip']));
-				echo "<td class='description'>";
-				if (count ($address['allocs']))
-					foreach($address['allocs'] as $bond)
-						echo mkA ("${bond['object_name']}(${bond['name']})", 'object', $bond['object_id']) . ' ';
-				elseif (strlen ($pf['remote_addr_name']))
-					echo '(' . $pf['remote_addr_name'] . ')';
-				echo "</td><td class='description'>${pf['description']}</td></tr>";
-			}
-			echo "</table><br><br>";
-		}
-		if (count($forwards['in']))
-		{
-			echo "<h3>arriving NAT connections</h3>";
-			echo "<table class='widetable' cellpadding=5 cellspacing=0 border=0 align='center'>\n";
-			echo "<tr><th>Matched endpoint</th><th>Source object</th><th>Translated to</th><th>Rule comment</th></tr>\n";
-			foreach ($forwards['in'] as $pf)
-			{
-				echo "<tr>";
-				echo "<td>${pf['proto']}/" . getRenderedIPPortPair ($pf['localip'], $pf['localport']) . "</td>";
-				echo '<td class="description">' . mkA ($pf['object_name'], 'object', $pf['object_id']);
-				echo "</td><td>" . getRenderedIPPortPair ($pf['remoteip'], $pf['remoteport']) . "</td>";
-				echo "<td class='description'>${pf['description']}</td></tr>";
-			}
-			echo "</table><br><br>";
+			call_user_func($portlet['callback'], $info );
 		}
-		finishPortlet();
+		echo "</td>\n";
 	}
-
-	renderSLBTriplets ($info);
-	echo "</td>\n";
-
-	// After left column we have (surprise!) right column with rackspace portlet only.
-	echo "<td class=pcright>";
-	if (!in_array($info['objtype_id'], $virtual_obj_types))
-	{
-		// rackspace portlet
-		startPortlet ('rackspace allocation');
-		foreach (getResidentRacksData ($object_id, FALSE) as $rack_id)
-			renderRack ($rack_id, $object_id);
-		echo '<br>';
-		finishPortlet();
-	}
-	echo "</td></tr>";
+	echo "</tr>";
 	echo "</table>\n";
 }
 
@@ -3863,24 +3650,27 @@
 
 function renderRackPage ($rack_id)
 {
-	$rackData = spotEntity ('rack', $rack_id);
-	amplifyCell ($rackData);
-	echo "<table border=0 class=objectview cellspacing=0 cellpadding=0><tr>";
-
-	// Left column with information.
-	echo "<td class=pcleft>";
-	renderRackInfoPortlet ($rackData);
-	renderFilesPortlet ('rack', $rack_id);
-	echo '</td>';
+        global $portlethandler;
+        $info = spotEntity ('rack', $rack_id);
+        amplifyCell ($info);
+        // Main layout starts.
+        echo "<table border=0 class=objectview cellspacing=0 cellpadding=0>";
+        echo "<tr><td colspan=2 align=center><h1>${info['dname']}</h1></td></tr>\n";
+        // left column with uknown number of portlets
+        echo "<tr>";
+        foreach (array('left','right') as $side)
+        {
+                echo "<td class=pc$side>";
+                foreach ($portlethandler['rack']['default'][$side] as $idx => $portlet)
+                {
+                        call_user_func($portlet['callback'], $info );
+                }
+                echo "</td>\n";
+        }
 
-	// Right column with rendered rack.
-	echo '<td class=pcright>';
-	startPortlet ('Rack diagram');
-	renderRack ($rack_id);
-	finishPortlet();
-	echo '</td>';
+        echo "</tr>";
+        echo "</table>\n";
 
-	echo '</tr></table>';
 }
 
 function renderDictionary ()
diff -uNr RackTables-0.20.4-orig/wwwroot/inc/navigation.php RackTables-0.20.4-dev/wwwroot/inc/navigation.php
--- RackTables-0.20.4-orig/wwwroot/inc/navigation.php	2013-04-14 21:27:19.000000000 +0100
+++ RackTables-0.20.4-dev/wwwroot/inc/navigation.php	2013-04-16 15:05:50.691661405 +0100
@@ -35,6 +35,8 @@
 $svghandler = array();
 $ajaxhandler = array();
 
+$portlethandler = array();
+
 $indexlayout = array
 (
 	array ('rackspace', 'depot', 'ipv4space', 'ipv6space'),
@@ -144,6 +146,9 @@
 $ophandler['rack']['files']['addFile'] = 'addFileToEntity';
 $ophandler['rack']['files']['linkFile'] = 'linkFileToEntity';
 $ophandler['rack']['files']['unlinkFile'] = 'unlinkFile';
+$portlethandler['rack']['default']['left'][] = array( 'name' => 'Summary', 'callback' => 'renderRackInfoPortlet' );
+$portlethandler['rack']['default']['left'][] = array( 'name' => 'Files', 'callback' => 'renderRkPortletFiles' );
+$portlethandler['rack']['default']['right'][] = array( 'name' => 'Rack Diagram', 'callback' => 'renderRkPortletDiagram' );
 
 $page['object']['bypass'] = 'object_id';
 $page['object']['bypass_type'] = 'uint';
@@ -259,6 +264,15 @@
 $ophandler['object']['munin']['del'] = 'tableHandler';
 $ophandler['object']['ucs']['autoPopulateUCS'] = 'autoPopulateUCS';
 $ophandler['object']['ucs']['cleanupUCS'] = 'cleanupUCS';
+$portlethandler['object']['default']['left'][] = array( 'name' => 'Summary', 'callback' => 'renderObPortletSummary' );
+$portlethandler['object']['default']['left'][] = array( 'name' => 'Comment', 'callback' => 'renderObPortletComment' );
+$portlethandler['object']['default']['left'][] = array( 'name' => 'Log', 'callback' => 'renderObPortletLog' );
+$portlethandler['object']['default']['left'][] = array( 'name' => 'Files', 'callback' => 'renderObPortletFiles' );
+$portlethandler['object']['default']['left'][] = array( 'name' => 'Ports', 'callback' => 'renderObPortletPorts' );
+$portlethandler['object']['default']['left'][] = array( 'name' => 'IPAddresses', 'callback' => 'renderObPortletIPAddresses' );
+$portlethandler['object']['default']['left'][] = array( 'name' => 'NATv4', 'callback' => 'renderObPortletNATv4' );
+$portlethandler['object']['default']['left'][] = array( 'name' => 'SLBTriplets', 'callback' => 'renderObPortletSLBTriplets' );
+$portlethandler['object']['default']['right'][] = array( 'name' => 'Rackspace', 'callback' => 'renderObPortletRackspace' );
 $delayauth['object-8021qports-save8021QConfig'] = TRUE;
 $delayauth['object-8021qorder-add'] = TRUE;
 $delayauth['object-8021qorder-del'] = TRUE;
diff -uNr RackTables-0.20.4-orig/wwwroot/inc/portlets.php RackTables-0.20.4-dev/wwwroot/inc/portlets.php
--- RackTables-0.20.4-orig/wwwroot/inc/portlets.php	1970-01-01 01:00:00.000000000 +0100
+++ RackTables-0.20.4-dev/wwwroot/inc/portlets.php	2013-04-16 15:05:00.991661666 +0100
@@ -0,0 +1,283 @@
+<?php
+
+function renderObPortletSummary( $info )
+{
+	$object_id = $info['id'];
+
+	// display summary portlet
+	$summary  = array();
+	if (strlen ($info['name']))
+		$summary['Common name'] = $info['name'];
+	elseif (considerConfiguredConstraint ($info, 'NAMEWARN_LISTSRC'))
+		$summary[] = array ('<tr><td colspan=2 class=msg_error>Common name is missing.</td></tr>');
+	$summary['Object type'] = '<a href="' . makeHref (array (
+		'page' => 'depot',
+		'tab' => 'default',
+		'cfe' => '{$typeid_' . $info['objtype_id'] . '}'
+	)) . '">' .  decodeObjectType ($info['objtype_id'], 'o') . '</a>';
+	if (strlen ($info['label']))
+		$summary['Visible label'] = $info['label'];
+	if (strlen ($info['asset_no']))
+		$summary['Asset tag'] = $info['asset_no'];
+	elseif (considerConfiguredConstraint ($info, 'ASSETWARN_LISTSRC'))
+		$summary[] = array ('<tr><td colspan=2 class=msg_error>Asset tag is missing.</td></tr>');
+	$parents = getEntityRelatives ('parents', 'object', $object_id);
+	if (count ($parents))
+	{
+		$fmt_parents = array();
+		foreach ($parents as $parent)
+			$fmt_parents[] =  "<a href='".makeHref(array('page'=>$parent['page'], $parent['id_name'] => $parent['entity_id']))."'>${parent['name']}</a>";
+		$summary[count($parents) > 1 ? 'Containers' : 'Container'] = implode ('<br>', $fmt_parents);
+	}
+	$children = getEntityRelatives ('children', 'object', $object_id);
+	if (count ($children))
+	{
+		$fmt_children = array();
+		foreach ($children as $child)
+			$fmt_children[] = "<a href='".makeHref(array('page'=>$child['page'], $child['id_name']=>$child['entity_id']))."'>${child['name']}</a>";
+		$summary['Contains'] = implode ('<br>', $fmt_children);
+	}
+	if ($info['has_problems'] == 'yes')
+		$summary[] = array ('<tr><td colspan=2 class=msg_error>Has problems</td></tr>');
+	foreach (getAttrValues ($object_id) as $record)
+		if
+		(
+			strlen ($record['value']) and
+			permitted (NULL, NULL, NULL, array (array ('tag' => '$attr_' . $record['id'])))
+		)
+			$summary['{sticker}' . $record['name']] = formatAttributeValue ($record);
+	$summary[] = array (getOutputOf ('printTagTRs',
+		$info,
+		makeHref
+		(
+			array
+			(
+				'page'=>'depot',
+				'tab'=>'default',
+				'andor' => 'and',
+				'cfe' => '{$typeid_' . $info['objtype_id'] . '}',
+			)
+		)."&"
+	));
+	renderEntitySummary ($info, 'summary', $summary);
+}
+
+function renderObPortletComments( $info )
+{
+	$object_id = $info['id'];
+
+	if (strlen ($info['comment']))
+	{
+		startPortlet ('Comment');
+		echo '<div class=commentblock>' . string_insert_hrefs ($info['comment']) . '</div>';
+		finishPortlet ();
+	}
+}
+
+function renderObPortletLog( $info )
+{
+	$object_id = $info['id'];
+
+	$logrecords = getLogRecordsForObject ($object_id);
+	if (count ($logrecords))
+	{
+		startPortlet ('log records');
+		echo "<table cellspacing=0 cellpadding=5 align=center class=widetable width='100%'>";
+		$order = 'odd';
+		foreach ($logrecords as $row)
+		{
+			echo "<tr class=row_${order} valign=top>";
+			echo '<td class=tdleft>' . $row['date'] . '<br>' . $row['user'] . '</td>';
+			echo '<td class="logentry">' . string_insert_hrefs (htmlspecialchars ($row['content'], ENT_NOQUOTES)) . '</td>';
+			echo '</tr>';
+			$order = $nextorder[$order];
+		}
+		echo '</table>';
+		finishPortlet();
+	}
+}
+
+
+function renderObPortletFiles( $info )
+{
+	$object_id = $info['id'];
+
+	renderFilesPortlet ('object', $object_id);
+}
+
+function renderObPortletPorts( $info )
+{
+	$object_id = $info['id'];
+
+	switchportInfoJS ($object_id); // load JS code to make portnames interactive
+	if (count ($info['ports']))
+	{
+		startPortlet ('ports and links');
+		$hl_port_id = 0;
+		if (isset ($_REQUEST['hl_port_id']))
+		{
+			assertUIntArg ('hl_port_id');
+			$hl_port_id = $_REQUEST['hl_port_id'];
+			addAutoScrollScript ("port-$hl_port_id");
+		}
+		echo "<table cellspacing=0 cellpadding='5' align='center' class='widetable'>";
+		echo '<tr><th class=tdleft>Local name</th><th class=tdleft>Visible label</th>';
+		echo '<th class=tdleft>Interface</th><th class=tdleft>L2 address</th>';
+		echo '<th class=tdcenter colspan=2>Remote object and port</th>';
+		echo '<th class=tdleft>Cable ID</th></tr>';
+		foreach ($info['ports'] as $port)
+			callHook ('renderObjectPortRow', $port, ($hl_port_id == $port['id']));
+		if (permitted (NULL, 'ports', 'set_reserve_comment'))
+			addJS ('js/inplace-edit.js');
+		echo "</table><br>";
+		finishPortlet();
+	}
+}
+
+function renderObPortletIpAddresses( $info )
+{
+	$object_id = $info['id'];
+
+	if (count ($info['ipv4']) + count ($info['ipv6']))
+	{
+		startPortlet ('IP addresses');
+		echo "<table cellspacing=0 cellpadding='5' align='center' class='widetable'>\n";
+		if (getConfigVar ('EXT_IPV4_VIEW') == 'yes')
+			echo "<tr><th>OS interface</th><th>IP address</th><th>network</th><th>routed by</th><th>peers</th></tr>\n";
+		else
+			echo "<tr><th>OS interface</th><th>IP address</th><th>peers</th></tr>\n";
+
+		// group IP allocations by interface name instead of address family
+		$allocs_by_iface = array();
+		foreach (array ('ipv4', 'ipv6') as $ip_v)
+			foreach ($info[$ip_v] as $ip_bin => $alloc)
+				$allocs_by_iface[$alloc['osif']][$ip_bin] = $alloc;
+
+		// sort allocs array by portnames
+		foreach (sortPortList ($allocs_by_iface) as $iface_name => $alloclist)
+		{
+			$is_first_row = TRUE;
+			foreach ($alloclist as $alloc)
+			{
+				$rendered_alloc = callHook ('getRenderedAlloc', $object_id, $alloc);
+				echo "<tr class='${rendered_alloc['tr_class']}' valign=top>";
+
+				// display iface name, same values are grouped into single cell
+				if ($is_first_row)
+				{
+					$rowspan = count ($alloclist) > 1 ? 'rowspan="' . count ($alloclist) . '"' : '';
+					echo "<td class=tdleft $rowspan>" . $iface_name . $rendered_alloc['td_name_suffix'] . "</td>";
+					$is_first_row = FALSE;
+				}
+				echo $rendered_alloc['td_ip'];
+				if (getConfigVar ('EXT_IPV4_VIEW') == 'yes')
+				{
+					echo $rendered_alloc['td_network'];
+					echo $rendered_alloc['td_routed_by'];
+				}
+				echo $rendered_alloc['td_peers'];
+
+				echo "</tr>\n";
+			}
+		}
+		echo "</table><br>\n";
+		finishPortlet();
+	}
+}
+
+function renderObPortletNATv4( $info )
+{
+	$object_id = $info['id'];
+
+	$forwards = $info['nat4'];
+	if (count($forwards['in']) or count($forwards['out']))
+	{
+		startPortlet('NATv4');
+
+		if (count($forwards['out']))
+		{
+
+			echo "<h3>locally performed NAT</h3>";
+
+			echo "<table class='widetable' cellpadding=5 cellspacing=0 border=0 align='center'>\n";
+			echo "<tr><th>Proto</th><th>Match endpoint</th><th>Translate to</th><th>Target object</th><th>Rule comment</th></tr>\n";
+
+			foreach ($forwards['out'] as $pf)
+			{
+				$class = 'trerror';
+				$osif = '';
+				if (isset ($alloclist [$pf['localip']]))
+				{
+					$class = $alloclist [$pf['localip']]['addrinfo']['class'];
+					$osif = $alloclist [$pf['localip']]['osif'] . ': ';
+				}
+				echo "<tr class='$class'>";
+				echo "<td>${pf['proto']}</td><td class=tdleft>${osif}" . getRenderedIPPortPair ($pf['localip'], $pf['localport']) . "</td>";
+				echo "<td class=tdleft>" . getRenderedIPPortPair ($pf['remoteip'], $pf['remoteport']) . "</td>";
+				$address = getIPAddress (ip4_parse ($pf['remoteip']));
+				echo "<td class='description'>";
+				if (count ($address['allocs']))
+					foreach($address['allocs'] as $bond)
+						echo mkA ("${bond['object_name']}(${bond['name']})", 'object', $bond['object_id']) . ' ';
+				elseif (strlen ($pf['remote_addr_name']))
+					echo '(' . $pf['remote_addr_name'] . ')';
+				echo "</td><td class='description'>${pf['description']}</td></tr>";
+			}
+			echo "</table><br><br>";
+		}
+		if (count($forwards['in']))
+		{
+			echo "<h3>arriving NAT connections</h3>";
+			echo "<table class='widetable' cellpadding=5 cellspacing=0 border=0 align='center'>\n";
+			echo "<tr><th>Matched endpoint</th><th>Source object</th><th>Translated to</th><th>Rule comment</th></tr>\n";
+			foreach ($forwards['in'] as $pf)
+			{
+				echo "<tr>";
+				echo "<td>${pf['proto']}/" . getRenderedIPPortPair ($pf['localip'], $pf['localport']) . "</td>";
+				echo '<td class="description">' . mkA ($pf['object_name'], 'object', $pf['object_id']);
+				echo "</td><td>" . getRenderedIPPortPair ($pf['remoteip'], $pf['remoteport']) . "</td>";
+				echo "<td class='description'>${pf['description']}</td></tr>";
+			}
+			echo "</table><br><br>";
+		}
+		finishPortlet();
+	}
+}
+
+function renderObPortletSLBTriplets( $info )
+{
+	$object_id = $info['id'];
+
+	renderSLBTriplets ($info);
+}
+
+function renderObPortletRackspace( $info )
+{
+	$object_id = $info['id'];
+
+	if (!in_array($info['objtype_id'], $virtual_obj_types))
+	{
+		// rackspace portlet
+		startPortlet ('rackspace allocation');
+		foreach (getResidentRacksData ($object_id, FALSE) as $rack_id)
+			renderRack ($rack_id, $object_id);
+		echo '<br>';
+		finishPortlet();
+	}
+}
+
+function renderRkPortletFiles ( $info )
+{
+	$rack_id = $info['id'];
+
+        renderFilesPortlet ('rack', $rack_id);
+}
+
+
+function renderRkPortletDiagram ( $info )
+{
+	$rack_id = $info['id'];
+        startPortlet ('Rack diagram');
+        renderRack ($rack_id);
+        finishPortlet();
+}
portlet-0.20.4.patch (27,799 bytes)   

Activities

MWilkinson

MWilkinson

2012-12-24 11:35

reporter   ~0001039

Have attached a new version of the patch against version 0.20.3
Slight increase in the demo plugin with regards to adding a placeholder for reports
MWilkinson

MWilkinson

2013-04-16 16:24

reporter   ~0001325

Have attached new version of patch for Racktables 0.20.4, extended functionality to renderRackPage for use with updated wattage_consumption plugin

Issue History

Date Modified Username Field Change
2012-12-07 13:57 MWilkinson New Issue
2012-12-07 13:57 MWilkinson File Added: racktables-0.20.1.patch
2012-12-12 11:23 andriyanov Assigned To => andriyanov
2012-12-12 11:23 andriyanov Status new => assigned
2012-12-24 11:27 MWilkinson File Added: racktables-0-20-3.patch
2012-12-24 11:35 MWilkinson Note Added: 0001039
2013-04-16 16:22 MWilkinson File Added: portlet-0.20.4.patch
2013-04-16 16:24 MWilkinson Note Added: 0001325