Testvar Constraints

Testvar Constraints

This section describes a new testvar property constraints for use in calls to buddy::testvar_define in a testpath’s configuration dictionary (config.dic):

buddy::testvar_define TESTVAR {
    type { ... }
    description {
        ...
    }
    constraints {
        SCRIPT...
    }
}

A constraint property is a Tcl script which is evaluated inside a special namespace which contains commands to make testvar validation easier. Constraints can be written to always apply, apply only if a testvar is defined/undefined, or apply only if a testvar does/doesn’t have certain values.

Testvar constraints are validated immediately after testvar type validation occurs (see the type property in Testvar Properties). Constraint validation is applied to each defined testvar group (i.e. main, wan2, lan2, etc.). For each defined group, the constraint validation code iterates over each testvar defined in the configuration dictionary. The testvar is skipped if the testvar is not relevant in the current testvar group (see the relevant-groups testvar property) or if the testvar does not have a constraints property. Otherwise, the value of the constraints property is evaluated as Tcl script. If the evaluated script raises an error, the error is caught and the reason is displayed to the user and no further validation is performed.

The following pseudo-code summarizes the above paragraph:

proc buddy::testvar_constraints_process { } {
    foreach g [ defined_testvar_groups ] {
        foreach tv [ testvars_defined_in_config_dic ] {
            if { ![ group_is_relevant $tv $g ] } { continue }
            if { ![ testvar_has_constraints_property $tv ] } { continue }
            if { [ catch { eval $constraint_property } reason ] } {
                puts "Error during configuration file test constraint validation"
                puts "$reason"
            }
        }
    }
}

Conditional Commands

Since they are commands like any other Tcl commands, conditional commands can be combined in complex boolean expressions using Tcl’s && and || operators. The example below shows how to use conditions:

buddy::testvar_define TESTVAR TYPE {
    constraints {
        # true if lanIp exists and lanMask's value is 255.255.255.0
        if { [ exists lanIp ] && [ values lanMask 255.255.255.0 ] } {
            CONSTRAINTS...
        }

        # true if lanIp is undefined or lanIp's value is 192.168.1.1
        if { ![ exists lanIp ] || [ values lanIp 192.168.1.1 ] } {
            CONSTRAINTS...
        }
    }
}

The following commands can be used inside a constraint property to apply constraints based on certain conditions:

Returns true if a testvar is defined

exists TESTVAR

Returns true if a testvar exists and has certain values

values TESTVAR VALUES...

Constraint Commands

Constraint commands impose restrictions on testvars which must be true for the configuration file to be considered valid. A constraint can restrict a testvar to a specific data type, to be defined/undefined, or to have certain values.

Constraint commands raise a Tcl exception if their restrictions are not met. The reason string accompanying the exception provides an error message suitable for displaying to the user (using the config_error command, see below). If not caught earlier, cdrouter-cli will catch the exception, display an error message and abort. The example below shows how to use constraint commands:

buddy::testvar_define TESTVAR TYPE {
    constraints {
        # fail if testvar acsAuth/acsCertPath exists
        must_not_exist acsAuth acsCertPath

        if { [ values lanMask 255.255.255.0 ] } {
            # fail if lanIp is undefined
            must_exist lanIp
            # fail if lanIp is not 192.168.1.1 or 192.168.1.2
            valid_values lanIp 192.168.1.1 192.168.1.2
        }
    }
}

Value tokens (VALUE/VALUES...) wrapped in curly braces are matched as Tcl regular expressions using the regexp command. For example:

if { [ values lanInterface {eth[13]} eth8 ] } {
    invalid_values wanIspAssignIp "192.168.200.99" {192.168.2.*\.2}
}

if testvar lanInterface is set to eth1, eth3 or eth8, the invalid_values constraint will be evaluated. Similarly, an error will be thrown if the invalid_values constraint is evaluated and the testvar wanIspAssignIp has a value of 192.168.200.99, 192.168.200.112 or 192.168.20.2.

Below are special commands which can be used inside a constraint property:

must_be

Fail if testvar is not one of the given data types.

must_be TESTVAR TYPES...

must_not_be

Fail if testvar is any of the given types.

must_not_be TESTVAR TYPES...

The must_be and must_not_be constraints use the same data type names as the type testvar property. For example, the following constraint fails if dnsIp is not an IPv4 address:

must_be dnsIp IPv4

must_exist

Fail if any testvar doesn’t exist

must_exist TESTVARS...

must_not_exist

Fail if any testvar exists

must_not_exist TESTVARS...

valid_values

Fail if testvar doesn’t have certain values

valid_values TESTVAR VALUES...

invalid_values

Fail if testvar has certain values

invalid_values TESTVAR VALUES...

unique_values

Ail if all testvar values are not unique

unique_values TESTVARS...

same_values

Fail if all testvar values are not the same

same_values TESTVARS...

unique_networks

Fail if all networks are not unique

unique_networks [ ADDR/MASK | ADDR/LEN | TESTVAR/TESTVAR | TESTVAR ]...

same_networks

Fail if all networks are not the same

same_networks [ ADDR/MASK | ADDR/LEN | TESTVAR/TESTVAR | TESTVAR ]...

file_readable

Fail if file does not exist or is not readable

file_readable FILE

file_max_size

Fail if file does not exist or is greater than given max. Max is bytes by default, append k/kb, m/mb or g/gb for kilobytes, megabytes or gigabytes, respectively

file_max_size FILE MAX

directory_exists

Fail if path does not exist or is not a directory

directory_exists PATH

valid_address

Fails if testvar’s value is not an address in the network defined by the given testvar property. Property should take the form lan/wan [ address/prefix-len ]. Also fails if any LAN network overlaps with any WAN network and vice versa. If not given, PROPERTY-NAME defaults to network. If PROPERTY-NAME is dynamic, network spec is instead taken from the optional DYNAMIC-SPEC argument.

valid_address TESTVAR [ PROPERTY-NAME/dynamic ] [ DYNAMIC-SPEC ]

valid_addresses

Fails if testvar’s value is not an address in one the networks defined by the given testvar property.

valid_addresses TESTVAR [ PROPERTY-NAME / dynamic DYNAMIC-SPEC ]...

valid_mac

Fails if testvar’s MAC address value is not unique across all other testvars with a type property of mac-address.

valid_mac TESTVAR

valid_root_cert

Fails if the certificate file given by testvar CERT cannot be loaded as a root certificate.

valid_root_cert CERT

valid_ssl_cipher

Fails if SSL cipher CIPHER is not valid for the given SSL protocol version SSL-PROTO.

valid_ssl_cipher CIPHER SSL-PROTO

For example, the following constraint would fail if the SSL cipher RC4-SHA is not valid when using SSL version 3:

valid_ssl_cipher RC4-SHA sslv3

See testvars acsCipherSuite and acsSslVersion for valid CIPHER and SSL-PROTO values, respectively.

valid_user_cert

Fails if the certificate file given by testvar CERT cannot be loaded as a user certificate. If given, the value of testvars PASSWORD and KEY specify the password and key file to use, respectively.

valid_user_cert CERT [ PASSWORD ] [ KEY ]

Testvar Group Commands

A testvar group can be explicitly specified in testvar tokens (TESTVAR/TESTVARS...) using the syntax GROUP:TESTVAR. For example:

buddy::testvar_define acsIp keyword-list {
    constraints {
        # true is testvar acsIp in testvar group wan2 is defined
        if { [ exists wan2:acsIp ] } {
            # fails if testvar acsAuth is undefined
            must_exist acsAuth
        }
    }
}

the testvar token wan2:acsIp in the exists condition will evaluate to the value of the acsIp testvar in the wan2 testvar group. To explicitly specify the default testvar group, use the group name main. The following additional commands are useful when writing constraints involving testvar groups:

group

Returns name of testvar group currently being validated

group

groups

Returns a list of all defined group names. If optional exclusive argument is given, return all names except the testvar group currently being validated. Defaults to inclusive.

groups [ exclusive ] [ TYPE ]

where TYPE is one of:

Type Description
wan-groups main or wan<N>
lan-groups main or lan<N>
wan-groups-mp wan<N>
lan-groups-mp lan<N>
smb-groups smbuser<N>
ftp-groups ftpuser<N>
cwmp-groups cwmp_profile<N>
tr069-groups tr069_device<N>
snmp-groups snmpuser<N>

group_is

Returns true if group is a given TYPE. Matching can be done on a different group name than the one currently being validated by specifying optional GROUP argument.

group_is TYPE [ GROUP ]

where TYPE is one of:

Type Description
wan-group main or wan<N>
lan-group main or lan<N>
wan-group-mp wan<N>
lan-group-mp lan<N>
smb-group smbuser<N>
ftp-group ftpuser<N>
cwmp-group cwmp_profile<N>
tr069-group tr069_device<N>
snmp-group snmpuser<N>

The plural form of each group type is also accepted and is treated identically to the singular form. For example, wan-groups is the same as wan-group and lan-groups-mp is the same as lan-group-mp. If TYPE does not match any of the above values, it is assumed to be a regular expression and will be matched against GROUP as a regular expression using Tcl’s regexp command.

group_exists

Returns true if given group name exists

group_exists GROUP

Wildcard Testvar Commands

A few commands exist to manage wildcard testvars:

is_wildcard

Returns true if testvar is a wildcard testvar

is_wildcard TESTVAR

wildcards

Returns sorted list of defined wildcard endings

wildcards TESTVAR

wildcard_values

Returns list of the values for instances of a wildcard testvar

wildcard_values TESTVAR

The example below verifies that the defined instances of the virtualTcpServicePort wildcard testvar in testvar group wan2 start at 0 and are monotonically increasing:

if { [ is_wildcard wan2:virtualTcpServicePort ] } {
    set prev -1
    foreach ending [ wildcards wan2:virtualTcpServicePort ] {
        if { [ expr $ending != ($prev+1) ] } {
            config_error "virtualTcpServicePort must start at 0 and be monotonically increasing"
        }

        incr prev
    }
}

Additional Commands

In addition to condition and constraint commands, there are a few additional commands available:

While processing testvar constraints, the name of the testvar being defined by buddy::testvar_define will be returned by the this command:

this

Returns name of testvar currently being defined. If optional first argument is value, instead return the value of the testvar currently being defined. If first argument is value, optional second argument can be a wildcard ending in which case the value returned will be the value of the given wildcard instance of the testvar being defined.

this [ value ] [ ENDING ]

For example:

buddy::testvar_define acsAuth keyword-list {
    constraints {
        # true if testvar acsAuth is defined
        if { [ exists [ this ] ] } {
            # fails if testvar acsCertPath is undefined
            must_exist acsCertPath
            # fails if testvar acsAuth is not null, basic or digest
            valid_values [ this ] null basic digest
        }
    }
}

before the exists condition is evaluated, the this invocation will be replaced with acsAuth. Similarly, before the valid_values constraint is evaluated, the this invocation will be replaced with acsAuth.

valid

Returns true if script does not fail

valid SCRIPT [ VARNAME ]

Constraint commands can be used as conditionals by wrapping them with a call to the valid command. If the script does not raise an exception, the valid command will save the script’s return value to the second argument (if provided) and return true. If the script does raise an exception, the valid command will catch it, save the reason to the second argument (if provided), and return false. For example:

buddy::testvar_define TESTVAR TYPE {
    constraints {
        # true if lanIp is *not* an IPv6 address
        if { ![ valid { [ must_be lanIp IPv6 ] } ] } {
            # fail if supportsIPv6's value is yes
            invalid_values supportsIPv6 yes
        }
    }
}

If the lanIp testvar is not an IPv6 address, the must_be constraint command will throw an error which will be caught by the valid command causing the invalid_values constraint to be evaluated.

tv

Returns the value of TESTVAR, or DEFAULT if undefined

tv TESTVAR [ DEFAULT ]

The tv command can be useful when a constraint requires runtime values that cannot be hardcoded. For example:

buddy::testvar_define acsIp keyword-list {
    constraints {
        if { [ exists [ this ] ] } {
            valid_values acsConnReqUrl "http://[ this value ]:8080/cwmp"

            if { [ exists main:acsInterface ] } {
                unique_values "[ tv main:acsInterface ]:wanVlanId" main:wanVlanId
            }

            must_exist wanIspIp wanIspMask
            if { [ ip_same_network [ tv wanIspIp ] [ tv wanIspMask ] [ this value ] ] == 0 } {
                config_error "testvar [ display [ this ] ] can not be on the WAN network, \
                                                 please choose a different IP address."
            }
        }
    }
}

display

Return a printable (withProperCaps) string of testvars

display TESTVARS...

config_error

Raises an error including the line number of the testvar being defined. If optional final argument is print, print REASON but don’t raise an error. If optional final argument is ret, don’t raise an error and simply return REASON prefixed with line number information.

config_error REASON [ error/print/ret ]

config_help

If called before a constraint command, the text will be shown to the user in addition to the generic error message generated by the constraint command. This command can be called multiple times, in which case each call appends its text to the text of the previous invocation. Constraint commands clear the help text after they return. If the optional second argument is inhibit, the generic error message generated by the constraint command will not be shown.

config_help TEXT [ inhibit ]

cdrouter_addon

Returns true if the expansion (ipv6, snmp, storage, etc) is enabled both in license file and with the appropriate testvar (supportsIPv6, supportsSNMP, supportsStorage, etc).

cdrouter_addon ADDON