From RackTables Wiki
Jump to navigation Jump to search

Strategic roadmap


Working with SVN repository

repository layout

RackTables repository currently employs a layout, which is quite standard for SVN.

The place for releases. Once a release have been copied here, its files must not be changed (with the only allowed exception of SVN metadata).
This is where most of unstable commits happen.
Branches are development lines, which can last up to several months, but eventually get deleted anyway (merged into the mainstream or not). A typical branch is "maintenance-0.X.Y", which is the place to commit bugfixes. These bugfixes become available by means of maintenance releases, and risky commits happen and get tested in trunk. Once the next stable line is ready, the old one becomes unmaintained and gets deleted.

getting a working copy of source

# Anyone can read from the repository, but writing requires being a project developer.
svn co https://racktables.svn.sourceforge.net/svnroot/racktables/branches/maintenance-0.19.x/

# Above command is for the maintenance line. For the trunk (see roadmap picture for exact
# difference between two) respective command is:
svn co https://racktables.svn.sourceforge.net/svnroot/racktables/trunk/

editing files

Use your favourite editor. Please consider existing code style: the default is to keep with Allman style (not with K&R, as was mistakenly stated here before). Indentation is always performed with tabs, please use any tab width you are comfortable with.

Here is an example of a function:

// take port list with order applied and return uplink ports in the same format
function produceUplinkPorts ($domain_vlanlist, $portlist)
	$ret = array();
	$employed = array();
	foreach ($domain_vlanlist as $vlan_id => $vlan)
		if ($vlan['vlan_type'] == 'compulsory')
			$employed[] = $vlan_id;
	foreach ($portlist as $port_name => $port)
		if ($port['vst_role'] != 'uplink')
			foreach ($port['allowed'] as $vlan_id)
				if (!in_array ($vlan_id, $employed))
					$employed[] = $vlan_id;
	foreach ($portlist as $port_name => $port)
		if ($port['vst_role'] == 'uplink')
			$employed_here = array();
			foreach ($employed as $vlan_id)
				if (matchVLANFilter ($vlan_id, $port['wrt_vlans']))
					$employed_here[] = $vlan_id;
			$ret[$port_name] = array
				'vst_role' => 'uplink',
				'mode' => 'trunk',
				'allowed' => $employed_here,
				'native' => 0,
	return $ret;

viewing exact changes

svn stat
svn diff

submitting work

# if you have an account
svn commit

# and if you don't
svn diff > my-cool-feature.patch
# send the patch file by e-mail

Cutting a release

pre-release checklist

First make sure, that all necessary changes are already committed into the repository:

  • Variable inc/config.php:$max_dict_key must be equal to the maximum value of key found at the end of inc/dictionary.php.
  • Function database.php:getDictStats() must enumerate all chapter IDs, which are listed in install/init-dictbase.sql:Chapter (install.php:get_pseudo_file():case 'dictbase' in version 0.19.0) otherwise the stats are shown wrong.
  • All release notes, if any, must appear both in upgrade script and in the README.
  • Current date must be present on the version line of ChangeLog.
  • Make sure, that upgrade.php has new version listed in $versionhistory and executeUpgradeBatch(). It is normal to accumulate updates in executeUpgradeBatch() long before the release, this way on the release day you will have nothing to do in upgrade.php. But if you had no changes to DB since the last release, you are likely to see there changes missing. Just make sure the new version is in both places anyway.
  • Bump up CODE_VERSION in inc/config.php and DB_VERSION in install.php:get_pseudo_file(). DON'T do this unless you really intend to make a release right now.
  • svn stat; svn commit

Now export the codebase from the trunk (or maintenance branch, whichever you are releasing from) and test it.

  • Demo data should also be loadable and functional (due to recent changes in 0.19 it's not really fixed yet).
racktables/contribs/demoreload.sh X.Y.z
  • The system must be able to install itself with own installer. Once this is found working, dump the database:
mysqldump --extended-insert=FALSE --order-by-primary racktables_db > ~/tmp/dump-fresh.sql
  • The source tree being tested must detect and upgrade a database from the previous release correctly. Load the database with the previous release data, then upgrade it with the current upgrader, then dump the DB. Now compare to the previous dump, there must be no meaningful differences.
mysqldump --extended-insert=FALSE --order-by-primary racktables_db > ~/tmp/dump-upgraded.sql
diff -u ~/tmp/dump-fresh.sql ~/tmp/dump-upgraded.sql
  • Test the source as much as you find possible.
  • Look into the error log of the server you used for the tests. There shouldn't be any error/warning messages.

The above rounds may happen repeated more, than once, until everything is fixed consistently.

the release itself

# from trunk (beta testing releases):
svn cp \
https://racktables.svn.sourceforge.net/svnroot/racktables/trunk \

# from maintenance (stable releases):
svn cp \
https://racktables.svn.sourceforge.net/svnroot/racktables/branches/maintenance-0.Y.x \

rolling out

  • Make a tarball:
svn export https://racktables.svn.sourceforge.net/svnroot/racktables/tags/RackTables-X.Y.z
tar czf RackTables-X.Y.z.tar.gz RackTables-X.Y.z
  • Upload the tarball to SF FRS (from browser or by other means).
  • In MantisBT mark version as released (Manage, Manage Projects, RackTables, Versions).
  • Log into SF shell service:
ssh -t YOUR_USERNAME,racktables@shell.sourceforge.net create
  • Edit (vim) /home/project-web/racktables/htdocs/header.php to update $lastrelease. Test the "download" link to work after that.
  • Post a Freshmeat release announce here.
  • Send a letter to the racktables-users list, if necessary.

Config options

Variables are stored in the Config SQL table:

mysql> DESCRIBE Config;
| Field          | Type                  | Null | Key | Default | Extra |
| varname        | char(32)              | NO   | PRI | NULL    |       |
| varvalue       | char(255)             | NO   |     | NULL    |       |
| vartype        | enum('string','uint') | NO   |     | string  |       |
| emptyok        | enum('yes','no')      | NO   |     | no      |       |
| is_hidden      | enum('yes','no')      | NO   |     | yes     |       |
| is_userdefined | enum('yes','no')      | NO   |     | no      |       |
| description    | text                  | YES  |     | NULL    |       |
7 rows in set (0.00 sec)

Options are read and written with getConfigVar() and setConfigVar() functions respectively. The current naming convention for new options is to use descriptive expressions in ALL_CAPITAL_LETTERS with spaces REPLACED_WITH_UNDERSCORES. When adding a new option, check the following places:

  1. upgrade.php
  2. install/init-dictbase.sql
  3. inc/ophandlers.php:resetUIConfig()

Default values must match regardless of the way they were set: initial setup, upgrade or UI reset.

Exceptions and error handling

Error handling now can be done by exceptions mechanism. index.php and process.php have been wrapped in

} catch {

kind of code, so every exception you miss and not catch will be printed instead of the page that had to be printed.

This saves a lot of code, take a look at a function

// before

function commitUpdateDictionary ($chapter_no = 0, $dict_key = 0, $dict_value = '')
        if ($chapter_no <= 0)
                showError ('Invalid args', __FUNCTION__);
        global $dbxlink;
        $query =
                "update Dictionary set dict_value = '${dict_value}' where chapter_id=${chapter_no} " .
                "and dict_key=${dict_key} limit 1";
        $result = $dbxlink->query ($query);
        if ($result == NULL)
                showError ('SQL query failed', __FUNCTION__);
        return TRUE;

// and after

function commitUpdateDictionary ($chapter_no = 0, $dict_key = 0, $dict_value = '')
        if ($chapter_no <= 0) 
                   throw InvalidArgException('$chapter_no', $chapter_no);
// the function below does not exist
        Database::updateWhere(array('dict_value'=>$dict_value), 'Dictionary', array('chapter_id'=>$chapter_no, 'dict_key'=>$dict_key));
        return TRUE;

Function Database::updateWhere throws an exception if query is bad, and you can catch it if you want and handle it as necessary, or you can just ignore it and application will display error message with a stacktrace and such.

Exceptions registered so far:

Fatal, final error, usually annotated.
"Soft" database error (UNIQUE/FOREIGN KEY constraint violation or timeout).
An external gateway raised an error or the data it returned was corrupt or otherwise invalid.
Requested record could not be found in the database.
At least one of the arguments provided to a function had invalid value, and that value did NOT come from user's input. This means a programming error and cannot be worked around.
Ditto, but the value was supplied by the user, and the error should be handled with an "inline" error message.
LVS keepalived text compilation failed.


BNF/regexp description
ID ::= ^[[:alnum:]]([\. _~-]?[[:alnum:]])*$
identifier in brackets
TAG ::= {ID}
identifier in curly braces
identifier in curly braces, starting with dollar sign
CTXMOD ::= clear | insert TAG | remove TAG
context modifier
context modifier list
logical AND
logical OR
RackCode permission text

Comments in RackCode last from the first # character to the end of current line and are filtered out automatically:

allow {tech support} # or {admins} <--- note where comment starts
and {assets} # <--- this is still a part of "allow" statement

deny {guests}


Hello, World!

To enable all RackTables functions and load all necessary system data, it is enough to include one file.


include ('inc/init.php');
// do something


There is a special parameter, which tells, if current file is intended to be run by web-server or from a command line. In the latter case neither authentication nor authorization is performed.


$script_mode = TRUE;
include ('inc/init.php');

echo "I am a crontab script!\n";
// do something



There are several principal realms in !RackTables database. Any realm contains zero, one or more records. Each such record is addressed by its ID (primary key).

All servers, switches, UPSes, wireless, cable management and any other stuff, which is viewed and managed on the main "Objects" page.
all IPv4 networks
all IPv6 networks
All local accounts. There is at least one local account in any RackTables system (admin). There may be more.
All racks.
All files.
All IPv4 virtual services.
All IPv4 real server pools.

function scanRealmByText ($realm, $ftext = "")

String equal to one of the realms shown above.
Optional filter text. If this text is empty, all records of the realm are returned. If it is not empty, it is interpreted as a !RackCode expression. This expression is then evaluated against each record of the realm and only those records, for which the filter returned TRUE, are returned.

include ('inc/init.php');

$allusers = scanRealmByText ('user');
$smallnetworks = scanRealmByText ('ipv4net', '{small network}');
$unmounted_objects = scanRealmByText ('object', '{$unmounted}');


function spotEntity ($realm, $id)

This is the right function to get information about some record, for which you know where it belongs to and what its key value is. If this information isn't available, it has to be discovered somehow first.

Realm name.
Record number (key value, ID...).

include ('inc/init.php');

$adminuser = spotEntity ('user', 1); // Admin account is always number 1.
$myspecialfile = spotEntity ('file', 12345);
$serverinfo = spotEntity ('object', 67890);


function amplifyCell (&$record, $dummy = NULL)

It is assumed, that spotEntity() loads only basic data about the record requested. "Basic" means enough for renderCell() to do its job or to render a row in a table. To get detailed information about the "cell" it is necessary to execute amplifyCell() on it:

include ('inc/init.php');

$adminuser = spotEntity ('user', 1); // Admin account is always number 1.
amplifyCell ($adminuser);

$unmounted_objects = scanRealmByText ('object', '{$unmounted}');
array_walk ($unmounted_objects, 'amplifyCell');

function renderCell ($cell)

Render cell structure as a rectangular block with icon, name, tags and other information. Available since 0.17.2.


Where the data is

Since release 0.17.2 all records are stored in file inc/dictionary.php:

$dictionary = array
	1 => array ('chapter_id' => 1, 'dict_value' => 'BlackBox'),
	2 => array ('chapter_id' => 1, 'dict_value' => 'PDU'),
	3 => array ('chapter_id' => 1, 'dict_value' => 'Shelf'),
	4 => array ('chapter_id' => 1, 'dict_value' => 'Server'),
	5 => array ('chapter_id' => 1, 'dict_value' => 'DiskArray'),
	6 => array ('chapter_id' => 1, 'dict_value' => 'TapeLibrary'),
	7 => array ('chapter_id' => 1, 'dict_value' => 'Router'),
	8 => array ('chapter_id' => 1, 'dict_value' => 'Network switch'),
	9 => array ('chapter_id' => 1, 'dict_value' => 'PatchPanel'),
	10 => array ('chapter_id' => 1, 'dict_value' => 'CableOrganizer'),
	11 => array ('chapter_id' => 1, 'dict_value' => 'spacer'),
	12 => array ('chapter_id' => 1, 'dict_value' => 'UPS'),

meaning of chapter ID

mysql> SELECT * FROM Chapter;
| id | sticky | name                        |
|  1 | yes    | RackObjectType              | 
|  2 | yes    | PortType                    | 
| 11 | no     | server models               | 
| 12 | no     | network switch models       | 
| 13 | no     | server OS type              | 
| 14 | no     | switch OS type              | 
| 16 | no     | router OS type              | 
| 17 | no     | router models               | 
| 18 | no     | disk array models           | 
| 19 | no     | tape library models         | 
| 21 | no     | KVM switch models           | 
| 22 | no     | multiplexer models          | 
| 23 | no     | console models              | 
| 24 | no     | network security models     | 
| 25 | no     | wireless models             | 
| 26 | no     | fibre channel switch models | 
| 27 | no     | PDU models                  | 
17 rows in set (0.00 sec)


It is possible to render a record as an URL:

[[ something | http://somewhere/ ]]

However, it is up to the editor, if to include URL into description or not. URLs can be added, changed and removed from particular records later, if it makes sense.


Text before the marker is always used for OPTGROUP in SELECT element. The difference is in the way the text is rendered as a plain text or a clickable URL. GSKIP makes OPTGROUP name to be skipped. GPASS passes OPTGROUP name on the text:

    719 => array ('chapter_id' => 24, 'dict_value' => '[[Cisco%GPASS%ASR 1006 | http://cisco.com/en/US/products/ps9438/index.html]]'),
    720 => array ('chapter_id' => 13, 'dict_value' => '[[BSD%GSKIP%OpenBSD 3.3 | http://openbsd.org/33.html]]'),


Adding a custom report

(contributed by Craig Hoffman)

A slightly modified and simple version of a PHP script posted to the RackTables mailing list (link). 98% of this code is not mine. It will generate a CSV file export.

In this example, I have a new attribute on some of my hosts called "IPv6"(attr_id 10000 for me). I wanted to be able to report on that, as well as its IPv4 counterpart. It also does a MySQL INET_NTOA conversion so that the IP address actually looks like something a human can read.

I place this file in the root of my webserver. If you place it elsewhere, and wish to call it from the "local.php" file included below, adjust the "/ipv6.php" location in local.php.



if (!$db) {

    die('Not connected : ' . mysql_error());


$db_selected = mysql_select_db('rt', $db);

if (!$db_selected) {

    die ('Can\'t use foo : ' . mysql_error());


//SQL query starts here

$sql = "SELECT DISTINCT(RackObject.name) AS DeviceName,

r2.name AS RackLoc,

INET_NTOA(ip1.ip) AS IPv4addr,

av6.string_value AS IPv6addr FROM RackObject

LEFT JOIN AttributeValue AS av1 ON (av1.object_id = RackObject.id AND av1.attr_id = 4)

LEFT JOIN Dictionary AS d1 ON d1.dict_key = av1.uint_value

LEFT JOIN AttributeValue AS av2 ON (av2.object_id = RackObject.id AND av2.attr_id = 5)

LEFT JOIN AttributeValue AS av3 ON (av3.object_id = RackObject.id AND av3.attr_id = 1)

LEFT JOIN AttributeValue AS av6 ON (av6.object_id = RackObject.id AND av6.attr_id = 10000)

LEFT JOIN Dictionary AS d2 ON d2.dict_key = av3.uint_value

LEFT JOIN RackSpace AS r1 ON r1.object_id = RackObject.id

LEFT JOIN IPv4Allocation AS ip1 ON ip1.object_id = RackObject.id

LEFT JOIN Rack AS r2 ON r1.rack_id = r2.id

LEFT JOIN Dictionary AS d4 ON d4.dict_key = r2.row_id

ORDER BY av6.string_value DESC;";

//SQL query end here

$rsSearchResults = mysql_query($sql, $db) or die(mysql_error());

$columns = mysql_num_fields($rsSearchResults);

while ($i < $columns) {

        $meta = mysql_fetch_field($rsSearchResults, $i);

        if ($meta) {

                $out .= '"'.$meta->name.'",';




$out .="\n";

while ($l = mysql_fetch_array($rsSearchResults)) {

        for ($i = 0; $i < $columns; $i++) {

                $out .='"'.$l["$i"].'",';


        $out .="\n";


// Output to browser with appropriate mime type, you choose ;)

header("Content-type: text/x-csv");

//header("Content-type: text/csv");

//header("Content-type: application/csv");

header("Content-Disposition: attachment; filename=RackTablesIPv6Report.csv");

echo $out;





$localreports[] = array


        'title' => 'Custom reports',

        'type' => 'custom',

        'func' => 'getOwnReports'


function getOwnReports() {

echo "<table>\n";

echo "<tr><th class=tdright>Serverlist</th><td class=tdleft><a


echo "</table>\n";



And this is the PHP code that goes into inc/local.php. This isn't required, but it makes it easier to find all of the reports that you may wish to generate.


$localreports[] = array


        'title' => 'Custom reports',

        'type' => 'custom',

        'func' => 'getOwnReports'


function getOwnReports() {

echo "<table>\n";

echo "<tr><th class=tdright>Standard Report</th><td class=tdleft><a


echo "</table>\n";

echo "<table>\n";

echo "<tr><th class=tdright>IPv6 Report</th><td class=tdleft><a


echo "</table>\n";

echo "<table>\n";

echo "<tr><th class=tdright>Circuit ID Report</th><td class=tdleft><a


echo "</table>\n";



802.1Q internals

execution of "pull-only" and "pull+push" sync requests