3. Tutorial

This chapter guides you through the most important features of slb and explains its configuration. Start here if you have never used slb before.

It omits some complicated details and rarely used features, which will be discussed further, in SLB Configuration File.

3.1 Option Basics

Options start with a dash. Most of them have two forms, called short and long forms. Both forms are absolutely identical in function; they are interchangeable.

The short form is a traditional form for UNIX utilities. In this form, the option consists of a single dash, followed by a single letter, e.g. ‘-c’.

Short options which require arguments take their arguments immediately following the option letter, optionally separated by white space. For example, you might write ‘-o name’, or ‘-oname’. Here, ‘-o’ is the option, and ‘name’ is its argument.

Short options' letters may be clumped together, but you are not required to do this. When short options are clumped as a set, use one (single) dash for them all, e.g. ‘-ne’ is equivalent to ‘-n -e’. However, only options that do not take arguments may be clustered this way. If an option takes an argument, it can only be the last option in such a cluster, otherwise it would be impossible to specify the argument for it. Anyway, it is much more readable to specify such options separated.

The long options consist of two dashes, followed by a multi-letter option name, which is usually selected to be a mnemonics for the operation it requests. Long option names can be abbreviated, provided that such an abbreviation is unique among the options understood by the program.

Arguments to long options follow the option name being separated from it either by an equal sign, or by any amount of white space characters. For example, the option ‘--eval’ with the argument ‘test’ can be written either as ‘--eval=test’ or as ‘--eval test’.

3.2 Configuration Basics

The configuration file defines most parameters needed for the normal operation of slb. The program will not start if its configuration file does not exist, cannot be read, or contains some errors.

The configuration file is located in your system configuration directory (normally ‘/etc’) and is named ‘slb.conf’. You can place it elsewhere as well, but in this case you will need to explicitly inform slb about its actual location, using the ‘--config-file’ (‘-c’) command line option:

 
slb --config-file ./new.conf

Before actually starting the program, it is wise to check the configuration file for errors. To do so, use the ‘--lint’ (‘-t’) command line option:

 
slb --lint

When started with this option, slb parses the configuration, reports any errors on the standard error and exits. If parsing succeeds, it exits with code 0. Otherwise, if any errors have been found, it exits with code 78 (configuration error).

The ‘--lint’ option can, of course, be used together with ‘--config-file’, e.g.:

 
slb --lint --config-file ./new.conf

If you are unsure about the correct configuration syntax, you can obtain a concise summary any time, by running:

 
slb --config-help

The summary is printed to the standard output and includes all configuration statements with short descriptions of their purpose and arguments.

In this section we will provide a quick start introduction to the slb configuration. For a more detailed and formal discussion, refer to SLB Configuration File.

The configuration file consists of statements. There are two kinds of statements, called simple and block statements. A simple statement consists of a keyword and value, or values, separated by any amount of whitespace and terminated with a semicolon, for example:

 
wakeup 15;

A block statement is used for logical grouping of other statements. It consists of a keyword, optionally followed by a value, and a set of other statements, enclosed in a pair of curly brackets. For example:

 
syslog {
  facility local1;
  tag slb;
}

A semicolon may follow the closing ‘}’, although this is not required.

Note that whitespace (i.e. space characters, tabs and newlines) has no special syntactical meaning, except that it serves to separate otherwise adjacent tokens. For example, the following form of the ‘syslog’ statement is entirely equivalent to the one shown above:

 
syslog { facility local1;   tag  slb; }

Several types of comments are supported. A single-line comment starts with ‘#’ or ‘//’ and continues to the end of the line. A multi-line or C-style comment starts with the two characters ‘/*’ (slash, star) and continues until the first occurrence of ‘*/’ (star, slash). Whatever comment type are used, they are removed from the configuration prior to parsing it.

After comment removal, the configuration is preprocessed using m4. This is a highly useful feature, which allows for considerable simplification of configuration files. It is described in Preprocessing with m4.

3.3 Daemon Configuration

The most important daemon parameters are the operation mode and wakeup interval. You are not required to explicitly configure them, but you may want to do so, if their default values don't suit you.

There are two operation modes: ‘standalone’ and ‘cron’. In standalone mode, slb detaches itself from the controlling terminal and continues operation in the background, as a daemon. It will periodically wake up, poll the servers, compute load table and format it to the output file or pipe. This is the default operation mode.

In cron mode, slb starts, polls the servers, computes and formats load table and exits when done. This mode is designed for use from crontabs, hence its name. It has some limitation, compared to the standalone mode. Most notably, the d() function (derivative) is not available in this mode.

If you wish to explicitly set the operation mode, use the ‘standalone’ configuration statement. The statement:

 
standalone yes;

configures the standalone mode, whereas the statement:

 
standalone no;

configures cron mode. The later can also be requested from the command line, using the ‘--cron’ option.

When operating in standalone mode, the wakeup interval specifies the amount of time, in seconds, between successive polls. The default value is 5 minutes. To set another value, use the ‘wakeup’ statement. For example, the following configures slb to do a poll each minute:

 
wakeup 60;

3.4 SNMP Configuration

Normally, this does not require additional configuration, unless you want to load some custom MIBs.

To load an additional MIB file, use the ‘add-mib’ statement. Its argument is the name of the file to load.

 
add-mib "MY-MIBS.txt";

Unless full pathname is specified, the file is searched in the SNMP search path. To modify the search path, use the ‘mib-directory’. Each ‘mib-directory’ adds a single directory to the end of the path:

 
mib-directory "/usr/share/slb/mibs";

3.5 Server Definition

Server definitions are principal part of any slb configurations. They define remote servers for which slb is to compute the load table. A server definition is a block statement which begins as:

 
server id {

The id argument specifies server ID, an arbitrary string of characters uniquely identifying this server. This ID will be used by log messages concerning this server. It can also be referred to in the output format definition (see section Output Configuration).

As usual, the definition ends with a closing ‘}’.

Statements inside the ‘server’ block supply configuration parameters for this server. Two parameters are mandatory for each server: its IP address and SNMP community:

 
  host ip;
  community string;

The ip argument can be either the IP address of the server in dotted-quad notation, or its host name. For example:

 
  host 192.168.10.6;
  community "public";

The most important part of a server definition is its load estimation function. It is basically an arithmetic expression of arbitrary complexity, with SNMP variables serving as its terms (see section Expression). It is defined using the ‘expression’ statement.

To begin with, suppose you want to use 1-minute load average as relative load. Let's name it ‘la1’. Then, the expression is very simple and can be defined as:

 
  expression "la1";

Now, ‘la1’ is a variable name, which should be bound to the corresponding SNMP variable. This binding is declared with the ‘variable’ statement:

 
  variable la1 "UCD-SNMP-MIB::laLoadFloat.1";

The ‘variable’ statement has two arguments. The first one is the name of a variable used in the expression and the second one is the SNMP OID which is bound to this variable. This OID is added to the list of OIDs queried during each poll of this server. When a SNMP reply is received, all instances of that variable in the expression are replaced with the value of the corresponding SNMP variable. Once all variables have been thus resolved, the expression is evaluated and its result is taken as the relative load for the given server.

Let's take a more complex example. Suppose you define the relative load to be a function of outgoing data transfer rate through the main network interface and the load average of the server. Data transfer rate is defined as first derivative of data transfer through the interface with respect to time. Let ‘out’ be the outgoing data transfer, ‘la1’ be the server's 1-minute load average, ‘k’ and ‘m’ be two server-dependent constants (weight coefficients). Given that, we define relative load as

 
  expression "sqrt(k * d(out)**2 + m * la1**2)";

The ‘**’ operator raises its left operand to the power given by its second operand. The two constructs ‘d(…)’ and ‘sqrt(…)’ are function calls. The ‘d()’ function computes first derivative of its argument with respect to time. The ‘sqrt()’ function computes the square root of its argument.

Now, let's define variable bindings:

 
  variable out "IF-MIB::ifOutOctets.3";
  variable la1 "UCD-SNMP-MIB::laLoadFloat.1";

The constantsk’ and ‘m’ are defined using the following statements:

 
  constant k 1.5;
  constant m 1.8;

The ‘constant’ statement is similar to ‘variable’, except that its second argument must be a floating-point number. Of course, in this particular example, the two constants could have been placed directly in the expression text, as in:

 
  expression "sqrt(1.5 * d(out)**2 + 1.8 * la1**2)";

However, defining them on a per-server basis is useful when the same expression is used for several different servers, as explained in the following section.

To conclude, here is our sample server definition:

 
server srv01 {
  host 192.168.10.6;
  community "public";
  expression "sqrt(k * d(out)**2 + m * la1**2)";
  variable out "IF-MIB::ifOutOctets.3";
  variable la1 "UCD-SNMP-MIB::laLoadFloat.1";
  constant k 1.5;
  constant m 1.8;
}

3.6 Named Expressions

In most cases, load estimation function is common for all servers (perhaps with server-dependent set of coefficients and/or variable bindings). To simplify configuration, such a common function can be defined once and then used by another ‘server’ definitions. It is defined as a named expression, i.e. an expression that can be referred to by its name.

Named expressions are defined using the ‘expression’ statement outside of ‘server’ block. Such statements take two arguments: the name of the expression and its definition, e.g.:

 
expression load "sqrt(k * d(out)**2 + m * la1**2)";

Named expressions can be invoked from another expressions using the ‘@’ operator, followed by expression's name. For example, the following statement in ‘server’ block:

 
  expression "@load";

is in fact equivalent to the expression defined above. The ‘@’ construct can, of course, also be used as a term in arithmetic calculations, e.g.:

 
  expression "2 * @load + 1";

A default expression is a named expression which is implicitly applied for ‘server’ statements that lack ‘expression’ statement. Default expression is defined using the ‘default-expression’ statement:

 
default-expression "load";

To finish this section, here is an example configuration declaring two servers which use the same default expression, but supply different sets of coefficients:

 
expression load "sqrt(k * d(out)**2 + m * la1**2)";
default-expression "load";

server srv01 {
  host 192.168.10.6;
  community "public";
  variable out "IF-MIB::ifOutOctets.3";
  variable la1 "UCD-SNMP-MIB::laLoadFloat.1";
  constant k 1.5;
  constant m 1.8;
}

server srv02 {
  host 192.168.10.1;
  community "private";
  variable out "IF-MIB::ifOutOctets.3";
  variable la1 "UCD-SNMP-MIB::laLoadFloat.1";
  constant k 2.5;
  constant m 2.2;
}

3.7 Preprocessing with m4

In the previous section we have seen how to define two servers which use the same expression to compute relative load. In real configurations, the number of servers is likely to be considerably greater than that, and adding each of them to the configuration soon becomes a tedious and error-prone task. This task is greatly simplified by the use of preprocessor. As mentioned earlier, the configuration file is preprocessed using m4, a traditional UNIX macro processor. It provides a powerful framework for implementing complex slb configurations.

For example, note that in our sample configuration the ‘server’ statements differ by only three values: the server ID, its IP and community. Taking this into account, we may define the following m4 macro:

 
m4_define(`defsrv',`server $1 {
  host $2;
  community "m4_ifelse(`$3',,`public',`$3')";
  variable out "IF-MIB::ifOutOctets.3";
  variable la1 "UCD-SNMP-MIB::laLoadFloat.1";
  constant k 2.5;
  constant m 2.2;
}')

The ‘defsrv’ macro takes two mandatory and one optional argument. Mandatory arguments are the server ID and its IP address. Optional argument is SNMP community; if it is absent, the default community ‘public’ is used.

Notice, that we use m4_define, instead of the familiar define. It is because the default slb setup renames all m4 built-in macro names so they all start with the prefix ‘m4_’. This avoids possible name clashes and makes preprocessor statements clearly visible in the configuration.

Using this new macro, the above configuration is reduced to the following two lines:

 
defsrv(srv01, 192.168.10.6)
defsrv(srv02, 192.168.10.1, private)

Declaring a new server is now a matter of adding a single line of text.

The default preprocessor setup defines a set of useful macros, among them m4_foreach (see (m4)Improved foreach section `foreach' in GNU M4 macro processor). This macro can be used to further simplify the configuration, as shown in the example below:

 
m4_foreach(args,
 ``srv01, 192.168.10.6',
  `srv02, 192.168.10.1, private',
  `srv03, 192.168.0.3',
  `srv04, 192.168.100.1, foo',
  `srv05, 192.168.100.2, bar'',
 `defserv(args)
')

The second argument to m4_foreach is a comma-separated list of values. The expansion is as follows: for each value from this list, the value is assigned to the first argument (‘args’). Then the third argument is expanded and the result is appended to the overall expansion.

In this particular example, each line produces an expansion of the ‘defserv’ macro with the arguments taken from that line. Note, that each argument in the list must be quoted, because it contains commas. Note also the use of the optional third arguments to supply community names that differ from the default one.

For a detailed information about slb preprocessor feature, see Preprocessor.

3.8 Output

When slb has finished building load table, it sends it to the output, line by line, using the output format string. This format string is similar to the format argument of printf(1): any characters, except ‘%’ are copied to the output verbatim; the ‘%’ introduces a conversion specifier. For example, ‘%i’ expands to the ID of the server this line refers to. The ‘%w’ specifier expands to the computed relative load for that server, etc. There is a number of such specifiers, they are all described in detail in Output Format String.

The default output format is ‘%i %w\n’. This produces a table, each line of which shows a server ID and its relative load. This default is suitable for testing purposes, but for real configurations you will most probably need to create a custom output format. You do so using the ‘output-format’ statement:

 
output-format "id=%i host=%h\n";

The output format string can be quite complex as shown in the following example:

 
output-format <<EOT
server 10.0.0.1
update add www.example.net 60 IN A %h
send
EOT;

This format creates, for each line from the load table, a set of commands for nsupdate(1). The ‘<<’ block introduces a here-document, a construct similar to that of shell and several other programming languages. It is discussed in here-document.

You may need to include server-dependent data in the corresponding output lines. For this purpose, slb provides server macros. Server macros are special text variables, defined in the ‘server’ block, which can be accessed from output format string.

Macros are defined using the ‘macro’ statement, whose syntax is similar to that of ‘variable’ or ‘constant’. The first argument supplies the name of the macro, and the second one, its contents, or expansion, e.g.:

 
server srv01 {
  host 192.168.10.6;
  community "public";
  macro description "some descriptive text";
}

Macros are accessed using the following conversion specifier:

 
%(name)

where name is the name of the macro. This specifier is replaced with the actual contents of the macro. The following example uses the ‘description’ macro to create a TXT record in the DNS:

 
output-format <<EOT
server 10.0.0.1
update add www.example.net 60 IN A %h
update add %i.example.net 60 IN TXT "%(description)"
send
EOT;

Sometimes you may need to produce some output immediately before the load table or after it. Two configuration statements are provided for that purpose: ‘begin-output-message’ and ‘end-output-message’. Both take a single string as argument. The string supplied with ‘begin-output-message’ is output before formatting the load table, and the one supplied with ‘end-output-message’ is output after formatting it.

Continuing our ‘nsupdate’ example, the following statement will remove all existing A records from the DNS prior to creating new ones:

 
begin-output-message <<EOT
server 10.0.0.1
update delete www.example.net
send
EOT;

You may want to output only a part of the load table. Two statements are provided for this purpose. The ‘head N’ statement outputs at most N entries from the top of the table. The ‘tail N’ statement outputs at most N entries from the bottom of the table. For example, to print only one entry corresponding to the least loaded server, use

 
head 1;

By default slb prints results to the standard output. To change this, use the ‘output-file’ statement. Its argument specifies the name of a file where slb output will be directed. If this name starts with a pipe character (‘|’), slb treats the remaining characters as the shell command. This command is executed (using /bin/sh -c), and the output is piped to its standard input. Thus, the following statement

 
output-file "| /usr/bin/nsupdate "
            "-k /usr/share/slb/Kvpn.+157+45756.private";

directs the formatted output to the standard input of nsupdate command.

3.9 Test Mode

The slb configuration can be quite complex, therefore it is important to verify that it behaves as expected before actually implementing it in production. Three command line options are provided for that purpose.

The ‘--dry-run’ (or ‘-n’) option instructs the program to start in foreground mode and print to the standard output what would have otherwise been printed to the output file. Additional debugging information is displayed on the standard error.

The ‘--eval’ option initiates expression evaluation mode. In this mode slb evaluates the expression whose name is supplied as argument to the option. Actual values of the variables and constants used in the expression are supplied in the command line in form of variable assignments. The result is printed to the standard output. For example, if the configuration file contains

 
expression load "(la1/100 + usr/1024)/2";

then the command

 
slb --eval=load la1=30 usr=800

will print ‘.540625’.

If the expression contains calls to ‘d()’ (derivative), you will need several evaluations to compute its value. The minimal number of evaluations equals the order of the highest derivative computed by the expression, plus 1. Thus, computing the following expression:

 
expression load "sqrt(k * d(out)**2 + m * la1**2)";

requires at least two evaluations. To supply several values to a single variable, separate them with commas, e.g.: ‘out=16000,20000’. This will run first evaluation with ‘out=16000’ and the second one with ‘out=20000’. For example:

 
$ slb --eval=load k=1.5 m=3 out=16000,20000 la1=0.4
16.3446

Notice, that you don't need to supply the same number of values for each variable. If a variable is assigned a single value, this value will be used in all evaluations.

A more elaborate test facility is enabled by the ‘--test’ option. This option instructs slb to read SNMP variables and their values from a file and act as if they were returned by SNMP. The output is directed to the standard output, unless the ‘--output-file’ option is also given.

The input file name is taken from the first non-option argument. If it is ‘-’ (a dash) or if no arguments are given, the standard input will be read:

 
slb --test input.slb

The input file consists of sections separated by exactly one empty line. A section contains assignments to SNMP variables in the style of snmpset(2). Such assignments form several server groups, each group being preceded by a single word on a line, followed by a semicolon. This word indicates the ID of the server to which the data in the group belong. For example:

 
srv01:
UCD-SNMP-MIB::laLoadFloat.1 F 0.080000
IF-MIB::ifOutUcastPkts.3 c 346120120
srv02:
UCD-SNMP-MIB::laLoadFloat.1 F 0.020000
IF-MIB::ifOutUcastPkts.3 c 2357911693

This section supplies data for two servers, named ‘srv01’ and ‘srv02’.

Footnotes

(1)

See http://www.manpagez.com/man/8/nsupdate.

(2)

See http://www.manpagez.com/man/1/snmpset.