Difference between revisions of "RackTablesDevelGuide"

From RackTables Wiki
Jump to navigation Jump to search
m (→‎Cutting a release: now an own page)
Line 1: Line 1:
 
[[File:RackTables-development-roadmap-2011Q3.png|391px|thumb|right]]
 
[[File:RackTables-development-roadmap-2011Q3.png|391px|thumb|right]]
= Working with SVN repository =
 
== repository layout ==
 
RackTables repository currently employs a layout, which is quite standard for SVN.
 
;tags
 
: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).
 
;trunk
 
:This is where most of unstable commits happen.
 
;branches
 
: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 ==
 
<pre>
 
# 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/
 
</pre>
 
 
== editing files ==
 
Use your favourite editor. Please consider existing code style: the default is to keep with [http://en.wikipedia.org/wiki/Indent_style#Allman_style_.28bsd_in_Emacs.29 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:
 
<pre>
 
// 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;
 
}
 
</pre>
 
 
== viewing exact changes ==
 
<pre>
 
svn stat
 
svn diff
 
</pre>
 
== submitting work ==
 
<pre>
 
# 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
 
</pre>
 
 
 
= Config options =
 
= Config options =
 
Variables are stored in the Config SQL table:
 
Variables are stored in the Config SQL table:

Revision as of 10:42, 1 September 2011

RackTables-development-roadmap-2011Q3.png

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

ob_start(); 
try{
  ...
  ob_end_flush();
} catch {
  print_error_nicely();
}

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__);
                die;
        }
        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__);
                die;
        }
        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:

RackTablesError
Fatal, final error, usually annotated.
RTDatabaseError
"Soft" database error (UNIQUE/FOREIGN KEY constraint violation or timeout).
RTGatewayError
An external gateway raised an error or the data it returned was corrupt or otherwise invalid.
EntityNotFoundException
Requested record could not be found in the database.
InvalidArgException
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.
InvalidRequestArgException
Ditto, but the value was supplied by the user, and the error should be handled with an "inline" error message.
RTBuildLVSConfigError
LVS keepalived text compilation failed.

RackCode

# identifier
ID ::= ^[[:alnum:]]([\. _~-]?[[:alnum:]])*$
# identifier in brackets
PREDICATE ::= [ID]
DEFINE ::= define PREDICATE
# identifier in curly braces
TAG ::= {ID}
# identifier in curly braces, starting with dollar sign
AUTOTAG ::= {$ID}
# context modifier
CTXMOD ::= clear | insert TAG | remove TAG
# context modifier list
CTXMODLIST ::= CTXMODLIST CTXMOD | CTXMOD
UNARY_EXPRESSION ::= true | false | TAG | AUTOTAG | PREDICATE | (EXPRESSION) | not UNARY_EXPRESSION
# logical AND
AND_EXPRESSION ::= AND_EXPRESSION and UNARY_EXPRESSION | UNARY_EXPRESSION
# logical OR
EXPRESSION ::= EXPRESSION or AND_EXPRESSION | AND_EXPRESSION
GRANT ::= allow EXPRESSION | deny EXPRESSION
DEFINITION ::= DEFINE EXPRESSION
ADJUSTMENT ::= context CTXMODLIST on EXPRESSION
# RackCode permission text
CODETEXT ::= CODETEXT GRANT | CODETEXT DEFINITION | CODETEXT ADJUSTMENT | GRANT | DEFINITION | ADJUSTMENT

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}

API

Hello, World!

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

<?php

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.

<?php

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

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

?>

Realms

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).

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

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

realm
String equal to one of the realms shown above.
ftext
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.
<?php

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
Realm name.
id
Record number (key value, ID...).
<?php

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:

<?php
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.

Dictionary

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)

wikilinks

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.

G-markers

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]]'),

Dictionary-G-markers.png

Adding a custom report

There is a simple way to write custom reports and embed them into the RackTables user interface.

All you need to do is write a report rendering function, apparently using such RackTables API functions like scanRealmByText, spotEntity and renderCell, and register this function as a report rendering handler. This example should make it clear:

1. Save the code below into the test-report.php file:

<?php

$tabhandler['reports']['test'] = 'renderTestReport'; // register a report rendering function
$tab['reports']['test'] = 'Test Report'; // title of the report tab

function renderTestReport()
{
	// fill the HW type stat array
	$stat = array();
	$total = 0;
	$filter = '{switch} and {Moscow}';
	foreach (scanRealmByText ('object', $filter) as $switch)
	{
		$attributes = getAttrValues ($switch['id']);
		if (isset ($attributes[2]))
		{
			$attr = $attributes[2];
			if (! isset ($stat[$attr['key']]))
				$stat[$attr['key']] = array
				(
					'value' => $attr['a_value'],
					'count' => 0,
				);
			++$stat[$attr['key']]['count'];
			++$total;
		}
	}
	
	// display the stat array
	echo "<h2>Moscow switches HW types report ($total)</h2><ul>";
	foreach ($stat as $type_id => $type)
	{
		$type_filter = $filter . ' and {$attr_2_' . $type_id . '}';
		$link = '<a href="' . makeHref (array ('page' => 'depot', 'cfe' => $type_filter)) . '">' . $type['count'] . ' devices</a>';
		echo "<li>${type['value']} - $link</li>";
	}
	echo '</ul>';
}

?>

2. To install your report into RackTables you add just one line into the wwwroot/inc/local.php file:

require_once '/path/to/test-report.php';

This report displays switch devices located in Moscow, grouped by their hardware model types. Each line of output contains the device count of corresponding model and a link to view the device list, like this:

Moscow switches HW types report (2)
 *   Cisco Catalyst 2960G-24PC - <a>1 devices</a>
 *   Cisco Catalyst 2960G-24TC - <a>1 devices</a>

802.1Q internals

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

RackTables-8021Q-sync.png