Difference between revisions of "RackTablesDevelGuide"

From RackTables Wiki
Jump to navigation Jump to search
m (use the current roadmap)
 
(71 intermediate revisions by 4 users not shown)
Line 1: Line 1:
= Working with SVN repository =
+
[[File:RackTables-development-roadmap-2019Q4.png|311px|thumb|right]]
== getting a working copy of maintenance line ==
 
{{{
 
# Anyone can read from the repository, but writing requires an account on the server.
 
svn co https://svn.racktables.org/branches/RackTables/maintenance-0.17.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://svn.racktables.org/trunk/RackTables/
 
}}}
 
== editing files ==
 
Use your favourite editor. Consider existing code style (tabbed indentation, K&R-styled curly braces).
 
== 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:
 
* Update {{{inc/config.php:$max_dict_key}}}.
 
* Function {{{database.php:getDictStats()}}} must indicate all chapters, which belong to dictionary in tarball. Otherwise the stats are shown wrong.
 
* All release notes, if any, must appear both in upgrade script and in the README.
 
* Add current date to the version line in !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/init-dictbase.sql}}}. DON'T do this unless you really intend to make a release right now.
 
* svn stat; svn commit
 
 
 
Export from the trunk (or maintenance branch) and test the codebase.
 
* The source must be installable from the web. Click next-next-next and you're done. Dump the DB.
 
* Demo data must also be loadable and functional.
 
* The source must detect and upgrade the previous release correctly. Load the last release with its demo data, then upgrade it and dump the DB. Now compare to the previous dump, there must be no meaningful differences.
 
{{{
 
mysqldump --extended-insert=FALSE --order-by-primary racktables > ~/tmp/dump-upgraded.sql
 
mysqldump --extended-insert=FALSE --order-by-primary racktables > ~/tmp/dump-fresh.sql
 
diff -u ~/tmp/dump-fresh.sql ~/tmp/dump-upgraded.sql
 
}}}
 
* Now click a couple of links.
 
* Look into the error log of the server you used for the tests. There shouldn't be any messages produced by any test.
 
The above may happen repeated more, than once, until everything is fixed consistently.
 
== the release itself ==
 
{{{
 
# from trunk (like early 0.16.x):
 
svn cp https://svn.racktables.org/trunk/RackTables https://svn.racktables.org/tags/RackTables-X.Y.Z
 
# from maintenance (like 0.17.x):
 
svn cp https://svn.racktables.org/branches/RackTables/maintenance-X.Y.x https://svn.racktables.org/tags/RackTables-X.Y.z
 
}}}
 
== rolling out ==
 
* Make a tarball:
 
{{{
 
svn export https://svn.racktables.org/tags/RackTables-X.Y.z
 
tar czf RackTables-X.Y.z.tar.gz RackTables-X.Y.z
 
}}}
 
* Upload the tarball onto the distibution site.
 
* Update the live demo page: create a new database, export source tree, add a crontab line. Let old demo run for a couple of days more.
 
* Update Trac records (roadmap is updated from web):
 
{{{
 
[root@racktables ~]# trac-admin /srv/trac/
 
Welcome to trac-admin 0.10.5
 
Interactive Trac administration console.
 
Copyright (c) 2003-2006 Edgewall Software
 
 
 
Type:  '?' or 'help' for help on commands.
 
 
 
Trac [/srv/trac]> version add X.Y.Z YYYY-MM-DD
 
}}}
 
* Edit {{{/var/www/vhosts/racktables.org/header.php}}} to update {{{$lastrelease}}}. Make sure "download" and "demo" do work.
 
* Write a Freshmeat release announce.
 
* Send a letter to the racktables-users list, if necessary.
 
 
 
 
= Config options =
 
= Config options =
 
Variables are stored in the Config SQL table:
 
Variables are stored in the Config SQL table:
{{{
+
<pre>
mysql> describe Config;
+
mysql> DESCRIBE Config;
+-------------+-----------------------+------+-----+---------+-------+
+
+----------------+-----------------------+------+-----+---------+-------+
| Field       | Type                  | Null | Key | Default | Extra |
+
| Field         | Type                  | Null | Key | Default | Extra |
+-------------+-----------------------+------+-----+---------+-------+
+
+----------------+-----------------------+------+-----+---------+-------+
| varname     | varchar(32)           | NO  | PRI |         |      |  
+
| varname       | char(32)             | NO  | PRI | NULL    |      |
| varvalue   | varchar(255)         | NO  |    |         |      |  
+
| varvalue       | char(255)             | NO  |    | NULL    |      |
| vartype     | enum('string','uint') | NO  |    | string  |      |  
+
| vartype       | enum('string','uint') | NO  |    | string  |      |
| emptyok     | enum('yes','no')      | NO  |    | no      |      |  
+
| emptyok       | enum('yes','no')      | NO  |    | no      |      |
| is_hidden   | enum('yes','no')      | NO  |    | yes    |      |  
+
| is_hidden     | enum('yes','no')      | NO  |    | yes    |      |
| description | text                  | YES  |    | NULL    |      |  
+
| is_userdefined | enum('yes','no')      | NO  |    | no      |      |
+-------------+-----------------------+------+-----+---------+-------+
+
| description   | text                  | YES  |    | NULL    |      |
6 rows in set (0.00 sec)
+
+----------------+-----------------------+------+-----+---------+-------+
}}}
+
7 rows in set (0.00 sec)
 +
</pre>
 
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:
 
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
+
# upgrade.php
1. install/init-dictbase.sql
+
# install/init-dictbase.sql
1. inc/ophandlers.php:resetUIConfig()
+
# inc/ophandlers.php:resetUIConfig()
 
Default values must match regardless of the way they were set: initial setup, upgrade or UI reset.
 
Default values must match regardless of the way they were set: initial setup, upgrade or UI reset.
  
= Exceptions and error handling (in development) =
+
= Exceptions and error handling =
 
Error handling now can be done by exceptions mechanism. index.php and process.php have been wrapped in  
 
Error handling now can be done by exceptions mechanism. index.php and process.php have been wrapped in  
{{{
+
<pre>
 
ob_start();  
 
ob_start();  
 
try{
 
try{
Line 112: Line 33:
 
   print_error_nicely();
 
   print_error_nicely();
 
}
 
}
}}}
+
</pre>
 
kind of code, so every exception you miss and not catch will be printed instead of the page that had to be printed.
 
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
+
This saves a lot of code, take a look at a function
{{{
+
<pre>
 +
// before
 +
 
 
function commitUpdateDictionary ($chapter_no = 0, $dict_key = 0, $dict_value = '')
 
function commitUpdateDictionary ($chapter_no = 0, $dict_key = 0, $dict_value = '')
 
{
 
{
Line 136: Line 59:
 
         return TRUE;
 
         return TRUE;
 
}
 
}
}}}
+
 
and after
+
// and after
{{{
+
 
 
function commitUpdateDictionary ($chapter_no = 0, $dict_key = 0, $dict_value = '')
 
function commitUpdateDictionary ($chapter_no = 0, $dict_key = 0, $dict_value = '')
 
{
 
{
 
         if ($chapter_no <= 0)  
 
         if ($chapter_no <= 0)  
 
                   throw InvalidArgException('$chapter_no', $chapter_no);
 
                   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));
 
         Database::updateWhere(array('dict_value'=>$dict_value), 'Dictionary', array('chapter_id'=>$chapter_no, 'dict_key'=>$dict_key));
 
         return TRUE;
 
         return TRUE;
 
}
 
}
}}}
+
</pre>
  
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 s stacktrace and such.
+
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:
 
Exceptions registered so far:
  
|| '''class name''' || '''by''' || '''when''' ||
+
;RackTablesError
|| !NotAuthorizedException || || Authentication failed or user is not authorized to view current page ||
+
:Fatal, final error, usually annotated.
|| !CodeCompilationError || || RackCode compilcation failed ||
 
|| !RealmNotFoundException || || Realm not found ||
 
|| !EntityNotFoundException || || when an entity cannot be found in the database ||
 
|| !NotUniqueException ||  || when a record cannot be INSERTed into SQL table due to UNIQUE KEY constraint ||
 
|| !InvalidArgException || lots of places || when at least one of the arguments provided to a function has invalid value ||
 
|| !InvalidRequestArgException || usually thrown by assert*Arg() functions || wrong argument in Request ||
 
|| !Exception ||  || When it's not worth to create a special exception class ||
 
  
 +
;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.
  
= !RackCode =
+
;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.
  
|| '''BNF/regexp''' || '''description''' ||
+
;InvalidRequestArgException
|| ID ::= {{{^[[:alnum:]]([\. _~-]?[[:alnum:]])*$}}} || identifier ||
+
:Ditto, but the value was supplied by the user, and the error should be handled with an "inline" error message.
|| PREDICATE ::= [ID] || identifier in brackets ||
 
|| DEFINE ::= define PREDICATE || ||
 
|| TAG ::= {ID} || identifier in curly braces ||
 
|| AUTOTAG ::= {$ID} || identifier in curly braces, starting with dollar sign ||
 
|| CTXMOD ::= clear | insert TAG | remove TAG || context modifier ||
 
|| CTXMODLIST ::= CTXMODLIST CTXMOD | CTXMOD || context modifier list ||
 
|| UNARY_EXPRESSION ::= true | false | TAG | AUTOTAG | PREDICATE | (EXPRESSION) | not UNARY_EXPRESSION || ||
 
|| AND_EXPRESSION ::= AND_EXPRESSION and UNARY_EXPRESSION | UNARY_EXPRESSION || logical AND ||
 
|| EXPRESSION ::= EXPRESSION or AND_EXPRESSION | AND_EXPRESSION || logical OR ||
 
|| GRANT ::= allow EXPRESSION | deny EXPRESSION || ||
 
|| DEFINITION ::= DEFINE EXPRESSION || ||
 
|| ADJUSTMENT ::= context CTXMODLIST on EXPRESSION || ||
 
|| CODETEXT ::= CODETEXT GRANT | CODETEXT DEFINITION | CODETEXT ADJUSTMENT | GRANT | DEFINITION | ADJUSTMENT || !RackCode permission text ||
 
  
== Comments ==
+
;RTBuildLVSConfigError
Comments last from the first # to the end of the line and are filtered out automatically.
+
:LVS keepalived text compilation failed.
 
----
 
----
 +
 +
= RackCode =
 +
<pre>
 +
# 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
 +
</pre>
 +
Comments in RackCode last from the first <tt>#</tt> character to the end of current line and are filtered out automatically:
 +
<pre>
 +
allow {tech support} # or {admins} <--- note where comment starts
 +
and {assets} # <--- this is still a part of "allow" statement
 +
 +
deny {guests}
 +
</pre>
  
 
= API =
 
= API =
 
== Hello, World! ==
 
== Hello, World! ==
To enable all !RackTables functions and load all necessary system data, it is enough to include one file.
+
To enable all RackTables functions and load all necessary system data, it is enough to include one file.
{{{
+
<pre>
 
<?php
 
<?php
  
Line 197: Line 142:
  
 
?>
 
?>
}}}
+
</pre>
  
 
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.
 
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.
{{{
+
<pre>
 
<?php
 
<?php
  
Line 210: Line 155:
  
 
?>
 
?>
}}}
+
</pre>
 +
 
 +
== Searching for the appropriate library function ==
 +
The easy way to determine which function does what you need to is analyze how the corresponding web interface request is processed.
 +
 
 +
Each modification through web causes HTTP POST request being sent with at least these base parameters: 'page', 'tab', 'op'.
 +
There is the $ophandler array in wwwroot/inc/navigation.php filled with function names for each triplet of base web operation parameters.
 +
 
 +
Then, when you determine the handler function name for you operation (like object creating), you could examine its code in wwwroot/inc/ophandlers.php. It usually calls appropriate functions from database.php.
 +
 
 +
Also, there are some trivial operations which have no special ophandler function. Then the universal database wrapper called tableHandler takes place. It makes direct SQL queries, building them by $opspec_list array data.
 +
 
  
 
== Realms ==
 
== 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).
 
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::
+
;object
    All servers, switches, UPSes, wireless, cable management and any other stuff, which is viewed and managed on the main "Objects" page.
+
:All servers, switches, UPSes, wireless, cable management and any other stuff, which is viewed and managed on the main "Objects" page.
  ipv4net::
+
;ipv4net
    All IPv4 networks.
+
:all IPv4 networks
  user::
+
;ipv6net
    All local accounts. There is at least one local account in any RackTables system (admin). There may be more.
+
:all IPv6 networks
  rack::
+
;user
    All racks.
+
:All local accounts. There is at least one local account in any RackTables system (admin). There may be more.
  file::
+
;rack
    All files.
+
:All racks.
  ipv4vs::
+
;file
    All IPv4 virtual services.
+
:All files.
  ipv4rspool::
+
;ipv4vs
    All IPv4 real server pools.
+
:All IPv4 virtual services.
 +
;ipv4rspool
 +
:All IPv4 real server pools.
  
 
== function scanRealmByText ($realm, $ftext = "") ==
 
== function scanRealmByText ($realm, $ftext = "") ==
  realm::
+
;realm
    String equal to one of the realms shown above.
+
:String equal to one of the realms shown above.
  ftext::
+
;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.
+
: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.
  
{{{
+
<pre>
 
<?php
 
<?php
  
Line 246: Line 204:
  
 
?>
 
?>
}}}
+
</pre>
  
 
== function spotEntity ($realm, $id) ==
 
== 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.
 
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
    Realm name.
+
:Realm name.
  id::
+
;id
    Record number (key value, ID...).
+
:Record number (key value, ID...).
  
{{{
+
<pre>
 
<?php
 
<?php
  
Line 266: Line 224:
  
 
?>
 
?>
}}}
+
</pre>
  
 
== function amplifyCell (&$record, $dummy = NULL) ==
 
== 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:
 
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:
{{{
+
<pre>
 
+
<?php
 
include ('inc/init.php');
 
include ('inc/init.php');
  
Line 279: Line 237:
 
$unmounted_objects = scanRealmByText ('object', '{$unmounted}');
 
$unmounted_objects = scanRealmByText ('object', '{$unmounted}');
 
array_walk ($unmounted_objects, 'amplifyCell');
 
array_walk ($unmounted_objects, 'amplifyCell');
 
+
?>
 
+
</pre>
}}}
 
  
 
== function renderCell ($cell) ==
 
== function renderCell ($cell) ==
 
Render cell structure as a rectangular block with icon, name, tags and other information. Available since 0.17.2.
 
Render cell structure as a rectangular block with icon, name, tags and other information. Available since 0.17.2.
----
 
  
 
= Dictionary =
 
= Dictionary =
Since release 0.17.2 all records are stored in file {{{inc/dictionary.php}}}:
+
== Where the data is ==
{{{
+
Since release 0.17.2 all records are stored in file <tt>inc/dictionary.php</tt>:
 +
<pre>
 
$dictionary = array
 
$dictionary = array
 
(
 
(
Line 305: Line 262:
 
12 => array ('chapter_id' => 1, 'dict_value' => 'UPS'),
 
12 => array ('chapter_id' => 1, 'dict_value' => 'UPS'),
 
[...]
 
[...]
}}}
+
</pre>
 +
 
 
== meaning of chapter ID ==
 
== meaning of chapter ID ==
{{{
+
<pre>
 
mysql> SELECT * FROM Chapter;
 
mysql> SELECT * FROM Chapter;
 
+----+--------+-----------------------------+
 
+----+--------+-----------------------------+
Line 331: Line 289:
 
+----+--------+-----------------------------+
 
+----+--------+-----------------------------+
 
17 rows in set (0.00 sec)
 
17 rows in set (0.00 sec)
}}}
+
</pre>
  
 
== wikilinks ==
 
== 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.
+
It is possible to render a record as an URL:
 +
<pre>[[ something | http://somewhere/ ]]</pre>
 +
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 ==
 
== 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:
 
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:
{{{
+
<pre>
 
     719 => array ('chapter_id' => 24, 'dict_value' => '[[Cisco%GPASS%ASR 1006 | http://cisco.com/en/US/products/ps9438/index.html]]'),
 
     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]]'),
 
     720 => array ('chapter_id' => 13, 'dict_value' => '[[BSD%GSKIP%OpenBSD 3.3 | http://openbsd.org/33.html]]'),
}}}
+
</pre>
 +
 
 +
[[Image:dictionary-G-markers.png]]
 +
 
 +
== How to pick dictionary changes up between the releases ==
 +
This is sometimes needed to keep git working copies up to date. There is no GUI for this to avoid confusion to the users of normal releases.
 +
<pre>
 +
<?php
 +
 
 +
$script_mode = TRUE;
 +
require_once '/path/to/racktables/wwwroot/inc/init.php';
 +
require_once '/path/to/racktables/wwwroot/inc/dictionary.php';
 +
require_once '/path/to/racktables/wwwroot/inc/upgrade.php';
 +
 
 +
executeUpgradeBatch ('dictionary');
 +
 
 +
?>
 +
</pre>
 +
 
 +
= 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:
 +
<pre>
 +
<?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);
 +
}
 +
 
 +
?>
 +
</pre>
 +
 
 +
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 plugins/test-report.php file:
 +
<pre>
 +
<?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>';
 +
}
 +
 
 +
?>
 +
</pre>
 +
 
 +
2. To install your report into RackTables, place the file in the plugins directory.
 +
 
 +
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:
 +
<pre>
 +
Moscow switches HW types report (2)
 +
*  Cisco Catalyst 2960G-24PC - <a>1 devices</a>
 +
*  Cisco Catalyst 2960G-24TC - <a>1 devices</a>
 +
</pre>
 +
 
 +
= 802.1Q internals =
 +
== execution of "pull-only" and "pull+push" sync requests ==
 +
[[Image:RackTables-8021Q-sync.png]]
 +
 
 +
 
 +
= SNMP sync =
 +
== Overview ==
 +
The feature evolved as a procedure intended to be run once for any given device. The main procedure is built around the ifTable SNMP tree, which belongs to IF-MIB (very old and ubiquitous one). With regard to network switches, everything is done in the doSwitchSNMPmining() function, located in inc/snmp.php.
 +
 
 +
The device is polled using the FQDN (hostname or IP/IPv6) attribute, if set.  If not, assigned IP addresses will be polled.
 +
 
 +
First, subsets of ifTable are taken and combined in a way to produce a PHP array of all interfaces (with MAC address info for each interface).
 +
 
 +
Then, for each item in the interface list, a "processor" is tried from the list of processors (which is known for each supported product number, that is, SNMP OID). This way, there is a list of "processor" items and a list of known switches, where each item uses one or more processors to process the list of interfaces. There is also a vendor-specific procedure for things like serial number or console port type.
 +
 
 +
Each processor is built as follows:
 +
* 'pattern' stands for PCRE pattern, which is tried against interface name (ifDescr in SNMP).
 +
* 'replacement' is a PCRE replacement for it (most often interface names need to be compressed into something of reasonable length, e.g. 'GigabitEthernet1/2/3' -> 'gi1/2/3').
 +
* 'label' stands for port's visible label ('replacement' was for port's interface name, i.e. the one which switch's operating system uses) in SQL it is Port.label.
 +
* 'dict_key' stands for a special value which can be defined in one of two forms: either "N2" or "N1-N2" (in the former case N1 is taken to be equal to 1).
 +
 
 +
N1 stands for IIF ID, its valid values are stored in the PortInnerInterface table.
 +
N2 stands for OIF ID, its values are stored in Dictionary chapter 2.
 +
 
 +
For example, the most popular interface, "hardwired 1000Base-T" may be written as either 24 or '1-24'.
 +
For an "empty XENPAK" port, use '5-1079'.
 +
IIF stands for "Inner InterFace" and OIF stands for "Outer InterFace"
 +
In the Port table, IIF ID is stored in the "iif_id" column and OIF ID in the "type" column.
 +
this way the description of "dict_key" in a processor item
 +
 
 +
The 'known_switches' array is used to map processors and other information to specific switch models.
 +
* 'dict_key' is the dictionary ID which represents the switch model.  The list of known models is defined in dictionary.php.
 +
* 'text' is a basic description of the device that is presented to the user after discovery is complete.
 +
* 'processors' lists which processors are applied to the device.
 +
* 'ifDescrOID' is an optional attribute used in cases where the SNMP ifDescr data does not include unique values (e.g. all interfaces are named 'Ethernet Interface').  It specifies the name of the table which does indeed contain unique interface names.
 +
* 'try_next_proc' controls the way different processors are applied.  It only refers to if the PCRE pattern has matched or not. In the case of TRUE, the port is created as prescribed and processing of the current interface goes on with the next processor item.  In the case of FALSE, the port is added and no more processor items are tried for the current interface (procedure goes on to the next interface).
 +
 
 +
When it comes to adding support for another switch, the best route is to study the device to find out which physical ports it has and how they are represented in SNMP. In particular, 'try_next_proc' is used for combo ports (ones allowing either copper or SFP media under the same OS interface).  The work is much easier when some other device from the same product line already exists in snmp.php. Some product lines only have different OIDs and dict_key values, while everything else is the same (e.g. a switch which has PoE ports and one which does not).
 +
 
 +
=== Environmental requirements ===
 +
This function uses symbolic OIDs to access the devices which in turn requires base MIBs to be installed and configured properly. If they're not you'll get ''"Fatal SNMP error"'' on the webpage and ''"PHP Warning: snmp2_get(): Invalid object identifier: sysObjectID.0 in /var/www/rt/inc/snmp.php"'' in the webserver error log.
 +
 
 +
In case of ''Debian GNU/Linux'' (squeeze and up), for example, this means that apart from <tt>php5-snmp</tt> there must be <tt>snmp-mibs-downloader</tt> installed, the MIBs downloaded and <tt>/etc/snmp/snmp.conf</tt> edited to allow their use. Do not forget to restart php (apache) after the changes.
 +
 
 +
== Example 1: Arista 7124S ==
 +
This is a switch which has 24 SFP+ ports, two management ports and one power supply.
 +
 
 +
SNMP returns the following names for the ifDescr table:
 +
* Ethernet1
 +
* Ethernet2
 +
* ...
 +
* Ethernet24
 +
* Management1
 +
* Management2
 +
 
 +
Add to dictionary.php:
 +
<pre>
 +
1610 => array ('chapter_id' => 12, 'dict_value' => 'Arista%GPASS%7124S'),
 +
</pre>
 +
 
 +
Add to snmp.php:
 +
<pre>
 +
$iftable_processors['arista-any-SFP+'] = array
 +
(
 +
'pattern' => '@^Ethernet([[:digit:]]+)$@',
 +
'replacement' => '\\1',
 +
'dict_key' => '9-1084',
 +
'label' => '\\1',
 +
'try_next_proc' => FALSE,
 +
);
 +
 
 +
$iftable_processors['arista-management'] = array
 +
(
 +
'pattern' => '@^Management(1|2)$@',
 +
'replacement' => 'mgmt\\1',
 +
'dict_key' => '1-24',
 +
'label' => 'Management',
 +
'try_next_proc' => FALSE,
 +
);
 +
</pre>
  
[[Image(dictionary-G-markers.png, nolink)]]
+
<pre>
 +
'30065.1.3011.7124.3282' => array
 +
(
 +
'dict_key' => 1610,
 +
'text' => 'DCS-7124S: 24 SFP+/10000',
 +
'processors' => array ('arista-any-SFP+', 'arista-management'),
 +
),
 +
</pre>

Latest revision as of 19:50, 17 April 2020

RackTables-development-roadmap-2019Q4.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

?>

Searching for the appropriate library function

The easy way to determine which function does what you need to is analyze how the corresponding web interface request is processed.

Each modification through web causes HTTP POST request being sent with at least these base parameters: 'page', 'tab', 'op'. There is the $ophandler array in wwwroot/inc/navigation.php filled with function names for each triplet of base web operation parameters.

Then, when you determine the handler function name for you operation (like object creating), you could examine its code in wwwroot/inc/ophandlers.php. It usually calls appropriate functions from database.php.

Also, there are some trivial operations which have no special ophandler function. Then the universal database wrapper called tableHandler takes place. It makes direct SQL queries, building them by $opspec_list array data.


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

How to pick dictionary changes up between the releases

This is sometimes needed to keep git working copies up to date. There is no GUI for this to avoid confusion to the users of normal releases.

<?php

$script_mode = TRUE;
require_once '/path/to/racktables/wwwroot/inc/init.php';
require_once '/path/to/racktables/wwwroot/inc/dictionary.php';
require_once '/path/to/racktables/wwwroot/inc/upgrade.php';

executeUpgradeBatch ('dictionary');

?>

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 plugins/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, place the file in the plugins directory.

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


SNMP sync

Overview

The feature evolved as a procedure intended to be run once for any given device. The main procedure is built around the ifTable SNMP tree, which belongs to IF-MIB (very old and ubiquitous one). With regard to network switches, everything is done in the doSwitchSNMPmining() function, located in inc/snmp.php.

The device is polled using the FQDN (hostname or IP/IPv6) attribute, if set. If not, assigned IP addresses will be polled.

First, subsets of ifTable are taken and combined in a way to produce a PHP array of all interfaces (with MAC address info for each interface).

Then, for each item in the interface list, a "processor" is tried from the list of processors (which is known for each supported product number, that is, SNMP OID). This way, there is a list of "processor" items and a list of known switches, where each item uses one or more processors to process the list of interfaces. There is also a vendor-specific procedure for things like serial number or console port type.

Each processor is built as follows:

  • 'pattern' stands for PCRE pattern, which is tried against interface name (ifDescr in SNMP).
  • 'replacement' is a PCRE replacement for it (most often interface names need to be compressed into something of reasonable length, e.g. 'GigabitEthernet1/2/3' -> 'gi1/2/3').
  • 'label' stands for port's visible label ('replacement' was for port's interface name, i.e. the one which switch's operating system uses) in SQL it is Port.label.
  • 'dict_key' stands for a special value which can be defined in one of two forms: either "N2" or "N1-N2" (in the former case N1 is taken to be equal to 1).

N1 stands for IIF ID, its valid values are stored in the PortInnerInterface table. N2 stands for OIF ID, its values are stored in Dictionary chapter 2.

For example, the most popular interface, "hardwired 1000Base-T" may be written as either 24 or '1-24'. For an "empty XENPAK" port, use '5-1079'. IIF stands for "Inner InterFace" and OIF stands for "Outer InterFace" In the Port table, IIF ID is stored in the "iif_id" column and OIF ID in the "type" column. this way the description of "dict_key" in a processor item

The 'known_switches' array is used to map processors and other information to specific switch models.

  • 'dict_key' is the dictionary ID which represents the switch model. The list of known models is defined in dictionary.php.
  • 'text' is a basic description of the device that is presented to the user after discovery is complete.
  • 'processors' lists which processors are applied to the device.
  • 'ifDescrOID' is an optional attribute used in cases where the SNMP ifDescr data does not include unique values (e.g. all interfaces are named 'Ethernet Interface'). It specifies the name of the table which does indeed contain unique interface names.
  • 'try_next_proc' controls the way different processors are applied. It only refers to if the PCRE pattern has matched or not. In the case of TRUE, the port is created as prescribed and processing of the current interface goes on with the next processor item. In the case of FALSE, the port is added and no more processor items are tried for the current interface (procedure goes on to the next interface).

When it comes to adding support for another switch, the best route is to study the device to find out which physical ports it has and how they are represented in SNMP. In particular, 'try_next_proc' is used for combo ports (ones allowing either copper or SFP media under the same OS interface). The work is much easier when some other device from the same product line already exists in snmp.php. Some product lines only have different OIDs and dict_key values, while everything else is the same (e.g. a switch which has PoE ports and one which does not).

Environmental requirements

This function uses symbolic OIDs to access the devices which in turn requires base MIBs to be installed and configured properly. If they're not you'll get "Fatal SNMP error" on the webpage and "PHP Warning: snmp2_get(): Invalid object identifier: sysObjectID.0 in /var/www/rt/inc/snmp.php" in the webserver error log.

In case of Debian GNU/Linux (squeeze and up), for example, this means that apart from php5-snmp there must be snmp-mibs-downloader installed, the MIBs downloaded and /etc/snmp/snmp.conf edited to allow their use. Do not forget to restart php (apache) after the changes.

Example 1: Arista 7124S

This is a switch which has 24 SFP+ ports, two management ports and one power supply.

SNMP returns the following names for the ifDescr table:

  • Ethernet1
  • Ethernet2
  • ...
  • Ethernet24
  • Management1
  • Management2

Add to dictionary.php:

1610 => array ('chapter_id' => 12, 'dict_value' => 'Arista%GPASS%7124S'),

Add to snmp.php:

$iftable_processors['arista-any-SFP+'] = array
(
	'pattern' => '@^Ethernet([[:digit:]]+)$@',
	'replacement' => '\\1',
	'dict_key' => '9-1084',
	'label' => '\\1',
	'try_next_proc' => FALSE,
);

$iftable_processors['arista-management'] = array
(
	'pattern' => '@^Management(1|2)$@',
	'replacement' => 'mgmt\\1',
	'dict_key' => '1-24',
	'label' => 'Management',
	'try_next_proc' => FALSE,
);
	'30065.1.3011.7124.3282' => array
	(
		'dict_key' => 1610,
		'text' => 'DCS-7124S: 24 SFP+/10000',
		'processors' => array ('arista-any-SFP+', 'arista-management'),
	),