vs groups feature
authorAlexey Andriyanov <alan@al-an.info>
Mon, 13 May 2013 18:00:54 +0000 (22:00 +0400)
committerAlexey Andriyanov <alan@al-an.info>
Mon, 13 May 2013 18:00:54 +0000 (22:00 +0400)
new realm 'ipvs'
new tables `VS`, `VSIPs`, `VSPorts`, `VSEnabledIPs`, `VSEnabledPorts`
new files slbv2.php slb2-interface.php, slb_editor.js, checked.png, unchecked.png

listCells:
- new realm 'ipvs',
- 'ordcolumns' key became optional in $SQLinfo
- call dos2unix when fetching DB SLB configs
spotEntity: idem

scanIPv4Space: fill 'vsglist' key of IPAddress structure
scanIPv6Space: idem
constructIPAddress: idem

generateEntityAutoTags: new realm 'ipvs'
formatEntityList: idem
renderCell: idem
dynamic_title_decoder: idem
renderSLBEntityCell: idem

array_sub: utitlity function, assosiative array_diff
nullEmptyStr: utitlity function, turns empty string into NULL

renderObject: inject redered vs groups info
renderIPv4NetworkAddresses: idem
renderIPv6NetworkAddresses: idem
renderIPAddress: idem
renderRSPool: idem

showPathAndSearch: support for specifying tab in 'parent' key of $page global (colon-separated)

SLBTriplet::generateSLBConfig: new macro %VS_PREPEND%
buildEntityLVSConfig: concat v2 and v1 configs
SLBTriplet::getRSList: call dos2unix when fetching DB SLB configs
SLBTriplet::getTriplets: idem

20 files changed:
ChangeLog
README
wwwroot/css/pi.css
wwwroot/inc/database.php
wwwroot/inc/functions.php
wwwroot/inc/init.php
wwwroot/inc/install.php
wwwroot/inc/interface.php
wwwroot/inc/navigation.php
wwwroot/inc/ophandlers.php
wwwroot/inc/slb-interface.php
wwwroot/inc/slb.php
wwwroot/inc/slb2-interface.php [new file with mode: 0644]
wwwroot/inc/slbv2.php [new file with mode: 0644]
wwwroot/inc/solutions.php
wwwroot/inc/upgrade.php
wwwroot/js/jquery.thumbhover.js
wwwroot/js/slb_editor.js [new file with mode: 0644]
wwwroot/pix/checked.png [new file with mode: 0644]
wwwroot/pix/unchecked.png [new file with mode: 0644]

index acba8e4e126f63ea5bb2ca182bfcbeeeea660bf5..b94a4e0fa9d657b7486ba09ae0e3eb7b34f873ae 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -21,6 +21,8 @@
        update: display parent locations when browsing rackspace (#845)
        update: display parent and child location(s) on View Location page (#847)
        new feature: Rackcode filter for RDP-managed objects (#819)
+       new feature: grouping of Virtual services, generating of
+               virtual_server_group-aware keepalived configs
 0.20.4 2013-04-15
        bugfix: %GPASS% dictionary markers were ignored in selectboxes
        bugfix: 802.1Q: better Force10 switches support
diff --git a/README b/README
index e2c7c329b50271aae096d389e9f456c7344b7016..32b69d7ca2d04e8e05d65d2ea224f39d9467f69b 100644 (file)
--- a/README
+++ b/README
@@ -102,6 +102,20 @@ should be sufficient:
 *                                                     *
 *******************************************************
 
+*** Upgrading to 0.20.5 ***
+
+This release introduces the VS groups feature. VS groups is a new way to store
+and display virtual services configuration. New realm 'ipvs' (VS group) is created.
+All the existing VS configuration is kept and displayed as-is, but user is free to convert
+it to the new format, which displays it in more natural way and allows to generate
+virtual_server_group keepalived configs. To convert a virtual servise to the new format,
+you need to manually create the vs group object and assign IP addresses to it. Then, if you
+have the old-style VSes configured, the Migrate tab will be displayed on the particular VS group's
+page. After successfull migration, you can remove the old-style VS objects.
+
+Old-style VS configuration becomes DEPRECATED. Its support will be removed in one of the following
+major releases. So it is strongly recommended to convert it to the new format.
+
 *** Upgrading to 0.20.4 ***
 
 Please note that some dictionary items of Cisco Catalyst 2960 series switches
index 2b6d087b0834d2cc93f9ce3ff96ea256406893ab..0f16950e410833af81d49fbb0d20552f77af38d9 100644 (file)
@@ -639,6 +639,7 @@ div.vlan {
        border: 1px solid #000000;
        background: #FFFFFF;
        padding: 0px 5px;
+       z-index: 999;
 }
 
 /*popup div with MAC list*/
@@ -807,3 +808,77 @@ a.noclick {
        margin: 0 0;
        font-size: 90%;
 }
+
+.slb-checks {
+       margin: 0px;
+       padding: 0px 2em;
+       list-style: square;
+}
+
+.slb-checks li {
+       margin: 5px 0px;
+       white-space: pre;
+}
+
+.slb-checks li.disabled {
+       color: #999999;
+       list-style-image: url(?module=chrome&uri=pix/unchecked.png)
+}
+
+.slb-checks li.disabled * {
+       color: #999999;
+}
+
+.slb-checks li.enabled {
+       list-style-image: url(?module=chrome&uri=pix/checked.png);
+}
+
+.slb-checks.editable li:hover {
+       list-style-image: url(?module=chrome&uri=pix/pencil-icon.png);
+       cursor: pointer;
+}
+
+.slb-checks.editable li a {
+       cursor: pointer;
+}
+
+.slb-checks.editable li * {
+       cursor: auto;
+}
+
+.slb-checks li form {
+       display: none;
+}
+
+.slb-prio {
+       border: 1px solid #aaaaaa;
+       padding: 0px 2px;
+       margin-left: 1em;
+}
+
+.slbconf-btn {
+       border: 1px solid #aaaaaa;
+       font-size: 12px;
+       font-weight: bold;
+       height: 10px;
+       line-height: 6px;
+
+       display: inline-block;
+       padding: 0px 2px;
+       margin-left: 1em;
+       position: relative;
+       top: -3px;
+}
+
+.popup-box h1 {
+       background-color: #3C78B5;
+       color: white;
+       text-align: center;
+       font-family: verdana, tahoma;
+       font-weight: normal;
+       padding: 0px;
+       margin: 0px -5px;
+       margin-top: 0.5em;
+       font-size: 12px;
+       line-height: 1.5em;
+}
index 794705b5dd3676e86d971ed6654ee344667d2fd9..3577cc1e7a4d13734f03a1675aad9ce97c278cc4 100644 (file)
@@ -115,6 +115,18 @@ $SQLSchema = array
                'keycolumn' => 'id',
                'ordcolumns' => array ('IPv4VS.vip', 'IPv4VS.proto', 'IPv4VS.vport'),
        ),
+       'ipvs' => array
+       (
+               'table' => 'VS',
+               'columns' => array
+               (
+                       'id' => 'id',
+                       'name' => 'name',
+                       'vsconfig' => 'vsconfig',
+                       'rsconfig' => 'rsconfig',
+               ),
+               'keycolumn' => 'id',
+       ),
        'ipv4rspool' => array
        (
                'table' => 'IPv4RSPool',
@@ -422,10 +434,13 @@ function listCells ($realm, $parent_id = 0)
                $query .= " WHERE ${SQLinfo['table']}.${SQLinfo['pidcolumn']} = ?";
                $qparams[] = $parent_id;
        }
-       $query .= " ORDER BY ";
-       foreach ($SQLinfo['ordcolumns'] as $oc)
-               $query .= "${oc}, ";
-       $query = trim($query, ', ');
+       if (isset ($SQLinfo['ordcolumns']))
+       {
+               $query .= " ORDER BY ";
+               foreach ($SQLinfo['ordcolumns'] as $oc)
+                       $query .= "${oc}, ";
+               $query = trim($query, ', ');
+       }
        $result = usePreparedSelectBlade ($query, $qparams);
        $ret = array();
        // Index returned result by the value of key column.
@@ -492,6 +507,16 @@ function listCells ($realm, $parent_id = 0)
                case 'ipv4vs':
                        $entity['vip'] = ip_format ($entity['vip_bin']);
                        setDisplayedName ($entity); // set $entity['dname']
+                       $entity['vsconfig'] = dos2unix ($entity['vsconfig']);
+                       $entity['rsconfig'] = dos2unix ($entity['rsconfig']);
+                       break;
+               case 'ipv4rspool':
+                       $entity['vsconfig'] = dos2unix ($entity['vsconfig']);
+                       $entity['rsconfig'] = dos2unix ($entity['rsconfig']);
+                       break;
+               case 'ipvs':
+                       $entity['vsconfig'] = dos2unix ($entity['vsconfig']);
+                       $entity['rsconfig'] = dos2unix ($entity['rsconfig']);
                        break;
                default:
                        break;
@@ -600,6 +625,16 @@ function spotEntity ($realm, $id, $ignore_cache = FALSE)
        case 'ipv4vs':
                $ret['vip'] = ip_format ($ret['vip_bin']);
                setDisplayedName ($ret); // set $ret['dname']
+               $ret['vsconfig'] = dos2unix ($ret['vsconfig']);
+               $ret['rsconfig'] = dos2unix ($ret['rsconfig']);
+               break;
+       case 'ipv4rspool':
+               $ret['vsconfig'] = dos2unix ($ret['vsconfig']);
+               $ret['rsconfig'] = dos2unix ($ret['rsconfig']);
+               break;
+       case 'ipvs':
+               $ret['vsconfig'] = dos2unix ($ret['vsconfig']);
+               $ret['rsconfig'] = dos2unix ($ret['rsconfig']);
                break;
        default:
                break;
@@ -726,6 +761,24 @@ function amplifyCell (&$record, $dummy = NULL)
                while ($row = $result->fetch (PDO::FETCH_ASSOC))
                        $record['switches'][$row['object_id']] = $row;
                break;
+       case 'ipvs':
+               $result = usePreparedSelectBlade ("SELECT proto, vport, vsconfig, rsconfig FROM VSPorts WHERE vs_id = ?", array ($record['id']));
+               while ($row = $result->fetch (PDO::FETCH_ASSOC))
+               {
+                       $row['vsconfig'] = dos2unix ($row['vsconfig']);
+                       $row['rsconfig'] = dos2unix ($row['rsconfig']);
+                       $record['ports'][] = $row;
+               }
+               unset ($result);
+               $result = usePreparedSelectBlade ("SELECT vip, vsconfig, rsconfig FROM VSIPs WHERE vs_id = ?", array ($record['id']));
+               while ($row = $result->fetch (PDO::FETCH_ASSOC))
+               {
+                       $row['vsconfig'] = dos2unix ($row['vsconfig']);
+                       $row['rsconfig'] = dos2unix ($row['rsconfig']);
+                       $record['vips'][] = $row;
+               }
+               unset ($result);
+               break;
        default:
        }
 }
@@ -1885,7 +1938,8 @@ function scanIPv4Space ($pairlist)
        $or = '';
        $whereexpr1 = '(';
        $whereexpr2 = '(';
-       $whereexpr3 = '(';
+       $whereexpr3a = '(';
+       $whereexpr3b = '(';
        $whereexpr4 = '(';
        $whereexpr5a = '(';
        $whereexpr5b = '(';
@@ -1896,7 +1950,8 @@ function scanIPv4Space ($pairlist)
        {
                $whereexpr1 .= $or . "ip between ? and ?";
                $whereexpr2 .= $or . "ip between ? and ?";
-               $whereexpr3 .= $or . "vip between ? and ?";
+               $whereexpr3a .= $or . "vip between ? and ?";
+               $whereexpr3b .= $or . "vip between ? and ?";
                $whereexpr4 .= $or . "rsip between ? and ?";
                $whereexpr5a .= $or . "remoteip between ? and ?";
                $whereexpr5b .= $or . "localip between ? and ?";
@@ -1909,7 +1964,8 @@ function scanIPv4Space ($pairlist)
        }
        $whereexpr1 .= ')';
        $whereexpr2 .= ')';
-       $whereexpr3 .= ')';
+       $whereexpr3a .= ')';
+       $whereexpr3b .= ')';
        $whereexpr4 .= ')';
        $whereexpr5a .= ')';
        $whereexpr5b .= ')';
@@ -1953,8 +2009,8 @@ function scanIPv4Space ($pairlist)
                );
        }
 
-       // 3. look for virtual services
-       $query = "select id, vip from IPv4VS where ${whereexpr3}";
+       // 3a. look for virtual services
+       $query = "select id, vip from IPv4VS where ${whereexpr3a}";
        $result = usePreparedSelectBlade ($query, $qparams_bin);
        $allRows = $result->fetchAll (PDO::FETCH_ASSOC);
        unset ($result);
@@ -1966,6 +2022,19 @@ function scanIPv4Space ($pairlist)
                $ret[$ip_bin]['vslist'][] = $row['id'];
        }
 
+       // 3b. look for virtual service groups
+       $query = "select vs_id, vip from VSIPs where ${whereexpr3b}";
+       $result = usePreparedSelectBlade ($query, $qparams_bin);
+       $allRows = $result->fetchAll (PDO::FETCH_ASSOC);
+       unset ($result);
+       foreach ($allRows as $row)
+       {
+               $ip_bin = $row['vip'];
+               if (!isset ($ret[$ip_bin]))
+                       $ret[$ip_bin] = constructIPAddress ($ip_bin);
+               $ret[$ip_bin]['vsglist'][] = $row['vs_id'];
+       }
+
        // 4. don't forget about real servers along with pools
        $query = "select rsip, rspool_id from IPv4RS where ${whereexpr4}";
        $result = usePreparedSelectBlade ($query, $qparams_bin);
@@ -2062,7 +2131,8 @@ function scanIPv6Space ($pairlist)
        $or = '';
        $whereexpr1 = '(';
        $whereexpr2 = '(';
-       $whereexpr3 = '(';
+       $whereexpr3a = '(';
+       $whereexpr3b = '(';
        $whereexpr4 = '(';
        $whereexpr6 = '(';
        $qparams = array();
@@ -2070,7 +2140,8 @@ function scanIPv6Space ($pairlist)
        {
                $whereexpr1 .= $or . "ip between ? and ?";
                $whereexpr2 .= $or . "ip between ? and ?";
-               $whereexpr3 .= $or . "vip between ? and ?";
+               $whereexpr3a .= $or . "vip between ? and ?";
+               $whereexpr3b .= $or . "vip between ? and ?";
                $whereexpr4 .= $or . "rsip between ? and ?";
                $whereexpr6 .= $or . "l.ip between ? and ?";
                $or = ' or ';
@@ -2079,7 +2150,8 @@ function scanIPv6Space ($pairlist)
        }
        $whereexpr1 .= ')';
        $whereexpr2 .= ')';
-       $whereexpr3 .= ')';
+       $whereexpr3a .= ')';
+       $whereexpr3b .= ')';
        $whereexpr4 .= ')';
        $whereexpr6 .= ')';
 
@@ -2121,8 +2193,8 @@ function scanIPv6Space ($pairlist)
                );
        }
 
-       // 3. look for virtual services
-       $query = "select id, vip from IPv4VS where ${whereexpr3}";
+       // 3a. look for virtual services
+       $query = "select id, vip from IPv4VS where ${whereexpr3a}";
        $result = usePreparedSelectBlade ($query, $qparams);
        $allRows = $result->fetchAll (PDO::FETCH_ASSOC);
        unset ($result);
@@ -2134,6 +2206,19 @@ function scanIPv6Space ($pairlist)
                $ret[$ip_bin]['vslist'][] = $row['id'];
        }
 
+       // 3b. look for virtual service groups
+       $query = "select vs_id, vip from VSIPs where ${whereexpr3b}";
+       $result = usePreparedSelectBlade ($query, $qparams);
+       $allRows = $result->fetchAll (PDO::FETCH_ASSOC);
+       unset ($result);
+       foreach ($allRows as $row)
+       {
+               $ip_bin = $row['vip'];
+               if (!isset ($ret[$ip_bin]))
+                       $ret[$ip_bin] = constructIPAddress ($ip_bin);
+               $ret[$ip_bin]['vsglist'][] = $row['vs_id'];
+       }
+
        // 4. don't forget about real servers along with pools
        $query = "select rsip, rspool_id from IPv4RS where ${whereexpr4}";
        $result = usePreparedSelectBlade ($query, $qparams);
@@ -3751,6 +3836,10 @@ function generateEntityAutoTags ($cell)
                                $ret[] = array ('tag' => '$unused');
                        $ret[] = array ('tag' => '$type_' . strtolower ($cell['proto'])); // $type_tcp, $type_udp or $type_mark
                        break;
+               case 'ipvs':
+                       $ret[] = array ('tag' => '$ipvsid_' . $cell['id']);
+                       $ret[] = array ('tag' => '$any_vs');
+                       break;
                case 'ipv4rspool':
                        $ret[] = array ('tag' => '$ipv4rspid_' . $cell['id']);
                        $ret[] = array ('tag' => '$any_ipv4rsp');
index 1408b9edf8eacfb890b75f728449ddc144348ae7..ad3e17e7f5e01f852130b6d75367d2cba833c88c 100644 (file)
@@ -59,6 +59,7 @@ $etype_by_pageno = array
        'ipv6net' => 'ipv6net',
        'ipv4rspool' => 'ipv4rspool',
        'ipv4vs' => 'ipv4vs',
+       'ipvs' => 'ipvs',
        'object' => 'object',
        'rack' => 'rack',
        'row' => 'row',
@@ -2464,6 +2465,7 @@ function constructIPAddress ($ip_bin)
                'reserved' => 'no',
                'allocs' => array(),
                'vslist' => array(),
+               'vsglist' => array(),
                'rsplist' => array(),
        );
 
@@ -2672,6 +2674,8 @@ function formatEntityName ($name)
                        return 'IPv4 RS Pool';
                case 'ipv4vs':
                        return 'IPv4 Virtual Service';
+               case 'ipvs':
+                       return 'IP Virtual Service';
                case 'object':
                        return 'Object';
                case 'rack':
@@ -5629,6 +5633,16 @@ function array_last ($array)
                return $single[0];
 }
 
+// returns array of key-value pairs from array $a such that keys are not present in $b
+function array_sub ($a, $b)
+{
+       $ret = array();
+       foreach ($a as $key => $value)
+               if (! array_key_exists($key, $b))
+                       $ret[$key] = $value;
+       return $ret;
+}
+
 // Registers additional ophandler on page-tab-opname triplet.
 // Valid $method values are 'before' and 'after'.
 //   'before' puts your ophandler in the beginning of the list (and thus before the default)
@@ -5873,6 +5887,9 @@ function formatEntityList ($list)
                        case 'ipv4vs':
                                $ret[$entity['id']] = $entity['name'] . (strlen ($entity['name']) ? ' ' : '') . '(' . $entity['dname'] . ')';
                                break;
+                       case 'ipvs':
+                               $ret[$entity['id']] = $entity['name'];
+                               break;
                        case 'ipv4rspool':
                                $ret[$entity['id']] = $entity['name'];
                                break;
@@ -6002,4 +6019,9 @@ function checkTypeAndAttribute ($object_id, $type_id, $attr_id, $values)
        return FALSE;
 }
 
+function nullEmptyStr ($str)
+{
+       return strlen ($str) ? $str : NULL;
+}
+
 ?>
index 3a293bdc22c246e79aa5a283e4c910516e5dfb95..7f798db8de63a49f10c93c48525b8e559635c6e2 100644 (file)
@@ -24,6 +24,7 @@ require_once 'triggers.php';
 require_once 'remote.php';
 require_once 'caching.php';
 require_once 'slb.php';
+require_once 'slbv2.php';
 
 // secret.php may be missing, in which case this is a special fatal error
 if (! fileSearchExists ($path_to_secret_php))
index e34a68fc715822341beba9925feeaa2abbcae2d5..33f793c87a1a0d6f9bacd5b05f40327b5c1b5436 100644 (file)
@@ -559,7 +559,7 @@ CREATE TABLE `Dictionary` (
 
 CREATE TABLE `EntityLink` (
   `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
-  `parent_entity_type` enum('ipv4net','ipv4rspool','ipv4vs','ipv6net','location','object','rack','row','user') NOT NULL,
+  `parent_entity_type` enum('ipv4net','ipv4rspool','ipv4vs','ipvs','ipv6net','location','object','rack','row','user') NOT NULL,
   `parent_entity_id` int(10) unsigned NOT NULL,
   `child_entity_type` enum('file','location','object','rack','row') NOT NULL,
   `child_entity_id` int(10) unsigned NOT NULL,
@@ -586,7 +586,7 @@ CREATE TABLE `File` (
 CREATE TABLE `FileLink` (
   `id` int(10) unsigned NOT NULL auto_increment,
   `file_id` int(10) unsigned NOT NULL,
-  `entity_type` enum('ipv4net','ipv4rspool','ipv4vs','ipv6net','location','object','rack','row','user') NOT NULL default 'object',
+  `entity_type` enum('ipv4net','ipv4rspool','ipv4vs','ipvs','ipv6net','location','object','rack','row','user') NOT NULL default 'object',
   `entity_id` int(10) NOT NULL,
   PRIMARY KEY  (`id`),
   KEY `FileLink-file_id` (`file_id`),
@@ -945,7 +945,7 @@ CREATE TABLE `Script` (
 ) ENGINE=InnoDB;
 
 CREATE TABLE `TagStorage` (
-  `entity_realm` enum('file','ipv4net','ipv4rspool','ipv4vs','ipv6net','location','object','rack','user','vst') NOT NULL default 'object',
+  `entity_realm` enum('file','ipv4net','ipv4rspool','ipv4vs','ipvs','ipv6net','location','object','rack','user','vst') NOT NULL default 'object',
   `entity_id` int(10) unsigned NOT NULL,
   `tag_id` int(10) unsigned NOT NULL default '0',
   `tag_is_assignable` enum('yes','no') NOT NULL DEFAULT 'yes',
@@ -1073,6 +1073,66 @@ CREATE TABLE `VLANValidID` (
   PRIMARY KEY  (`vlan_id`)
 ) ENGINE=InnoDB;
 
+CREATE TABLE `VS` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `name` char(255) DEFAULT NULL,
+  `vsconfig` text,
+  `rsconfig` text,
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB;
+
+CREATE TABLE `VSIPs` (
+  `vs_id` int(10) unsigned NOT NULL,
+  `vip` varbinary(16) NOT NULL,
+  `vsconfig` text,
+  `rsconfig` text,
+  PRIMARY KEY (`vs_id`,`vip`),
+  KEY `vip` (`vip`),
+  CONSTRAINT `VSIPs-vs_id` FOREIGN KEY (`vs_id`) REFERENCES `VS` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE `VSPorts` (
+  `vs_id` int(10) unsigned NOT NULL,
+  `proto` enum('TCP','UDP','MARK') CHARACTER SET latin1 NOT NULL,
+  `vport` int(10) unsigned NOT NULL,
+  `vsconfig` text,
+  `rsconfig` text,
+  PRIMARY KEY (`vs_id`,`proto`,`vport`),
+  CONSTRAINT `VS-vs_id` FOREIGN KEY (`vs_id`) REFERENCES `VS` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE `VSEnabledIPs` (
+  `object_id` int(10) unsigned NOT NULL,
+  `vs_id` int(10) unsigned NOT NULL,
+  `vip` varbinary(16) NOT NULL,
+  `rspool_id` int(10) unsigned NOT NULL,
+  `prio` varchar(255) DEFAULT NULL,
+  `vsconfig` text,
+  `rsconfig` text,
+  PRIMARY KEY (`object_id`,`vs_id`,`vip`),
+  KEY `vip` (`vip`),
+  KEY `VSEnabledIPs-FK-vs_id-vip` (`vs_id`,`vip`),
+  KEY `VSEnabledIPs-FK-rspool_id` (`rspool_id`),
+  CONSTRAINT `VSEnabledIPs-FK-rspool_id` FOREIGN KEY (`rspool_id`) REFERENCES `IPv4RSPool` (`id`) ON DELETE CASCADE,
+  CONSTRAINT `VSEnabledIPs-FK-vs_id-vip` FOREIGN KEY (`vs_id`, `vip`) REFERENCES `VSIPs` (`vs_id`, `vip`) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE `VSEnabledPorts` (
+  `object_id` int(10) unsigned NOT NULL,
+  `vs_id` int(10) unsigned NOT NULL,
+  `proto` enum('TCP','UDP','MARK') CHARACTER SET latin1 NOT NULL,
+  `vport` int(10) unsigned NOT NULL,
+  `rspool_id` int(10) unsigned NOT NULL,
+  `vsconfig` text,
+  `rsconfig` text,
+  PRIMARY KEY (`object_id`,`vs_id`,`proto`,`vport`),
+  KEY `VSEnabledPorts-FK-vs_id-proto-vport` (`vs_id`,`proto`,`vport`),
+  KEY `VSEnabledPorts-FK-rspool_id` (`rspool_id`),
+  CONSTRAINT `VSEnabledPorts-FK-object_id` FOREIGN KEY (`object_id`) REFERENCES `Object` (`id`) ON DELETE CASCADE,
+  CONSTRAINT `VSEnabledPorts-FK-rspool_id` FOREIGN KEY (`rspool_id`) REFERENCES `IPv4RSPool` (`id`) ON DELETE CASCADE,
+  CONSTRAINT `VSEnabledPorts-FK-vs_id-proto-vport` FOREIGN KEY (`vs_id`, `proto`, `vport`) REFERENCES `VSPorts` (`vs_id`, `proto`, `vport`) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
 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 7c940e002e37991c21c402bc7837ab418bdbf11a..059d34ee36c45fcc2c113e9d7f04f8802f123976 100644 (file)
@@ -1283,6 +1283,7 @@ function renderObject ($object_id)
                finishPortlet();
        }
 
+       renderSLBTriplets2 ($info);
        renderSLBTriplets ($info);
        echo "</td>\n";
 
@@ -2595,6 +2596,12 @@ function renderIPv4NetworkAddresses ($range)
                        echo $delim . mkA ("${vs['name']}:${vs['vport']}/${vs['proto']}", 'ipv4vs', $vs['id']) . '&rarr;';
                        $delim = '<br>';
                }
+               foreach ($addr['vsglist'] as $vs_id)
+               {
+                       $vs = spotEntity ('ipvs', $vs_id);
+                       echo $delim . mkA ($vs['name'], 'ipvs', $vs['id']) . '&rarr;';
+                       $delim = '<br>';
+               }
                foreach ($addr['rsplist'] as $rsp_id)
                {
                        $rsp = spotEntity ('ipv4rspool', $rsp_id);
@@ -2708,6 +2715,12 @@ function renderIPv6NetworkAddresses ($netinfo)
                        echo $delim . mkA ("${vs['name']}:${vs['vport']}/${vs['proto']}", 'ipv4vs', $vs['id']) . '&rarr;';
                        $delim = '<br>';
                }
+               foreach ($addr['vsglist'] as $vs_id)
+               {
+                       $vs = spotEntity ('ipvs', $vs_id);
+                       echo $delim . mkA ($vs['name'], 'ipvs', $vs['id']) . '&rarr;';
+                       $delim = '<br>';
+               }
                foreach ($addr['rsplist'] as $rsp_id)
                {
                        $rsp = spotEntity ('ipv4rspool', $rsp_id);
@@ -2780,9 +2793,16 @@ function renderIPAddress ($ip_bin)
        renderEntitySummary ($address, 'summary', $summary);
 
        // render SLB portlet
-       if (! empty ($address['vslist']) or ! empty ($address['rsplist']))
+       if (! empty ($address['vslist']) or ! empty ($address['vsglist']) or ! empty ($address['rsplist']))
        {
                startPortlet ("");
+               if (! empty ($address['vsglist']))
+               {
+                       printf ("<h2>virtual service groups (%d):</h2>", count ($address['vsglist']));
+                       foreach ($address['vsglist'] as $vsg_id)
+                               renderSLBEntityCell (spotEntity ('ipvs', $vsg_id));
+               }
+
                if (! empty ($address['vslist']))
                {
                        printf ("<h2>virtual services (%d):</h2>", count ($address['vslist']));
@@ -2822,7 +2842,22 @@ function renderIPAddress ($ip_bin)
                finishPortlet();
        }
 
-       if (! empty ($address['vslist']) or ! empty ($address['rsplist']))
+       if (! empty ($address['rsplist']))
+       {
+               startPortlet ("RS pools:");
+               foreach ($address['rsplist'] as $rsp_id)
+               {
+                       renderSLBEntityCell (spotEntity ('ipv4rspool', $rsp_id));
+                       echo '<br>';
+               }
+               finishPortlet();
+       }
+
+       if (! empty ($address['vsglist']))
+               foreach ($address['vsglist'] as $vsg_id)
+                       renderSLBTriplets2 (spotEntity ('ipvs', $vsg_id), FALSE, $ip_bin);
+
+       if (! empty ($address['vslist']))
                renderSLBTriplets ($address);
 
        foreach (array ('outpf' => 'departing NAT rules', 'inpf' => 'arriving NAT rules') as $key => $title)
@@ -5679,6 +5714,7 @@ function renderCell ($cell)
                echo "</td></tr></table>";
                break;
        case 'ipv4vs':
+       case 'ipvs':
        case 'ipv4rspool':
                renderSLBEntityCell ($cell);
                break;
@@ -5831,8 +5867,9 @@ function showPathAndSearch ($pageno, $tabno)
                $self = __FUNCTION__;
                global $page;
                $path = array();
+               $page_name = preg_replace ('/:.*/', '', $targetno);
                // Recursion breaks at first parentless page.
-               if ($targetno == 'ipaddress')
+               if ($page_name == 'ipaddress')
                {
                        // case ipaddress is a universal v4/v6 page, it has two parents and requires special handling
                        $ip_bin = ip_parse ($_REQUEST['ip']);
@@ -5840,22 +5877,28 @@ function showPathAndSearch ($pageno, $tabno)
                        $path = $self ($parent);
                        $path[] = $targetno;
                }
-               elseif (!isset ($page[$targetno]['parent']))
+               elseif (!isset ($page[$page_name]['parent']))
                        $path = array ($targetno);
                else
                {
-                       $path = $self ($page[$targetno]['parent']);
+                       $path = $self ($page[$page_name]['parent']);
                        $path[] = $targetno;
                }
                return $path;
        }
-       global $page;
+       global $page, $tab;
        // Path.
        $path = getPath ($pageno);
        $items = array();
        foreach (array_reverse ($path) as $no)
        {
-               if (isset ($page[$no]['title']))
+               if (preg_match ('/(.*):(.*)/', $no, $m) && isset ($tab[$m[1]][$m[2]]))
+                       $title = array
+                       (
+                               'name' => $tab[$m[1]][$m[2]],
+                               'params' => array('page' => $m[1], 'tab' => $m[2]),
+                       );
+               elseif (isset ($page[$no]['title']))
                        $title = array
                        (
                                'name' => $page[$no]['title'],
@@ -5983,6 +6026,13 @@ function dynamic_title_decoder ($path_position)
                        'name' => $vs_info['dname'],
                        'params' => array ('vs_id' => $vs_info['id'])
                );
+       case 'ipvs':
+               $vs_info = spotEntity ('ipvs', assertUIntArg ('vs_id'));
+               return array
+               (
+                       'name' => $vs_info['name'],
+                       'params' => array ('vs_id' => $vs_info['id'])
+               );
        case 'object':
                $object = spotEntity ('object', assertUIntArg ('object_id'));
                return array
index a4b8002d34972f47b922e56f163cb3a9e7806afd..48e6497d86ee691e23613514f65fdfdfcb8e2ca0 100644 (file)
@@ -160,7 +160,8 @@ $tab['object']['liveports'] = 'Live ports';
 $tab['object']['livecdp'] = 'Live CDP';
 $tab['object']['livelldp'] = 'Live LLDP';
 $tab['object']['snmpportfinder'] = 'SNMP sync';
-$tab['object']['editrspvs'] = 'RS pools';
+$tab['object']['editrspvs'] = 'VS links';
+$tab['object']['editvslinks'] = 'VSG links';
 $tab['object']['lvsconfig'] = 'keepalived.conf';
 $tab['object']['autoports'] = 'AutoPorts';
 $tab['object']['tags'] = 'Tags';
@@ -187,6 +188,7 @@ $tabhandler['object']['autoports'] = 'renderAutoPortsForm';
 $tabhandler['object']['tags'] = 'renderEntityTags';
 $tabhandler['object']['files'] = 'renderFilesForEntity';
 $tabhandler['object']['editrspvs'] = 'renderSLBEditTab';
+$tabhandler['object']['editvslinks'] = 'renderTripletForm';
 $tabhandler['object']['8021qorder'] = 'render8021QOrderForm';
 $tabhandler['object']['8021qports'] = 'renderObject8021QPorts';
 $tabhandler['object']['8021qsync'] = 'renderObject8021QSync';
@@ -202,6 +204,7 @@ $trigger['object']['livecdp'] = 'trigger_LiveCDP';
 $trigger['object']['livelldp'] = 'trigger_LiveLLDP';
 $trigger['object']['snmpportfinder'] = 'trigger_snmpportfinder';
 $trigger['object']['editrspvs'] = 'trigger_isloadbalancer';
+$trigger['object']['editvslinks'] = 'trigger_isloadbalancer';
 $trigger['object']['lvsconfig'] = 'trigger_isloadbalancer';
 $trigger['object']['autoports'] = 'trigger_autoports';
 $trigger['object']['tags'] = 'trigger_tags';
@@ -243,6 +246,10 @@ $ophandler['object']['files']['unlinkFile'] = 'unlinkFile';
 $ophandler['object']['editrspvs']['addLB'] = 'addLoadBalancer';
 $ophandler['object']['editrspvs']['delLB'] = 'tableHandler';
 $ophandler['object']['editrspvs']['updLB'] = 'tableHandler';
+$ophandler['object']['editvslinks']['addLink'] = 'createTriplet';
+$ophandler['object']['editvslinks']['del'] = 'removeTriplet';
+$ophandler['object']['editvslinks']['updPort'] = 'updateTripletConfig';
+$ophandler['object']['editvslinks']['updIp'] = 'updateTripletConfig';
 $ophandler['object']['lvsconfig']['submitSLBConfig'] = 'submitSLBConfig';
 $ophandler['object']['snmpportfinder']['querySNMPData'] = 'querySNMPData';
 $ophandler['object']['8021qorder']['add'] = 'add8021QOrder';
@@ -355,13 +362,15 @@ $ophandler['ipaddress']['assignment']['add'] = 'addIPAllocation';
 $page['ipv4slb']['title'] = 'IP SLB';
 $page['ipv4slb']['parent'] = 'index';
 $tab['ipv4slb']['default'] = 'Virtual services';
+$tab['ipv4slb']['vs'] = 'VS groups';
 $tab['ipv4slb']['lbs'] = 'Load balancers';
-$tab['ipv4slb']['rspools'] = 'Real server pools';
+$tab['ipv4slb']['rspools'] = 'RS pools';
 $tab['ipv4slb']['rservers'] = 'Real servers';
 $tab['ipv4slb']['defconfig'] = 'Default configs';
 $tab['ipv4slb']['new_vs'] = 'new VS';
 $tab['ipv4slb']['new_rs'] = 'new RS pool';
 $tabhandler['ipv4slb']['default'] = 'renderVSList';
+$tabhandler['ipv4slb']['vs'] = 'renderVSGList';
 $tabhandler['ipv4slb']['lbs'] = 'renderLBList';
 $tabhandler['ipv4slb']['rspools'] = 'renderRSPoolList';
 $tabhandler['ipv4slb']['rservers'] = 'renderRealServerList';
@@ -372,12 +381,12 @@ $ophandler['ipv4slb']['new_vs']['add'] = 'addVService';
 $ophandler['ipv4slb']['new_rs']['add'] = 'addRSPool';
 $ophandler['ipv4slb']['defconfig']['save'] = 'updateSLBDefConfig';
 
-$page['ipv4vs']['parent'] = 'ipv4slb';
+$page['ipv4vs']['parent'] = 'ipv4slb:default';
 $page['ipv4vs']['bypass'] = 'vs_id';
 $page['ipv4vs']['bypass_type'] = 'uint';
 $tab['ipv4vs']['default'] = 'View';
 $tab['ipv4vs']['edit'] = 'Edit';
-$tab['ipv4vs']['editlblist'] = 'Load balancers';
+$tab['ipv4vs']['editlblist'] = 'VSG links';
 $tab['ipv4vs']['tags'] = 'Tags';
 $tab['ipv4vs']['files'] = 'Files';
 $tabhandler['ipv4vs']['default'] = 'renderVirtualService';
@@ -396,12 +405,47 @@ $ophandler['ipv4vs']['files']['addFile'] = 'addFileToEntity';
 $ophandler['ipv4vs']['files']['linkFile'] = 'linkFileToEntity';
 $ophandler['ipv4vs']['files']['unlinkFile'] = 'unlinkFile';
 
-$page['ipv4rspool']['parent'] = 'ipv4slb';
+$page['ipvs']['parent'] = 'ipv4slb:vs';
+$page['ipvs']['bypass'] = 'vs_id';
+$page['ipvs']['bypass_type'] = 'uint';
+$tab['ipvs']['default'] = 'View';
+$tab['ipvs']['edit'] = 'Edit';
+$tab['ipvs']['editvslinks'] = 'VSG links';
+$tab['ipvs']['tags'] = 'Tags';
+$tab['ipvs']['files'] = 'Files';
+$tab['ipvs']['convert'] = 'Migrate';
+$tabhandler['ipvs']['default'] = 'renderVS';
+$tabhandler['ipvs']['edit'] = 'renderEditVS';
+$tabhandler['ipvs']['editvslinks'] = 'renderTripletForm';
+$tabhandler['ipvs']['tags'] = 'renderEntityTags';
+$tabhandler['ipvs']['files'] = 'renderFilesForEntity';
+$tabhandler['ipvs']['convert'] = 'renderIPVSConvert';
+$ophandler['ipvs']['edit']['updVS'] = 'updateVS';
+$ophandler['ipvs']['edit']['addIP'] = 'addIPToVS';
+$ophandler['ipvs']['edit']['addPort'] = 'addPortToVS';
+$ophandler['ipvs']['edit']['updIP'] = 'updateIPInVS';
+$ophandler['ipvs']['edit']['updPort'] = 'updatePortInVS';
+$ophandler['ipvs']['edit']['delIP'] = 'removeIPFromVS';
+$ophandler['ipvs']['edit']['delPort'] = 'removePortFromVS';
+$ophandler['ipvs']['editvslinks']['addLink'] = 'createTriplet';
+$ophandler['ipvs']['editvslinks']['del'] = 'removeTriplet';
+$ophandler['ipvs']['editvslinks']['updPort'] = 'updateTripletConfig';
+$ophandler['ipvs']['editvslinks']['updIp'] = 'updateTripletConfig';
+$ophandler['ipvs']['tags']['saveTags'] = 'saveEntityTags';
+$ophandler['ipvs']['files']['addFile'] = 'addFileToEntity';
+$ophandler['ipvs']['files']['linkFile'] = 'linkFileToEntity';
+$ophandler['ipvs']['files']['unlinkFile'] = 'unlinkFile';
+$ophandler['ipvs']['convert']['convert'] = 'doVSMigrate';
+$trigger['ipvs']['tags'] = 'trigger_tags';
+$trigger['ipvs']['convert'] = 'trigger_ipvs_convert';
+
+$page['ipv4rspool']['parent'] = 'ipv4slb:rspools';
 $page['ipv4rspool']['bypass'] = 'pool_id';
 $page['ipv4rspool']['bypass_type'] = 'uint';
 $tab['ipv4rspool']['default'] = 'View';
 $tab['ipv4rspool']['edit'] = 'Edit';
-$tab['ipv4rspool']['editlblist'] = 'Load balancers';
+$tab['ipv4rspool']['editlblist'] = 'VS links';
+$tab['ipv4rspool']['editvslinks'] = 'VSG links';
 $tab['ipv4rspool']['editrslist'] = 'RS list';
 $tab['ipv4rspool']['tags'] = 'Tags';
 $tab['ipv4rspool']['files'] = 'Files';
@@ -409,6 +453,7 @@ $trigger['ipv4rspool']['tags'] = 'trigger_tags';
 $tabhandler['ipv4rspool']['default'] = 'renderRSPool';
 $tabhandler['ipv4rspool']['edit'] = 'renderEditRSPool';
 $tabhandler['ipv4rspool']['editrslist'] = 'renderRSPoolServerForm';
+$tabhandler['ipv4rspool']['editvslinks'] = 'renderTripletForm';
 $tabhandler['ipv4rspool']['editlblist'] = 'renderSLBEditTab';
 $tabhandler['ipv4rspool']['tags'] = 'renderEntityTags';
 $tabhandler['ipv4rspool']['files'] = 'renderFilesForEntity';
@@ -422,6 +467,10 @@ $ophandler['ipv4rspool']['editrslist']['addMany'] = 'addRealServers';
 $ophandler['ipv4rspool']['editlblist']['addLB'] = 'addLoadBalancer';
 $ophandler['ipv4rspool']['editlblist']['delLB'] = 'tableHandler';
 $ophandler['ipv4rspool']['editlblist']['updLB'] = 'tableHandler';
+$ophandler['ipv4rspool']['editvslinks']['addLink'] = 'createTriplet';
+$ophandler['ipv4rspool']['editvslinks']['del'] = 'removeTriplet';
+$ophandler['ipv4rspool']['editvslinks']['updPort'] = 'updateTripletConfig';
+$ophandler['ipv4rspool']['editvslinks']['updIp'] = 'updateTripletConfig';
 $ophandler['ipv4rspool']['tags']['saveTags'] = 'saveEntityTags';
 $ophandler['ipv4rspool']['files']['addFile'] = 'addFileToEntity';
 $ophandler['ipv4rspool']['files']['linkFile'] = 'linkFileToEntity';
@@ -745,5 +794,6 @@ $ajaxhandler['upd-rack-sort-order'] = 'updateRackSortOrderAJAX';
 $ajaxhandler['upd-reservation-port'] = 'updatePortRsvAJAX';
 $ajaxhandler['upd-reservation-cable'] = 'updateCableIdAJAX';
 $ajaxhandler['net-usage'] = 'getNetUsageAJAX';
+$ajaxhandler['get-slb-form'] = 'renderSLBFormAJAX';
 
 ?>
index 84c47eeade2a169577989e14be0a5d6b9702e1cc..88ebcbefc149feef1db28e9fb8b7c2ae29e8b2bf 100644 (file)
@@ -1658,6 +1658,249 @@ function updateVService ()
        return showFuncMessage (__FUNCTION__, 'OK');
 }
 
+function updateVS ()
+{
+       $vs_id = assertUIntArg ('vs_id');
+       $name = assertStringArg ('name');
+       $vsconfig = nullEmptyStr (assertStringArg ('vsconfig', TRUE));
+       $rsconfig = nullEmptyStr (assertStringArg ('rsconfig', TRUE));
+
+       usePreparedUpdateBlade ('VS', array ('name' => $name, 'vsconfig' => $vsconfig, 'rsconfig' => $rsconfig), array ('id' => $vs_id));
+       showSuccess ("Service updated successfully");
+}
+
+function addIPToVS()
+{
+       $ip_bin = assertIPArg ('ip');
+       $vsinfo = spotEntity ('ipvs', assertUIntArg ('vs_id'));
+       amplifyCell ($vsinfo);
+       $row = array ('vs_id' => $vsinfo['id'], 'vip' => $ip_bin, 'vsconfig' => NULL, 'rsconfig' => NULL);
+       if ($vip = isVIPEnabled ($row, $vsinfo['vips']))
+               return showError ("Service already contains IP " . formatVSIP ($vip));
+       usePreparedInsertBlade ('VSIPs', $row);
+       showSuccess ("IP addded");
+}
+
+function addPortToVS()
+{
+       global $vs_proto;
+       $proto = assertStringArg ('proto');
+       if (! in_array ($proto, $vs_proto))
+               throw new InvalidRequestArgException ('proto', "Invalid VS protocol");
+       $vport = assertUIntArg ('port', TRUE);
+       if ($proto == 'MARK')
+       {
+               if ($vport > 0xFFFFFFFF)
+                       return showError ("fwmark value is too large");
+       }
+       else
+               if ($vport == 0 || $vport >= 0xFFFF)
+                       return showError ("Invalid $proto port value");
+
+       $vsinfo = spotEntity ('ipvs', assertUIntArg ('vs_id'));
+       amplifyCell ($vsinfo);
+       $row = array ('vs_id' => $vsinfo['id'], 'proto' => $proto, 'vport' => $vport, 'vsconfig' => NULL, 'rsconfig' => NULL);
+       if ($port = isPortEnabled ($row, $vsinfo['ports']))
+               return showError ("Service already contains port " . formatVSPort ($port));
+       usePreparedInsertBlade ('VSPorts', $row);
+       showSuccess ("port addded");
+}
+
+function updateIPInVS()
+{
+       $vs_id = assertUIntArg ('vs_id');
+       $ip_bin = assertIPArg ('ip');
+       $vsconfig = nullEmptyStr (assertStringArg ('vsconfig', TRUE));
+       $rsconfig = nullEmptyStr (assertStringArg ('rsconfig', TRUE));
+       if (usePreparedUpdateBlade ('VSIPs', array ('vsconfig' => $vsconfig, 'rsconfig' => $rsconfig), array ('vs_id' => $vs_id, 'vip' => $ip_bin)))
+               showSuccess ("IP configuration updated");
+       else
+               showNotice ("Nothing changed");
+}
+
+function updatePortInVS()
+{
+       $vs_id = assertUIntArg ('vs_id');
+       $proto = assertStringArg ('proto');
+       $vport = assertUIntArg ('port', TRUE);
+       $vsconfig = nullEmptyStr (assertStringArg ('vsconfig', TRUE));
+       $rsconfig = nullEmptyStr (assertStringArg ('rsconfig', TRUE));
+       if (usePreparedUpdateBlade ('VSPorts', array ('vsconfig' => $vsconfig, 'rsconfig' => $rsconfig), array ('vs_id' => $vs_id, 'proto' => $proto, 'vport' => $vport)))
+               showSuccess ("Port configuration updated");
+       else
+               showNotice ("Nothing changed");
+}
+
+function removeIPFromVS()
+{
+       $vip = array ('vip' => assertIPArg ('ip'));
+       $vsinfo = spotEntity ('ipvs', assertUIntArg ('vs_id'));
+       amplifyCell ($vsinfo);
+       $used = 0;
+       foreach (getTriplets ($vsinfo) as $triplet)
+               if (isVIPEnabled ($vip, $triplet['vips']))
+                       $used++;
+       if (usePreparedDeleteBlade ('VSIPs', array ('vs_id' => $vsinfo['id']) + $vip))
+               showSuccess ("IP removed" . ($used ? ", it was binded with $used SLBs" : ''));
+       else
+               showNotice ("Nothing changed");
+}
+
+function removePortFromVS()
+{
+       $port = array ('proto' => assertStringArg ('proto'), 'vport' => assertUIntArg ('port', TRUE));
+       $vsinfo = spotEntity ('ipvs', assertUIntArg ('vs_id'));
+       amplifyCell ($vsinfo);
+       $used = 0;
+       foreach (getTriplets ($vsinfo) as $triplet)
+               if (isPortEnabled ($port, $triplet['ports']))
+                       $used++;
+       if (usePreparedDeleteBlade ('VSPorts', array ('vs_id' => $vsinfo['id']) + $port))
+               showSuccess ("Port removed" . ($used ? ", it was binded with $used SLBs" : ''));
+       else
+               showNotice ("Nothing changed");
+}
+
+function updateTripletConfig()
+{
+       $key_fields = array
+       (
+               'object_id' => assertUIntArg ('object_id'),
+               'vs_id' => assertUIntArg ('vs_id'),
+               'rspool_id' => assertUIntArg ('rspool_id'),
+       );
+       $config_fields = array
+       (
+               'vsconfig' => nullEmptyStr (assertStringArg ('vsconfig', TRUE)),
+               'rsconfig' => nullEmptyStr (assertStringArg ('rsconfig', TRUE)),
+       );
+
+       $vsinfo = spotEntity ('ipvs', $key_fields['vs_id']);
+       amplifyCell ($vsinfo);
+       $found = FALSE;
+
+       if ($_REQUEST['op'] == 'updPort')
+       {
+               $table = 'VSEnabledPorts';
+               $proto = assertStringArg ('proto');
+               $vport = assertUIntArg ('port', TRUE);
+               $key_fields['proto'] = $proto;
+               $key_fields['vport'] = $vport;
+               $key = "Port $proto-$vport";
+               // check if such port exists in VS
+               foreach ($vsinfo['ports'] as $vs_port)
+                       if ($vs_port['proto'] == $proto && $vs_port['vport'] == $vport)
+                       {
+                               $found = TRUE;
+                               break;
+                       }
+       }
+       else
+       {
+               $table = 'VSEnabledIPs';
+               $vip = assertIPArg ('vip');
+               $config_fields['prio'] = nullEmptyStr (assertStringArg ('prio', TRUE));
+               $key_fields['vip'] = $vip;
+               $key = "IP " . ip_format ($vip);
+               // check if such VIP exists in VS
+               foreach ($vsinfo['vips'] as $vs_vip)
+                       if ($vs_vip['vip'] === $vip)
+                       {
+                               $found = TRUE;
+                               break;
+                       }
+       }
+       if (! $found)
+               return showError ("$key not found in VS");
+
+       $nchanged = 0;
+       if (! isCheckSet ('enabled'))
+       {
+               if ($nchanged += usePreparedDeleteBlade ($table, $key_fields))
+                       return showSuccess ("$key disabled");
+       }
+       else
+       {
+               global $dbxlink;
+               $dbxlink->beginTransaction();
+               $q = "SELECT * FROM $table WHERE";
+               $sep = '';
+               $params = array();
+               foreach ($key_fields as $field => $value)
+               {
+                       $q .= " $sep $field = ?";
+                       $params[] = $value;
+                       $sep = 'AND';
+               }
+               $result = usePreparedSelectBlade ("$q FOR UPDATE", $params);
+               $row = $result->fetch (PDO::FETCH_ASSOC);
+               unset ($result);
+               if ($row)
+               {
+                       if ($nchanged += usePreparedUpdateBlade ($table, $config_fields, $key_fields))
+                               showSuccess ("$key config updated");
+               }
+               else
+               {
+                       $constraint_failed = FALSE;
+                       // search VIP in some other triplet on this balancer
+                       if (isset ($vip))
+                       {
+                               $result = usePreparedSelectBlade ("SELECT * FROM $table WHERE object_id = ? AND vip = ?", array ($key_fields['object_id'], $vip));
+                               if ($result->fetch (PDO::FETCH_ASSOC))
+                               {
+                                       $constraint_failed = TRUE;
+                                       showError ("$key is already used on this server");
+                               }
+                       }
+                       if (! $constraint_failed)
+                               if ($nchanged += usePreparedInsertBlade ($table, $key_fields + $config_fields))
+                                       showSuccess ("$key enabled");
+               }
+               $dbxlink->commit();
+       }
+       if (! $nchanged)
+               showNotice ("No changes made");
+}
+
+function removeTriplet()
+{
+       $key_fields = array
+       (
+               'object_id' => assertUIntArg ('object_id'),
+               'vs_id' => assertUIntArg ('vs_id'),
+               'rspool_id' => assertUIntArg ('rspool_id'),
+       );
+
+       global $dbxlink;
+       $dbxlink->beginTransaction();
+       usePreparedDeleteBlade ('VSEnabledIPs', $key_fields);
+       usePreparedDeleteBlade ('VSEnabledPorts', $key_fields);
+       $dbxlink->commit();
+       showSuccess ('Triplet deleted');
+}
+
+function createTriplet()
+{
+       $object_id = assertUIntArg ('object_id');
+       $vs_id = assertUIntArg ('vs_id');
+       $rspool_id = assertUIntArg ('rspool_id');
+       $vips = array_key_exists ('enabled_vips', $_REQUEST) ? genericAssertion ('enabled_vips', 'array') : array();
+       $ports = array_key_exists ('enabled_ports', $_REQUEST) ? genericAssertion ('enabled_ports', 'array') : array();
+       if (getTriplet ($object_id, $vs_id, $rspool_id))
+               return showError ("SLB triplet already exists");
+
+       $vsinfo = spotEntity ('ipvs', $vs_id);
+       amplifyCell ($vsinfo);
+       foreach ($vsinfo['vips'] as $vip)
+               if (in_array (ip_format ($vip['vip']), $vips))
+                       usePreparedInsertBlade ('VSEnabledIPs', array ('object_id' => $object_id, 'vs_id' => $vs_id, 'rspool_id' => $rspool_id, 'vip' => $vip['vip']));
+       foreach ($vsinfo['ports'] as $port)
+               if (in_array($port['proto'] . '-' . $port['vport'], $ports))
+                       usePreparedInsertBlade ('VSEnabledPorts', array ('object_id' => $object_id, 'vs_id' => $vs_id, 'rspool_id' => $rspool_id, 'proto' => $port['proto'], 'vport' => $port['vport']));
+       showSuccess ("SLB triplet created");
+}
+
 $msgcode['addLoadBalancer']['OK'] = 48;
 function addLoadBalancer ()
 {
@@ -2996,6 +3239,71 @@ function cloneRSPool()
        return buildRedirectURL ('ipv4rspool', 'default', array ('pool_id' => $new_id));
 }
 
+function doVSMigrate()
+{
+       global $dbxlink;
+       $vs_id = assertUIntArg ('vs_id');
+       $vs_cell = spotEntity ('ipvs', $vs_id);
+       amplifyCell ($vs_cell);
+       $tag_ids = genericAssertion ('taglist', 'array');
+       $old_vs_list = genericAssertion ('vs_list', 'array');
+       $plan = callHook ('buildVSMigratePlan', $vs_id, $old_vs_list);
+
+       $dbxlink->beginTransaction();
+
+       // remove all triplets
+       usePreparedDeleteBlade ('VSEnabledIPs', array ('vs_id' => $vs_id));
+       usePreparedDeleteBlade ('VSEnabledPorts', array ('vs_id' => $vs_id));
+
+       // remove all VIPs and ports which are in $plan,and create new ones
+       foreach ($plan['vips'] as $vip)
+       {
+               usePreparedDeleteBlade ('VSIPs', array ('vs_id' => $vs_id, 'vip' => $vip['vip']));
+               usePreparedInsertBlade ('VSIPs', array ('vs_id' => $vs_id) + $vip);
+       }
+       foreach ($plan['ports'] as $port)
+       {
+               usePreparedDeleteBlade ('VSPorts', array ('vs_id' => $vs_id, 'proto' => $port['proto'], 'vport' => $port['vport']));
+               usePreparedInsertBlade ('VSPorts', array ('vs_id' => $vs_id) + $port);
+       }
+
+       // create triplets
+       foreach ($plan['triplets'] as $triplet)
+       {
+               $tr_key = array
+               (
+                       'vs_id' => $triplet['vs_id'],
+                       'object_id' => $triplet['object_id'],
+                       'rspool_id' => $triplet['rspool_id'],
+               );
+
+               foreach ($triplet['ports'] as $port)
+                       usePreparedInsertBlade ('VSEnabledPorts', $tr_key + $port);
+               foreach ($triplet['vips'] as $vip)
+                       usePreparedInsertBlade ('VSEnabledIPs', $tr_key + $vip);
+       }
+
+       // update configs
+       usePreparedUpdateBlade ('VS', $plan['properties'], array ('id' => $vs_id));
+
+       // replace tags
+       global $taglist;
+       $chain = array();
+       foreach ($tag_ids as $tid)
+               if (! isset ($taglist[$tid]))
+               {
+                       $dbxlink->rollback();
+                       showError ("Unknown tag id $tid");
+               }
+               else
+                       $chain[] = $taglist[$tid];
+       rebuildTagChainForEntity ('ipvs', $vs_id, $chain, TRUE);
+
+       $dbxlink->commit();
+       showSuccess ("old VS configs were copied to VS group");
+       return buildRedirectURL (NULL, 'default');
+}
+
 # validate user input and produce SQL columns per the opspec descriptor
 function buildOpspecColumns ($opspec, $listname)
 {
index 0289801a1ecac558d7f32965f16546c3021849ee..31bebf4e1c6ffa37446dec462af449cc20b8121a 100644 (file)
@@ -4,6 +4,8 @@
 # framework. See accompanying file "COPYING" for the full copyright and
 # licensing information.
 
+require_once 'slb2-interface.php';
+
 function renderSLBDefConfig()
 {
        $defaults = getSLBDefaults();
@@ -41,6 +43,13 @@ function renderSLBEntityCell ($cell, $highlighted = FALSE)
                echo $cell['dname'] . "</a></td></tr><tr><td>";
                echo $cell['name'] . '</td></tr>';
                break;
+       case 'ipvs':
+               echo "<tr><td rowspan=3 width='5%'>";
+               printImageHREF ('VS');
+               echo "</td><td>";
+               echo "<a class='$a_class' href='index.php?page=ipvs&vs_id=${cell['id']}'>";
+               echo $cell['name'] . "</a></td></tr>";
+               break;
        case 'ipv4rspool':
                echo "<tr><td>";
                echo "<a class='$a_class' href='index.php?page=ipv4rspool&pool_id=${cell['id']}'>";
@@ -318,6 +327,7 @@ function renderRSPool ($pool_id)
        }
 
        echo "</td><td class=pcright>\n";
+       renderSLBTriplets2 ($poolInfo);
        renderSLBTriplets ($poolInfo);
        echo "</td></tr><tr><td colspan=2>\n";
        renderFilesPortlet ('ipv4rspool', $pool_id);
index a929083067b75b55193adeba3c841c6eb394fe54..928e2e5fc4df0e460833546d12b147f4b550acb0 100644 (file)
@@ -82,6 +82,8 @@ class SLBTriplet
                global $triplet_class;
                foreach ($rows as $row)
                {
+                       $row['vsconfig'] = dos2unix ($row['vsconfig']);
+                       $row['rsconfig'] = dos2unix ($row['rsconfig']);
                        $triplet = new $triplet_class ($row['object_id'], $row['vs_id'], $row['rspool_id'], $row);
                        $triplet->display_cells = $display_cells;
                        $ret[] = $triplet;
@@ -126,6 +128,10 @@ function generateSLBConfig ($triplet_list)
        $gl_parser->addMacro ('GLOBAL_VS_CONF', dos2unix ($defaults['vsconfig']));
        $gl_parser->addMacro ('GLOBAL_RS_CONF', dos2unix ($defaults['rsconfig']));
        $gl_parser->addMacro ('RSPORT', '%VPORT%');
+       $gl_parser->addMacro ('VS_PREPEND',
+"# LB (id == %LB_ID%): %LB_NAME%
+# VS (id == %VS_ID%): %VS_NAME%
+# RS (id == %RSP_ID%): %RSP_NAME%");
 
        // group triplets by object_id, vs_id
        $grouped = array();
@@ -182,9 +188,7 @@ function generateSLBConfig ($triplet_list)
                                $rs_parser->addMacro ('SLB_RS_CONF', dos2unix ($triplet->slb['rsconfig']));
 
                                $ret .= $rs_parser->expand ("
-# LB (id == %LB_ID%): %LB_NAME%
-# VS (id == %VS_ID%): %VS_NAME%
-# RS (id == %RSP_ID%): %RSP_NAME%
+%VS_PREPEND%
 virtual_server %VS_HEADER% {
        protocol %PROTO%
        %GLOBAL_VS_CONF%
@@ -532,7 +536,12 @@ class MacroParser
 function buildEntityLVSConfig ($cell)
 {
        $ret = "#\n#\n# This configuration has been generated automatically by RackTables\n#\n#\n";
-       $ret .= generateSLBConfig (SLBTriplet::getTriplets ($cell));
+       // slbv2
+       if ($cell['realm'] != 'ipv4vs')
+               $ret .= generateSLBConfig2 (getTriplets ($cell));
+       // slbv1
+       if ($cell['realm'] != 'ipvs')
+               $ret .= generateSLBConfig (SLBTriplet::getTriplets ($cell));
        return $ret;
 }
 
@@ -694,6 +703,7 @@ function getRSList ()
        while ($row = $result->fetch (PDO::FETCH_ASSOC))
        {
                $row['rsip'] = ip_format ($row['rsip_bin']);
+               $row['rsconfig'] = dos2unix ($row['rsconfig']);
                foreach (array ('inservice', 'rsip_bin', 'rsip', 'rsport', 'rspool_id', 'rsconfig') as $cname)
                        $ret[$row['id']][$cname] = $row[$cname];
        }
diff --git a/wwwroot/inc/slb2-interface.php b/wwwroot/inc/slb2-interface.php
new file mode 100644 (file)
index 0000000..cff98f4
--- /dev/null
@@ -0,0 +1,531 @@
+<?php
+
+# This file is a part of RackTables, a datacenter and server room management
+# framework. See accompanying file "COPYING" for the full copyright and
+# licensing information.
+
+function renderVSGList ()
+{
+       renderCellList ('ipvs', 'VS groups');
+}
+
+function formatVSPort ($port, $plain_text = FALSE)
+{
+       if ($port['proto'] == 'MARK')
+               return 'fwmark ' . $port['vport'];
+       $proto = strtolower ($port['proto']);
+       $name = $port['vport'] . '/' . $proto;
+       if (!$plain_text && NULL !== ($srv = getservbyport ($port['vport'], $proto)))
+               return '<span title="' . $name . '">' . $srv . '</span>';
+       else
+               return $name;
+}
+
+function formatVSIP ($vip, $plain_text = FALSE)
+{
+       $fmt_ip = ip_format ($vip['vip']);
+       if ($plain_text)
+               return $fmt_ip;
+       $ret = '<a href="' . makeHref (array ('page' => 'ipaddress', 'ip' => $fmt_ip)) . '">' . $fmt_ip . '</a>';
+       return $ret;
+}
+
+function renderVS ($vsid)
+{
+       $vsinfo = spotEntity ('ipvs', $vsid);
+       amplifyCell ($vsinfo);
+
+       echo '<table border=0 class=objectview cellspacing=0 cellpadding=0>';
+       if (strlen ($vsinfo['name']))
+               echo "<tr><td colspan=2 align=center><h1>${vsinfo['name']}</h1></td></tr>\n";
+       echo '<tr>';
+
+       echo '<td class=pcleft>';
+       $summary = array();
+       $summary['Name'] = $vsinfo['name'] . getPopupSLBConfig ($vsinfo);
+       $summary['tags'] = '';
+
+       $ips = '<ul class="slb-checks">';
+       foreach ($vsinfo['vips'] as $vip)
+               $ips .= '<li>' . formatVSIP ($vip) . getPopupSLBConfig ($vip) . '</li>';
+       $ips .= '</ul>';
+       $summary['IPs'] = $ips;
+
+       $ports = '<ul class="slb-checks">';
+       foreach ($vsinfo['ports'] as $port)
+               $ports .= '<li>' . formatVSPort ($port) . getPopupSLBConfig ($port) . '</li>';
+       $ports .= '</ul>';
+       $summary['Ports'] = $ports;
+
+       renderEntitySummary ($vsinfo, 'Summary', $summary);
+       echo '</td>';
+
+       echo '<td class=pcright>';
+       renderSLBTriplets2 ($vsinfo);
+       echo '</td></tr><tr><td colspan=2>';
+       renderFilesPortlet ('ipvs', $vsid);
+       echo '</tr><table>';
+}
+
+function renderTripletForm ($bypass_id)
+{
+       global $pageno, $etype_by_pageno;
+       $cell = spotEntity ($etype_by_pageno[$pageno], $bypass_id);
+       renderSLBTriplets2 ($cell, TRUE);
+}
+
+// either $port of $vip argument should be NULL
+function renderPopupTripletForm ($triplet, $port, $vip, $row)
+{
+       printOpFormIntro (isset ($port) ? 'updPort' : 'updIp');
+       echo '<input type=hidden name=object_id value="' . htmlspecialchars ($triplet['object_id'], ENT_QUOTES) . '">';
+       echo '<input type=hidden name=vs_id value="' . htmlspecialchars ($triplet['vs_id'], ENT_QUOTES) . '">';
+       echo '<input type=hidden name=rspool_id value="' . htmlspecialchars ($triplet['rspool_id'], ENT_QUOTES) . '">';
+       echo '<p><label><input type=checkbox name=enabled' . (is_array ($row) ? ' checked' : '') . '> ' . (isset ($port) ? 'Enable port' : 'Enable IP') . '</label>';
+       if (isset ($port))
+       {
+               echo '<input type=hidden name=proto value="' . htmlspecialchars ($port['proto'], ENT_QUOTES) . '">';
+               echo '<input type=hidden name=port value="'  . htmlspecialchars ($port['vport'], ENT_QUOTES) . '">';
+       }
+       else
+       {
+               echo '<input type=hidden name=vip value="'  . htmlspecialchars (ip_format ($vip['vip']), ENT_QUOTES) . '">';
+               echo '<p><label>Priority:<br><input type=text name=prio value="'  . htmlspecialchars (is_array ($row) ? $row['prio'] : '', ENT_QUOTES) . '"></label>';
+       }
+       echo '<p><label>VS config:<br>';
+       echo '<textarea name=vsconfig rows=3 cols=80>' . htmlspecialchars (is_array ($row) ? $row['vsconfig'] : '') . '</textarea></label>';
+       echo '<p><label>RS config:<br>';
+       echo '<textarea name=rsconfig rows=3 cols=80>' . htmlspecialchars (is_array ($row) ? $row['rsconfig'] : '') . '</textarea></label>';
+       echo '<p align=center>' . getImageHREF ('SAVE', 'Save changes', TRUE);
+       echo '</form>';
+}
+
+function renderPopupVSPortForm ($port, $used = 0)
+{
+       $keys = array ('proto' => $port['proto'], 'port' => $port['vport']);
+       $title = 'remove port ' . formatVSPort ($port) . ($used ? " (used $used times)" : '');
+       printOpFormIntro ('updPort', $keys);
+       echo '<p align=center>';
+       echo getOpLink (array ('op' => 'delPort') + $keys, $title, 'destroy', '', ($used ? 'del-used-slb' : '') );
+       echo '<p><label>VS config:<br>';
+       echo '<textarea name=vsconfig rows=3 cols=80>' . htmlspecialchars ($port['vsconfig']) . '</textarea></label>';
+       echo '<p><label>RS config:<br>';
+       echo '<textarea name=rsconfig rows=3 cols=80>' . htmlspecialchars ($port['rsconfig']) . '</textarea></label>';
+       echo '<p align=center>' . getImageHREF ('SAVE', 'Save changes', TRUE);
+       echo '</form>';
+}
+
+function renderPopupVSVIPForm ($vip, $used = 0)
+{
+       $fmt_ip = ip_format ($vip['vip']);
+       $title = 'remove IP ' . formatVSIP ($vip) . ($used ? " (used $used times)" : '');
+       printOpFormIntro ('updIP', array ('ip' => $fmt_ip));
+       echo '<p align=center>';
+       echo getOpLink (array ('op' => 'delIP', 'ip' => $fmt_ip), $title, 'destroy', '', ($used ? 'del-used-slb' : '') );
+       echo '<p><label>VS config:<br>';
+       echo '<textarea name=vsconfig rows=3 cols=80>' . htmlspecialchars ($vip['vsconfig']) . '</textarea></label>';
+       echo '<p><label>RS config:<br>';
+       echo '<textarea name=rsconfig rows=3 cols=80>' . htmlspecialchars ($vip['rsconfig']) . '</textarea></label>';
+       echo '<p align=center>' . getImageHREF ('SAVE', 'Save changes', TRUE);
+       echo '</form>';
+}
+
+function renderEditVS ($vs_id)
+{
+       global $vs_proto;
+       $vsinfo = spotEntity ('ipvs', $vs_id);
+       amplifyCell ($vsinfo);
+       $triplets = getTriplets ($vsinfo);
+
+       // first form - common VS settings
+       printOpFormIntro ('updVS');
+       echo '<table border=0 align=center>';
+       echo '<tr><th class=tdright>Name:</th><td class=tdleft><input type=text name=name value="' . htmlspecialchars ($vsinfo['name'], ENT_QUOTES) . '"></td></tr>';
+       echo '<tr><th class=tdright>VS config:</th><td class=tdleft><textarea name=vsconfig rows=3 cols=80>' . htmlspecialchars ($vsinfo['vsconfig']) . '</textarea></td></tr>';
+       echo '<tr><th class=tdright>RS config:</th><td class=tdleft><textarea name=rsconfig rows=3 cols=80>' . htmlspecialchars ($vsinfo['rsconfig']) . '</textarea></td></tr>';
+       echo '<tr><th></th><th>';
+       printImageHREF ('SAVE', 'Save changes', TRUE);
+       echo '</th></tr>';
+       echo '</table></form>';
+
+       addJS ('js/jquery.thumbhover.js');
+       addJS ('js/slb_editor.js');
+
+       // second form - ports and IPs settings
+       echo '<p>'; // vertical indentation
+       echo '<table width=50% border=0 align=center>';
+
+       echo '<tr><th style="white-space:nowrap">';
+       printOpFormIntro ('addPort');
+       echo 'Add new port:<br>';
+       echo getSelect ($vs_proto, array ('name' => 'proto'));
+       echo ' <input name=port size=5> ';
+       echo getImageHREF ('add', 'Add port', TRUE);
+       echo '</form></th>';
+
+       echo '<td width=99%></td>';
+
+       echo '<th style="white-space:nowrap">';
+       printOpFormIntro ('addIP');
+       echo 'Add new IP:<br>';
+       echo '<input name=ip size=14> ';
+       echo getImageHREF ('add', 'Add IP', TRUE);
+       echo '</form></th></tr>';
+
+       echo '<tr><td valign=top class=tdleft><ul class="slb-checks editable">';
+       foreach ($vsinfo['ports'] as $port)
+       {
+               $used = 0;
+               foreach ($triplets as $triplet)
+                       if (isPortEnabled ($port, $triplet['ports']))
+                               $used++;
+               echo '<li class="enabled">';
+               echo formatVSPort ($port) . getPopupSLBConfig ($port);
+               renderPopupVSPortForm ($port, $used);
+               echo '</li>';
+       }
+       echo '</ul></td>';
+
+       echo '<td width=99%></td>';
+
+       echo '<td valign=top class=tdleft><ul class="slb-checks editable">';
+       foreach ($vsinfo['vips'] as $vip)
+       {
+               $used = 0;
+               foreach ($triplets as $triplet)
+                       if (isVIPEnabled ($vip, $triplet['vips']))
+                               $used++;
+               echo '<li class="enabled">';
+               echo formatVSIP ($vip) . getPopupSLBConfig ($vip);
+               renderPopupVSVIPForm ($vip, $used);
+               echo '</li>';
+       }
+       echo '</ul></td>';
+
+       echo '</tr></table>';
+}
+
+// supports object, ipvs, ipv4rspool cell types
+function renderSLBTriplets2 ($cell, $editable = FALSE, $hl_ip = NULL)
+{
+       list ($realm1, $realm2) = array_values (array_diff (array ('object', 'ipvs', 'ipv4rspool'), array ($cell['realm'])));
+       if ($editable && getConfigVar ('ADDNEW_AT_TOP') == 'yes')
+               callHook ('renderNewTripletForm', $realm1, $realm2);
+
+       $fields = array
+       (
+               'ipvs' => 'vs_id',
+               'object' => 'object_id',
+               'ipv4rspool' => 'rspool_id',
+       );
+
+       $headers = array
+       (
+               'ipvs' => 'VS',
+               'object' => 'LB',
+               'ipv4rspool' => 'RS pool',
+       );
+
+       $triplets = getTriplets ($cell);
+
+       // render table header
+       if (count ($triplets))
+       {
+               startPortlet ('VS group instances (' . count ($triplets) . ')');
+               echo "<table cellspacing=0 cellpadding=5 align=center class=widetable><tr>";
+               foreach ($headers as $realm => $header)
+                       if ($realm != $cell['realm'])
+                               echo "<th>$header</th>";
+               echo '<th>Ports</th>';
+               echo '<th>VIPs</th>';
+               echo "</tr>";
+       }
+
+       $class = 'slb-checks';
+       if ($editable)
+       {
+               addJS ('js/jquery.thumbhover.js');
+               addJS ('js/slb_editor.js');
+               $class .= ' editable';
+       }
+
+       // render table rows
+       global $nextorder;
+       $order = 'odd';
+       foreach ($triplets as $slb)
+       {
+               $vs_cell = spotEntity ('ipvs', $slb['vs_id']);
+               amplifyCell ($vs_cell);
+               echo "<tr valign=top class='row_${order} triplet-row'>";
+               foreach (array_keys ($headers) as $realm)
+               {
+                       if ($realm == $cell['realm'])
+                               continue;
+                       echo "<td class=tdleft>";
+                       $slb_cell = spotEntity ($realm, $slb[$fields[$realm]]);
+                       renderSLBEntityCell ($slb_cell);
+                       echo "</td>";
+               }
+               // render ports
+               echo "<td class=tdleft><ul class='$class'>";
+               foreach ($vs_cell['ports'] as $port)
+               {
+                       echo '<li class="' . (($row = isPortEnabled ($port, $slb['ports'])) ? 'enabled' : 'disabled') . '">';
+                       echo formatVSPort ($port) . getPopupSLBConfig ($row);
+                       if ($editable)
+                               renderPopupTripletForm ($slb, $port, NULL, $row);
+                       echo '</li>';
+               }
+               echo '</ul></td>';
+
+               // render VIPs
+               echo "<td class=tdleft><ul class='$class'>";
+               foreach ($vs_cell['vips'] as $vip)
+               {
+                       $li_class = isVIPEnabled ($vip, $slb['vips']) ? 'enabled' : 'disabled';
+                       if ($vip['vip'] === $hl_ip && $li_class == 'enabled')
+                               $li_class .= ' highlight';
+                       echo "<li class='$li_class'>";
+                       echo formatVSIP ($vip);
+                       if (is_array ($row) && !empty ($row['prio']))
+                       {
+                               $prio_class = 'slb-prio slb-prio-' . preg_replace ('/\s.*/', '', $row['prio']);
+                               echo '<span class="' . htmlspecialchars ($prio_class, ENT_QUOTES) . '">' . htmlspecialchars($row['prio']) . '</span>';
+                       }
+                       echo getPopupSLBConfig ($row);
+                       if ($editable)
+                               renderPopupTripletForm ($slb, NULL, $vip, $row);
+                       echo '</li>';
+               }
+               echo '<ul></td>';
+
+               if ($editable)
+               {
+                       echo '<td valign=middle>';
+                       printOpFormIntro ('del', array (
+                               'object_id' => $slb['object_id'],
+                               'vs_id' => $slb['vs_id'],
+                               'rspool_id' => $slb['rspool_id'],
+                       ));
+                       printImageHREF ('DELETE', 'Remove triplet', TRUE);
+                       echo '</form></td>';
+               }
+
+               echo "</tr>\n";
+               $order = $nextorder[$order];
+       }
+       if (count ($triplets))
+       {
+               echo "</table>\n";
+               finishPortlet();
+       }
+
+       if ($editable && getConfigVar ('ADDNEW_AT_TOP') != 'yes')
+               callHook ('renderNewTripletForm', $realm1, $realm2);
+
+}
+
+function renderSLBFormAJAX()
+{
+       global $pageno, $tabno;
+       parse_str (assertStringArg ('form'), $orig_request);
+       parse_str (ltrim (assertStringArg ('action'), '?'), $action);
+       $pageno = $action['page'];
+       $tabno = $action['tab'];
+       printOpFormIntro ($action['op'], $orig_request);
+
+       $realm_list = array_diff (array ('ipvs', 'object', 'ipv4rspool'), array ($pageno));
+       echo '<table align=center><tr class="tdleft">';
+       foreach ($realm_list as $realm)
+       {
+               switch ($realm)
+               {
+                       case 'object':
+                               $slb_cell = spotEntity ('object', $orig_request['object_id']);
+                               break;
+                       case 'ipv4rspool':
+                               $slb_cell = spotEntity ('ipv4rspool', $orig_request['rspool_id']);
+                               break;
+                       case 'ipvs':
+                               $slb_cell = spotEntity ('ipvs', $orig_request['vs_id']);
+                               break;
+               }
+               echo '<td>';
+               renderSLBEntityCell ($slb_cell);
+               echo '</td>';
+       }
+
+       $vsinfo = spotEntity ('ipvs', $orig_request['vs_id']);
+       amplifyCell ($vsinfo);
+
+       echo '<td><ul style="list-style: none">';
+       foreach ($vsinfo['ports'] as $port)
+       {
+               $key = $port['proto'] . '-' . $port['vport'];
+               echo '<li><label><input type=checkbox name="enabled_ports[]" value="' . htmlspecialchars ($key, ENT_QUOTES) . '" checked>' . formatVSPort ($port) . '</label></li>';
+       }
+       echo '</ul></td>';
+
+       echo '<td><ul style="list-style: none">';
+       foreach ($vsinfo['vips'] as $vip)
+       {
+               $key = ip_format ($vip['vip']);
+               echo '<li><label><input type=checkbox name="enabled_vips[]" value="' . htmlspecialchars ($key, ENT_QUOTES) . '" checked>' . $key . '</label></li>';
+       }
+       echo '</ul></td>';
+       echo '<td>';
+       printImageHREF ('ADD', 'Configure LB', TRUE);
+       echo '</td>';
+       echo '</tr></table>';
+       echo '</form>';
+}
+
+function renderNewTripletForm ($realm1, $realm2)
+{
+       function get_realm_data ($realm)
+       {
+               $name = NULL;
+               $list = array();
+               $options = array();
+               switch ($realm)
+               {
+                       case 'object':
+                               $name = 'Load balancer';
+                               //FIXME: remove Y-specific optimizer
+                               // $list = getNarrowObjectList ('IPV4LB_LISTSRC');
+                               $list = formatEntityList (quickListTaggedBy ($realm, 58));
+                               $options = array ('name' => 'object_id', 'tabindex' => 100);
+                               break;
+                       case 'ipvs':
+                               $name = 'Virtual service';
+                               $list = formatEntityList (listCells ('ipvs'));
+                               $options = array ('name' => 'vs_id', 'tabindex' => 101);
+                               break;
+                       case 'ipv4rspool':
+                               $name = 'RS pool';
+                               $list = formatEntityList (listCells ('ipv4rspool'));
+                               $options = array ('name' => 'rspool_id', 'tabindex' => 102);
+                               break;
+                       default:
+                               throw new InvalidArgException('realm', $realm);
+               }
+               return array ('name' => $name, 'list' => $list, 'options' => $options);
+       }
+
+       $realm1_data = get_realm_data ($realm1);
+       $realm2_data = get_realm_data ($realm2);
+       startPortlet ('Add new VS group');
+       if (count ($realm1_data['list']) && count ($realm2_data['list']))
+               printOpFormIntro ('addLink');
+       echo "<table cellspacing=0 cellpadding=5 align=center>";
+       echo "<tr valign=top><th class=tdright>{$realm1_data['name']}</th><td class=tdleft>";
+       printSelect ($realm1_data['list'], $realm1_data['options']);
+       echo '</td><td class=tdcenter valign=middle rowspan=2>';
+       if (count ($realm1_data['list']) && count ($realm2_data['list']))
+               printImageHREF ('ADD', 'Configure LB', TRUE, 120);
+       else
+       {
+               $names = array();
+               if (! count ($realm1_data['list']))
+                       $names[] = 'a ' . $realm1_data['name'];
+               if (! count ($realm2_data['list']))
+                       $names[] = 'a ' . $realm2_data['name'];
+               $message = 'Please create ' . (implode (' and ', $names)) . '.';
+               showNotice ($message);
+               printImageHREF ('DENIED', $message, FALSE);
+       }
+       echo "<tr valign=top><th class=tdright>{$realm2_data['name']}</th><td class=tdleft>";
+       printSelect ($realm2_data['list'], $realm2_data['options']);
+       echo "</td></tr>\n";
+       echo "</table></form>\n";
+       finishPortlet();
+}
+
+function getPopupSLBConfig ($row)
+{
+       $do_vs = (isset ($row) && isset ($row['vsconfig']) && strlen ($row['vsconfig']));
+       $do_rs = (isset ($row) && isset ($row['rsconfig']) && strlen ($row['rsconfig']));
+       if (!$do_vs && !$do_rs)
+               return;
+
+       $ret = '';
+       $ret .= '<div class="slbconf-btn">…</div>';
+       $ret .= '<div class="slbconf popup-box">';
+       if ($do_vs)
+       {
+               $ret .= '<h1>VS config:</h1>';
+               $ret .= $row['vsconfig'];
+       }
+       if ($do_rs)
+       {
+               $ret .= '<h1>RS config:</h1>';
+               $ret .= $row['rsconfig'];
+       }
+       $ret .= '</div>';
+
+       static $js_added = FALSE;
+       if (! $js_added)
+       {
+               addJS ('js/jquery.thumbhover.js');
+               addJS (<<<END
+$(document).ready (function () {
+       $('.slbconf-btn').each (function () {
+               $(this).thumbPopup($(this).siblings('.slbconf.popup-box'), { showFreezeHint: false });
+       });
+});
+END
+               , TRUE);
+       }
+       return $ret;
+}
+
+function trigger_ipvs_convert ()
+{
+       return count (callHook ('getVSIDsByGroup', getBypassValue())) ? 'std' : '';
+}
+
+function renderIPVSConvert ($vs_id)
+{
+       $old_vs_list = callHook ('getVSIDsByGroup', $vs_id);
+
+       $grouped = array();
+       $used_tags = array();
+       foreach ($old_vs_list as $old_vs_id)
+       {
+               $vsinfo = spotEntity ('ipv4vs', $old_vs_id);
+               foreach ($vsinfo['etags'] as $taginfo)
+                       $used_tags[$taginfo['id']] = $taginfo;
+               $port_key = $vsinfo['proto'] . '-' . $vsinfo['vport'];
+               $grouped[$port_key][] = $vsinfo;
+       }
+
+       startPortlet ("Found " . count ($old_vs_list) . " matching VS");
+       printOpFormIntro ('convert');
+       if (count ($used_tags))
+       {
+               echo '<p>Assign these tags to VS group:</p>';
+               foreach ($used_tags as $taginfo)
+                       echo '<p><label><input type=checkbox checked name="taglist[]" value="' . htmlspecialchars ($taginfo['id'], ENT_QUOTES) . '""> ' . serializeTags (array ($taginfo)) . '<label>';
+       }
+
+       echo '<p>Import settings of these VS:</p>';
+       echo '<table align=center><tr>';
+       foreach ($grouped as $port_key => $list)
+               echo '<th>' . $port_key . '</th>';
+       echo '</tr><tr>';
+       foreach ($grouped as $port_key => $list)
+       {
+               echo '<td><table>';
+               foreach ($list as $vsinfo)
+               {
+                       echo '<tr><td><input type=checkbox name="vs_list[]" checked value="' . htmlspecialchars ($vsinfo['id'], ENT_QUOTES) . '"></td><td>';
+                       renderSLBEntityCell ($vsinfo);
+                       echo '</td></tr>';
+               }
+               echo '</table></td>';
+       }
+       echo '</tr></table>';
+       printImageHREF ('next', "Import settings of the selected services", TRUE);
+       echo '</form>';
+       finishPortlet();
+}
diff --git a/wwwroot/inc/slbv2.php b/wwwroot/inc/slbv2.php
new file mode 100644 (file)
index 0000000..34ef6cd
--- /dev/null
@@ -0,0 +1,614 @@
+<?php
+
+# This file is a part of RackTables, a datacenter and server room management
+# framework. See accompanying file "COPYING" for the full copyright and
+# licensing information.
+
+// *********************  Config-generating functions  *********************
+
+$parser_class = 'MacroParser';
+
+// Returns array of triplets:
+//triplet = array
+//(
+//     'ports' => $db_port_row,
+//     'vips' => $db_ip_row,
+//     'object_id' => $object_id,
+//     'vs_id' => $vs_id,
+//     'rspool_id' => $rspool_id,
+//)
+function getTriplets ($cell)
+{
+       $filter_fields = array();
+       $order_fields = array();
+       switch ($cell['realm'])
+       {
+               case 'object':
+                       $filter_fields['object_id'] = $cell['id'];
+                       $order_fields[] = 'vs_id';
+                       break;
+               case 'ipvs':
+                       $filter_fields['vs_id'] = $cell['id'];
+                       $order_fields[] = 'rspool_id';
+                       break;
+               case 'ipv4rspool':
+                       $filter_fields['rspool_id'] = $cell['id'];
+                       $order_fields[] = 'vs_id';
+                       break;
+               default:
+                       throw new InvalidArgException ('realm', $cell['realm']);
+       }
+       return fetchTripletRows ($filter_fields, $order_fields);
+}
+
+function fetchTripletRows ($filter_fields, $order_fields = array())
+{
+       $order = count ($order_fields) ? "ORDER BY " . implode (',', $order_fields) : '';
+       $filter = 'TRUE';
+       $params = array();
+       foreach ($filter_fields as $key => $value)
+       {
+               $filter .= " AND `$key` = ?";
+               $params[] = $value;
+       }
+
+       $triplets = array();
+       foreach (array ('ports' => 'VSEnabledPorts', 'vips' => 'VSEnabledIPs') as $key => $table)
+       {
+               $result = usePreparedSelectBlade ("SELECT * FROM $table WHERE $filter $order", $params);
+               while ($row = $result->fetch (PDO::FETCH_ASSOC))
+               {
+                       $data = $row;
+                       unset ($data['object_id']);
+                       unset ($data['vs_id']);
+                       unset ($data['rspool_id']);
+                       $triplet_key = implode ('-', array ($row['vs_id'], $row['rspool_id'], $row['object_id']));
+                       if (! isset ($triplets[$triplet_key]))
+                               $triplets[$triplet_key] = array
+                               (
+                                       'vips' => array(),
+                                       'ports' => array(),
+                               );
+                       $triplets[$triplet_key][$key][] = $data;
+                       $triplets[$triplet_key]['vs_id'] = $row['vs_id'];
+                       $triplets[$triplet_key]['object_id'] = $row['object_id'];
+                       $triplets[$triplet_key]['rspool_id'] = $row['rspool_id'];
+               }
+               unset ($result);
+       }
+       return $triplets;
+}
+
+function getTriplet ($object_id, $vs_id, $rspool_id)
+{
+       return array_first (fetchTripletRows (
+               array ('object_id' => $object_id, 'vs_id' => $vs_id, 'rspool_id' => $rspool_id)
+       ));
+}
+
+function generateVSSection ($rspool_id, $vs_parser)
+{
+       $ret = $vs_parser->expand (
+"      protocol %PROTO%
+       %GLOBAL_VS_CONF%
+       %RSP_VS_CONF%
+       %VS_VS_CONF%
+       %PORT_VS_CONF%
+       %VIP_VS_CONF%
+       %SLB_PORT_VS_CONF%
+       %SLB_VIP_VS_CONF%
+");
+       $vip_bin = ip_checkparse ($vs_parser->expandMacro ('VIP'));
+       if ($vip_bin === FALSE)
+               $family_length = 4;
+       else
+               $family_length = strlen ($vip_bin);
+
+       foreach (getRSListInPool ($rspool_id) as $rs)
+       {
+               if ($rs['inservice'] != 'yes')
+                       continue;
+               $parser = clone $vs_parser;
+               $parser->addMacro ('RSIP', $rs['rsip']);
+               $parser->addMacro ('RS_COMMENT', $rs['comment']);
+               $parser->addMacro ('RS_RS_CONF', $rs['rsconfig']);
+
+               // do not add v6 reals into v4 service and vice versa
+               $rsip_bin = ip_checkparse ($parser->expandMacro ('RSIP'));
+               if ($rsip_bin !== FALSE && strlen ($rsip_bin) == $family_length)
+                       foreach (explode (',', $parser->expandMacro ('RSPORT')) as $rsp_token)
+                       {
+                               $port_range = explode ('-', $rsp_token);
+                               if (count ($port_range) < 1)
+                                       throw new InvalidArgException ('RSPORT', $rsp_token, "invalid RS port range");
+                               if (count ($port_range) < 2)
+                                       $port_range[] = $port_range[0];
+                               if ($port_range[0] > $port_range[1])
+                                       throw new InvalidArgException ('RSPORT', $rsp_token, "invalid RS port range");
+
+                               for ($rsport = $port_range[0]; $rsport <= $port_range[1]; $rsport++)
+                               {
+                                       $rs_parser = clone $parser;
+                                       $rs_parser->addMacro ('RSPORT', $rsport);
+                                       $ret .= $rs_parser->expand ("
+       %RS_PREPEND%
+       real_server %RS_HEADER% {
+               %GLOBAL_RS_CONF%
+               %VS_RS_CONF%
+               %RSP_RS_CONF%
+               %VIP_RS_CONF%
+               %PORT_RS_CONF%
+               %SLB_VIP_RS_CONF%
+               %SLB_PORT_RS_CONF%
+               %RS_RS_CONF%
+       }
+");
+                               }
+                       }
+       }
+       return $ret;
+}
+
+function generateSLBConfig2 ($triplet_list)
+{
+       $ret = '';
+
+       global $parser_class;
+       $gl_parser = new $parser_class;
+       $defaults = getSLBDefaults (TRUE);
+       $gl_parser->addMacro ('GLOBAL_VS_CONF', dos2unix ($defaults['vsconfig']));
+       $gl_parser->addMacro ('GLOBAL_RS_CONF', dos2unix ($defaults['rsconfig']));
+       $gl_parser->addMacro ('RSPORT', '%VPORT%');
+       $gl_parser->addMacro ('VS_PREPEND',
+"# LB (id == %LB_ID%): %LB_NAME%
+# VSG (id == %VSG_ID%): %VS_NAME%
+# RS (id == %RSP_ID%): %RSP_NAME%");
+
+       // group triplets by object_id, vs_id
+       $grouped = array();
+       foreach ($triplet_list as $triplet)
+               $grouped[$triplet['object_id']][$triplet['vs_id']][] = $triplet;
+
+       foreach ($grouped as $object_id => $subarr)
+       {
+               $seen_vs_groups = array();
+               $lb_parser = clone $gl_parser;
+               $lb_cell = spotEntity ('object', $object_id);
+               $lb_parser->addMacro ('LB_ID', $lb_cell['id']);
+               $lb_parser->addMacro ('LB_NAME', $lb_cell['name']);
+
+               foreach ($subarr as $vs_id => $triplets)
+               {
+                       $vs_parser = clone $lb_parser;
+                       $vs_cell = spotEntity ('ipvs', $vs_id);
+                       if (! isset ($vs_cell['ports']) || ! isset ($vs_cell['vips']))
+                               amplifyCell ($vs_cell);
+                       $vs_parser->addMacro ('VS_ID', $vs_cell['id']);
+                       $vs_parser->addMacro ('VSG_ID', $vs_cell['id']);
+                       $vs_parser->addMacro ('VS_NAME', $vs_cell['name']);
+                       $vs_parser->addMacro ('VS_RS_CONF', dos2unix ($vs_cell['rsconfig']));
+
+                       $virtual_services = array();
+                       foreach ($triplets as $triplet)
+                       {
+                               $tr_parser = clone $vs_parser;
+                               $rs_cell = spotEntity ('ipv4rspool', $triplet['rspool_id']);
+                               $tr_parser->addMacro ('RSP_ID', $rs_cell['id']);
+                               $tr_parser->addMacro ('RSP_NAME', $rs_cell['name']);
+                               $tr_parser->addMacro ('RSP_VS_CONF', dos2unix ($rs_cell['vsconfig']));
+                               $tr_parser->addMacro ('RSP_RS_CONF', dos2unix ($rs_cell['rsconfig']));
+                               $tr_parser->addMacro ('VS_VS_CONF', dos2unix ($vs_cell['vsconfig'])); // VS-driven vsconfig has higher priority than RSP-driven
+
+                               foreach ($triplet['ports'] as $port_row)
+                               {
+                                       $is_mark = ($port_row['proto'] == 'MARK');
+                                       $p_parser = clone $tr_parser;
+                                       $p_parser->addMacro ('VS_HEADER', $is_mark ? 'fwmark %MARK%' : '%VIP% %VPORT%');
+                                       $p_parser->addMacro ('PROTO', $is_mark ? 'TCP' : $port_row['proto']);
+                                       $p_parser->addMacro ($is_mark ? 'MARK' : 'VPORT', $port_row['vport']);
+                                       foreach ($vs_cell['ports'] as $vport)
+                                               if ($vport['vport'] == $port_row['vport'] && $vport['proto'] == $port_row['proto'])
+                                               {
+                                                       $p_parser->addMacro ('PORT_VS_CONF', dos2unix ($vport['vsconfig']));
+                                                       $p_parser->addMacro ('PORT_RS_CONF', dos2unix ($vport['rsconfig']));
+                                                       break;
+                                               }
+                                       $p_parser->addMacro ('SLB_PORT_VS_CONF', dos2unix ($port_row['vsconfig']));
+                                       $p_parser->addMacro ('SLB_PORT_RS_CONF', dos2unix ($port_row['rsconfig']));
+
+                                       if ($is_mark)
+                                       {
+                                               $p_parser->addMacro ('RS_HEADER', '%RSIP%');
+                                               $virtual_services[$parser->expandMacro ('VS_HEADER')] = generateVSSection ($triplet['rspool_id'], $p_parser);
+                                       }
+                                       else
+                                       {
+                                               $p_parser->addMacro ('RS_HEADER', '%RSIP% %RSPORT%');
+                                               foreach ($triplet['vips'] as $ip_row)
+                                               {
+                                                       $ip_parser = clone $p_parser;
+                                                       $ip_parser->addMacro ('VIP', ip_format ($ip_row['vip']));
+                                                       $ip_parser->addMacro ('IP_VER', (strlen ($ip_row['vip']) == 16) ? 6 : 4);
+                                                       $ip_parser->addMacro ('PRIO', $ip_row['prio']);
+                                                       foreach ($vs_cell['vips'] as $vip)
+                                                               if ($vip['vip'] === $ip_row['vip'])
+                                                               {
+                                                                       $ip_parser->addMacro ('VIP_VS_CONF', dos2unix ($vip['vsconfig']));
+                                                                       $ip_parser->addMacro ('VIP_RS_CONF', dos2unix ($vip['rsconfig']));
+                                                                       break;
+                                                               }
+                                                       $ip_parser->addMacro ('SLB_VIP_VS_CONF', dos2unix ($ip_row['vsconfig']));
+                                                       $ip_parser->addMacro ('SLB_VIP_RS_CONF', dos2unix ($ip_row['rsconfig']));
+                                                       $virtual_services[$ip_parser->expandMacro ('VS_HEADER')] = generateVSSection ($triplet['rspool_id'], $ip_parser);
+                                               }
+                                       }
+                               }
+
+                               // group multiple virtual_services into vs_groups
+                               $groups = array();
+                               foreach ($virtual_services as $key => $content)
+                                       $groups[$content][] = $key;
+                               $gid = 1;
+                               foreach ($groups as $content => $keys)
+                               {
+                                       $ret .= $tr_parser->expand ("\n%VS_PREPEND%\n");
+                                       if (count ($keys) == 1)
+                                               $ret .= "virtual_server " . array_first ($keys) . " {\n" . $content . "}\n";
+                                       else
+                                       {
+                                               // come up with the name for new VS group
+                                               $vsg_name = makeUniqueVSGName ($seen_vs_groups, $keys, $vs_cell);
+                                               $seen_vs_groups[$vsg_name] = 1;
+                                               $tr_parser->addMacro ('VSG_NAME', $vsg_name);
+
+                                               $ret .= $tr_parser->expand ("virtual_server_group %VSG_NAME% {\n");
+                                               foreach ($keys as $vs_header)
+                                                       $ret .= "\t$vs_header\n";
+                                               $ret .= "}\n";
+                                               $ret .= $tr_parser->expand ("virtual_server group %VSG_NAME% {\n");
+                                               $ret .= $content . "}\n";
+                                       }
+                               }
+                       }
+               }
+       }
+       return $ret;
+}
+
+function makeUniqueVSGName ($seen_names, $keys, $vs_cell)
+{
+       $seen_ports = array();
+       $seen_marks = array();
+       sort ($keys);
+       foreach ($keys as $key)
+               if (preg_match('/^fwmark\s+(\d+)$/', $key, $m))
+                       $seen_marks[$m[1]] = $m[1];
+               elseif (preg_match('/^[0-9a-fA-F:.]+\s+(\d+)$/', $key, $m))
+                       $seen_ports[$m[1]] = $m[1];
+       $base_name = preg_replace('/\s+/', '_', $vs_cell['name']);
+
+       $vsg_name = NULL;
+       if (! isset ($vsg_name) && count ($seen_ports) == 1)
+       {
+               $cname = $base_name . '_' . array_first ($seen_ports);
+               if (! array_key_exists ($cname, $seen_names))
+                       $vsg_name = $cname;
+       }
+       if (! isset ($vsg_name) && count ($seen_marks))
+       {
+               $cname = $base_name . '_fwm' . implode('_fwm', $seen_marks);
+               if (! array_key_exists ($cname, $seen_names))
+                       $vsg_name = $cname;
+       }
+       if (! isset ($vsg_name))
+       {
+               $cname = $base_name;
+               if (count ($seen_ports))
+                       $cname .= '_' . implode('_', $seen_ports);
+               if (count ($seen_marks))
+                       $cname .= '_fwm' . implode('_fwm', $seen_marks);
+               if (! array_key_exists ($cname, $seen_names))
+                       $vsg_name = $cname;
+       }
+       if (! isset ($vsg_name))
+       {
+               $cname = $basename . '_' . substr (sha1 (serialize ($vsg['keys'])), 0, 6);
+               if (! array_key_exists ($cname, $seen_names))
+                       $vsg_name = $cname;
+       }
+       if (! isset ($vsg_name))
+               throw new RackTablesError ("Could not produce unique VS group name for ${vs_cell['name']}", RackTablesError::INTERNAL);
+       return $vsg_name;
+}
+
+// $vs_list is array of VS text configs, indexed by VS headers
+// function returns a list of groups. Services with equal configs are grouped together.
+// Each group has one or more VS headers in 'keys' subarray, and 'content' field.
+
+function groupVS ($vs_list)
+{
+       $ret = array();
+       $tmp = array();
+       foreach ($vs_list as $key => $content)
+               $tmp[$content][] = $key;
+       foreach ($tmp as $content => $keys)
+               $ret[] = array ('keys' => $keys, 'content' => $content);
+       return $ret;
+}
+
+// returns list item or FALSE
+function isPortEnabled ($port, $port_list)
+{
+       foreach ($port_list as $i_port)
+               if ($i_port['proto'] == $port['proto'] && $i_port['vport'] == $port['vport'])
+                       return $i_port;
+       return FALSE;
+}
+
+// returns list item or FALSE
+function isVIPEnabled ($vip, $vip_list)
+{
+       foreach ($vip_list as $i_vip)
+               if ($i_vip['vip'] === $vip['vip'])
+                       return $i_vip;
+       return FALSE;
+}
+
+// returns list of ipv4vs ids which have one of the IPs or fwmarks of group $group_id
+function getVSIDsByGroup ($group_id)
+{
+       $ret = array();
+       $vsinfo = spotEntity ('ipvs', $group_id);
+       amplifyCell ($vsinfo);
+       if (count ($vsinfo['vips']))
+       {
+               $ips = reduceSubarraysToColumn ($vsinfo['vips'], 'vip');
+               $qm = questionMarks (count ($ips));
+               $result = usePreparedSelectBlade ("SELECT id FROM IPv4VS WHERE vip IN ($qm) ORDER BY vip", $ips);
+               $ret = array_merge ($ret, $result->fetchAll (PDO::FETCH_COLUMN, 0));
+               unset ($result);
+       }
+
+       $bin_marks = array();
+       foreach ($vsinfo['ports'] as $port)
+               if ($port['proto'] == 'MARK')
+                       $bin_marks[] = pack ('N', $port['vport']);
+       if (count ($bin_marks))
+       {
+               $qm = questionMarks (count ($bin_marks));
+               $result = usePreparedSelectBlade ("SELECT id FROM IPv4VS WHERE proto = 'MARK' AND vip IN ($qm) ORDER BY vip", $bin_marks);
+               $ret = array_merge ($ret, $result->fetchAll (PDO::FETCH_COLUMN, 0));
+       }
+
+       return $ret;
+}
+
+function concatConfig (&$config, $line)
+{
+       if (strlen ($config))
+               $config .= "\n";
+       $config .= $line;
+}
+
+// splits $text to configuration directives (lines)
+// each macro declaration is one configuration directive
+// empty lines are skipped, trailing spaces are cut.
+function tokenizeConfig ($text)
+{
+       $ret = array();
+       $pos = 0;
+       $len = strlen ($text);
+       $state = 0;
+       while ($pos < $len || $state != 0)
+               switch ($state)
+               {
+                       case 0:
+                               if (preg_match ('/[?:]?=`?|\n/s', $text, $m, PREG_OFFSET_CAPTURE, $pos))
+                               {
+                                       if ($m[0][0] == "\n")
+                                       {
+                                               $ret[] = substr ($text, $pos, $m[0][1] - $pos);
+                                               $pos = $m[0][1] + 1; // skip \n
+                                       }
+                                       else
+                                       {
+                                               $macro_name = substr ($text, $pos, $m[0][1] - $pos);
+                                               if (preg_match('/^[a-zA-Z0-9_]+$/', $macro_name))
+                                               {
+                                                       $assignment_start = $pos;
+                                                       $pos = $m[0][1] + 1;
+                                                       $state = $m[0][0][strlen ($m[0][0]) - 1] == '`' ? 1 : 2;
+                                               }
+                                       }
+                               }
+                               else
+                               {
+                                       $ret[] = substr ($text, $pos);
+                                       break 2;
+                               }
+                               break;
+                       case 1:
+                               if (FALSE === ($i = strpos ($text, "'", $pos)))
+                                       break 2;
+                               else
+                               {
+                                       $ret[$macro_name] = substr ($text, $assignment_start, $i - $assignment_start + 1);
+                                       $pos = $i + 1;
+                                       $state = 0;
+                               }
+                               break;
+                       case 2:
+                               if (FALSE === ($i = strpos ($text, "\n", $pos)))
+                               {
+                                       $ret[$macro_name] = substr ($text, $assignment_start);
+                                       break 2;
+                               }
+                               else
+                               {
+                                       $ret[$macro_name] = substr ($text, $assignment_start, $i - $assignment_start);
+                                       $pos = $i + 1;
+                                       $state = 0;
+                               }
+                               break;
+               }
+
+       return array_diff (array_map ('rtrim', $ret), array (''));
+}
+
+// returns array with keys: 'properties' 'ports', 'vips', 'triplets'
+function buildVSMigratePlan ($new_vs_id, $vs_id_list)
+{
+       $ret = array
+       (
+               'properties' => array ('vsconfig' => '', 'rsconfig' => ''),
+               'ports' => array(),
+               'vips' => array(),
+               'triplets' => array(),
+               'messages' => array(), // keys are $old_tr_key
+       );
+       $config_stat = array('vsconfig' => array(), 'rsconfig' => array());
+       $gt = array(); // grouped triplets
+
+       foreach ($vs_id_list as $vs_id)
+       {
+               $vsinfo = spotEntity ('ipv4vs', $vs_id);
+
+               // create nesessary vips and ports
+               if ($vsinfo['proto'] != 'MARK')
+               {
+                       $vip_key = $vsinfo['vip_bin'];
+                       $port_key = $vsinfo['proto'] . '-' . $vsinfo['vport'];
+                       $ret['vips'][$vip_key] = array ('vip' => $vsinfo['vip_bin'], 'vsconfig' => '', 'rsconfig' => '');
+                       $ret['ports'][$port_key] = array ('proto' => $vsinfo['proto'], 'vport' => $vsinfo['vport'], 'vsconfig' => '', 'rsconfig' => '');
+               }
+               else
+               {
+                       $vip_key = '';
+                       $mark = implode('', unpack ('N', $vsinfo['vip_bin']));
+                       $port_key = $vsinfo['proto'] . '-' . $mark;
+                       $ret['ports'][$port_key] = array ('proto' => $vsinfo['proto'], 'vport' => $mark, 'vsconfig' => '', 'rsconfig' => '');
+               }
+
+               // fill triplets
+               foreach (SLBTriplet::getTriplets ($vsinfo) as $triplet)
+               {
+                       $tr_key = $triplet->lb['id'] . '-' . $triplet->rs['id'];
+                       if (! isset ($ret['triplets'][$tr_key]))
+                               $ret['triplets'][$tr_key] = array
+                               (
+                                       'object_id' => $triplet->lb['id'],
+                                       'rspool_id' => $triplet->rs['id'],
+                                       'vs_id' => $new_vs_id,
+                                       'vips' => array(),
+                                       'ports' => array(),
+                               );
+
+                       $configs = array
+                       (
+                               'vsconfig' => tokenizeConfig ($triplet->vs['vsconfig'] . "\n" . $triplet->slb['vsconfig']),
+                               'rsconfig' => tokenizeConfig ($triplet->vs['rsconfig'] . "\n" . $triplet->slb['rsconfig']),
+                       );
+
+                       if ($vsinfo['proto'] != 'MARK')
+                       {
+                               if (! isset ($ret['triplets'][$tr_key]['ports'][$port_key]))
+                                       $ret['triplets'][$tr_key]['ports'][$port_key] = array ('proto' => $vsinfo['proto'], 'vport' => $vsinfo['vport'], 'vsconfig' => '', 'rsconfig' => '');
+                               if (! isset ($ret['triplets'][$tr_key]['vips'][$vip_key]))
+                                       $ret['triplets'][$tr_key]['vips'][$vip_key] = array ('vip' => $vsinfo['vip_bin'], 'prio' => NULL, 'vsconfig' => '', 'rsconfig' => '');
+                               if ('' != $triplet->slb['prio'])
+                                       $ret['triplets'][$tr_key]['vips'][$vip_key]['prio'] = $triplet->slb['prio'];
+                       }
+                       else
+                               $ret['triplets'][$tr_key]['ports'][$port_key] = array ('proto' => $vsinfo['proto'], 'vport' => $mark, 'vsconfig' => '', 'rsconfig' => '');
+
+                       $old_tr_key = $tr_key . '-' . $vip_key . '-' . $port_key;
+                       $gt['all'][$old_tr_key] = $triplet;
+                       $gt['ports'][$port_key][$old_tr_key] = $triplet;
+                       $gt['vips'][$vip_key][$old_tr_key] = $triplet;
+                       $gt['vip_links'][$tr_key][$vip_key][$old_tr_key] = $triplet;
+                       $gt['port_links'][$tr_key][$port_key][$old_tr_key] = $triplet;
+
+                       foreach ($configs as $conf_type => $list)
+                               foreach ($list as $line)
+                                       $config_stat[$conf_type][$line][$old_tr_key] = $triplet;
+               }
+       }
+
+       // reduce common config lines and move them from $config_stat into $ret
+       foreach ($config_stat as $conf_type => $stat)
+               foreach ($stat as $line => $used_in_triplets)
+               {
+                       $added_to_triplets = array();
+                       $wrong_triplets = array();
+
+                       if (! array_sub ($gt['all'], $used_in_triplets))
+                       {
+                               // line used in all triplets
+                               concatConfig ($ret['properties'][$conf_type], $line);
+                               continue;
+                       }
+
+                       foreach ($gt['ports'] as $port_key => $port_triplets)
+                       {
+                               $diff = array_sub ($port_triplets, $used_in_triplets);
+                               if (count ($diff) < count ($port_triplets) / 2)
+                               {
+                                       // line used in most triplets of this port
+                                       $added_to_triplets += $port_triplets;
+                                       $wrong_triplets += $diff;
+                                       concatConfig ($ret['ports'][$port_key][$conf_type], $line);
+                               }
+                       }
+
+                       foreach ($gt['vips'] as $vip_key => $vip_triplets)
+                               if (! array_sub ($vip_triplets, $used_in_triplets))
+                                       if (count ($vip_triplets) == count (array_sub ($vip_triplets, $added_to_triplets)))
+                                       {
+                                               // if none of the $vip_triplets are in $added_to_triplets,
+                                               // line used in all triplets of this vip
+                                               $added_to_triplets += $vip_triplets;
+                                               concatConfig ($ret['vips'][$vip_key][$conf_type], $line);
+                                       }
+
+                       foreach ($used_in_triplets as $old_tr_key => $triplet)
+                       {
+                               if (isset ($added_to_triplets[$old_tr_key]))
+                                       continue;
+                               $tr_key = $triplet->lb['id'] . '-' . $triplet->rs['id'];
+                               if ($triplet->vs['proto'] != 'MARK')
+                               {
+                                       $vip_key = $triplet->vs['vip_bin'];
+                                       $port_key = $triplet->vs['proto'] . '-' . $triplet->vs['vport'];
+                               }
+                               else
+                               {
+                                       $vip_key = '';
+                                       $port_key = $triplet->vs['proto'] . '-' . implode ('', unpack ('N', $triplet->vs['vip_bin']));
+                               }
+                               // if all the triplets for a given port contain line, add it to the ports' config
+                               if (! array_sub ($gt['port_links'][$tr_key][$port_key], $used_in_triplets))
+                                       if (count ($gt['port_links'][$tr_key][$port_key]) == count (array_sub ($gt['port_links'][$tr_key][$port_key], $added_to_triplets)))
+                                       {
+                                               $added_to_triplets += $gt['port_links'][$tr_key][$port_key];
+                                               concatConfig ($ret['triplets'][$tr_key]['ports'][$port_key][$conf_type], $line);
+                                       }
+
+                               // if all the triplets for a given vip contain line, add it to the vips' config
+                               if ($vip_key != '')
+                                       if (! array_sub ($gt['vip_links'][$tr_key][$vip_key], $used_in_triplets))
+                                               if (count ($gt['vip_links'][$tr_key][$vip_key]) == count (array_sub ($gt['vip_links'][$tr_key][$vip_key], $added_to_triplets)))
+                                               {
+                                                       $added_to_triplets += $gt['vip_links'][$tr_key][$vip_key];
+                                                       concatConfig ($ret['triplets'][$tr_key]['vips'][$vip_key][$conf_type], $line);
+                                               }
+                       }
+
+                       // check for failed-to-insert lines
+                       foreach (array_sub ($used_in_triplets, $added_to_triplets) as $old_tr_key => $unused_triplet)
+                               $ret['messages'][$old_tr_key][] = "Failed to add $conf_type line '$line'";
+                       foreach ($wrong_triplets as $old_tr_key => $triplet)
+                               $ret['messages'][$old_tr_key][] = "Added $conf_type line '$line'";
+               } // for $line
+
+       return $ret;
+}
index 0805d9416fca2cf0d7e50071865b4a648e689d77..e266603e6de7507fc947161380b953cf9284a58c 100644 (file)
@@ -13,6 +13,8 @@ amount of other code (which should eventually be placed in a sort of
 
 */
 
+require_once 'slb-interface.php';
+
 define ('RE_STATIC_URI', '#^([[:alpha:]]+)/(?:[[:alpha:]]+/)*[[:alnum:]\._-]+\.([[:alpha:]]+)$#');
 
 function dispatchImageRequest()
index 612da5d3e66c812128b61b07f354b719e1d38841..3b0f40a154313241a2a409fd06b2be8fe261416a 100644 (file)
@@ -188,6 +188,22 @@ REVERSED_RACKS_LISTSRC and NEAREST_RACKS_CHECKBOX.
 [1] http://php.net/manual/en/function.strftime.php
 ENDOFTEXT
 ,
+
+       '0.20.5' => <<<ENDOFTEXT
+This release introduces the VS groups feature. VS groups is a new way to store
+and display virtual services configuration. New realm 'ipvs' (VS group) is created.
+All the existing VS configuration is kept and displayed as-is, but user is free to convert
+it to the new format, which displays it in more natural way and allows to generate
+virtual_server_group keepalived configs. To convert a virtual servise to the new format,
+you need to manually create the vs group object and assign IP addresses to it. Then, if you
+have the old-style VSes configured, the Migrate tab will be displayed on the particular VS group's
+page. After successfull migration, you can remove the old-style VS objects.
+
+Old-style VS configuration becomes DEPRECATED. Its support will be removed in one of the following
+major releases. So it is strongly recommended to convert it to the new format.
+ENDOFTEXT
+,
+
 );
 
 // At the moment we assume, that for any two releases we can
@@ -1682,6 +1698,73 @@ CREATE OR REPLACE VIEW `Rack` AS SELECT O.id, O.name AS name, O.asset_no, O.has_
                        $query[] = "UPDATE AttributeMap SET sticky = 'yes' WHERE objtype_id = 1787 AND attr_id = 30"; // Management interface -> Mgmt type
 
                        $query[] = "INSERT INTO `Config` (varname, varvalue, vartype, emptyok, is_hidden, is_userdefined, description) VALUES ('RDP_OBJS_LISTSRC','false','string','yes','no','yes','Rackcode filter for RDP-managed objects')";
+
+                       // SLB v2 tables
+                       $query[] = "
+CREATE TABLE `VS` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `name` char(255) DEFAULT NULL,
+  `vsconfig` text,
+  `rsconfig` text,
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB
+";
+                       $query[] = "
+CREATE TABLE `VSIPs` (
+  `vs_id` int(10) unsigned NOT NULL,
+  `vip` varbinary(16) NOT NULL,
+  `vsconfig` text,
+  `rsconfig` text,
+  PRIMARY KEY (`vs_id`,`vip`),
+  KEY `vip` (`vip`),
+  CONSTRAINT `VSIPs-vs_id` FOREIGN KEY (`vs_id`) REFERENCES `VS` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB
+";
+                       $query[] = "
+CREATE TABLE `VSPorts` (
+  `vs_id` int(10) unsigned NOT NULL,
+  `proto` enum('TCP','UDP','MARK') CHARACTER SET latin1 NOT NULL,
+  `vport` int(10) unsigned NOT NULL,
+  `vsconfig` text,
+  `rsconfig` text,
+  PRIMARY KEY (`vs_id`,`proto`,`vport`),
+  CONSTRAINT `VS-vs_id` FOREIGN KEY (`vs_id`) REFERENCES `VS` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB
+";
+                       $query[] = "
+CREATE TABLE `VSEnabledIPs` (
+  `object_id` int(10) unsigned NOT NULL,
+  `vs_id` int(10) unsigned NOT NULL,
+  `vip` varbinary(16) NOT NULL,
+  `rspool_id` int(10) unsigned NOT NULL,
+  `prio` varchar(255) DEFAULT NULL,
+  `vsconfig` text,
+  `rsconfig` text,
+  PRIMARY KEY (`object_id`,`vs_id`,`vip`),
+  KEY `vip` (`vip`),
+  KEY `VSEnabledIPs-FK-vs_id-vip` (`vs_id`,`vip`),
+  KEY `VSEnabledIPs-FK-rspool_id` (`rspool_id`),
+  CONSTRAINT `VSEnabledIPs-FK-rspool_id` FOREIGN KEY (`rspool_id`) REFERENCES `IPv4RSPool` (`id`) ON DELETE CASCADE,
+  CONSTRAINT `VSEnabledIPs-FK-vs_id-vip` FOREIGN KEY (`vs_id`, `vip`) REFERENCES `VSIPs` (`vs_id`, `vip`) ON DELETE CASCADE
+) ENGINE=InnoDB
+";
+                       $query[] = "
+CREATE TABLE `VSEnabledPorts` (
+  `object_id` int(10) unsigned NOT NULL,
+  `vs_id` int(10) unsigned NOT NULL,
+  `proto` enum('TCP','UDP','MARK') CHARACTER SET latin1 NOT NULL,
+  `vport` int(10) unsigned NOT NULL,
+  `rspool_id` int(10) unsigned NOT NULL,
+  `vsconfig` text,
+  `rsconfig` text,
+  PRIMARY KEY (`object_id`,`vs_id`,`proto`,`vport`),
+  KEY `VSEnabledPorts-FK-vs_id-proto-vport` (`vs_id`,`proto`,`vport`),
+  KEY `VSEnabledPorts-FK-rspool_id` (`rspool_id`),
+  CONSTRAINT `VSEnabledPorts-FK-object_id` FOREIGN KEY (`object_id`) REFERENCES `Object` (`id`) ON DELETE CASCADE,
+  CONSTRAINT `VSEnabledPorts-FK-rspool_id` FOREIGN KEY (`rspool_id`) REFERENCES `IPv4RSPool` (`id`) ON DELETE CASCADE,
+  CONSTRAINT `VSEnabledPorts-FK-vs_id-proto-vport` FOREIGN KEY (`vs_id`, `proto`, `vport`) REFERENCES `VSPorts` (`vs_id`, `proto`, `vport`) ON DELETE CASCADE
+) ENGINE=InnoDB
+";
                        $query[] = "UPDATE Config SET varvalue = '0.20.5' WHERE varname = 'DB_VERSION'";
                        break;
                case 'dictionary':
index fb0760a2549bd059f2c595fde5c2184458e54a6a..699669df10bd3c0b4fef7724606599f5739d243e 100644 (file)
                        }
                        if (popup.data("sticked"))
                                return;
+                       var left;
                        if (windowSize.width + windowSize.scrollLeft < event.pageX + popupSize.width + settings.cursorLeftOffset){
-                               $(popup).css("left", event.pageX - popupSize.width - settings.cursorLeftOffset);
+                               left = event.pageX - popupSize.width - settings.cursorLeftOffset;
                        } else {
-                               $(popup).css("left", event.pageX + settings.cursorLeftOffset);
+                               left = event.pageX + settings.cursorLeftOffset;
                        }
+                       // center pop-up if it does not fit entirely into window
+                       if (
+                               left < windowSize.scrollLeft ||
+                               left + popupSize.width > windowSize.scrollLeft + windowSize.width
+                       ) {
+                               left = (windowSize.width - popupSize.width) / 2;
+                       }
+                       $(popup).css("left", left);
+
                        if (bPopupShownAbove) {
                                $(popup).css("top", event.pageY - popupSize.height - settings.cursorTopOffset);
                        } else {
diff --git a/wwwroot/js/slb_editor.js b/wwwroot/js/slb_editor.js
new file mode 100644 (file)
index 0000000..6ddca17
--- /dev/null
@@ -0,0 +1,42 @@
+$(document).ready (function () {
+       // popup form for port/ip config update
+       $('.slb-checks.editable li').click (function (e) {
+               if (e.target.tagName != 'LI')
+                       return true;
+               var form = $('<div>').addClass ('popup-box').appendTo ('body').append ($(this).find('form').clone());
+               // confirmation box on port/vip deletion
+               $('a.del-used-slb').click (function (e) {
+                       return confirm ('This entity is used. Please confirm the deletion');
+               });
+
+               $(this).thumbPopup (form, { event: e });
+               return false;
+       });
+
+       // confirmation box on triplet deletion
+       $('form#del input').click (function (e) {
+               return confirm ('Please confirm the deletion of triplet');
+       });
+
+       // new triplet form stage1 handler
+       var forms = $('form#addLink');
+       forms.submit (function() { return false });
+       forms.find('input[name="submit"]').click (function (e) {
+               var form = $(this).parents('form');
+               $.ajax ({
+                       type: 'POST',
+                       url: 'index.php',
+                       data: {
+                               'module': 'ajax',
+                               'ac': 'get-slb-form',
+                               'form': form.serialize(),
+                               'action': form.attr('action')
+                       },
+                       'success': function (data) {
+                               form.replaceWith (data);
+                       }
+               });
+
+               return false;
+       });
+});
diff --git a/wwwroot/pix/checked.png b/wwwroot/pix/checked.png
new file mode 100644 (file)
index 0000000..11d3c52
Binary files /dev/null and b/wwwroot/pix/checked.png differ
diff --git a/wwwroot/pix/unchecked.png b/wwwroot/pix/unchecked.png
new file mode 100644 (file)
index 0000000..5ed5db6
Binary files /dev/null and b/wwwroot/pix/unchecked.png differ