add database triggers to the Link table
authorAaron Dummer <aaron@dummer.info>
Sat, 21 Dec 2013 19:22:56 +0000 (11:22 -0800)
committerAaron Dummer <aaron@dummer.info>
Sat, 21 Dec 2013 19:22:56 +0000 (11:22 -0800)
README
tests/LinkTriggerTest.php [new file with mode: 0644]
tests/README
tests/phpunit.xml
wwwroot/inc/dictionary.php
wwwroot/inc/install.php
wwwroot/inc/upgrade.php

diff --git a/README b/README
index 9d4bbbcbd99dd235b2153bd1de3e8772c4ea74a1..6a31f6f9175a5abe9b4748832c2db057ba27fa48 100644 (file)
--- a/README
+++ b/README
@@ -101,6 +101,10 @@ and initialize the application.
 
 *** Upgrading to 0.20.7 ***
 
+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.
+
 The IPV4OBJ_LISTSRC configuration option is reset to an expression which enables
 the IP addressing feature for all object types except those listed.
 
diff --git a/tests/LinkTriggerTest.php b/tests/LinkTriggerTest.php
new file mode 100644 (file)
index 0000000..241689e
--- /dev/null
@@ -0,0 +1,143 @@
+<?php
+
+// Test the effectiveness of the INSERT and UPDATE triggers on the Link table
+//   - porta != portb
+//   - porta < portb
+//   - porta is compatibile with portb
+
+class LinkTriggerTest extends PHPUnit_Framework_TestCase
+{
+       protected $autoports_config_var = NULL;
+       protected $object_id = NULL;
+       protected $porta = NULL;
+       protected $portb = NULL;
+       protected $portc = NULL;
+       protected $portc_type = NULL;
+
+       public function setUp ()
+       {
+               // make sure AUTOPORTS_CONFIG is empty
+               $this->autoports_config_var = getConfigVar ('AUTOPORTS_CONFIG'); 
+               if ($this->autoports_config_var != '')
+                       setConfigVar ('AUTOPORTS_CONFIG', '');
+
+               // find a port type that is incompatible with 1000Base-T
+               $result = usePreparedSelectBlade
+               (
+                       'SELECT type1 FROM PortCompat WHERE type1 != 24 AND type2 != 24 LIMIT 1'
+               );
+               $this->portc_type = $result->fetchColumn ();
+
+               // add sample data
+               //   - set port a & b's type to 1000Base-T
+               //   - set port c's type to the incompatible one
+               $this->object_id = commitAddObject ('unit test object', NULL, 4, NULL);
+               $this->porta = commitAddPort ($this->object_id, 'test porta', '1-24', NULL, NULL);
+               $this->portb = commitAddPort ($this->object_id, 'test portb', '1-24', NULL, NULL);
+               $this->portc = commitAddPort ($this->object_id, 'test portc', $this->portc_type, NULL, NULL);
+       }
+
+       public function tearDown ()
+       {
+               // restore AUTOPORTS_CONFIG to original setting
+               if ($this->autoports_config_var != '')
+                       setConfigVar ('AUTOPORTS_CONFIG', $this->autoports_config_var);
+
+               // remove sample data
+               commitDeleteObject ($this->object_id);
+       }
+
+       /**
+        * @expectedException PDOException
+        */
+       public function testCreateLinkToSelf ()
+       {
+               global $dbxlink;
+               usePreparedInsertBlade (
+                       'Link',
+                       array ('porta' => $this->porta, 'portb' => $this->porta)
+               );
+       }
+
+       /**
+        * @expectedException PDOException
+        */
+       public function testUpdateLinkToSelf ()
+       {
+               global $dbxlink;
+               usePreparedInsertBlade (
+                       'Link',
+                       array ('porta' => $this->porta, 'portb' => $this->portb)
+               );
+               usePreparedUpdateBlade (
+                       'Link',
+                       array ('porta' => $this->porta, 'portb' => $this->porta),
+                       array ('porta' => $this->porta, 'portb' => $this->portb)
+               );
+       }
+
+       public function testCreateLinkWithPortAGreaterThanPortB ()
+       {
+               global $dbxlink;
+               usePreparedInsertBlade (
+                       'Link',
+                       array ('porta' => $this->portb, 'portb' => $this->porta)
+               );
+               $result = usePreparedSelectBlade
+               (
+                       'SELECT COUNT(*) FROM Link WHERE porta=? AND portb=?',
+                       array ($this->porta, $this->portb)
+               );
+               $this->assertEquals ($result->fetchColumn (), 1);
+       }
+
+       public function testUpdateLinkWithPortAGreaterThanPortB ()
+       {
+               global $dbxlink;
+               usePreparedInsertBlade (
+                       'Link',
+                       array ('porta' => $this->porta, 'portb' => $this->portb)
+               );
+               usePreparedUpdateBlade (
+                       'Link',
+                       array ('porta' => $this->portb, 'portb' => $this->porta),
+                       array ('porta' => $this->porta, 'portb' => $this->portb)
+               );
+               $result = usePreparedSelectBlade
+               (
+                       'SELECT COUNT(*) FROM Link WHERE porta=? AND portb=?',
+                       array ($this->porta, $this->portb)
+               );
+               $this->assertEquals ($result->fetchColumn (), 1);
+       }
+
+       /**
+        * @expectedException PDOException
+        */
+       public function testCreateLinkBetweenIncompatiblePorts ()
+       {
+               global $dbxlink;
+               usePreparedInsertBlade (
+                       'Link',
+                       array ('porta' => $this->porta, 'portb' => $this->portc)
+               );
+       }
+
+       /**
+        * @expectedException PDOException
+        */
+       public function testUpdateLinkBetweenIncompatiblePorts ()
+       {
+               global $dbxlink;
+               usePreparedInsertBlade (
+                       'Link',
+                       array ('porta' => $this->porta, 'portb' => $this->portb)
+               );
+               usePreparedUpdateBlade (
+                       'Link',
+                       array ('porta' => $this->porta, 'portb' => $this->portc),
+                       array ('porta' => $this->porta, 'portb' => $this->portb)
+               );
+       }
+}
+?>
index d5371a576583232fc3ffa1302f15825d21de92f9..34638015a9c788a96b01af1932ebd2c2da7fe296 100644 (file)
@@ -7,5 +7,8 @@ Do not run these tests against a production instance of RackTables!
 Data may be added, modified or deleted as part of the tests.
 If you understand the risk, edit bootstrap.php and delete the warning lines.
 
-To run a test from the command-line:
+To run a specific test:
 $ phpunit TestName
+
+To run all tests:
+$ phpunit
index 6dd4f769a3a478df2449a97a34199228259a6c88..9d7e34ed2d30c813bf30a519fa2b2193fdfc2bec 100644 (file)
@@ -1,4 +1,9 @@
 <phpunit backupGlobals="false"
          backupStaticAttributes="false"
          bootstrap="bootstrap.php">
+  <testsuites>
+    <testsuite name="all">
+      <directory suffix="Test.php">./</directory>
+    </testsuite>
+  </testsuites>
 </phpunit>
index 073175c98b663e6ee1c20608964a4faf453ec852..9eaa35d684b631f6ecd91504267faf841a565778 100644 (file)
@@ -36,11 +36,13 @@ function reloadDictionary ($release = NULL)
 function isInnoDBSupported ()
 {
        global $dbxlink;
-       // create a temp table
+       // create a temp table and a trigger
        $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`");
-       return $row['Engine'] == 'InnoDB';
+       return ($innodb_row['Engine'] == 'InnoDB' and $trigger_row['count'] == 1);
 }
 
 function platform_function_test ($funcname, $extname, $what_if_not = 'not found', $error_class = 'trerror')
index ebbb986e66391323b5008b713da451a7cfe14ff1..8974b06fd5568dd2a29b65588641f1e4d8728a86 100644 (file)
@@ -1151,6 +1151,34 @@ function get_pseudo_file ($name)
   CONSTRAINT `VSEnabledPorts-FK-vs_id-proto-vport` FOREIGN KEY (`vs_id`, `proto`, `vport`) REFERENCES `VSPorts` (`vs_id`, `proto`, `vport`) ON DELETE CASCADE
 ) ENGINE=InnoDB";
 
+               $link_trigger_body = <<<ENDOFTRIGGER
+BEGIN
+  DECLARE tmp, porta_type, portb_type, count INTEGER;
+
+  IF NEW.porta = NEW.portb THEN
+    # forbid connecting a port to itself
+    SET NEW.porta = NULL;
+  ELSEIF NEW.porta > NEW.portb THEN
+    # force porta < portb
+    SET tmp = NEW.porta;
+    SET NEW.porta = NEW.portb;
+    SET NEW.portb = tmp;
+  END IF; 
+
+  # lock ports to prevent concurrent link establishment
+  SELECT type INTO porta_type FROM Port WHERE id = NEW.porta FOR UPDATE;
+  SELECT type INTO portb_type FROM Port WHERE id = NEW.portb FOR UPDATE;
+
+  # only permit the link if ports are compatibile
+  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;
+ENDOFTRIGGER;
+               $query[] = "CREATE TRIGGER `Link-before-insert` BEFORE INSERT ON `Link` FOR EACH ROW $link_trigger_body";
+               $query[] = "CREATE TRIGGER `Link-before-update` BEFORE UPDATE ON `Link` FOR EACH ROW $link_trigger_body";
+
                $query[] = "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 e98f78dccca52691f30a62d556ebbb46b8cd2673..0f60b6dd123d702485800df33bb7df5d47188e15 100644 (file)
@@ -193,6 +193,10 @@ ENDOFTEXT
 ,
 
        '0.20.7' => <<<ENDOFTEXT
+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.
+
 The IPV4OBJ_LISTSRC configuration option is reset to an expression which enables
 the IP addressing feature for all object types except those listed.
 ENDOFTEXT
@@ -1369,6 +1373,40 @@ CREATE TABLE `VSEnabledPorts` (
                        $query[] = "UPDATE Config SET varvalue = '0.20.6' WHERE varname = 'DB_VERSION'";
                        break;
                case '0.20.7':
+                       if (! isInnoDBSupported ())
+                       {
+                               showUpgradeError ('Cannot upgrade because triggers are not supported by your MySQL server.', __FUNCTION__);
+                               die;
+                       }
+
+                       $link_trigger_body = <<<ENDOFTRIGGER
+BEGIN
+  DECLARE tmp, porta_type, portb_type, count INTEGER;
+
+  IF NEW.porta = NEW.portb THEN
+    # forbid connecting a port to itself
+    SET NEW.porta = NULL;
+  ELSEIF NEW.porta > NEW.portb THEN
+    # force porta < portb
+    SET tmp = NEW.porta;
+    SET NEW.porta = NEW.portb;
+    SET NEW.portb = tmp;
+  END IF; 
+
+  # lock ports to prevent concurrent link establishment
+  SELECT type INTO porta_type FROM Port WHERE id = NEW.porta FOR UPDATE;
+  SELECT type INTO portb_type FROM Port WHERE id = NEW.portb FOR UPDATE;
+
+  # only permit the link if ports are compatibile
+  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;
+ENDOFTRIGGER;
+                       $query[] = "CREATE TRIGGER `Link-before-insert` BEFORE INSERT ON `Link` FOR EACH ROW $link_trigger_body";
+                       $query[] = "CREATE TRIGGER `Link-before-update` BEFORE UPDATE ON `Link` FOR EACH ROW $link_trigger_body";
+
                        // enable IP addressing for all object types unless specifically excluded
                        $query[] = "UPDATE `Config` SET varvalue = 'not ({\$typeid_3} or {\$typeid_9} or {\$typeid_10} or {\$typeid_11})' WHERE varname = 'IPV4OBJ_LISTSRC'";