add support for one-to-many port links, and cable path tracing
authorAaron Dummer <aaron@dummer.info>
Sun, 17 Mar 2013 02:00:14 +0000 (19:00 -0700)
committerAaron Dummer <aaron@dummer.info>
Tue, 16 Jul 2013 22:17:35 +0000 (15:17 -0700)
README
README.patchpanel [new file with mode: 0644]
wwwroot/inc/ajax-interface.php
wwwroot/inc/database.php
wwwroot/inc/dictionary.php
wwwroot/inc/functions.php
wwwroot/inc/install.php
wwwroot/inc/interface.php
wwwroot/inc/ophandlers.php
wwwroot/inc/popup.php
wwwroot/inc/upgrade.php

diff --git a/README b/README
index 312b5639d6621ceabcc5a9caa9aaa12f45f10c08..76a56b41317e69d520e87249d50f7b1b745b9ad4 100644 (file)
--- a/README
+++ b/README
@@ -102,6 +102,15 @@ should be sufficient:
 *                                                     *
 *******************************************************
 
+*** Upgrading to 0.21.0 ***
+
+Database triggers are used for some data consistency measures.  The database
+user account must have the 'TRIGGER' privilege, which was introduced in
+MySQL 5.1.7.
+
+Cable paths can be traced and displayed in a graphical format. This requires
+the Image_GraphViz  PEAR module (http://pear.php.net/package/Image_GraphViz).
+
 *** Upgrading to 0.20.5 ***
 
 This release introduces the VS groups feature. VS groups is a new way to store
diff --git a/README.patchpanel b/README.patchpanel
new file mode 100644 (file)
index 0000000..4fc51fc
--- /dev/null
@@ -0,0 +1,70 @@
+OVERVIEW
+The patchpanel branch was created by adoom42. It adds support for linking ports 
+multiple times and tracing cable paths.  Several people have created patches 
+for this already.  I haven't seen one which handles all potential use cases.  
+
+Generally speaking, a port is linked to one other port, but there are 
+exceptions.
+1.  Patch panels.  The front of the port is linked to an object's port.  The 
+back of the port is usually linked to the back of a port on another patch panel.
+2.  "Y" power cables and other splitters.  Example: the male end connects to an 
+outlet, and each female end connects to an object's power supply.  The cable 
+has a varying number of female ends (my personal favorite is the 'octo-cable').
+3.  Multi-point wireless links.  Although less common in data centers, they are
+used in areas such as college campuses.  An access point is mounted in a central
+location and multiple buildings have client units, with multiple clients 
+associating to the same AP.
+
+CHANGELOG
+The installer & upgrader check for the GraphViz PEAR module.  A non-blocking 
+warning is displayed if it is not found.
+
+The port linker pop-up includes a checkbox which allows already-linked ports to 
+be linked again.
+
+Triggers are used to prevent duplicate links.  They ensure that the value of 
+portb is always greater than porta, which forces the UNIQUE KEY (porta,portb) 
+to take effect.  They also ensure that porta is compatible with portb.
+
+The (porta,portb) primary key was removed from the Link table.  An 
+auto-increment id column was added to uniquely identify links.  This results in
+less PHP code.  For example, assume that rows exist where porta > portb 
+(they were entered manually before the triggers were installed).  In that case,
+all update and delete operations would need to identify a specific row using 
+something like ((porta = X AND portb = Y) OR (porta = Y AND portb = X)).  Now 
+such operations can identify the row using a single id.
+
+Triggers are used to maintain integrity of the PortCompat table.  Without them,
+you would be able to add a compatibility rule, link ports using that rule, then
+delete the rule without warning.  The ports would still be linked, even though
+they are now incompatible.  The trigger checks for the existence of such links
+before allowing rules to be updated or deleted.  MySQL doesn't support trigger
+exceptions, so I used a workaround to display a more meaningful error message:
+UPDATE `Cannot update: rule still used` SET x = 1.
+
+
+HELP/FEEDBACK REQUESTED
+fetchPortList() has been modified to include all links as a nested array.  The
+SQL query works but is not optimal.  Performance tips would be appreciated.
+
+A new pop-up provides the ability to link two patch panels.  The link is only
+displayed for objects of the type "PatchPanel".  The list includes all patch
+panels with the same number of ports.  It doesn't check for port compatibility.
+When the form is submitted, the handler function adds a link for each port.
+The handler function does check for port compatibility.  If any port is
+incompatible, the entire linking operation is rolled back.  Ideally, the pop-up
+would only list panels which are fully compatible.  SQL help in this area would
+also be appreciated.  It should be possible using a single query, but I'm not
+sure how to do it.
+
+A common usage scenario involves connecting an object's port to patch panel A,
+which is connected to patch panel B, which is then connected to another
+object's port.  When the patch panel A's ports are linked to patch panel B's,
+the ports will be excluded from the 'Link this port' pop-up unless you check
+the 'Include linked ports' box.  Patch panel ports which are linked less than
+twice should be included without having to check the 'Include linked ports'
+box.  I'm not sure how to deal with this efficiently.
+
+MISC. NOTES
+This branch should only be tested in a development environment.  It sets the
+RackTables version to 0.21.0 which will break any future upgrades to 0.20.x.
\ No newline at end of file
index f61ce4f8f9741e41dae85407a2fcfe22718602e2..85ad7c38506f31eb23d53cd0f47bd82e04ac6e86 100644 (file)
@@ -254,13 +254,17 @@ function updateCableIdAJAX()
        global $sic;
        assertUIntArg ('id');
        assertStringArg ('text', TRUE);
-       $port_info = getPortInfo ($sic['id']);
-       fixContext (spotEntity ('object', $port_info['object_id']));
+       $link_info = getPortLinkInfo ($sic['id']);
+       // verify permissions for both sides of the link
+       $porta_info = getPortInfo ($link_info['porta']);
+       $portb_info = getPortInfo ($link_info['portb']);
+       fixContext();
+       spreadContext (spotEntity ('object', $porta_info['object_id']));
+       spreadContext (spotEntity ('object', $portb_info['object_id']));
        assertPermission ('object', 'ports', 'editPort');
-       if (! $port_info['linked'])
+       if (! $link_info['porta'])
                throw new RackTablesError ('Cant update cable ID: port is not linked');
-       if ($port_info['reservation_comment'] !== $sic['text'])
-               commitUpdatePortLink ($sic['id'], $sic['text']);
+       commitUpdatePortLink ($sic['id'], $sic['text']);
        echo 'OK';
 }
 
index a0cb8bc75dbdbd3b47584415e8d7b03515e69a76..b4c4fe802847b27c4d43a914b906e08d0cf73fdd 100644 (file)
@@ -832,34 +832,34 @@ SELECT
        Port.type AS oif_id,
        (SELECT PortInnerInterface.iif_name FROM PortInnerInterface WHERE PortInnerInterface.id = Port.iif_id) AS iif_name,
        (SELECT Dictionary.dict_value FROM Dictionary WHERE Dictionary.dict_key = Port.type) AS oif_name,
-       IF(la.porta, la.cable, lb.cable) AS cableid,
-       IF(la.porta, pa.id, pb.id) AS remote_id,
-       IF(la.porta, pa.name, pb.name) AS remote_name,
-       IF(la.porta, pa.object_id, pb.object_id) AS remote_object_id,
-       IF(la.porta, oa.name, ob.name) AS remote_object_name,
-       (SELECT COUNT(*) FROM PortLog WHERE PortLog.port_id = Port.id) AS log_count,
+       Link.id AS link_id,
+       Link.cable AS cableid,
+       IF(Link.porta = Port.id, Link.portb, Link.porta) AS remote_id,
+       rp.name AS remote_name,
+       rp.object_id AS remote_object_id,
+       ro.name AS remote_object_name,
+       Dictionary.dict_value AS remote_object_type,
+       (SELECT COUNT(*) FROM PortLog WHERE PortLog.port_id = rp.id) AS log_count,
        PortLog.user,
-       UNIX_TIMESTAMP(PortLog.date) as time
+       UNIX_TIMESTAMP(PortLog.date) AS time
 FROM
        Port
        INNER JOIN Object ON Port.object_id = Object.id
-       LEFT JOIN Link AS la ON la.porta = Port.id
-       LEFT JOIN Port AS pa ON pa.id = la.portb
-       LEFT JOIN Object AS oa ON pa.object_id = oa.id
-       LEFT JOIN Link AS lb on lb.portb = Port.id
-       LEFT JOIN Port AS pb ON pb.id = lb.porta
-       LEFT JOIN Object AS ob ON pb.object_id = ob.id
-       LEFT JOIN PortLog ON PortLog.id = (SELECT id FROM PortLog WHERE PortLog.port_id = Port.id ORDER BY date DESC LIMIT 1)
+       LEFT JOIN Link ON Port.id = Link.porta OR Port.id = Link.portb
+       LEFT JOIN Port AS rp ON (IF(Link.porta = Port.id, Link.portb, Link.porta)) = rp.id
+       LEFT JOIN Object AS ro ON rp.object_id = ro.id
+       LEFT JOIN Dictionary ON ro.objtype_id = Dictionary.dict_key
+       LEFT JOIN PortLog ON PortLog.id = (SELECT id FROM PortLog WHERE PortLog.port_id = rp.id ORDER BY date DESC LIMIT 1)
 WHERE
        $sql_where_clause
 END;
 
        $result = usePreparedSelectBlade ($query, $query_params);
+       $id = 0;
        $ret = array();
        while ($row = $result->fetch (PDO::FETCH_ASSOC))
        {
                $row['l2address'] = l2addressFromDatabase ($row['l2address']);
-               $row['linked'] = isset ($row['remote_id']) ? 1 : 0;
 
                // last changed log
                $row['last_log'] = array();
@@ -868,10 +868,34 @@ END;
                        $row['last_log']['user'] = $row['user'];
                        $row['last_log']['time'] = $row['time'];
                }
-               unset ($row['user']);
-               unset ($row['time']);
+               unset ($row['user'], $row['time']);
 
-               $ret[] = $row;
+               // create a temporary array containing link info
+               $row['linked'] = 0;
+               $link_details = array();
+               if (isset ($row['remote_id']))
+               {
+                       $row['linked'] = 1;
+                       $remote_object_name = empty($row['remote_object_name']) ? '['.$row['remote_object_type'].']' : $row['remote_object_name'];
+                       $link_details = array
+                       (
+                               'link_id' => $row['link_id'],
+                               'cableid' => $row['cableid'],
+                               'remote_id' => $row['remote_id'],
+                               'remote_name' => $row['remote_name'],
+                               'remote_object_id' => $row['remote_object_id'],
+                               'remote_object_name' => $remote_object_name,
+                       );
+                       $link_details['last_log'] = $row['last_log'];
+               }
+               unset ($row['cableid'], $row['remote_id'], $row['remote_name'], $row['remote_object_id'], $row['remote_object_name']);
+
+               // see if this row represents an actual port or just an additional link
+               if ($row['id'] != $id)
+                       $ret[$row['id']] = $row;
+               if (count ($link_details))
+                       $ret[$row['id']]['links'][] = $link_details;
+               $id = $row['id'];
        }
        return $ret;
 }
@@ -913,6 +937,41 @@ function checkObjectNameUniqueness ($name, $type_id, $object_id = 0)
                throw new InvalidRequestArgException ('name', $name, 'An object with that name already exists');
 }
 
+// Recursive function to create an array containing a chain of related links
+function getNeighborPorts ($port_id, $neighbors = array (), $level = 0)
+{
+       $self = __FUNCTION__;
+       if ($level >= 10)
+               throw new InvalidArgException ('port', $port_id, 'tracing depth too deep - a loop probably exists');
+
+       $query = "
+SELECT L.portb AS port_id, P.name AS port_name, P.object_id, O.name AS object_name 
+FROM Link L 
+LEFT JOIN Port P ON L.portb = P.id 
+LEFT JOIN Object O ON P.object_id = O.id 
+WHERE L.porta = ? 
+UNION 
+SELECT L.porta AS port_id, P.name AS port_name, P.object_id, O.name AS object_name  
+FROM Link L 
+LEFT JOIN Port P ON L.porta = P.id 
+LEFT JOIN Object O ON P.object_id = O.id 
+WHERE L.portb = ?";
+       $result = usePreparedSelectBlade ($query, array ($port_id, $port_id));
+       while ($row = $result->fetch (PDO::FETCH_ASSOC))
+       {
+               if (!array_key_exists ($row['port_id'], $neighbors))
+               {
+                       $row_port_id = $row['port_id'];
+                       $neighbors[$row_port_id]['port_name'] = $row['port_name'];
+                       $neighbors[$row_port_id]['object_id'] = intval ($row['object_id']);
+                       $neighbors[$row_port_id]['object_name'] = $row['object_name'];
+                       $neighbors[$row_port_id]['remote_port_id'] = intval ($port_id);
+                       $neighbors = $self ($row_port_id, $neighbors, $level+1);
+               }
+       }
+       return $neighbors;
+}
+
 function commitAddObject ($new_name, $new_label, $new_type_id, $new_asset_no, $taglist = array())
 {
        checkObjectNameUniqueness ($new_name, $new_type_id);
@@ -1699,25 +1758,6 @@ function linkPorts ($porta, $portb, $cable = NULL)
        if ($porta == $portb)
                throw new InvalidArgException ('porta/portb', $porta, "Ports can't be the same");
 
-       global $dbxlink;
-       $dbxlink->exec ('LOCK TABLES Link WRITE');
-       $result = usePreparedSelectBlade
-       (
-               'SELECT COUNT(*) FROM Link WHERE porta IN (?,?) OR portb IN (?,?)',
-               array ($porta, $portb, $porta, $portb)
-       );
-       if ($result->fetchColumn () != 0)
-       {
-               $dbxlink->exec ('UNLOCK TABLES');
-               return "Port ${porta} or ${portb} is already linked";
-       }
-       unset ($result);
-       if ($porta > $portb)
-       {
-               $tmp = $porta;
-               $porta = $portb;
-               $portb = $tmp;
-       }
        usePreparedInsertBlade
        (
                'Link',
@@ -1728,7 +1768,6 @@ function linkPorts ($porta, $portb, $cable = NULL)
                        'cable' => mb_strlen ($cable) ? $cable : NULL
                )
        );
-       $dbxlink->exec ('UNLOCK TABLES');
        usePreparedExecuteBlade
        (
                'UPDATE Port SET reservation_comment=NULL WHERE id IN(?, ?)',
@@ -1751,18 +1790,16 @@ function linkPorts ($porta, $portb, $cable = NULL)
        }
 }
 
-function commitUpdatePortLink ($port_id, $cable = NULL)
+function commitUpdatePortLink ($link_id, $cable = NULL)
 {
-       return usePreparedUpdateBlade
+       return usePreparedExecuteBlade
        (
-               'Link',
-               array ('cable' => mb_strlen ($cable) ? $cable : NULL),
-               array ('porta' => $port_id, 'portb' => $port_id),
-               'OR'
+               'UPDATE Link SET cable = ? WHERE id = ?',
+               array (mb_strlen ($cable) ? $cable : NULL, $link_id)
        );
 }
 
-function commitUnlinkPort ($port_id)
+function commitUnlinkPort ($link_id)
 {
        // fetch and log existing link
        $result = usePreparedSelectBlade
@@ -1774,9 +1811,8 @@ function commitUnlinkPort ($port_id)
                "INNER JOIN Port pb ON pb.id = Link.portb " .
                "INNER JOIN RackObject oa ON pa.object_id = oa.id " .
                "INNER JOIN RackObject ob ON pb.object_id = ob.id " .
-               "WHERE " .
-               "Link.porta = ? OR Link.portb = ?",
-               array ($port_id, $port_id)
+               "WHERE Link.id = ?",
+               array ($link_id)
        );
        $rows = $result->fetchAll (PDO::FETCH_ASSOC);
        unset ($result);
@@ -1787,7 +1823,7 @@ function commitUnlinkPort ($port_id)
        }
 
        // remove existing link
-       usePreparedDeleteBlade ('Link', array ('porta' => $port_id, 'portb' => $port_id), 'OR');
+       usePreparedDeleteBlade ('Link', array ('id' => $link_id));
 }
 
 function addPortLogEntry ($port_id, $message)
@@ -4700,7 +4736,15 @@ function getPortIIFStats ($args)
 function getPortInfo ($port_id)
 {
        $result = fetchPortList ('Port.id = ?', array ($port_id));
-       return empty ($result) ? NULL : $result[0];
+       if (empty ($result))
+               return NULL;
+       return array_first ($result);
+}
+
+function getPortLinkInfo ($link_id)
+{
+       $result = usePreparedSelectBlade ('SELECT porta, portb, cable FROM Link WHERE id = ?', array ($link_id));
+       return $result->fetch (PDO::FETCH_ASSOC);
 }
 
 function getVLANDomainStats ()
index f645788b4b647696fef681015dcddfec3571f8b9..f2b3adbee121db7b14ba7c3510f77c27fb55865d 100644 (file)
@@ -38,9 +38,11 @@ function isInnoDBSupported ()
        global $dbxlink;
        // create a temp table
        $dbxlink->query("CREATE TABLE `innodb_test` (`id` int) ENGINE=InnoDB");
-       $row = $dbxlink->query("SHOW TABLE STATUS LIKE 'innodb_test'")->fetch(PDO::FETCH_ASSOC);
+       $innodb_row = $dbxlink->query("SHOW TABLE STATUS LIKE 'innodb_test'")->fetch(PDO::FETCH_ASSOC);
+       $dbxlink->query("CREATE TRIGGER `trigger_test` BEFORE INSERT ON `innodb_test` FOR EACH ROW BEGIN END");
+       $trigger_row = $dbxlink->query("SELECT COUNT(*) AS count FROM information_schema.TRIGGERS WHERE TRIGGER_SCHEMA = SCHEMA() AND TRIGGER_NAME = 'trigger_test'")->fetch(PDO::FETCH_ASSOC);
        $dbxlink->query("DROP TABLE `innodb_test`");
-       if ($row['Engine'] == 'InnoDB')
+       if ($innodb_row['Engine'] == 'InnoDB' && $trigger_row['count'] == 1)
                return TRUE;
 
        return FALSE;
@@ -85,6 +87,14 @@ function platform_is_ok ()
        $nerrs += platform_function_test ('json_encode', 'JSON extension', 'JavaScript interface bits may fail.');
        platform_generic_test (in_array  ('curl', get_loaded_extensions()), 'cURL extension', 'Not found, Cacti Graph integration is unavailable.', 'trwarning');
        $nerrs += platform_function_test ('bcmul', 'BC Math extension');
+       @include_once 'Image/GraphViz.php';
+       platform_generic_test
+       (
+               class_exists ('Image_GraphViz'),
+               'GraphViz PEAR module',
+               'Not found, graphical cable path tracing is unavailable',
+               'trwarning'
+       );
        platform_generic_test
        (
                (!empty($_SERVER['HTTPS']) and $_SERVER['HTTPS'] != 'off'),
index b68b07c15e39743b0967a72a06e0e56a35a01095..4d0d530612e46d7e1b8d68a7522e0106cdabd728 100644 (file)
@@ -5937,6 +5937,39 @@ function isCLIMode ()
        return !isset ($_SERVER['REQUEST_METHOD']);
 }
 
+// Used to sort a chain of links from start to finish (sorts by port_id)
+function sortLinks ($port_id, $unsorted_links, $sorted_links = array (), $level = 0)
+{
+       $self = __FUNCTION__;
+
+       if ($level >= 10)
+               throw new InvalidArgException ('port', $port_id, 'tracing depth too deep - a loop probably exists');
+
+       // add the provided link to the sorted array
+       foreach ($unsorted_links as $link_id => $link)
+       {
+               $remote_port_id = FALSE;
+               if ($link[0] == $port_id) $remote_port_id = $link[1];
+               if ($link[1] == $port_id) $remote_port_id = $link[0];
+               if ($remote_port_id)
+               {
+                       // note that this link has been sorted
+                       unset ($unsorted_links[$link_id]);
+
+                       //make sure this port_id is on the left (except if it's the last link, then it should be on the right)
+                       if ($remote_port_id == $link[0])
+                       {
+                               $tmp = $link[0];
+                               $link[0] = $link[1];
+                               $link[1] = $tmp;
+                       }
+                       $sorted_links[] = $link;
+                       $sorted_links = $self ($remote_port_id, $unsorted_links, $sorted_links, $level+1);
+               }
+       }
+       return $sorted_links;
+}
+
 // Checks if 802.1Q port uplink/downlink feature is misconfigured.
 // Returns FALSE if 802.1Q port role/linking is wrong, TRUE otherwise.
 function checkPortRole ($vswitch, $port_name, $port_order)
index 31e11996360cbe8cb307fa2940d7a68df1638891..68e0afe79c66ce7e8965c1da9bb62426f9be7baf 100644 (file)
@@ -748,12 +748,14 @@ CREATE TABLE `LDAPCache` (
 ) ENGINE=InnoDB;
 
 CREATE TABLE `Link` (
+  `id` int(10) unsigned NOT NULL auto_increment,
   `porta` int(10) unsigned NOT NULL default '0',
   `portb` int(10) unsigned NOT NULL default '0',
   `cable` char(64) DEFAULT NULL,
-  PRIMARY KEY  (`porta`,`portb`),
-  UNIQUE KEY `porta` (`porta`),
-  UNIQUE KEY `portb` (`portb`),
+  PRIMARY KEY  (`id`),
+  UNIQUE KEY `porta-portb-unique` (`porta`,`portb`),
+  KEY `porta` (`porta`),
+  KEY `portb` (`portb`),
   CONSTRAINT `Link-FK-a` FOREIGN KEY (`porta`) REFERENCES `Port` (`id`) ON DELETE CASCADE,
   CONSTRAINT `Link-FK-b` FOREIGN KEY (`portb`) REFERENCES `Port` (`id`) ON DELETE CASCADE
 ) ENGINE=InnoDB;
@@ -1134,6 +1136,66 @@ CREATE TABLE `VSEnabledPorts` (
   CONSTRAINT `VSEnabledPorts-FK-vs_id-proto-vport` FOREIGN KEY (`vs_id`, `proto`, `vport`) REFERENCES `VSPorts` (`vs_id`, `proto`, `vport`) ON DELETE CASCADE
 ) ENGINE=InnoDB;
 
+DELIMITER ;;
+CREATE TRIGGER `checkLinkBeforeInsert` BEFORE INSERT ON `Link`
+  FOR EACH ROW
+BEGIN
+  DECLARE tmp, porta_type, portb_type, count INTEGER;
+  IF NEW.porta = NEW.portb THEN
+    SET NEW.porta = NULL;
+  ELSEIF NEW.porta > NEW.portb THEN
+    SET tmp = NEW.porta;
+    SET NEW.porta = NEW.portb;
+    SET NEW.portb = tmp;
+  END IF; 
+  SELECT type INTO porta_type FROM Port WHERE id = NEW.porta;
+  SELECT type INTO portb_type FROM Port WHERE id = NEW.portb;
+  SELECT COUNT(*) INTO count FROM PortCompat WHERE (type1 = porta_type AND type2 = portb_type) OR (type1 = portb_type AND type2 = porta_type);
+  IF count = 0 THEN
+    SET NEW.porta = NULL;
+  END IF;
+END;;
+
+CREATE TRIGGER `checkLinkBeforeUpdate` BEFORE UPDATE ON `Link`
+  FOR EACH ROW
+BEGIN
+  DECLARE tmp, porta_type, portb_type, count INTEGER;
+  IF NEW.porta = NEW.portb THEN
+    SET NEW.porta = NULL;
+  ELSEIF NEW.porta > NEW.portb THEN
+    SET tmp = NEW.porta;
+    SET NEW.porta = NEW.portb;
+    SET NEW.portb = tmp;
+  END IF; 
+  SELECT type INTO porta_type FROM Port WHERE id = NEW.porta;
+  SELECT type INTO portb_type FROM Port WHERE id = NEW.portb;
+  SELECT COUNT(*) INTO count FROM PortCompat WHERE (type1 = porta_type AND type2 = portb_type) OR (type1 = portb_type AND type2 = porta_type);
+  IF count = 0 THEN
+    SET NEW.porta = NULL;
+  END IF;
+END;;
+
+CREATE TRIGGER `checkPortCompatBeforeDelete` BEFORE DELETE ON `PortCompat`
+  FOR EACH ROW
+BEGIN
+  DECLARE count INTEGER;
+  SELECT COUNT(*) INTO count FROM Link LEFT JOIN Port AS PortA ON Link.porta = PortA.id LEFT JOIN Port AS PortB ON Link.portb = PortB.id WHERE (PortA.type = OLD.type1 AND PortB.type = OLD.type2) OR (PortA.type = OLD.type2 AND PortB.type = OLD.type1);
+  IF count > 0 THEN
+    UPDATE `Cannot delete: rule still used` SET x = 1;
+  END IF;
+END;;
+
+CREATE TRIGGER `checkPortCompatBeforeUpdate` BEFORE UPDATE ON `PortCompat`
+  FOR EACH ROW
+BEGIN
+  DECLARE count INTEGER;
+  SELECT COUNT(*) INTO count FROM Link LEFT JOIN Port AS PortA ON Link.porta = PortA.id LEFT JOIN Port AS PortB ON Link.portb = PortB.id WHERE (PortA.type = OLD.type1 AND PortB.type = OLD.type2) OR (PortA.type = OLD.type2 AND PortB.type = OLD.type1);
+  IF count > 0 THEN
+    UPDATE `Cannot update: rule still used` SET x = 1;
+  END IF;
+END;;
+DELIMITER ;
+
 CREATE VIEW `Location` AS SELECT O.id, O.name, O.has_problems, O.comment, P.id AS parent_id, P.name AS parent_name
 FROM `Object` O
 LEFT JOIN (
index 81dcfbc94af59ba7a23b16ab1eb06d52803488f8..ca626fde058817473f79494e01ceeca7c4c02a9c 100644 (file)
@@ -1135,20 +1135,44 @@ function renderObjectPortRow ($port, $is_highlighted)
        echo "><td class='tdleft' NOWRAP><a name='port-${port['id']}' class='ancor interactive-portname nolink $a_class'>${port['name']}</a></td>";
        echo "<td class=tdleft>${port['label']}</td>";
        echo "<td class=tdleft>" . formatPortIIFOIF ($port) . "</td><td class=tdleft><tt>${port['l2address']}</tt></td>";
-       if ($port['remote_object_id'])
-       {
-               echo "<td class=tdleft>" .
-                       formatPortLink ($port['remote_object_id'], $port['remote_object_name'], $port['remote_id'], NULL) .
-                       "</td>";
-               echo "<td class=tdleft>" . formatLoggedSpan ($port['last_log'], $port['remote_name'], 'underline') . "</td>";
-               $editable = permitted ('object', 'ports', 'editPort')
-                       ? 'editable'
-                       : '';
-               echo "<td class=tdleft><span class='rsvtext $editable id-${port['id']} op-upd-reservation-cable'>${port['cableid']}</span></td>";
+       $links = array ();
+       if ($port['linked'] == 1)
+       {
+               $links = $port['links'];
+               echo '<td class=tdleft>'.formatLoggedSpan ($links[0]['last_log'], formatPortLink ($links[0]['remote_object_id'], $links[0]['remote_object_name'], $links[0]['remote_id'], NULL)).'</td>';
+               echo '<td class=tdleft>'.formatLoggedSpan ($links[0]['last_log'], $links[0]['remote_name'], 'underline').'&nbsp;';
+               $helper_args = array ('port' => $port['id']);
+               $popup_args = 'height=700, width=700, location=no, menubar=no, resizable=yes, scrollbars=yes, status=no, titlebar=no, toolbar=no';
+               echo "<span onclick='window.open(\"" . makeHrefForHelper ('traceroute', $helper_args);
+               echo "\",\"findlink\",\"${popup_args}\");'>";
+               printImageHREF ('find', 'Trace this port');
+               echo '</span></td>';
+               $editable = permitted ('object', 'ports', 'editPort')? 'editable' : '';
+               echo "<td class=tdleft><span class='rsvtext $editable id-".$links[0]['link_id']." op-upd-reservation-cable'>".$links[0]['cableid'].'</span></td>';
+               // display other links, if any
+               if (count ($links) > 1)
+               {
+                       // finish off the current row
+                       echo "</tr>\n";
+
+                       // place each link on a new row
+                       for ($i=1; $i<count($links); $i++) {
+                               echo '<tr';
+                               if ($is_highlighted)
+                                       echo ' class=highlight';
+                               echo "><td class=tdleft colspan=4>&nbsp;</td>";
+                               echo '<td class=tdleft>'.formatLoggedSpan ($links[$i]['last_log'], formatPortLink ($links[$i]['remote_object_id'], $links[$i]['remote_object_name'], $links[$i]['remote_id'], NULL)).'</td>';
+                               echo '<td class=tdleft>'.formatLoggedSpan ($links[$i]['last_log'], $links[$i]['remote_name'], 'underline').'</td>';
+                               $editable = permitted ('object', 'ports', 'editPort')? 'editable' : '';
+                               echo "<td class=tdleft><span class='rsvtext $editable id-".$links[$i]['link_id']." op-upd-reservation-cable'>".$links[$i]['cableid']."</span></td></tr>\n";
+                       }
+               }
        }
        else
                echo implode ('', formatPortReservation ($port)) . '<td></td>';
-       echo "</tr>";
+       // if there are 1 or fewer links, we need to conclude the row (otherwise it would have been done already)
+       if (count ($links) <= 1)
+               echo "</tr>\n";
 }
 
 function renderObject ($object_id)
@@ -1217,7 +1241,7 @@ function renderObject ($object_id)
                        )
                )."&"
        ));
-       renderEntitySummary ($info, 'summary', $summary);
+       renderEntitySummary ($info, 'Summary', $summary);
 
        if (strlen ($info['comment']))
        {
@@ -1249,7 +1273,7 @@ function renderObject ($object_id)
 
        if (count ($info['ports']))
        {
-               startPortlet ('ports and links');
+               startPortlet ('Ports and links');
                $hl_port_id = 0;
                if (isset ($_REQUEST['hl_port_id']))
                {
@@ -1257,11 +1281,11 @@ function renderObject ($object_id)
                        $hl_port_id = $_REQUEST['hl_port_id'];
                        addAutoScrollScript ("port-$hl_port_id");
                }
-               echo "<table cellspacing=0 cellpadding='5' align='center' class='widetable'>";
+               echo "<table border=0 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>';
+               echo "<th class=tdleft>Cable ID</th></tr>\n";
                foreach ($info['ports'] as $port)
                        callHook ('renderObjectPortRow', $port, ($hl_port_id == $port['id']));
                if (permitted (NULL, 'ports', 'set_reserve_comment'))
@@ -1423,7 +1447,7 @@ function renderPortsForObject ($object_id)
                echo "<tr><td>";
                printImageHREF ('add', 'add a port', TRUE);
                echo "</td><td class='tdleft'><input type=text size=8 name=port_name tabindex=100></td>\n";
-               echo "<td><input type=text name=port_label tabindex=101></td><td>";
+               echo "<td><input type=text name=port_label tabindex=101></td><td class=tdleft>";
                printNiftySelect (getNewPortTypeOptions(), array ('name' => 'port_type_id', 'tabindex' => 102), $prefs['selected']);
                echo "<td><input type=text name=port_l2address tabindex=103 size=18 maxlength=24></td>\n";
                echo "<td colspan=4>&nbsp;</td><td>";
@@ -1448,21 +1472,43 @@ function renderPortsForObject ($object_id)
                printNiftySelect (getNewPortTypeOptions(), array ('name' => 'port_type_id', 'tabindex' => 107), $prefs['selected']);
                echo "<td><input type=text name=port_numbering_start tabindex=108 size=3 maxlength=3></td>\n";
                echo "<td><input type=text name=port_numbering_count tabindex=109 size=3 maxlength=3></td>\n";
-               echo "<td>&nbsp;</td><td>";
+               echo "<td>";
                printImageHREF ('add', 'add ports', TRUE, 110);
                echo "</td></tr></form>";
                echo "</table><br>\n";
        }
 
-       echo "<table cellspacing=0 cellpadding='5' align='center' class='widetable'>\n";
+       // clear ports link
+       echo "<a href='".
+               makeHrefProcess(array ('op'=>'deleteAll')).
+               "' onclick=\"javascript:return confirm('Are you sure you want to delete all existing ports?')\">" . getImageHREF ('clear', 'Clear port list') . " Clear port list</a>";
+
+       // link patch panels link
+       if ($object['objtype_id'] == 9)
+       {
+               $helper_args = array ('object_id' => $object_id);
+               $popup_args = 'height=700, width=400, location=no, menubar=no, resizable=yes, scrollbars=yes, status=no, titlebar=no, toolbar=no';
+               echo "&nbsp;&nbsp;&nbsp;&nbsp;<a href='javascript:;' ";
+               echo "onclick='window.open(\"" . makeHrefForHelper ('patchpanellist', $helper_args);
+               echo "\",\"findlink\",\"${popup_args}\");'>";
+               printImageHREF ('plug', 'Link to another patch panel');
+               echo 'Link to another patch panel</a>';
+       }
+
+       $helper_args = array ('object_id' => $object_id);
+       $popup_args = 'height=700, width=700, location=no, menubar=no, resizable=yes, scrollbars=yes, status=no, titlebar=no, toolbar=no';
+       echo "&nbsp;&nbsp;&nbsp;&nbsp;<a href='javascript:;' ";
+       echo "onclick='window.open(\"" . makeHrefForHelper ('traceroute', $helper_args);
+       echo "\",\"findlink\",\"${popup_args}\");'>";
+       printImageHREF ('find', 'Trace all port links');
+       echo 'Trace all ports</a>';
+
+       echo "<table border=0 cellspacing=0 cellpadding='5' align='center' class='widetable'>\n";
        echo "<tr><th>&nbsp;</th><th class=tdleft>Local name</th><th class=tdleft>Visible label</th><th class=tdleft>Interface</th><th class=tdleft>L2 address</th>";
-       echo "<th class=tdcenter colspan=2>Remote object and port</th><th>Cable ID</th><th class=tdcenter>(Un)link or (un)reserve</th><th>&nbsp;</th></tr>\n";
+       echo "<th class=tdleft colspan=2>Remote object and port</th><th class=tdleft>Cable ID</th><th class=tdleft>(Un)link or (un)reserve</th><th>&nbsp;</th></tr>\n";
        if (getConfigVar ('ADDNEW_AT_TOP') == 'yes')
                printNewItemTR ($prefs);
 
-       // clear ports link
-       echo getOpLink (array ('op'=>'deleteAll'), 'Clear port list', 'clear', '', 'need-confirmation');
-
        if (isset ($_REQUEST['hl_port_id']))
        {
                assertUIntArg ('hl_port_id');
@@ -1472,6 +1518,7 @@ function renderPortsForObject ($object_id)
        switchportInfoJS ($object_id); // load JS code to make portnames interactive
        foreach ($object['ports'] as $port)
        {
+               $links = array ();
                $tr_class = isset ($hl_port_id) && $hl_port_id == $port['id'] ? 'class="highlight"' : '';
                printOpFormIntro ('editPort', array ('port_id' => $port['id']));
                echo "<tr $tr_class><td><a name='port-${port['id']}' href='".makeHrefProcess(array('op'=>'delPort', 'port_id'=>$port['id']))."'>";
@@ -1480,7 +1527,7 @@ function renderPortsForObject ($object_id)
                $a_class = isEthernetPort ($port) ? 'port-menu' : '';
                echo "<td class='tdleft' NOWRAP><input type=text name=name class='interactive-portname $a_class' value='${port['name']}' size=8></td>";
                echo "<td><input type=text name=label value='${port['label']}'></td>";
-               if (!$port['remote_object_id'])
+               if ($port['linked'] == 0)
                {
                        echo '<td>';
                        if ($port['iif_id'] != 1)
@@ -1498,21 +1545,68 @@ function renderPortsForObject ($object_id)
                // 18 is enough to fit 6-byte MAC address in its longest form,
                // while 24 should be Ok for WWN
                echo "<td><input type=text name=l2address value='${port['l2address']}' size=18 maxlength=24></td>\n";
-               if ($port['remote_object_id'])
-               {
-                       echo "<td>" .
-                               formatLoggedSpan ($port['last_log'], formatPortLink ($port['remote_object_id'], $port['remote_object_name'], $port['remote_id'], NULL)) .
-                               "</td>";
-                       echo "<td> " . formatLoggedSpan ($port['last_log'], $port['remote_name'], 'underline') .
-                               "<input type=hidden name=reservation_comment value=''></td>";
-                       echo "<td><input type=text name=cable value='${port['cableid']}'></td>";
-                       echo "<td class=tdcenter>";
-                       echo getOpLink (array('op'=>'unlinkPort', 'port_id'=>$port['id'], ), '', 'cut', 'Unlink this port');
-                       echo "</td>";
+               if ($port['linked'] == 1)
+               {
+                       $links = $port['links'];
+                       echo "<input type=hidden name=reservation_comment value=''>";
+                       echo "<input type=hidden name=link_id value='".$links[0]['link_id']."'>";
+                       echo '<td class=tdleft>'.formatLoggedSpan ($port['last_log'], formatPortLink ($links[0]['remote_object_id'], $links[0]['remote_object_name'], $links[0]['remote_id'], NULL)).'</td>';
+                       echo '<td class=tdleft>'.formatLoggedSpan ($port['last_log'], $links[0]['remote_name'], 'underline').'&nbsp;';
+                       $helper_args = array ('port' => $port['id']);
+                       $popup_args = 'height=700, width=700, location=no, menubar=no, resizable=yes, scrollbars=yes, status=no, titlebar=no, toolbar=no';
+                       echo "<span onclick='window.open(\"" . makeHrefForHelper ('traceroute', $helper_args);
+                       echo "\",\"findlink\",\"${popup_args}\");'>";
+                       printImageHREF ('find', 'Trace this port');
+                       echo '</span></td>';
+
+                       echo "<td><input type=text name=cable value='".$links[0]['cableid']."'></td>";
+                       echo '<td class=tdcenter><span';
+                       $helper_args = array ('port' => $port['id'], 'in_rack' => 'on');
+                       $popup_args = 'height=700, width=400, location=no, menubar=no, resizable=yes, scrollbars=yes, status=no, titlebar=no, toolbar=no';
+                       echo " onclick='window.open(\"" . makeHrefForHelper ('portlist', $helper_args);
+                       echo "\",\"findlink\",\"${popup_args}\");'>";
+                       printImageHREF ('plug', 'Link this port');
+                       echo "</span>&nbsp;<a href='".
+                               makeHrefProcess(array(
+                                       'op'=>'unlinkPort',
+                                       'link_id'=>$links[0]['link_id'],
+                                       'object_id'=>$object_id)).
+                               "'>";
+                       printImageHREF ('cut', 'Unlink this port');
+                       echo '</a></td>';
+                       // display other links, if any
+                       if (count($links) > 1)
+                       {
+                               // finish off the current row
+                               echo '<td>';
+                               printImageHREF ('save', 'Save changes', TRUE);
+                               echo "</td></form></tr>\n";
+
+                               // place each link on a new row
+                               for ($i=1; $i<count($links); $i++)
+                               {
+                                       printOpFormIntro ('editPort', array ('port_id' => $port['id']));
+                                       echo "<input type=hidden name=name value='".$links[$i]['remote_name']."'>";
+                                       echo "<input type=hidden name=link_id value='".$links[$i]['link_id']."'>";
+                                       echo "<tr $tr_class><td colspan=5>&nbsp;</td><td class=tdleft>";
+                                       echo formatLoggedSpan ($links[$i]['last_log'], formatPortLink ($links[$i]['remote_object_id'], $links[$i]['remote_object_name'], $links[$i]['remote_id'], NULL));
+                                       echo '</td><td class=tdleft>'.formatLoggedSpan ($links[$i]['last_log'], $links[$i]['remote_name'], 'underline').'</td>';
+                                       echo "<td><input type=text name=cable value='".$links[$i]['cableid']."'></td><td class=tdcenter><a href='".
+                                               makeHrefProcess(array(
+                                                       'op'=>'unlinkPort',
+                                                       'link_id'=>$links[$i]['link_id'],
+                                                       'object_id'=>$object_id)).
+                                               "'>";
+                                       printImageHREF ('cut', 'Unlink this port');
+                                       echo '</a></td><td>';
+                                       printImageHREF ('save', 'Save changes', TRUE);
+                                       echo "</td></form></tr>\n";
+                               }
+                       }
                }
                elseif (strlen ($port['reservation_comment']))
                {
-                       echo "<td>" . formatLoggedSpan ($port['last_log'], 'Reserved:', 'strong underline') . "</td>";
+                       echo "<td class=tdleft>" . formatLoggedSpan ($port['last_log'], 'Reserved:', 'strong underline') . "</td>";
                        echo "<td><input type=text name=reservation_comment value='${port['reservation_comment']}'></td>";
                        echo "<td></td>";
                        echo "<td class=tdcenter>";
@@ -1530,21 +1624,18 @@ function renderPortsForObject ($object_id)
                        );
                        $popup_args = 'height=700, width=400, location=no, menubar=no, '.
                                'resizable=yes, scrollbars=yes, status=no, titlebar=no, toolbar=no';
-                       echo " ondblclick='window.open(\"" . makeHrefForHelper ('portlist', $helper_args);
-                       echo "\",\"findlink\",\"${popup_args}\");'";
-                       // end of onclick=
                        echo " onclick='window.open(\"" . makeHrefForHelper ('portlist', $helper_args);
-                       echo "\",\"findlink\",\"${popup_args}\");'";
-                       // end of onclick=
-                       echo '>';
-                       // end of <a>
+                       echo "\",\"findlink\",\"${popup_args}\");'>";
                        printImageHREF ('plug', 'Link this port');
-                       echo "</span>";
-                       echo " <input type=text name=reservation_comment></td>\n";
+                       echo "</span>&nbsp;<input type=text name=reservation_comment></td>\n";
+               }
+               // if there are 1 or fewer links, we need to conclude the row (otherwise it would have been done already)
+               if (count($links) <= 1)
+               {
+                       echo '<td>';
+                       printImageHREF ('save', 'Save changes', TRUE);
+                       echo "</td></form></tr>\n";
                }
-               echo "<td>";
-               printImageHREF ('save', 'Save changes', TRUE);
-               echo "</td></form></tr>\n";
        }
        if (getConfigVar ('ADDNEW_AT_TOP') != 'yes')
                printNewItemTR ($prefs);
index 4bbb035302facd6601640dfc3a5b877d7b679945..953619e1191f0b111d178f5526ff04011cee0af8 100644 (file)
@@ -735,13 +735,19 @@ function editPortForObject ()
 {
        global $sic;
        assertUIntArg ('port_id');
-       assertUIntArg ('port_type_id');
-       assertStringArg ('reservation_comment', TRUE);
-       genericAssertion ('l2address', 'l2address0');
-       genericAssertion ('name', 'string');
-       commitUpdatePort ($sic['object_id'], $sic['port_id'], $sic['name'], $sic['port_type_id'], $sic['label'], $sic['l2address'], $sic['reservation_comment']);
+       if (array_key_exists ('port_type_id', $_REQUEST))
+       {
+               assertUIntArg ('port_type_id');
+               assertStringArg ('reservation_comment', TRUE);
+               genericAssertion ('l2address', 'l2address0');
+               genericAssertion ('name', 'string');
+               commitUpdatePort ($sic['object_id'], $sic['port_id'], $sic['name'], $sic['port_type_id'], $sic['label'], $sic['l2address'], $sic['reservation_comment']);
+       }
        if (array_key_exists ('cable', $_REQUEST))
-               commitUpdatePortLink ($sic['port_id'], $sic['cable']);
+       {
+               assertUIntArg ('link_id');
+               commitUpdatePortLink ($sic['link_id'], $sic['cable']);
+       }
        return showFuncMessage (__FUNCTION__, 'OK', array ($_REQUEST['name']));
 }
 
@@ -3179,9 +3185,9 @@ function getOpspec()
 
 function unlinkPort ()
 {
-       assertUIntArg ('port_id');
-       commitUnlinkPort ($_REQUEST['port_id']);
-       showSuccess ("Port unlinked successfully");
+       assertUIntArg ('link_id');
+       commitUnlinkPort ($_REQUEST['link_id']);
+       showSuccess ('Port unlinked successfully');
 }
 
 function clearVlan()
index bde3d3238c0e825a8a43852dc677036e618b139c..9281f22509a20ef4867cd80aa881a821839bd7d4 100644 (file)
@@ -85,12 +85,15 @@ INNER JOIN (
 ) AS sub2 ON sub2.id = p.id
 ";
        $qparams[] = $port_info['oif_id'];
-
-       // self and linked ports filter
-       $query .= " WHERE p.id <> ? " .
-               "AND p.id NOT IN (SELECT porta FROM Link) " .
-               "AND p.id NOT IN (SELECT portb FROM Link) ";
        $qparams[] = $port_info['id'];
+
+       // don't allow a port to be linked to itself
+       $query .= " WHERE p.id <> ? ";
+
+       // don't allow already linked ports
+       if (!$filter['linked'])
+               $query .= "AND p.id NOT IN (SELECT porta FROM Link) AND p.id NOT IN (SELECT portb FROM Link) ";
+
        // rack filter
        if (! empty ($filter['racks']))
        {
@@ -155,6 +158,46 @@ INNER JOIN (
        return $ret;
 }
 
+// Return a list of all compatible patch panels
+function findPatchPanelCandidates ($panel_info, $filter)
+{
+       // return patch panels which have the same number of ports
+       // exclude those with ports which are linked more than once
+       // TODO: add port compatibility checks
+       $query = "
+SELECT Object.id, 
+IF(ISNULL(Object.name),CONCAT('[PatchPanel] - object_id: ',Object.id),Object.name) AS name 
+FROM Object 
+WHERE Object.objtype_id = 9 
+AND Object.id != ? 
+AND (SELECT COUNT(Port.id) FROM Port WHERE Port.object_id = Object.id) = (SELECT COUNT(Port.id) FROM Port WHERE Port.object_id = ?) ";
+       $qparams = array ($panel_info['id'], $panel_info['id']);
+
+       // name filter
+       if (! empty ($filter['name']))
+       {
+               $query .= 'AND Object.name LIKE ? ';
+               $qparams[] = '%' . $filter['name'] . '%';
+       }
+
+       // exclude panels which contain one or more ports which are already linked to two or more other ports
+       if (!$filter['linked'])
+               $query .= "AND Object.id NOT IN (SELECT DISTINCT(Object.id) 
+FROM Object 
+LEFT JOIN Port ON Object.id = Port.object_id 
+WHERE Object.objtype_id = 9 
+AND Port.id IN (SELECT Port.id FROM Port WHERE (SELECT COUNT(*) FROM Link WHERE Link.porta = Port.id OR Link.portb = Port.id) > 1)) ";
+
+       // ordering
+       $query .= ' ORDER BY Object.name';
+
+       $ret = array();
+       $result = usePreparedSelectBlade ($query, $qparams);
+       while ($row = $result->fetch (PDO::FETCH_ASSOC))
+               $ret[$row['id']] = $row['name'];
+       return $ret;
+}
+
 // Return a list of all objects which are possible parents
 //    Special case for VMs and VM Virtual Switches
 //        - only select Servers with the Hypervisor attribute set to Yes
@@ -356,9 +399,10 @@ END
 function renderPopupPortSelector()
 {
        assertUIntArg ('port');
-       $port_id = $_REQUEST['port'];
+       $port_id = $_REQUEST['port']; 
        $port_info = getPortInfo ($port_id);
        $in_rack = isCheckSet ('in_rack');
+       $linked = isCheckSet ('linked');
 
        // fill port filter structure
        $filter = array
@@ -366,6 +410,7 @@ function renderPopupPortSelector()
                'racks' => array(),
                'objects' => '',
                'ports' => '',
+               'linked' => $linked
        );
        if (isset ($_REQUEST['filter-obj']))
                $filter['objects'] = trim($_REQUEST['filter-obj']);
@@ -401,10 +446,11 @@ function renderPopupPortSelector()
        echo '<input type=hidden name="port" value="' . $port_id . '">';
        echo '<table align="center" valign="bottom"><tr>';
        echo '<td class="tdleft"><label>Object name:<br><input type=text size=8 name="filter-obj" value="' . htmlspecialchars ($filter['objects'], ENT_QUOTES) . '"></label></td>';
-       echo '<td class="tdleft"><label>Port name:<br><input type=text size=6 name="filter-port" value="' . htmlspecialchars ($filter['ports'], ENT_QUOTES) . '"></label></td>';
+       echo '<td class="tdleft"><label>Port name:<br><input type=text size=6 name="filter-port" value="' . htmlspecialchars ($filter['ports'], ENT_QUOTES) . '"></label></td></tr>';
        echo '<td class="tdleft" valign="bottom"><label><input type=checkbox name="in_rack"' . ($in_rack ? ' checked' : '') . '>Nearest racks</label></td>';
-       echo '<td valign="bottom"><input type=submit value="show ports"></td>';
-       echo '</tr></table>';
+       echo '<td class="tdleft" vlaign="bottom"><label><input type=checkbox name="linked"'. ($linked ? ' checked' : '') .'>Include linked ports</label></td></tr>';
+       echo '<tr><td colspan=2 valign="bottom"><input type=submit value="show ports"></td></tr>';
+       echo '</table>';
        finishPortlet();
 
        // display results
@@ -421,6 +467,184 @@ function renderPopupPortSelector()
        echo '</form>';
 }
 
+function handlePopupPatchPanelLink()
+{
+       global $dbxlink;
+       assertUIntArg ('object_id');
+       assertUIntArg ('remote_object_id');
+       $object_id = $_REQUEST['object_id']; 
+       $object = spotEntity ('object', $object_id);
+       amplifyCell ($object);
+       $remote_object_id = $_REQUEST['remote_object_id']; 
+       $remote_object = spotEntity ('object', $remote_object_id);
+       amplifyCell ($remote_object);
+
+       // reindex numerically instead of by port_id
+       $ports = array_values ($object['ports']);
+       $remote_ports = array_values ($remote_object['ports']);
+
+       $POIFC = getPortOIFCompat();
+       $dbxlink->beginTransaction();
+       $error = FALSE;
+       for ($i=0; $i<count($ports); $i++)
+       {
+               $matches = FALSE;
+               foreach ($POIFC as $pair)
+               {
+                       if ($pair['type1'] == $ports[$i]['oif_id'] && $pair['type2'] == $remote_ports[$i]['oif_id'])
+                       {
+                               $matches = TRUE;
+                               break;
+                       }
+               }
+               if ($matches)
+                       linkPorts ($ports[$i]['id'], $remote_ports[$i]['id']);
+               else
+               {
+                       $error = TRUE;
+                       break;
+               }
+       }
+       if ($error)
+       {
+               $dbxlink->rollBack();
+               showError ('Not all ports are compatible');
+       }
+       else
+       {
+               $dbxlink->commit();
+               showSuccess ('Patch panels linked successfully');
+       }
+       addJS (<<<END
+window.opener.location.reload(true);
+window.close();
+END
+       , TRUE);
+}
+
+function renderPopupPatchPanelSelector ()
+{
+       assertUIntArg ('object_id');
+       $object_id = $_REQUEST['object_id']; 
+       $object = spotEntity ('object', $object_id);
+       amplifyCell ($object);
+       $linked = isset ($_REQUEST['linked']);
+
+       $filter = array
+       (
+               'name' => '',
+               'linked' => $linked
+       );
+       if (isset ($_REQUEST['filter-name']))
+               $filter['name'] = $_REQUEST['filter-name'];
+       $found_panels = findPatchPanelCandidates ($object, $filter);
+
+       // display search form
+       echo "Link <a href='index.php?page=object&object_id=${object_id}'>${object['name']}</a> to...";
+       echo '<form method=GET>';
+       startPortlet ('Patch panel list filter');
+       echo '<input type=hidden name="module" value="popup">';
+       echo '<input type=hidden name="helper" value="patchpanellist">';
+       echo '<input type=hidden name="object_id" value="' . $object_id . '">';
+       echo '<table border=0 align="center" valign="bottom"><tr>';
+       echo '<td class="tdleft"><label>Panel name:<br><input type=text size=10 name="filter-name" value="' . htmlspecialchars ($filter['name'], ENT_QUOTES) . '"></label></td></tr>';
+       echo '<tr><td class="tdleft" vlaign="bottom"><label><input type=checkbox name="linked"'. ($linked ? ' checked' : '') .'>Include panels already linked</label></td></tr>';
+       echo '<tr><td valign="bottom"><input type=submit value="Show panels"></td></tr>';
+       echo '</table>';
+       finishPortlet();
+
+       // display results
+       startPortlet ('Compatible patch panels');
+       if (empty ($found_panels))
+               echo '(nothing found)';
+       else
+       {
+               echo getSelect ($found_panels, array ('name' => 'remote_object_id', 'size' => getConfigVar ('MAXSELSIZE')), NULL, FALSE);
+               echo "<p><input type='submit' value='Link' name='do_link'>";
+       }
+       finishPortlet();
+       echo '</form>';
+}
+
+function renderPopupTraceRoute ()
+{
+       // disable strict error reporting (GraphViz generates several)
+       error_reporting(E_ERROR | E_WARNING | E_PARSE);
+       @include_once 'Image/GraphViz.php';
+       if (!class_exists ('Image_GraphViz'))
+       {
+               echo ('<p>The GraphViz PEAR module could not be found.</p>');
+               return;
+       }
+       // determine if an object or port is being traced
+       if (isset ($_REQUEST['object_id']))
+       {
+               assertUIntArg ('object_id');
+               $object = spotEntity ('object', $_REQUEST['object_id']);
+               amplifyCell ($object);
+               $port_data = array ();
+               foreach ($object['ports'] as $port_id => $port_details)
+                       $port_data = $port_data + getNeighborPorts ($port_id);
+       }
+       else
+       {
+               assertUIntArg ('port');
+               $port_id = intval ($_REQUEST['port']);
+               $port_data = getNeighborPorts ($port_id);
+       }
+
+       $graph = new Image_GraphViz(NULL, NULL, 'Trace route');
+
+       // add a cluster to the graph for each unique object
+       $objects = array ();
+       foreach ($port_data as $port_id => $port_details)
+       {
+               $object_id = $port_details['object_id'];
+               if (!array_key_exists ($object_id, $objects))
+               {
+                       $objects[$object_id] = $port_details['object_name'];
+                       $graph->addCluster("${object_id}Cluster", $port_details['object_name'], array ('URL' => "index.php?page=object&object_id=${object_id}"));
+               }
+       }
+
+       // add ports to the graph
+       foreach ($port_data as $port_id => $port_details)
+       {
+               $object_id = $port_details['object_id'];
+               $graph->addNode("${port_id}Node", array ('fontsize' => 8, 'label' => $port_details['port_name'], 'tooltip' => $port_details['port_name']), "${object_id}Cluster");
+       }
+
+       // identify the links
+       $links = $link_count = array ();
+       foreach ($port_data as $port_id => $port_details)
+       {
+               $remote_port_id = $port_details['remote_port_id'];
+               // skip this if the link has already been recorded
+               if (in_array (array ($port_id, $remote_port_id), $links) || in_array (array ($remote_port_id, $port_id), $links))
+                       continue;
+
+               // record the link, also increment the endpoint counters
+               $links[] = array ($port_id, $remote_port_id);
+               $link_count[$port_id] = (!isset ($link_count[$port_id])) ? 1 : $link_count[$port_id]+1;
+               $link_count[$remote_port_id] = (!isset ($link_count[$remote_port_id])) ? 1 : $link_count[$remote_port_id]+1;
+       }
+
+       // if there are only two endpoints, flatten the graph
+       $endpoints = array_keys ($link_count, 1);
+       if (count ($endpoints) == 2)
+       {
+               sort ($endpoints);
+               $links = sortLinks ($endpoints[0], $links);
+       }
+
+       // add links to the graph
+       foreach ($links as $link)
+               $graph->addEdge(array ("${link[0]}Node" => "${link[1]}Node"), array ('arrowhead' => 'none', 'tooltip' => 'link'));
+
+       // display the graph
+       $graph->image();
+}
+
 function renderPopupIPv4Selector()
 {
        echo '<div style="background-color: #f0f0f0; border: 1px solid #3c78b5; padding: 10px; height: 100%; text-align: center; margin: 5px;">';
@@ -444,10 +668,6 @@ function renderPopupIPv4Selector()
 function renderPopupHTML()
 {
        global $pageno, $tabno;
-header ('Content-Type: text/html; charset=UTF-8');
-?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" style="height: 100%;">
-<?php
        assertStringArg ('helper');
        $text = '';
        switch ($_REQUEST['helper'])
@@ -460,17 +680,27 @@ header ('Content-Type: text/html; charset=UTF-8');
                        $text .= getOutputOf ('renderPopupObjectSelector');
                        break;
                case 'portlist':
+               case 'patchpanellist':
+                       $target = ($_REQUEST['helper'] == 'portlist') ? 'Port' : 'PatchPanel';
                        $pageno = 'depot';
                        $tabno = 'default';
                        fixContext();
                        assertPermission();
                        $text .= '<div style="background-color: #f0f0f0; border: 1px solid #3c78b5; padding: 10px; height: 100%; text-align: center; margin: 5px;">';
                        if (isset ($_REQUEST['do_link']))
-                               $text .= getOutputOf ('callHook', 'handlePopupPortLink');
+                               $text .= getOutputOf ('callHook', "handlePopup${target}Link");
                        else
-                               $text .= getOutputOf ('callHook' , 'renderPopupPortSelector');
+                               $text .= getOutputOf ('callHook' , "renderPopup${target}Selector");
                        $text .= '</div>';
                        break;
+               case 'traceroute':
+                       $pageno = 'depot';
+                       $tabno = 'default';
+                       fixContext();
+                       assertPermission();
+                       renderPopupTraceroute ();
+                       exit;
+                       break;
                case 'inet4list':
                        $pageno = 'ipv4space';
                        $tabno = 'default';
@@ -481,6 +711,10 @@ header ('Content-Type: text/html; charset=UTF-8');
                default:
                        throw new InvalidRequestArgException ('helper', $_REQUEST['helper']);
        }
+header ('Content-Type: text/html; charset=UTF-8');
+?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" style="height: 100%;">
+<?php
        echo '<head><title>RackTables pop-up</title>';
        printPageHeaders();
        echo '</head>';
index 57f8a58474ae921f6121b46c57e4f49034ad0b34..21d90da1fc5632a96c4ae41fbd30221646bdf1c1 100644 (file)
@@ -261,6 +261,7 @@ function getDBUpgradePath ($v1, $v2)
                '0.20.3',
                '0.20.4',
                '0.20.5',
+               '0.21.0',
        );
        if (!in_array ($v1, $versionhistory) or !in_array ($v2, $versionhistory))
                return NULL;
@@ -1772,6 +1773,82 @@ CREATE TABLE `VSEnabledPorts` (
                        $query[] = "ALTER TABLE `UserConfig` DROP FOREIGN KEY `UserConfig-FK-user`";
                        $query[] = "UPDATE Config SET varvalue = '0.20.5' WHERE varname = 'DB_VERSION'";
                        break;
+               case '0.21.0':
+                       if (!isInnoDBSupported ())
+                       {
+                               showUpgradeError ("Cannot upgrade because triggers are not supported by your MySQL server.", __FUNCTION__);
+                               die;
+                       }
+                       // allow one-to-many port links
+                       $query[] = "ALTER TABLE `Link` DROP FOREIGN KEY `Link-FK-a`, DROP FOREIGN KEY `Link-FK-b`";
+                       $query[] = "ALTER TABLE `Link` DROP PRIMARY KEY, DROP KEY `porta`, DROP KEY `portb`";
+                       $query[] = "ALTER TABLE `Link` ADD UNIQUE KEY `porta-portb-unique` (`porta`,`portb`), ADD KEY `porta` (`porta`), ADD KEY `portb` (`portb`)";
+                       $query[] = "ALTER TABLE `Link` ADD CONSTRAINT `Link-FK-a` FOREIGN KEY (`porta`) REFERENCES `Port` (`id`) ON DELETE CASCADE, ADD CONSTRAINT `Link-FK-b` FOREIGN KEY (`portb`) REFERENCES `Port` (`id`) ON DELETE CASCADE";
+                       $query[] = "ALTER TABLE `Link` ADD COLUMN `id` int(10) unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT FIRST";
+                       $query[] = "
+CREATE TRIGGER `checkLinkBeforeInsert` BEFORE INSERT ON `Link`
+  FOR EACH ROW
+BEGIN
+  DECLARE tmp, porta_type, portb_type, count INTEGER;
+  IF NEW.porta = NEW.portb THEN
+    SET NEW.porta = NULL;
+  ELSEIF NEW.porta > NEW.portb THEN
+    SET tmp = NEW.porta;
+    SET NEW.porta = NEW.portb;
+    SET NEW.portb = tmp;
+  END IF; 
+  SELECT type INTO porta_type FROM Port WHERE id = NEW.porta;
+  SELECT type INTO portb_type FROM Port WHERE id = NEW.portb;
+  SELECT COUNT(*) INTO count FROM PortCompat WHERE (type1 = porta_type AND type2 = portb_type) OR (type1 = portb_type AND type2 = porta_type);
+  IF count = 0 THEN
+    SET NEW.porta = NULL;
+  END IF;
+END;
+";
+                       $query[] = "
+CREATE TRIGGER `checkLinkBeforeUpdate` BEFORE UPDATE ON `Link`
+  FOR EACH ROW
+BEGIN
+  DECLARE tmp, porta_type, portb_type, count INTEGER;
+  IF NEW.porta = NEW.portb THEN
+    SET NEW.porta = NULL;
+  ELSEIF NEW.porta > NEW.portb THEN
+    SET tmp = NEW.porta;
+    SET NEW.porta = NEW.portb;
+    SET NEW.portb = tmp;
+  END IF; 
+  SELECT type INTO porta_type FROM Port WHERE id = NEW.porta;
+  SELECT type INTO portb_type FROM Port WHERE id = NEW.portb;
+  SELECT COUNT(*) INTO count FROM PortCompat WHERE (type1 = porta_type AND type2 = portb_type) OR (type1 = portb_type AND type2 = porta_type);
+  IF count = 0 THEN
+    SET NEW.porta = NULL;
+  END IF;
+END;
+";
+                       $query[] = "
+CREATE TRIGGER `checkPortCompatBeforeDelete` BEFORE DELETE ON `PortCompat`
+  FOR EACH ROW
+BEGIN
+  DECLARE count INTEGER;
+  SELECT COUNT(*) INTO count FROM Link LEFT JOIN Port AS PortA ON Link.porta = PortA.id LEFT JOIN Port AS PortB ON Link.portb = PortB.id WHERE (PortA.type = OLD.type1 AND PortB.type = OLD.type2) OR (PortA.type = OLD.type2 AND PortB.type = OLD.type1);
+  IF count > 0 THEN
+    UPDATE `Cannot delete: rule still used` SET x = 1;
+  END IF;
+END;
+";
+                       $query[] = "
+CREATE TRIGGER `checkPortCompatBeforeUpdate` BEFORE UPDATE ON `PortCompat`
+  FOR EACH ROW
+BEGIN
+  DECLARE count INTEGER;
+  SELECT COUNT(*) INTO count FROM Link LEFT JOIN Port AS PortA ON Link.porta = PortA.id LEFT JOIN Port AS PortB ON Link.portb = PortB.id WHERE (PortA.type = OLD.type1 AND PortB.type = OLD.type2) OR (PortA.type = OLD.type2 AND PortB.type = OLD.type1);
+  IF count > 0 THEN
+    UPDATE `Cannot update: rule still used` SET x = 1;
+  END IF;
+END;
+";
+                       $query[] = "UPDATE Config SET varvalue = '0.21.0' WHERE varname = 'DB_VERSION'";
+                       break;
                case 'dictionary':
                        $query = reloadDictionary();
                        break;