move Live VLANs code to contribs repository
authorDenis Ovsienko <infrastation@yandex.ru>
Wed, 17 Oct 2012 09:17:14 +0000 (13:17 +0400)
committerDenis Ovsienko <infrastation@yandex.ru>
Wed, 17 Oct 2012 09:17:14 +0000 (13:17 +0400)
LiveVLANs/gateways/switchvlans/cisco.connector [new file with mode: 0755]
LiveVLANs/gateways/switchvlans/cisco.secrets.php-sample [new file with mode: 0644]
LiveVLANs/gateways/switchvlans/main [new file with mode: 0755]
LiveVLANs/gateways/switchvlans/vlandecoder-sample [new file with mode: 0755]
LiveVLANs/gateways/switchvlans/vlantable-sample [new file with mode: 0644]
LiveVLANs/livevlans.php [new file with mode: 0644]

diff --git a/LiveVLANs/gateways/switchvlans/cisco.connector b/LiveVLANs/gateways/switchvlans/cisco.connector
new file mode 100755 (executable)
index 0000000..5e7d4a6
--- /dev/null
@@ -0,0 +1,211 @@
+#!/bin/sh
+
+# 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.
+
+[ $# = 7 ] || exit 1
+
+ENDPOINT=$1
+HW=$2
+SW=$3
+COMMAND=$4
+FILE1=$5
+FILE2=$6
+FILE3=$7
+MYDIR=`dirname $0`
+ostype=`uname -s`
+case "$ostype" in
+       Linux)
+       SEDFLAG='-r'
+       ;;
+       FreeBSD)
+       SEDFLAG='-E'
+       ;;
+       *)
+       exit 7
+esac
+
+
+prepare_connect_commands()
+{
+       [ $# = 1 ] || exit 2
+       local skip=yes cval found=no
+       while read line; do
+               if [ "$skip" = "yes" -a "$line" = "# S-T-A-R-T" ]; then
+                       skip=no
+                       continue
+               fi
+               if [ "$skip" = "no" -a "$line" = "# S-T-O-P" ]; then
+                       skip=yes
+                       continue
+               fi
+               [ "$skip" = "yes" ] && continue
+               # Allow comments.
+               [ -z "${line###*}" ] && continue
+
+               # First endpoint string/regexp match is sufficient for us.
+               cval=`echo $line | cut -s -d' ' -f1`
+               if [ -z "${1##$cval}" ]; then
+                       found=yes
+                       # Don't be too smart at the moment, just be able to handle
+                       # the known-good case ;-)
+
+                       username=`echo $line | cut -s -d' ' -f5`
+                       [ "$username" != "-" ] && echo $username > $CMDS1
+                       # access password
+                       echo $line | cut -s -d' ' -f6 >> $CMDS1
+                       enable_password=`echo $line | cut -s -d' ' -f7`
+                       [ "$enable_password" != "-" ] && {
+                               echo en >> $CMDS1
+                               echo $enable_password >> $CMDS1
+                       }
+                       # same for ports
+                       cat "$CMDS1" > "$CMDS2"
+                       # ...and MAC addresses
+                       cat "$CMDS1" > "$CMDS3"
+                       break
+               fi
+       done < "$MYDIR/cisco.secrets.php"
+       [ "$found" = "yes" ] && return
+       echo "E!connector could not find credentials for $1" >> "$FILE2"
+       exit 3
+}
+
+prepare_fetch_commands()
+{
+       printf 'term len 0\nshow vlan brief\nquit\n' >> $CMDS1
+       printf 'term len 0\nshow int status\nquit\n' >> $CMDS2
+       printf 'term len 0\nshow mac-address-table\nquit\n' >> $CMDS3
+}
+
+prepare_push_commands()
+{
+       printf 'term len 0\nconf t\n' >> $CMDS1
+       while read portname vlanid; do
+               if [ -z "$portname" -o -z "$vlanid" ]; then
+                       echo "E!could not parse input in connector" >> "$FILE2"
+                       continue
+               fi
+               if [ "$vlanid" = "trunk" ]; then
+                       echo "E!trunking is not allowed" >> "$FILE2"
+                       continue
+               fi
+               printf "int $portname\n" >> $CMDS1
+               if [ $vlanid -lt 4096 ]; then
+                       printf "no description\n" >> $CMDS1
+               else
+                       printf "descr VLAN$vlanid\n" >> $CMDS1
+               fi
+               "$MYDIR/vlandecoder" $vlanid >> $CMDS1
+               printf "exit\n" >> $CMDS1
+               echo "I!Port $portname@$ENDPOINT has been assigned to VLAN $vlanid" >> "$FILE2"
+       done < "$FILE1"
+       printf "end\nwri\nquit\n" >> $CMDS1
+}
+
+do_fetch()
+{
+       local tmp_ifname tmp_ifdescr tmp_status tmp_vlanid
+       nc $ENDPOINT 23 < $CMDS1 > "$OUT1"
+       if fgrep -q '% Bad passwords' "$OUT1"; then
+               echo "E!password mismatch while trying to connect to $ENDPOINT" >> "$FILE2"
+               exit 4
+       fi
+       nc $ENDPOINT 23 < $CMDS2 > "$OUT2a"
+       nc $ENDPOINT 23 < $CMDS3 > "$OUT3"
+       cat "$OUT1" | fgrep ' active    ' | sed $SEDFLAG 's/^([[:digit:]]+)[[:space:]]+(.+)[[:space:]]+active    (.*)/\1=\2/;s/[[:space:]]+$//' > $FILE1
+       # Add trunk data, if appropriate.
+       [ -s "$MYDIR/vlantable" ] && cat "$MYDIR/vlantable" >> $FILE1
+
+       # First extract structured info about VLAN membership, then map
+       # special descriptions into VLAN IDs.
+       cat "$OUT2a" | egrep '^(Et|Fa|Gi|Te)' | sed $SEDFLAG 's/[~%]/__RTTMP_percent_sign__/g;s/^([A-Za-z/0-9]+) +(.*) +(connected|notconnect|disabled|err-disabled|monitoring|suspended) +/\1~\2%\3%/;s/%(trunk|routed|([0-9]+)) .*$/%\1/;s/%(monitoring|suspended)%/%connected%/;s/%(err-disabled)%/%disabled%/;s/ +%/%/;s/~/%/' > $OUT2b
+       while read line; do
+               tmp_ifname=`echo $line | cut -d% -f1`
+               tmp_ifdescr=`echo $line | cut -d% -f2`
+               tmp_status=`echo $line | cut -d% -f3`
+               tmp_vlanid=`echo $line | cut -d% -f4`
+               # If the port has a description pretending to be a martian VLAN, map it onto the VLAN ID.
+               if [ -n "$tmp_ifdescr" -a -z "${tmp_ifdescr##VLAN*}" ]; then
+                       tmp_vlanid=${tmp_ifdescr##VLAN}
+               fi
+               echo "$tmp_ifname=$tmp_status,$tmp_vlanid" >> $FILE2
+       done < $OUT2b
+       # FIXME
+       # Here we need to distinguish between different platforms and IOS version,
+       # cause they produce output in different formats.
+       if [ "$SW" = "Cisco+IOS+12.0" ]; then
+               cat "$OUT3" | LC_ALL=C tr -d '\r' | fgrep Dynamic | sed $SEDFLAG 's/ +Dynamic +([0-9]+) +(.+)/=\1@\2/;s/FastEthernet/Fa/;s/GigabitEthernet/Gi/' > "$FILE3"
+       elif [ "$SW" = "Cisco+IOS+12.2" -o "$SW" = "Cisco+IOS+12.1" ]; then
+               case "$HW" in
+                       Cisco+Catalyst+35*|Cisco+Catalyst+37*|Cisco+Catalyst+29*)
+                               cat "$OUT3" | LC_ALL=C tr -d '\r' | egrep 'STATIC|DYNAMIC' | \
+                               sed $SEDFLAG 's/ +([0-9]+|All) +(.+)    (DYNAMIC|STATIC) +(.+)/\2=\1@\4/;s/FastEthernet/Fa/;s/GigabitEthernet/Gi/' > "$FILE3"
+                       ;;
+                       Cisco+Catalyst+49*)
+                               cat "$OUT3" | LC_ALL=C tr -d '\r' | fgrep dynamic | \
+                               sed $SEDFLAG 's/ +([0-9]+) +([0-9a-f\.]+)   dynamic ip +([a-zA-Z/0-9]+) */\2=\1@\3/;s/FastEthernet/Fa/;s/GigabitEthernet/Gi/;s/TenGi/Te/' > "$FILE3"
+                       ;;
+               esac
+       fi
+}
+
+do_push()
+{
+       nc $ENDPOINT 23 < $CMDS1 >/dev/null
+}
+
+remove_tempfiles()
+{
+       [ -f "$CMDS1" ] && rm -f "$CMDS1"
+       [ -f "$CMDS2" ] && rm -f "$CMDS2"
+       [ -f "$CMDS3" ] && rm -f "$CMDS3"
+       [ -f "$OUT1" ] && rm -f "$OUT1"
+       [ -f "$OUT2a" ] && rm -f "$OUT2a"
+       [ -f "$OUT2b" ] && rm -f "$OUT2b"
+       [ -f "$OUT3" ] && rm -f "$OUT3"
+}
+
+create_tempfiles()
+{
+       # This one is for VLAN list.
+       CMDS1=`mktemp /tmp/cisco.connector.XXXXXX`
+       # And this one holds ports list...
+       CMDS2=`mktemp /tmp/cisco.connector.XXXXXX`
+       # ...and one more for MAC address table
+       CMDS3=`mktemp /tmp/cisco.connector.XXXXXX`
+       # The following are buffers to hold the whole switch output
+       # before filtering.
+       OUT1=`mktemp /tmp/cisco.connector.XXXXXX`
+       OUT2a=`mktemp /tmp/cisco.connector.XXXXXX`
+       OUT2b=`mktemp /tmp/cisco.connector.XXXXXX`
+       OUT3=`mktemp /tmp/cisco.connector.XXXXXX`
+       [ -f "$CMDS1" -a -f "$CMDS2" -a -f "$CMDS3" -a -f "$OUT1" -a -f "$OUT2a" -a -f "$OUT2b" -a -f "$OUT3" ] && return
+       echo "E!connector cannot create tempfiles" >> "$FILE2"
+       remove_tempfiles
+       exit 5
+}
+
+case $COMMAND in
+       fetch)
+               create_tempfiles
+               prepare_connect_commands $ENDPOINT
+               prepare_fetch_commands
+               do_fetch
+               remove_tempfiles
+       ;;
+       push)
+               create_tempfiles
+               prepare_connect_commands $ENDPOINT
+               prepare_push_commands
+               do_push
+               remove_tempfiles
+       ;;
+       *)
+               echo "E!unknown command for connector" >> "$FILE2"
+               exit 6
+       ;;
+esac
+
+exit 0
diff --git a/LiveVLANs/gateways/switchvlans/cisco.secrets.php-sample b/LiveVLANs/gateways/switchvlans/cisco.secrets.php-sample
new file mode 100644 (file)
index 0000000..e716f54
--- /dev/null
@@ -0,0 +1,13 @@
+<?php ob_start(); ?>
+
+# Syntax:
+# <endpoint|*> <telnet> <hostname|-> <port|-> <username|-> <line password> <enable password>
+# FIXME: <endpoint|*> <rsh> <username>
+
+# S-T-A-R-T
+switch1 telnet - - - password2 enablepassword
+switch2 telnet - - - password enablepassword
+switch3 telnet - - username3 password3 enablepassword3
+# S-T-O-P
+
+<?php ob_end_clean(); ?>
diff --git a/LiveVLANs/gateways/switchvlans/main b/LiveVLANs/gateways/switchvlans/main
new file mode 100755 (executable)
index 0000000..8149443
--- /dev/null
@@ -0,0 +1,242 @@
+#!/bin/sh
+
+# 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.
+#
+# This is a RackTables gateway for changing switch ports membership
+# across VLANs. It works accordingly to the gateway protocol described
+# in gateways.php and accepts the following commands on its stdin:
+#
+# * connect: connect to a switch, fetch all necessary data, store and
+#   disconnect
+#
+# * listvlans: list all VLANs found on the switch, propably filtering
+# out those administratively prohibited. Only the VLANs from this
+# list will be allowed as new destination for 'set' command.
+#
+# * listports: list all ports on the switch and their current status.
+#   Untagged (switchport mode access) ports will be shown with their
+#   VLAN ID and tagged ports will be shown as 'trunk' regardless of
+#   how many VLANs they are members of.
+#
+# * listmacs: output unsorted list of all dynamically learned MAC
+#   addresses present on the switch
+
+endpoint=
+hw=
+sw=
+user=
+handler=
+CONNECTED=0
+MYDIR=`dirname $0`
+
+decode_error()
+{
+       case "$1" in
+               0)
+                       echo -n 'success'
+               ;;
+               1)
+                       echo -n 'internal error 1'
+               ;;
+               2)
+                       echo -n 'internal error 2'
+               ;;
+               3)
+                       echo -n 'password not found'
+               ;;
+               4)
+                       echo -n 'invalid password'
+               ;;
+               5)
+                       echo -n 'cannot create temporary files'
+               ;;
+               6)
+                       echo -n 'invalid command'
+               ;;
+               7)
+                       echo -n 'unknown host OS'
+               ;;
+               *)
+                       echo -n 'unknown error'
+               ;;
+       esac
+}
+
+# Not only connect, but gather all the data at once and remember the context.
+do_connect()
+{
+       endpoint=`echo $args | cut -s -d' ' -f1`
+       hw=`echo $args | cut -s -d' ' -f2`
+       sw=`echo $args | cut -s -d' ' -f3`
+       user=`echo $args | cut -s -d' ' -f4`
+       # sanity checks
+       if [ -z "$endpoint" -o -z "$hw" -o -z "$sw" -o -z "$user" ]; then
+               echo 'ERR!too few arguments to connect'
+               return
+       fi
+       case "$sw" in
+               Cisco+IOS+12.0|Cisco+IOS+12.1|Cisco+IOS+12.2)
+                       handler=cisco
+               ;;
+               *)
+                       echo "ERR!Don't know how to handle $sw on $endpoint"
+                       return
+               ;;
+       esac
+
+       # prepare temp files
+       PORTINFO=`mktemp /tmp/racktables.XXXXXX`
+       if ! [ -f "$PORTINFO" ]; then
+               echo 'ERR!could not create portinfo tmpfile'
+               return
+       fi
+       VLANINFO=`mktemp /tmp/racktables.XXXXXX`
+       if ! [ -f "$VLANINFO" ]; then
+               echo 'ERR!could not create vlaninfo tmpfile'
+               rm -f "$PORTINFO"
+               return
+       fi
+       MACINFO=`mktemp /tmp/racktables.XXXXXX`
+       if ! [ -f "$MACINFO" ]; then
+               echo 'ERR!could not create MACinfo tmpfile'
+               rm -f "$PORTINFO" "$VLANINFO"
+               return
+       fi
+
+       # get the data
+       "$MYDIR/$handler.connector" $endpoint $hw $sw fetch "$VLANINFO" "$PORTINFO" "$MACINFO"
+       ret=$?
+       if [ $ret = 0 ]; then
+               CONNECTED=1
+               echo "OK!connected to $endpoint";
+       else
+               echo -n "ERR!Cannot connect to $endpoint ("
+               decode_error $ret
+               echo ')'
+       fi
+}
+
+do_listfile()
+{
+       local F=$1
+       if ! [ -f "$F" ]; then
+               echo "ERR!Lost temp file '$F' on the way"
+               return
+       fi
+       echo -n 'OK!'
+       local semicolon=''
+       # tr might do the work, but use our chance to perform filtering once more
+       cat "$F" | while read line; do
+               [ "$line" = "" ] && continue
+               echo -n "$semicolon$line"
+               semicolon=';'
+       done
+       echo
+}
+
+do_set()
+{
+       # sanity checks
+       local setline=$1
+       if [ -z "$setline" ]; then
+               echo 'ERR!missing set argument'
+               return
+       fi
+       local REQUESTS=`mktemp /tmp/racktables.XXXXXX`
+       local REPLIES=`mktemp /tmp/racktables.XXXXXX`
+       echo $1 | tr ';' '\n' | while read setexpr; do
+               portname=`echo $setexpr | cut -s -d'=' -f1`
+               newvlan=`echo $setexpr | cut -s -d'=' -f2`
+               curvlan=`egrep "^$portname=" $PORTINFO | cut -s -d'=' -f2 | cut -d',' -f2`
+               if [ -z "$curvlan" ]; then
+                       echo "E!Could not find port $portname" >> "$REPLIES"
+                       continue
+               fi
+               if [ "$curvlan" = "trunk" ]; then
+                       echo "E!Port $portname is a trunk" >> "$REPLIES"
+                       continue
+               fi
+               [ "$curvlan" = "$newvlan" ] && continue
+               echo "$portname $newvlan" >> "$REQUESTS"
+               cmembers=`grep -c ",$newvlan$" "$PORTINFO"`
+               if [ "$cmembers" = "0" -a $newvlan -lt 4096 ]; then
+                       echo "W!Port $portname seems to be the first in VLAN $newvlan at this switch" >> "$REPLIES"
+                       echo "W!Check uplink/downlink configuration for proper operation" >> "$REPLIES"
+               fi
+       done
+       nr=`egrep -c '^E!.' "$REPLIES"`
+       if [ "$nr" -ge 1 ]; then
+               echo "W!$nr change request(s) have been ignored" >> "$REPLIES"
+       fi
+
+       nq=`egrep -c '^.' "$REQUESTS"`
+       if [ "$nq" -ge 1 ]; then
+               # Go!
+               "$MYDIR/$handler.connector" $endpoint $hw $sw push "$REQUESTS" "$REPLIES" "$MACINFO"
+               local ret=$?
+
+               if [ $ret != 0 ]; then
+                       echo "ERR!Failed to configure $endpoint, connector returned code $ret"
+                       return
+               fi
+               echo "I!$nq change request(s) have been processed" >> "$REPLIES"
+       fi
+       echo -n 'OK!'
+       local SEMICOLON=
+       while read reply; do
+               echo -n $SEMICOLON$reply
+               SEMICOLON=';'
+               timestamp=`date '+%Y-%m-%d %H:%M:%S'`
+               [ -w "$MYDIR/changes.log" ] && echo "$timestamp $user@$endpoint $reply" >> "$MYDIR/changes.log"
+       done < "$REPLIES"
+       echo
+       rm -f "$REQUESTS" "$REPLIES"
+}
+
+# main loop
+while read cmd args; do
+       case $cmd in
+               connect)
+                       if [ $CONNECTED = 1 ]; then
+                               echo 'ERR!Already connected'
+                       else
+                               do_connect $args
+                       fi
+                       ;;
+               listvlans)
+                       if [ $CONNECTED = 1 ]; then
+                               do_listfile "$VLANINFO"
+                       else
+                               echo 'ERR!Not connected'
+                       fi
+                       ;;
+               listports)
+                       if [ $CONNECTED = 1 ]; then
+                               do_listfile "$PORTINFO"
+                       else
+                               echo 'ERR!Not connected'
+                       fi
+                       ;;
+               listmacs)
+                       if [ $CONNECTED = 1 ]; then
+                               do_listfile "$MACINFO"
+                       else
+                               echo 'ERR!Not connected'
+                       fi
+                       ;;
+               set)
+                       if [ $CONNECTED = 1 ]; then
+                               do_set $args
+                       else
+                               echo 'ERR!Not connected'
+                       fi
+                       ;;
+               *)
+                       echo "ERR!unknown command $cmd"
+       esac
+done
+
+rm -f "$PORTINFO" "$VLANINFO" "$MACINFO"
+exit 0
diff --git a/LiveVLANs/gateways/switchvlans/vlandecoder-sample b/LiveVLANs/gateways/switchvlans/vlandecoder-sample
new file mode 100755 (executable)
index 0000000..e1cdf39
--- /dev/null
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+[ $# = 1 ] || exit 1
+
+case $1 in
+       6001)
+               NATIVE=100
+               ;;
+       6002)
+               NATIVE=200
+               ;;
+       *)
+               exit 2
+               ;;
+esac
+VOICE=300
+
+echo switchport trunk native vlan $NATIVE
+echo switchport trunk allowed vlan $NATIVE,$VOICE
+echo switchport voice vlan $VOICE
+echo spanning-tree portfast trunk
diff --git a/LiveVLANs/gateways/switchvlans/vlantable-sample b/LiveVLANs/gateways/switchvlans/vlantable-sample
new file mode 100644 (file)
index 0000000..7ece953
--- /dev/null
@@ -0,0 +1,2 @@
+6001=tech-and-phone
+6002=mgmt-and-phone
diff --git a/LiveVLANs/livevlans.php b/LiveVLANs/livevlans.php
new file mode 100644 (file)
index 0000000..e4d6679
--- /dev/null
@@ -0,0 +1,431 @@
+<?php
+
+/*
+The "Live VLANs" RackTables feature was introduced in release 0.14.6 and
+removed in release 0.20.2. The "802.1Q" RackTables feature, which was
+introduced in release 0.18.0, fits 99% of VLAN management cases and
+should be used instead.
+*/
+
+$tab['object']['livevlans'] = 'Live VLANs';
+$tabhandler['object']['livevlans'] = 'renderVLANMembership';
+$trigger['object']['livevlans'] = 'trigger_livevlans';
+$ophandler['object']['livevlans']['setPortVLAN'] = 'setPortVLAN';
+$delayauth['object-livevlans-setPortVLAN'] = TRUE;
+
+// This trigger filters out everything except switches with known-good
+// software.
+function trigger_livevlans ()
+{
+       return checkTypeAndAttribute
+       (
+               getBypassValue(),
+               8, // network switch
+               4, // SW type
+               // Cisco IOS 12.0
+               // Cisco IOS 12.1
+               // Cisco IOS 12.2
+               array (244, 251, 252)
+       );
+}
+
+// This function launches specified gateway with specified
+// command-line arguments and feeds it with the commands stored
+// in the second arg as array.
+// The answers are stored in another array, which is returned
+// by this function. In the case when a gateway cannot be found,
+// finishes prematurely or exits with non-zero return code,
+// a single-item array is returned with the only "ERR" record,
+// which explains the reason.
+function queryGateway ($gwname, $questions)
+{
+       global $racktables_gwdir;
+       $execpath = "${racktables_gwdir}/{$gwname}/main";
+       $dspec = array
+       (
+               0 => array ("pipe", "r"),
+               1 => array ("pipe", "w"),
+               2 => array ("file", "/dev/null", "a")
+       );
+       $pipes = array();
+       $gateway = proc_open ($execpath, $dspec, $pipes);
+       if (!is_resource ($gateway))
+               return array ('ERR proc_open() failed in ' . __FUNCTION__);
+
+// Dialogue starts. Send all questions.
+       foreach ($questions as $q)
+               fwrite ($pipes[0], "$q\n");
+       fclose ($pipes[0]);
+
+// Fetch replies.
+       $answers = array ();
+       while (!feof($pipes[1]))
+       {
+               $a = fgets ($pipes[1]);
+               if (!strlen ($a))
+                       continue;
+               // Somehow I got a space appended at the end. Kick it.
+               $answers[] = trim ($a);
+       }
+       fclose($pipes[1]);
+
+       $retval = proc_close ($gateway);
+       if ($retval != 0)
+               throw new RTGatewayError ("gateway failed with code ${retval}");
+       if (!count ($answers))
+               throw new RTGatewayError ('no response from gateway');
+       if (count ($answers) != count ($questions))
+               throw new RTGatewayError ('protocol violation');
+       foreach ($answers as $a)
+               if (strpos ($a, 'OK!') !== 0)
+                       throw new RTGatewayError ("subcommand failed with status: ${a}");
+       return $answers;
+}
+
+// This functions returns an array for VLAN list, and an array for port list (both
+// form another array themselves) and another one with MAC address list.
+// The ports in the latter array are marked with either VLAN ID or 'trunk'.
+// We don't sort the port list, as the gateway is believed to have done this already
+// (or at least the underlying switch software ought to). This is important, as the
+// port info is transferred to/from form not by names, but by numbers.
+function getSwitchVLANs ($object_id = 0)
+{
+       global $remote_username;
+       $objectInfo = spotEntity ('object', $object_id);
+       $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
+       if (count ($endpoints) == 0)
+               throw new RTGatewayError ('no management address set');
+       if (count ($endpoints) > 1)
+               throw new RTGatewayError ('cannot pick management address');
+       $hwtype = $swtype = 'unknown';
+       foreach (getAttrValues ($object_id) as $record)
+       {
+               if ($record['name'] == 'SW type' && strlen ($record['o_value']))
+                       $swtype = str_replace (' ', '+', execGMarker ($record['o_value']));
+               if ($record['name'] == 'HW type' && strlen ($record['o_value']))
+                       $hwtype = str_replace (' ', '+', execGMarker ($record['o_value']));
+       }
+       $endpoint = str_replace (' ', '+', $endpoints[0]);
+       $commands = array
+       (
+               "connect ${endpoint} ${hwtype} ${swtype} ${remote_username}",
+               'listvlans',
+               'listports',
+               'listmacs'
+       );
+       $data = queryGateway ('switchvlans', $commands);
+       if (strpos ($data[0], 'OK!') !== 0)
+               throw new RTGatewayError ("gateway failed with status: ${data[0]}.");
+       // Now we have VLAN list in $data[1] and port list in $data[2]. Let's sort this out.
+       $tmp = array_unique (explode (';', substr ($data[1], strlen ('OK!'))));
+       if (count ($tmp) == 0)
+               throw new RTGatewayError ('gateway returned no records');
+       $vlanlist = array();
+       foreach ($tmp as $record)
+       {
+               list ($vlanid, $vlandescr) = explode ('=', $record);
+               $vlanlist[$vlanid] = $vlandescr;
+       }
+       $portlist = array();
+       foreach (explode (';', substr ($data[2], strlen ('OK!'))) as $pair)
+       {
+               list ($portname, $pair2) = explode ('=', $pair);
+               list ($status, $vlanid) = explode (',', $pair2);
+               $portlist[] = array ('portname' => $portname, 'status' => $status, 'vlanid' => $vlanid);
+       }
+       if (count ($portlist) == 0)
+               throw new RTGatewayError ('gateway returned no records');
+       $maclist = array();
+       foreach (explode (';', substr ($data[3], strlen ('OK!'))) as $pair)
+               if (preg_match ('/^([^=]+)=(.+)/', $pair, $m))
+               {
+                       $macaddr = $m[1];
+                       list ($vlanid, $ifname) = explode ('@', $m[2]);
+                       $maclist[$ifname][$vlanid][] = $macaddr;
+               }
+       return array ($vlanlist, $portlist, $maclist);
+}
+
+function setSwitchVLANs ($object_id = 0, $setcmd)
+{
+       global $remote_username;
+       $objectInfo = spotEntity ('object', $object_id);
+       $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
+       if (count ($endpoints) == 0)
+               throw new RTGatewayError ('no management address set');
+       if (count ($endpoints) > 1)
+               throw new RTGatewayError ('cannot pick management address');
+       $hwtype = $swtype = 'unknown';
+       foreach (getAttrValues ($object_id) as $record)
+       {
+               if ($record['name'] == 'SW type' && strlen ($record['o_value']))
+                       $swtype = strtr (execGMarker ($record['o_value']), ' ', '+');
+               if ($record['name'] == 'HW type' && strlen ($record['o_value']))
+                       $hwtype = strtr (execGMarker ($record['o_value']), ' ', '+');
+       }
+       $endpoint = str_replace (' ', '+', $endpoints[0]);
+       $data = queryGateway
+       (
+               'switchvlans',
+               array ("connect ${endpoint} ${hwtype} ${swtype} ${remote_username}", $setcmd)
+       );
+       // Finally we can parse the response into message array.
+       foreach (explode (';', substr ($data[1], strlen ('OK!'))) as $text)
+       {
+               $message = 'gw: ' . substr ($text, 2);
+               if (strpos ($text, 'I!') === 0)
+                       showSuccess ($message); // generic gateway success
+               elseif (strpos ($text, 'W!') === 0)
+                       showWarning ($message); // generic gateway warning
+               elseif (strpos ($text, 'E!') === 0)
+                       showError ($message); // generic gateway error
+               else // All improperly formatted messages must be treated as error conditions.
+                       showError ('unexpected line from gw: ' . $text);
+       }
+}
+
+$msgcode['setPortVLAN']['ERR'] = 164;
+// This handler's context is pre-built, but not authorized. It is assumed, that the
+// handler will take existing context and before each commit check authorization
+// on the base chain plus necessary context added.
+function setPortVLAN ()
+{
+       assertUIntArg ('portcount');
+       try
+       {
+               $data = getSwitchVLANs ($_REQUEST['object_id']);
+       }
+       catch (RTGatewayError $re)
+       {
+               return showFuncMessage (__FUNCTION__, 'ERR', array ($re->getMessage()));
+       }
+       list ($vlanlist, $portlist) = $data;
+       // Here we just build up 1 set command for the gateway with all of the ports
+       // included. The gateway is expected to filter unnecessary changes silently
+       // and to provide a list of responses with either error or success message
+       // for each of the rest.
+       $nports = $_REQUEST['portcount'];
+       $prefix = 'set ';
+       $setcmd = '';
+       for ($i = 0; $i < $nports; $i++)
+       {
+               genericAssertion ('portname_' . $i, 'string');
+               genericAssertion ('vlanid_' . $i, 'string');
+               if ($_REQUEST['portname_' . $i] != $portlist[$i]['portname'])
+                       throw new InvalidRequestArgException ('portname_' . $i, $_REQUEST['portname_' . $i], 'expected to be ' . $portlist[$i]['portname']);
+               if
+               (
+                       $_REQUEST['vlanid_' . $i] == $portlist[$i]['vlanid'] ||
+                       $portlist[$i]['vlanid'] == 'TRUNK'
+               )
+                       continue;
+               $portname = $_REQUEST['portname_' . $i];
+               $oldvlanid = $portlist[$i]['vlanid'];
+               $newvlanid = $_REQUEST['vlanid_' . $i];
+               if
+               (
+                       !permitted (NULL, NULL, NULL, array (array ('tag' => '$fromvlan_' . $oldvlanid), array ('tag' => '$vlan_' . $oldvlanid))) or
+                       !permitted (NULL, NULL, NULL, array (array ('tag' => '$tovlan_' . $newvlanid), array ('tag' => '$vlan_' . $newvlanid)))
+               )
+               {
+                       showOneLiner (159, array ($portname, $oldvlanid, $newvlanid));
+                       continue;
+               }
+               $setcmd .= $prefix . $portname . '=' . $newvlanid;
+               $prefix = ';';
+       }
+       // Feed the gateway and interpret its (non)response.
+       if ($setcmd == '')
+               showOneLiner (201);
+       else
+       {
+               try
+               {
+                       setSwitchVLANs ($_REQUEST['object_id'], $setcmd); // shows messages by itself
+               }
+               catch (RTGatewayError $e)
+               {
+                       showFuncMessage (__FUNCTION__, 'ERR', array ($e->getMessage()));
+               }
+       }
+}
+
+// This function queries the gateway about current VLAN configuration and
+// renders a form suitable for submit. Ah, and it does submit processing as well.
+function renderVLANMembership ($object_id)
+{
+       try
+       {
+               $data = getSwitchVLANs ($object_id);
+       }
+       catch (RTGatewayError $re)
+       {
+               showWarning ('Device configuration unavailable:<br>' . $re->getMessage());
+               return;
+       }
+       list ($vlanlist, $portlist, $maclist) = $data;
+       $vlanpermissions = array();
+       foreach ($portlist as $port)
+       {
+               if (array_key_exists ($port['vlanid'], $vlanpermissions))
+                       continue;
+               $vlanpermissions[$port['vlanid']] = array();
+               foreach (array_keys ($vlanlist) as $to)
+                       if
+                       (
+                               permitted (NULL, NULL, 'setPortVLAN', array (array ('tag' => '$fromvlan_' . $port['vlanid']), array ('tag' => '$vlan_' . $port['vlanid']))) and
+                               permitted (NULL, NULL, 'setPortVLAN', array (array ('tag' => '$tovlan_' . $to), array ('tag' => '$vlan_' . $to)))
+                       )
+                               $vlanpermissions[$port['vlanid']][] = $to;
+       }
+
+       if (isset ($_REQUEST['hl_port_id']))
+       {
+               assertUIntArg ('hl_port_id');
+               $hl_port_id = intval ($_REQUEST['hl_port_id']);
+               $object = spotEntity ('object', $object_id);
+               amplifyCell ($object);
+               foreach ($object['ports'] as $port)
+                       if (mb_strlen ($port['name']) && $port['id'] == $hl_port_id)
+                       {
+                               $hl_port_name = $port['name'];
+                               break;
+                       }
+       }
+
+       echo '<table border=0 width="100%"><tr><td colspan=3>';
+       startPortlet ('Current status');
+       echo "<table class=widetable cellspacing=3 cellpadding=5 align=center width='100%'><tr>";
+       printOpFormIntro ('setPortVLAN');
+       $portcount = count ($portlist);
+       echo "<input type=hidden name=portcount value=" . $portcount . ">\n";
+       $portno = 0;
+       $ports_per_row = getConfigVar ('PORTS_PER_ROW');
+       foreach ($portlist as $port)
+       {
+               // Don't let wide forms break our fancy pages.
+               if ($portno % $ports_per_row == 0)
+               {
+                       if ($portno > 0)
+                               echo "</tr>\n";
+                       echo "<tr><th>" . ($portno + 1) . "-" . ($portno + $ports_per_row > $portcount ? $portcount : $portno + $ports_per_row) . "</th>";
+               }
+               $td_class = 'port_';
+               if ($port['status'] == 'notconnect')
+                       $td_class .= 'notconnect';
+               elseif ($port['status'] == 'disabled')
+                       $td_class .= 'disabled';
+               elseif ($port['status'] != 'connected')
+                       $td_class .= 'unknown';
+               elseif (!isset ($maclist[$port['portname']]))
+                       $td_class .= 'connected_none';
+               else
+               {
+                       $maccount = 0;
+                       foreach ($maclist[$port['portname']] as $vlanid => $addrs)
+                               $maccount += count ($addrs);
+                       if ($maccount == 1)
+                               $td_class .= 'connected_single';
+                       else
+                               $td_class .= 'connected_multi';
+               }
+               if (isset ($hl_port_name) and strcasecmp ($hl_port_name, $port['portname']) == 0)
+                       $td_class .= (strlen($td_class) ? ' ' : '') . 'border_highlight';
+               echo "<td class='$td_class'>" . $port['portname'] . '<br>';
+               echo "<input type=hidden name=portname_${portno} value=" . $port['portname'] . '>';
+               if ($port['vlanid'] == 'trunk')
+               {
+                       echo "<input type=hidden name=vlanid_${portno} value='trunk'>";
+                       echo "<select disabled multiple='multiple' size=1><option>TRUNK</option></select>";
+               }
+               elseif ($port['vlanid'] == 'routed')
+               {
+                       echo "<input type=hidden name=vlanid_${portno} value='routed'>";
+                       echo "<select disabled multiple='multiple' size=1><option>ROUTED</option></select>";
+               }
+               elseif (!array_key_exists ($port['vlanid'], $vlanpermissions) or !count ($vlanpermissions[$port['vlanid']]))
+               {
+                       echo "<input type=hidden name=vlanid_${portno} value=${port['vlanid']}>";
+                       echo "<select disabled name=vlanid_${portno}>";
+                       echo "<option value=${port['vlanid']} selected>${port['vlanid']}</option>";
+                       echo "</select>";
+               }
+               else
+               {
+                       echo "<select name=vlanid_${portno}>";
+                       // A port may belong to a VLAN, which is absent from the VLAN table, this is normal.
+                       // We must be able to render its SELECT properly at least.
+                       $in_table = FALSE;
+                       foreach ($vlanpermissions[$port['vlanid']] as $v)
+                       {
+                               echo "<option value=${v}";
+                               if ($v == $port['vlanid'])
+                               {
+                                       echo ' selected';
+                                       $in_table = TRUE;
+                               }
+                               echo ">${v}</option>\n";
+                       }
+                       if (!$in_table)
+                               echo "<option value=${port['vlanid']} selected>${port['vlanid']}</option>\n";
+                       echo "</select>";
+               }
+               $portno++;
+               echo "</td>";
+       }
+       echo "</tr><tr><td colspan=" . ($ports_per_row + 1) . "><input type=submit value='Save changes'></form></td></tr></table>";
+       finishPortlet();
+
+       echo '</td></tr><tr><td class=pcleft>';
+       startPortlet ('VLAN table');
+       echo '<table class=cooltable cellspacing=0 cellpadding=5 align=center width="100%">';
+       echo "<tr><th>ID</th><th>Description</th></tr>";
+       $order = 'even';
+       global $nextorder;
+       foreach ($vlanlist as $id => $descr)
+       {
+               echo "<tr class=row_${order}><td class=tdright>${id}</td><td class=tdleft>${descr}</td></tr>";
+               $order = $nextorder[$order];
+       }
+       echo '</table>';
+       finishPortlet();
+
+       echo '</td><td class=pcright>';
+
+       startPortlet ('Color legend');
+       echo '<table>';
+       echo "<tr><th>port state</th><th>color code</th></tr>";
+       echo "<tr><td>not connected</td><td class=port_notconnect>SAMPLE</td></tr>";
+       echo "<tr><td>disabled</td><td class=port_disabled>SAMPLE</td></tr>";
+       echo "<tr><td>unknown</td><td class=port_unknown>SAMPLE</td></tr>";
+       echo "<tr><td>connected with none MAC addresses active</td><td class=port_connected_none>SAMPLE</td></tr>";
+       echo "<tr><td>connected with 1 MAC addresses active</td><td class=port_connected_single>SAMPLE</td></tr>";
+       echo "<tr><td>connected with 1+ MAC addresses active</td><td class=port_connected_multi>SAMPLE</td></tr>";
+       echo '</table>';
+       finishPortlet();
+
+       echo '</td><td class=pcright>';
+
+       if (count ($maclist))
+       {
+               startPortlet ('MAC address table');
+               echo '<table border=0 class=cooltable align=center cellspacing=0 cellpadding=5>';
+               echo "<tr><th>Port</th><th>VLAN ID</th><th>MAC address</th></tr>\n";
+               $order = 'even';
+               foreach ($maclist as $portname => $portdata)
+                       foreach ($portdata as $vlanid => $addrgroup)
+                               foreach ($addrgroup as $addr)
+                               {
+                                       echo "<tr class=row_${order}><td class=tdleft>$portname</td><td class=tdleft>$vlanid</td>";
+                                       echo "<td class=tdleft>$addr</td></tr>\n";
+                                       $order = $nextorder[$order];
+                               }
+               echo '</table>';
+               finishPortlet();
+       }
+
+       // End of main table.
+       echo '</td></tr></table>';
+}
+
+?>