RackTablesDevelGuide
Contents
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:
- upgrade.php
- install/init-dictbase.sql
- 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]]'),
Customizing
Overriding the search procedure
RackTables contains API allowing to customize search. Two ways of customizing are available:
a) You could change text request (search terms) before passing it to standard search procedure;
b) You could either modify the search results list returned by the standard search procedure, or fill that list completely by yourself.
Of course, you can combine the methods above.
First, you need Racktables to include your code. Racktables tries to include the user's php lib called "wwwroot/inc/local.php". To be honest, the actual include path is "$path_to_local_php", which by default equals to "$racktables_confdir . '/local.php'". $racktables_confdir, consequently, by default is the dir where the library files are stored (wwwroot/inc). You could override either of these paths by making separate entry point (custom index.php), or in secret.php. If you want to actively use plugin model, you may want to write a generic local.php scanning your custom plugins directory and including files from it. This technique will allow you install plugins by simply putting them into this dir, and to easily exchange plugins with the community.
So, you've decided to store your local.php in the default location and override the search procedure. Lets take an example. We want to take a URL in search box, extract hostname from it, resolve it into an IP address and than display the search results for this address. Proper local.php is below:
<?php //step 1 $page['search']['handler'] = 'searchHandler_Local'; function searchHandler_Local () { // step 2 $terms = trim ($_REQUEST['q']); // step 3 if (preg_match("/^(http:\/\/)/", $terms)) { // Search by IP if URL was given preg_match("/^(http:\/\/)?([^\/]+)/i", $terms, $matches); $terms = gethostbyname($matches[2]); } // step 4 $results = searchEntitiesByText ($terms); // step 5 // modify the $results you need to // step 6 renderSearchResults ($terms, $results); } ?>
Let's describe this simple script step-by step.
Step 1. First we overwrite the search handler procedure name, replacing the standard one by the custom 'searchHandler_Local'. Please note that you can not chain overrides - as soon you set procedure name in $page['search']['handler'] (as well as any other handler) the old handler is forgotten, and the new one is responsible for calling it, if it decides to.
Step 2. We need to retrieve the search request. It is passed in 'q' HTTP GET parameter to the search handler.
Step 3. We rewrite the search request text replacing URL to the corresponding IP address. It is an example of using the way (a) of customizing RackTables search (see above)
Step 4. Let the standard search procedure do its job. We call searchEntitiesByText for that.
Step 5. Here you could change the results returned by standard function. This array structure is pretty complex, you may want to examine it by calling var_dump. The array is indexed by search method (like 'object' or 'ipv6addressbydescr'). The rest structure depends of the first key value. If you want to display your very own search results section, you could add the unknown key into this array which will be used as a header for this results section. The HTML code for its body will be the value of $results by the given key.
Step 6. Display the search results using renderSearchResults standard function.
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>