Fortinet black logo

Administration Guide

Tcl scripts

Tcl scripts

Tcl is a dynamic scripting language that extends the functionality of CLI scripting. In FortiManager Tcl scripts, the first line of the script is “#!” as it is for standard Tcl scripts.

Do not include the exit command that normally ends Tcl scripts; it will prevent the script from running.

This guide assumes you are familiar with the Tcl language and regular expressions, and instead focuses on how to use CLI commands in your Tcl scripts. Where you require more information about Tcl commands than this guide contains, please refer to resources such as the Tcl newsgroup, Tcl reference books, and the official Tcl website at https://www.tcl.tk.

Tcl scripts can do more than just get and set information. The benefits of Tcl come from:

  • variables to store information,
  • loops to repeats commands that are slightly different each time
  • decisions to compare information from the device

The sample scripts in this section will contain procedures that you can combine to use your scripts. The samples will each focus on one of four areas:

To enable Tcl scripting, use the following CLI commands:

config system admin setting

set show_tcl_script enable

end

Limitations of FortiManager Tcl

FortiManager Tcl executes in a controlled environment. You do not have to know the location of the Tcl interpreter or environment variables to execute your scripts. This also means some of the commands normally found in Tcl are not used in FortiManager Tcl.

Depending on the CLI commands you use in your Tcl scripts, you may not be able to run some scripts on some versions of FortiOS as CLI commands change periodically.

Before testing a new script on a FortiGate device, you should backup that device’s configuration and data to ensure it is not lost if the script does not work as expected.

Tcl variables

Variables allow you to store information from the FortiGate device, and use it later in the script. Arrays allow you to easily manage information by storing multiple pieces of data under a variable name. The next script uses an array to store the FortiGate system information.

Example: Save system status information in an array.

Script:

#!

proc get_sys_status aname {

upvar $aname a

puts [exec "#This is an example Tcl script to get the system status of the FortiGate\n" "# " 15 ]

set input [exec "get system status\n" "# " 15 ]

# puts $input

set linelist [split $input \n]

# puts $linelist

foreach line $linelist {

if {![regexp {([^:]+):(.*)} $line dummy key value]} continue

switch -regexp -- $key {

Version {

regexp {FortiGate-([^ ]+) ([^,]+),build([\d]+),.*} $value dummy a(platform) a(version) a(build)

}

Serial-Number {

set a(serial-number) [string trim $value]

}

Hostname {

set a(hostname) [string trim $value]

} }

}

}

get_sys_status status

puts "This machine is a $status(platform) platform."

puts "It is running version $status(version) of FortiOS."

puts "The firmware is build# $status(build)."

puts "S/N: $status(serial-number)"

puts "This machine is called $status(hostname)"

Output:

------- Executing time: 2013-10-21 09:58:06 ------

Starting log (Run on device)

FortiGate-VM64 #

This machine is a VM64 platform.

It is running version v5.0 of FortiOS.

The firmware is build# 0228.

S/N: FGVM02Q105060070

This machine is called FortiGate-VM64

------- The end of log ----------

Variations:

Once the information is in the variable array, you can use it as part of commands you send to the FortiGate device or to make decisions based on the information. For example:

if {$status(version) == 5.0} {

# follow the version 5.0 commands

} elseif {$status(version) == 5.0} {

# follow the version 5.0 commands

}

This script introduces the concept of executing CLI commands within Tcl scripts using the following method:

set input [exec "get system status\n" "# "]

This command executes the CLI command “get system status” and passes the result into the variable called input. Without the “\n” at the end of the CLI command, the CLI command will not execute to provide output.

In analyzing this script:

  • line 1 is the required #! to indicate this is a Tcl script
  • lines 2-3 open the procedure declaration
  • lines 4-5 puts the output from the CLI command into a Tcl variable as a string, and breaks it up at each return character into an array of smaller strings
  • line 6 starts a loop to go through the array of strings
  • line 7 loops if the array element is punctuation or continues if its text
  • line 8 takes the output of line 7’s regular expression command and based on a match, performs one of the actions listed in lines 9 through 17
  • lines 9-11 if regular expression matches ‘Version’ then parse the text and store values for the platform, version, and build number in the named array elements
  • line 12-14 if regular expression matches ‘Serial-Number’ then store the value in an array element named that after trimming the string down to text only
  • lines 15-17 is similar to line 12 except the regular expression is matched against ‘Hostname’
  • line 17-19 close the switch decision statement, the for each loop, and the procedure
  • line 20 calls the procedure with an array name of status
  • lines 21-25 output the information stored in the status array

Tcl loops

Even though the last script used a loop, that script’s main purpose was storing information in the array. The next script uses a loop to create a preset number of users on the FortiGate device, in this case 10 users. The output is only shown for the first two users due to space considerations.

Example: Create 10 users from usr0001 to usr0010:

Script:

#!

proc do_cmd {cmd} {

puts [exec "$cmd\n" "# " 15]

}

set num_users 10

do_cmd "config vdom"

do_cmd "edit root"

do_cmd "config user local"

for {set i 1} {$i <= $num_users} {incr i} {

set name [format "usr%04d" $i]

puts "Adding user: $name"

do_cmd "edit $name"

do_cmd "set status enable"

do_cmd "set type password"

do_cmd "next"

}

do_cmd "end"

do_cmd "end"

do_cmd "config vdom"

do_cmd "edit root"

do_cmd "show user local"

do_cmd "end"

Output:

View the log of script running on device:FortiGate-VM64

------- Executing time: 2013-10-16 15:27:18 ------

Starting log (Run on device)

config vdom

FortiGate-VM64 (vdom) #

edit root

current vf=root:0

FortiGate-VM64 (root) #

config user local

FortiGate-VM64 (local) #

Adding user: usr0001

edit usr0001

new entry 'usr0001' added

FortiGate-VM64 (usr0001) #

set status enable

FortiGate-VM64 (usr0001) #

set type password

FortiGate-VM64 (usr0001) #

next

FortiGate-VM64 (local) #

Adding user: usr0002

edit usr0002

new entry 'usr0002' added

FortiGate-VM64 (usr0002) #

set status enable

FortiGate-VM64 (usr0002) #

set type password

FortiGate-VM64 (usr0002) #

next

Variations:

There are a number of uses for this kind of looping script. One example is to create firewall policies for each interface that deny all non-HTTPS and non-SSH traffic by default. Another example is a scheduled script to loop through the static routing table to check that each entry is still reachable, and if not remove it from the table.

This script loops 10 times creating a new user each time whose name is based on the loop counter. The format command is used to force a four digit number.

In analyzing this script:

  • line 1 is the required #! to indicate this is a Tcl script
  • lines 2-4 open CLI command wrapper procedure
  • line 5 declares the number of users to create
  • line 6 gets the FortiGate ready for entering local users
  • line 7 opens the for loop that will loop ten times
  • line 8 sets the user name based on the incremented loop counter variable
  • line 9 is just a comment to the administrator which user is being created
  • lines 10-13 create and configure the user, leaving the CLI ready for the next user to be added
  • line 14 ends the for loop
  • line 15 ends the adding of users in the CLI
  • line 16 executes a CLI command to prove the users were added properly

Tcl decisions

Tcl has a number of decision structures that allow you to execute different CLI commands based on what information you discover.

This script is more complex than the previous scripts as it uses two procedures that read FortiGate information, make a decision based on that information, and then executes one of the CLI sub-scripts based on that information.

Example: Add information to existing firewall policies.

Script:

#!

# need to define procedure do_cmd

# the second parameter of exec should be "# "

# If split one command to multiple lines use "\" to continue

proc do_cmd {cmd} {

puts [exec "$cmd\n" "# "]

}

foreach line [split [exec "show firewall policy\n" "# "] \n] {

if {[regexp {edit[ ]+([0-9]+)} $line match policyid]} {

continue

} elseif {[regexp {set[ ]+(\w+)[ ]+(.*)\r} $line match key value]} {

lappend fw_policy($policyid) "$key $value"

}

}

do_cmd "config firewall policy"

foreach policyid [array names fw_policy] {

if {[lsearch $fw_policy($policyid){diffservcode_forward 000011}] == -1} {

do_cmd "edit $policyid"

do_cmd "set diffserv-forward enable"

do_cmd "set diffservcode-forward 000011"

do_cmd "next"

}

}

do_cmd "end"

Variations:

This type of script is useful for updating long lists of records. For example if the FortiOS version adds new keywords to user accounts, you can create a script similar to this one to get the list of user accounts and for each one edit it, add the new information, and move on to the next.

This script uses two decision statements. Both are involved in text matching. The first decision is checking each line of input for the policy ID and if its not there it skips the line. If it is there, all the policy information is saved to an array for future use. The second decision searches the array of policy information to see which polices are miss

In analyzing this script:

  • line 1 is the required #! to indicate this is a Tcl script
  • line 2-8 is a loop that reads each policy’s information and appends only the policy ID number to an array variable called fw_policy
  • line 9 opens the CLI to the firewall policy section to prepare for the loop
  • line 10 starts the for each loop that increments through all the firewall policy names stored in fw_policy
  • line 11 checks each policy for an existing differvcode_forward 000011 entry - if its not found lines 12-15 are executed, otherwise they are skipped
  • line 12 opens the policy determined by the loop counter
  • line 13-14 enable diffserv_forward, and set it to 000011
  • line 15 saves this entry and prepares for the next one
  • line 16 closes the if statement
  • line 17 closes the for each loop
  • line 18 saves all the updated firewall policy entries

Additional Tcl Scripts

Example: Get and display state information about the FortiGate device:

Script:

#!

#Run on FortiOS v5.00

#This script will display FortiGate's CPU states,

#Memory states, and Up time

puts [exec "# This is an example Tcl script to get the system performance of the FortiGate\n" "# " 15 ]

set input [exec "get system status\n" "# " 15]

regexp {Version: *([^ ]+) ([^,]+),build([0-9]+),[0-9]+} $input dummy status(Platform) status(Version) status(Build)

if {$status(Version) eq "v5.0"} {

puts -nonewline [exec "config global\n" "# " 30]

puts -nonewline [exec "get system performance status\n" "# " 30]

puts -nonewline [exec "end\n" "# " 30]

} else {

puts -nonewline [exec "get system performance\n" "#" 30]

}

Output:

------- Executing time: 2013-10-21 16:21:43 ------

Starting log (Run on device)

FortiGate-VM64 #

config global

FortiGate-VM64 (global) # get system performance status

CPU states: 0% user 0% system 0% nice 90% idle

CPU0 states: 0% user 0% system 0% nice 90% idle

CPU1 states: 0% user 0% system 0% nice 90% idle

Memory states: 73% used

Average network usage: 0 kbps in 1 minute, 0 kbps in 10 minutes, 0 kbps in 30 minutes

Average sessions: 1 sessions in 1 minute, 2 sessions in 10 minutes, 2 sessions in 30 minutes

Average session setup rate: 0 sessions per second in last 1 minute, 0 sessions per second in last 10 minutes, 0 sessions per second in last 30 minutes

Virus caught: 0 total in 1 minute

IPS attacks blocked: 0 total in 1 minute

Uptime: 6 days, 1 hours, 34 minutes

FortiGate-VM64 (global) # end

FortiGate-VM64 #

------- The end of log ----------

------- Executing time: 2013-10-21 16:16:58 ------

Example: Configure common global settings.

Script:

#!

#Run on FortiOS v5.00

#This script will configure common global, user group and ntp settings

#if you do not want to set a parameter, comment the

#corresponding set command

#if you want to reset a parameter to it's default

#value, set it an empty string

puts [exec "# This is an example Tcl script to configure global, user group and ntp setting of FortiGate\n" "# " 15 ]

# global

set sys_global(admintimeout) ""

# user group

set sys_user_group(authtimeout) 20

# ntp

set sys_ntp(source-ip) "0.0.0.0"

set sys_ntp(ntpsync) "enable"

#procedure to execute FortiGate command

proc fgt_cmd cmd {

puts -nonewline [exec "$cmd\n" "# " 30]

}

#config system global---begin

fgt_cmd "config global"

fgt_cmd "config system global"

foreach key [array names sys_global] {

if {$sys_global($key) ne ""} {

fgt_cmd "set $key $sys_global($key)"

} else {

fgt_cmd "unset $key"

}

}

fgt_cmd "end"

fgt_cmd "end"

#config system global---end

#config system user group---begin

fgt_cmd "config vdom"

fgt_cmd "edit root"

fgt_cmd "config user group"

fgt_cmd "edit groupname"

foreach key [array names sys_user_group] {

if {$sys_user_group($key) ne ""} {

fgt_cmd "set $key $sys_user_group($key)"

} else {

fgt_cmd "unset $key"

}

}

fgt_cmd "end"

fgt_cmd "end"

#config system user group---end

#config system ntp---begin

fgt_cmd "config global"

fgt_cmd "config system ntp"

foreach key [array names sys_ntp] {

if {$sys_ntp($key) ne ""} {

fgt_cmd "set $key $sys_ntp($key)"

} else {

fgt_cmd "unset $key"

}

}

fgt_cmd "end"

fgt_cmd "end"

#config system ntp---end

Output:

------- Executing time: 2013-10-22 09:12:57 ------

Starting log (Run on device)

FortiGate-VM64 # config global

FortiGate-VM64 (global) # config system global

FortiGate-VM64 (global) # unset admintimeout

FortiGate-VM64 (global) # end

FortiGate-VM64 (global) # end

FortiGate-VM64 # config vdom

FortiGate-VM64 (vdom) # edit root

current vf=root:0

FortiGate-VM64 (root) # config user group

FortiGate-VM64 (group) # edit groupname

FortiGate-VM64 (groupname) # set authtimeout 20

FortiGate-VM64 (groupname) # end

FortiGate-VM64 (root) # end

FortiGate-VM64 # config global

FortiGate-VM64 (global) # config system ntp

FortiGate-VM64 (ntp) # set ntpsync enable

FortiGate-VM64 (ntp) # set source-ip 0.0.0.0

FortiGate-VM64 (ntp) # end

FortiGate-VM64 (global) # end

FortiGate-VM64 #

------- The end of log ----------

Example: Configure syslogd settings and filters.

Script:

#!

#Run on FortiOS v5.00

#This script will configure log syslogd setting and

#filter

#key-value pairs for 'config log syslogd setting', no

#value means default value.

set setting_list {{status enable} {csv enable}

{facility alert} {port} {server 1.1.1.2}}

#key-value pairs for 'config log syslogd filter', no

#value means default value.

puts [exec "# This is an example Tcl script to configure log syslogd setting and filter setting of FortiGate\n" "# " 15 ]

set filter_list {{attack enable} {email enable} {severity} {traffic enable} {virus disable}

{web enable}}

#set the number of syslogd server, "", "2" or "3"

set syslogd_no "2"

#procedure to execute FortiGate CLI command

proc fgt_cmd cmd {

puts -nonewline [exec "$cmd\n" "# "]

}

#procedure to set a series of key-value pairs

proc set_kv kv_list {

foreach kv $kv_list {

set len [llength $kv]

if {$len == 0} {

continue

} elseif {$len == 1} {

fgt_cmd "unset [lindex $kv 0]"

} else {

fgt_cmd "set [lindex $kv 0] [lindex $kv 1]"

} } }

#configure log syslogd setting---begin

fgt_cmd "config global"

fgt_cmd "config log syslogd$syslogd_no setting"

set_kv $setting_list

fgt_cmd "end"

#configure log syslogd setting---end

#configure log syslogd filter---begin

fgt_cmd "config log syslogd$syslogd_no filter"

set_kv $filter_list

fgt_cmd "end"

#configure log syslogd filter---end

Output:

Starting log (Run on device)

FortiGate-VM64 # config global

FortiGate-VM64 (global) # config log syslogd2 setting

FortiGate-VM64 (setting) # set status enable

FortiGate-VM64 (setting) # set csv enable

FortiGate-VM64 (setting) # set facility alert

FortiGate-VM64 (setting) # unset port

FortiGate-VM64 (setting) # set server 1.1.1.2

FortiGate-VM64 (setting) # end

FortiGate-VM64 (global) # config log syslogd2 filter

FortiGate-VM64 (filter) # set attack enable

FortiGate-VM64 (filter) # set email enable

FortiGate-VM64 (filter) # unset severity

FortiGate-VM64 (filter) # set traffic enable

FortiGate-VM64 (filter) # set virus disable

FortiGate-VM64 (filter) # set web enable

FortiGate-VM64 (filter) # end

FortiGate-VM64 (global) #

------- The end of log ----------

Example: Configure the FortiGate device to communicate with a FortiAnalyzer unit:

Script:

#!

#This script will configure the FortiGate device to

#communicate with a FortiAnalyzer unit

#Enter the following key-value pairs for 'config

#system fortianalyzer'

set status enable

set enc-algorithm high

#localid will be set as the hostname automatically

#later

puts [exec "# This is an example Tcl script to configure the FortiGate to communicate with a FortiAnalyzer\n" "# " 15 ]

set server 1.1.1.1

#for fortianalyzer, fortianalyzer2 or

#fortianalyzer3, enter the corresponding value "",

#"2", "3"

set faz_no ""

#keys used for 'config system fortianalyzer', if you

#do not want to change the value of a key, do not put

#it in the list

set key_list {status enc-algorithm localid server }

##procedure to get system status from a FortiGate

proc get_sys_status aname {

upvar $aname a

set input [split [exec "get system status\n" "# "] \n]

foreach line $input {

if {![regexp {([^:]+):(.*)} $line dummy key value]} continue

set a([string trim $key]) [string trim $value]

}

}

#procedure to execute FortiGate command

proc fgt_cmd cmd {

puts -nonewline [exec "$cmd\n" "# "]

}

#set the localid as the FortiGate's hostname

get_sys_status sys_status

set localid $sys_status(Hostname)

#config system fortianalyzer---begin

fgt_cmd "config global"

fgt_cmd "config log fortianalyzer$faz_no setting"

foreach key $key_list {

if [info exists $key] {

fgt_cmd "set $key [set $key]"

} else {

fgt_cmd "unset $key"

}

}

fgt_cmd "end"

fgt_cmd "end"

#config system fortianalyzer---end

Output:

Starting log (Run on device)

FortiGate-VM64 # config global

FortiGate-VM64 (global) # config log fortianalyzer setting

FortiGate-VM64 (setting) # set status enable

FortiGate-VM64 (setting) # set enc-algorithm high

FortiGate-VM64 (setting) # set localid FortiGate-VM64

FortiGate-VM64 (setting) # set server 1.1.1.1

FortiGate-VM64 (setting) # end

FortiGate-VM64 (global) # end

FortiGate-VM64 #

------- The end of log ---------

Example: Create custom IPS signatures and add them to a custom group.

Script:

#!

#Run on FortiOS v5.00

#This script will create custom ips signatures and

#change the settings for the custom ips signatures

puts [exec "# This is an example Tcl script to create custom ips signatures and change the settings for the custom ips signatures on a FortiGate\n" "# " 15 ]

#Enter custom ips signatures, signature names are the

#names of array elements

set custom_sig(c1) {"F-SBID(--protocol icmp;--icmp_type 10; )"}

set custom_sig(c2) {"F-SBID(--protocol icmp;--icmp_type 0; )"}

#Enter custom ips settings

set custom_rule(c1) {{status enable} {action block} {log enable} {log-packet} {severity high}}

set custom_rule(c2) {{status enable} {action pass} {log} {log-packet disable} {severity low}}

#procedure to execute FortiGate command

proc fgt_cmd cmd {

puts -nonewline [exec "$cmd\n" "# "]

}

#procedure to set a series of key-value pairs

proc set_kv kv_list {

foreach kv $kv_list {

set len [llength $kv]

if {$len == 0} {

continue

} elseif {$len == 1} {

fgt_cmd "unset [lindex $kv 0]"

} else {

fgt_cmd "set [lindex $kv 0] [lindex $kv 1]"

}

} }

#config ips custom---begin

fgt_cmd "config vdom"

fgt_cmd "edit root"

fgt_cmd "config ips custom"

foreach sig_name [array names custom_sig] {

fgt_cmd "edit $sig_name"

fgt_cmd "set signature $custom_sig($sig_name)"

fgt_cmd "next"

}

fgt_cmd "end"

#config ips custom settings---begin

foreach rule_name [array names custom_rule] {

fgt_cmd "config ips custom"

fgt_cmd "edit $rule_name"

set_kv $custom_rule($rule_name)

fgt_cmd "end"

}

fgt_cmd "end"

#config ips custom settings---end

Output:

Starting log (Run on device)

FortiGate-VM64 # config vdom

FortiGate-VM64 (vdom) # edit root

current vf=root:0

FortiGate-VM64 (root) # config ips custom

FortiGate-VM64 (custom) # edit c1

set signature "F-SBID(--protocol icmp;--icmp_type 10; )"

FortiGate-VM64 (c1) # set signature "F-SBID(--protocol icmp;--icmp_type 10; )"

FortiGate-VM64 (c1) # next

FortiGate-VM64 (custom) # edit c2

FortiGate-VM64 (c2) # set signature "F-SBID(--protocol icmp;--icmp_type 0; )"

FortiGate-VM64 (c2) # next

FortiGate-VM64 (custom) # end

FortiGate-VM64 (root) # config ips custom

FortiGate-VM64 (custom) # edit c1

FortiGate-VM64 (c1) # set status enable

FortiGate-VM64 (c1) # set action block

FortiGate-VM64 (c1) # set log enable

FortiGate-VM64 (c1) # unset log-packet

FortiGate-VM64 (c1) # set severity high

FortiGate-VM64 (c1) # end

FortiGate-VM64 (root) # config ips custom

FortiGate-VM64 (custom) # edit c2

FortiGate-VM64 (c2) # set status enable

FortiGate-VM64 (c2) # set action pass

FortiGate-VM64 (c2) # unset log

FortiGate-VM64 (c2) # set log-packet disable

FortiGate-VM64 (c2) # set severity low

FortiGate-VM64 (c2) # end

FortiGate-VM64 (root) # end

FortiGate-VM64 #

------- The end of log ----------

Variations:

None.

Tcl file IO

You can write to and read from files using Tcl scripts. For security reasons there is only one directory on the FortiManager where scripts can access files. For this reason, there is no reason to include the directory in the file name you are accessing. For example “/var/temp/myfile” or “~/myfile” will cause an error, but “myfile” or “/myfile” is OK.

The Tcl commands that are supported for file IO are: file, open, gets, read, tell, seek, eof, flush, close, fcopy, fconfigure, and fileevent.

The Tcl file command only supports delete subcommand, and does not support the -force option.

There is 10MB of diskspace allocated for Tcl scripts. An error will be reported if this size is exceeded.

These files will be reset when the following CLI commands are run: exec format, exec reset partition, or exec reset all. The files will not be reset when the firmware is updated unless otherwise specified.

To write to a file:

Script

#!

set somefile [open “tcl_test” w]

puts $somefile "Hello, world!"

close $somefile

To read from a file:

Script

#!

set otherfile [open “tcl_test” r]

while {[gets $otherfile line] >= 0} {

puts [string length $line]

}

close $otherfile

Output

Hello, world!

These two short scripts write a file called tcl_test and then read it back.

Line 3 in both scripts opens the file either for reading (r) or writing (w) and assigns it to a filehandle (somefile or otherfile). Later in the script when you see these filehandles, its input or output passing to the open file.

When reading from the file, lines 4 and 5 loop through the file line by line until it reaches the end of the file. Each line that is read is put to the screen.

Both scripts close the file before they exit.

Troubleshooting Tips

This section includes suggestions to help you find and fix problems you may be having with your scripts.

  • Make sure the commands you are trying to execute are valid for the version of FortiOS running on your target FortiGate device.
  • You should always use braces when evaluating code that may contain user input, to avoid possible security breaches. To illustrate the danger, consider this interactive session:

    % set userinput {[puts DANGER!]}

    [puts DANGER!]

    % expr $userinput == 1

    DANGER!

    0

    % expr {$userinput == 1}

    0

    In the first example, the code contained in the user-supplied input is evaluated, whereas in the second the braces prevent this potential danger. As a general rule, always surround expressions with braces, whether using expr directly or some other command that takes an expression.

  • A number that includes a leading zero or zeros, such as 0500 or 0011, is interpreted as an octal number, not a decimal number. So 0500 is actually 320 in decimal, and 0011 is 9 in decimal.
  • There is a limit to the number of scripts allowed on the FortiManager unit. Try removing an old script before trying to save your current one.
  • Using the Tcl command “catch” you can add custom error messages in your script to alert you to problems during the script execution. When catch encounters an error it will return 1, but if there is no error it will return 0. For example:

    if { [catch {open $someFile w} fid] } {

    puts stderr "Could not open $someFile for writing\n$fid"

    exit 1 ;# error opening the file!

    } else {

    # put the rest of your script here

    }

Tcl scripts

Tcl is a dynamic scripting language that extends the functionality of CLI scripting. In FortiManager Tcl scripts, the first line of the script is “#!” as it is for standard Tcl scripts.

Do not include the exit command that normally ends Tcl scripts; it will prevent the script from running.

This guide assumes you are familiar with the Tcl language and regular expressions, and instead focuses on how to use CLI commands in your Tcl scripts. Where you require more information about Tcl commands than this guide contains, please refer to resources such as the Tcl newsgroup, Tcl reference books, and the official Tcl website at https://www.tcl.tk.

Tcl scripts can do more than just get and set information. The benefits of Tcl come from:

  • variables to store information,
  • loops to repeats commands that are slightly different each time
  • decisions to compare information from the device

The sample scripts in this section will contain procedures that you can combine to use your scripts. The samples will each focus on one of four areas:

To enable Tcl scripting, use the following CLI commands:

config system admin setting

set show_tcl_script enable

end

Limitations of FortiManager Tcl

FortiManager Tcl executes in a controlled environment. You do not have to know the location of the Tcl interpreter or environment variables to execute your scripts. This also means some of the commands normally found in Tcl are not used in FortiManager Tcl.

Depending on the CLI commands you use in your Tcl scripts, you may not be able to run some scripts on some versions of FortiOS as CLI commands change periodically.

Before testing a new script on a FortiGate device, you should backup that device’s configuration and data to ensure it is not lost if the script does not work as expected.

Tcl variables

Variables allow you to store information from the FortiGate device, and use it later in the script. Arrays allow you to easily manage information by storing multiple pieces of data under a variable name. The next script uses an array to store the FortiGate system information.

Example: Save system status information in an array.

Script:

#!

proc get_sys_status aname {

upvar $aname a

puts [exec "#This is an example Tcl script to get the system status of the FortiGate\n" "# " 15 ]

set input [exec "get system status\n" "# " 15 ]

# puts $input

set linelist [split $input \n]

# puts $linelist

foreach line $linelist {

if {![regexp {([^:]+):(.*)} $line dummy key value]} continue

switch -regexp -- $key {

Version {

regexp {FortiGate-([^ ]+) ([^,]+),build([\d]+),.*} $value dummy a(platform) a(version) a(build)

}

Serial-Number {

set a(serial-number) [string trim $value]

}

Hostname {

set a(hostname) [string trim $value]

} }

}

}

get_sys_status status

puts "This machine is a $status(platform) platform."

puts "It is running version $status(version) of FortiOS."

puts "The firmware is build# $status(build)."

puts "S/N: $status(serial-number)"

puts "This machine is called $status(hostname)"

Output:

------- Executing time: 2013-10-21 09:58:06 ------

Starting log (Run on device)

FortiGate-VM64 #

This machine is a VM64 platform.

It is running version v5.0 of FortiOS.

The firmware is build# 0228.

S/N: FGVM02Q105060070

This machine is called FortiGate-VM64

------- The end of log ----------

Variations:

Once the information is in the variable array, you can use it as part of commands you send to the FortiGate device or to make decisions based on the information. For example:

if {$status(version) == 5.0} {

# follow the version 5.0 commands

} elseif {$status(version) == 5.0} {

# follow the version 5.0 commands

}

This script introduces the concept of executing CLI commands within Tcl scripts using the following method:

set input [exec "get system status\n" "# "]

This command executes the CLI command “get system status” and passes the result into the variable called input. Without the “\n” at the end of the CLI command, the CLI command will not execute to provide output.

In analyzing this script:

  • line 1 is the required #! to indicate this is a Tcl script
  • lines 2-3 open the procedure declaration
  • lines 4-5 puts the output from the CLI command into a Tcl variable as a string, and breaks it up at each return character into an array of smaller strings
  • line 6 starts a loop to go through the array of strings
  • line 7 loops if the array element is punctuation or continues if its text
  • line 8 takes the output of line 7’s regular expression command and based on a match, performs one of the actions listed in lines 9 through 17
  • lines 9-11 if regular expression matches ‘Version’ then parse the text and store values for the platform, version, and build number in the named array elements
  • line 12-14 if regular expression matches ‘Serial-Number’ then store the value in an array element named that after trimming the string down to text only
  • lines 15-17 is similar to line 12 except the regular expression is matched against ‘Hostname’
  • line 17-19 close the switch decision statement, the for each loop, and the procedure
  • line 20 calls the procedure with an array name of status
  • lines 21-25 output the information stored in the status array

Tcl loops

Even though the last script used a loop, that script’s main purpose was storing information in the array. The next script uses a loop to create a preset number of users on the FortiGate device, in this case 10 users. The output is only shown for the first two users due to space considerations.

Example: Create 10 users from usr0001 to usr0010:

Script:

#!

proc do_cmd {cmd} {

puts [exec "$cmd\n" "# " 15]

}

set num_users 10

do_cmd "config vdom"

do_cmd "edit root"

do_cmd "config user local"

for {set i 1} {$i <= $num_users} {incr i} {

set name [format "usr%04d" $i]

puts "Adding user: $name"

do_cmd "edit $name"

do_cmd "set status enable"

do_cmd "set type password"

do_cmd "next"

}

do_cmd "end"

do_cmd "end"

do_cmd "config vdom"

do_cmd "edit root"

do_cmd "show user local"

do_cmd "end"

Output:

View the log of script running on device:FortiGate-VM64

------- Executing time: 2013-10-16 15:27:18 ------

Starting log (Run on device)

config vdom

FortiGate-VM64 (vdom) #

edit root

current vf=root:0

FortiGate-VM64 (root) #

config user local

FortiGate-VM64 (local) #

Adding user: usr0001

edit usr0001

new entry 'usr0001' added

FortiGate-VM64 (usr0001) #

set status enable

FortiGate-VM64 (usr0001) #

set type password

FortiGate-VM64 (usr0001) #

next

FortiGate-VM64 (local) #

Adding user: usr0002

edit usr0002

new entry 'usr0002' added

FortiGate-VM64 (usr0002) #

set status enable

FortiGate-VM64 (usr0002) #

set type password

FortiGate-VM64 (usr0002) #

next

Variations:

There are a number of uses for this kind of looping script. One example is to create firewall policies for each interface that deny all non-HTTPS and non-SSH traffic by default. Another example is a scheduled script to loop through the static routing table to check that each entry is still reachable, and if not remove it from the table.

This script loops 10 times creating a new user each time whose name is based on the loop counter. The format command is used to force a four digit number.

In analyzing this script:

  • line 1 is the required #! to indicate this is a Tcl script
  • lines 2-4 open CLI command wrapper procedure
  • line 5 declares the number of users to create
  • line 6 gets the FortiGate ready for entering local users
  • line 7 opens the for loop that will loop ten times
  • line 8 sets the user name based on the incremented loop counter variable
  • line 9 is just a comment to the administrator which user is being created
  • lines 10-13 create and configure the user, leaving the CLI ready for the next user to be added
  • line 14 ends the for loop
  • line 15 ends the adding of users in the CLI
  • line 16 executes a CLI command to prove the users were added properly

Tcl decisions

Tcl has a number of decision structures that allow you to execute different CLI commands based on what information you discover.

This script is more complex than the previous scripts as it uses two procedures that read FortiGate information, make a decision based on that information, and then executes one of the CLI sub-scripts based on that information.

Example: Add information to existing firewall policies.

Script:

#!

# need to define procedure do_cmd

# the second parameter of exec should be "# "

# If split one command to multiple lines use "\" to continue

proc do_cmd {cmd} {

puts [exec "$cmd\n" "# "]

}

foreach line [split [exec "show firewall policy\n" "# "] \n] {

if {[regexp {edit[ ]+([0-9]+)} $line match policyid]} {

continue

} elseif {[regexp {set[ ]+(\w+)[ ]+(.*)\r} $line match key value]} {

lappend fw_policy($policyid) "$key $value"

}

}

do_cmd "config firewall policy"

foreach policyid [array names fw_policy] {

if {[lsearch $fw_policy($policyid){diffservcode_forward 000011}] == -1} {

do_cmd "edit $policyid"

do_cmd "set diffserv-forward enable"

do_cmd "set diffservcode-forward 000011"

do_cmd "next"

}

}

do_cmd "end"

Variations:

This type of script is useful for updating long lists of records. For example if the FortiOS version adds new keywords to user accounts, you can create a script similar to this one to get the list of user accounts and for each one edit it, add the new information, and move on to the next.

This script uses two decision statements. Both are involved in text matching. The first decision is checking each line of input for the policy ID and if its not there it skips the line. If it is there, all the policy information is saved to an array for future use. The second decision searches the array of policy information to see which polices are miss

In analyzing this script:

  • line 1 is the required #! to indicate this is a Tcl script
  • line 2-8 is a loop that reads each policy’s information and appends only the policy ID number to an array variable called fw_policy
  • line 9 opens the CLI to the firewall policy section to prepare for the loop
  • line 10 starts the for each loop that increments through all the firewall policy names stored in fw_policy
  • line 11 checks each policy for an existing differvcode_forward 000011 entry - if its not found lines 12-15 are executed, otherwise they are skipped
  • line 12 opens the policy determined by the loop counter
  • line 13-14 enable diffserv_forward, and set it to 000011
  • line 15 saves this entry and prepares for the next one
  • line 16 closes the if statement
  • line 17 closes the for each loop
  • line 18 saves all the updated firewall policy entries

Additional Tcl Scripts

Example: Get and display state information about the FortiGate device:

Script:

#!

#Run on FortiOS v5.00

#This script will display FortiGate's CPU states,

#Memory states, and Up time

puts [exec "# This is an example Tcl script to get the system performance of the FortiGate\n" "# " 15 ]

set input [exec "get system status\n" "# " 15]

regexp {Version: *([^ ]+) ([^,]+),build([0-9]+),[0-9]+} $input dummy status(Platform) status(Version) status(Build)

if {$status(Version) eq "v5.0"} {

puts -nonewline [exec "config global\n" "# " 30]

puts -nonewline [exec "get system performance status\n" "# " 30]

puts -nonewline [exec "end\n" "# " 30]

} else {

puts -nonewline [exec "get system performance\n" "#" 30]

}

Output:

------- Executing time: 2013-10-21 16:21:43 ------

Starting log (Run on device)

FortiGate-VM64 #

config global

FortiGate-VM64 (global) # get system performance status

CPU states: 0% user 0% system 0% nice 90% idle

CPU0 states: 0% user 0% system 0% nice 90% idle

CPU1 states: 0% user 0% system 0% nice 90% idle

Memory states: 73% used

Average network usage: 0 kbps in 1 minute, 0 kbps in 10 minutes, 0 kbps in 30 minutes

Average sessions: 1 sessions in 1 minute, 2 sessions in 10 minutes, 2 sessions in 30 minutes

Average session setup rate: 0 sessions per second in last 1 minute, 0 sessions per second in last 10 minutes, 0 sessions per second in last 30 minutes

Virus caught: 0 total in 1 minute

IPS attacks blocked: 0 total in 1 minute

Uptime: 6 days, 1 hours, 34 minutes

FortiGate-VM64 (global) # end

FortiGate-VM64 #

------- The end of log ----------

------- Executing time: 2013-10-21 16:16:58 ------

Example: Configure common global settings.

Script:

#!

#Run on FortiOS v5.00

#This script will configure common global, user group and ntp settings

#if you do not want to set a parameter, comment the

#corresponding set command

#if you want to reset a parameter to it's default

#value, set it an empty string

puts [exec "# This is an example Tcl script to configure global, user group and ntp setting of FortiGate\n" "# " 15 ]

# global

set sys_global(admintimeout) ""

# user group

set sys_user_group(authtimeout) 20

# ntp

set sys_ntp(source-ip) "0.0.0.0"

set sys_ntp(ntpsync) "enable"

#procedure to execute FortiGate command

proc fgt_cmd cmd {

puts -nonewline [exec "$cmd\n" "# " 30]

}

#config system global---begin

fgt_cmd "config global"

fgt_cmd "config system global"

foreach key [array names sys_global] {

if {$sys_global($key) ne ""} {

fgt_cmd "set $key $sys_global($key)"

} else {

fgt_cmd "unset $key"

}

}

fgt_cmd "end"

fgt_cmd "end"

#config system global---end

#config system user group---begin

fgt_cmd "config vdom"

fgt_cmd "edit root"

fgt_cmd "config user group"

fgt_cmd "edit groupname"

foreach key [array names sys_user_group] {

if {$sys_user_group($key) ne ""} {

fgt_cmd "set $key $sys_user_group($key)"

} else {

fgt_cmd "unset $key"

}

}

fgt_cmd "end"

fgt_cmd "end"

#config system user group---end

#config system ntp---begin

fgt_cmd "config global"

fgt_cmd "config system ntp"

foreach key [array names sys_ntp] {

if {$sys_ntp($key) ne ""} {

fgt_cmd "set $key $sys_ntp($key)"

} else {

fgt_cmd "unset $key"

}

}

fgt_cmd "end"

fgt_cmd "end"

#config system ntp---end

Output:

------- Executing time: 2013-10-22 09:12:57 ------

Starting log (Run on device)

FortiGate-VM64 # config global

FortiGate-VM64 (global) # config system global

FortiGate-VM64 (global) # unset admintimeout

FortiGate-VM64 (global) # end

FortiGate-VM64 (global) # end

FortiGate-VM64 # config vdom

FortiGate-VM64 (vdom) # edit root

current vf=root:0

FortiGate-VM64 (root) # config user group

FortiGate-VM64 (group) # edit groupname

FortiGate-VM64 (groupname) # set authtimeout 20

FortiGate-VM64 (groupname) # end

FortiGate-VM64 (root) # end

FortiGate-VM64 # config global

FortiGate-VM64 (global) # config system ntp

FortiGate-VM64 (ntp) # set ntpsync enable

FortiGate-VM64 (ntp) # set source-ip 0.0.0.0

FortiGate-VM64 (ntp) # end

FortiGate-VM64 (global) # end

FortiGate-VM64 #

------- The end of log ----------

Example: Configure syslogd settings and filters.

Script:

#!

#Run on FortiOS v5.00

#This script will configure log syslogd setting and

#filter

#key-value pairs for 'config log syslogd setting', no

#value means default value.

set setting_list {{status enable} {csv enable}

{facility alert} {port} {server 1.1.1.2}}

#key-value pairs for 'config log syslogd filter', no

#value means default value.

puts [exec "# This is an example Tcl script to configure log syslogd setting and filter setting of FortiGate\n" "# " 15 ]

set filter_list {{attack enable} {email enable} {severity} {traffic enable} {virus disable}

{web enable}}

#set the number of syslogd server, "", "2" or "3"

set syslogd_no "2"

#procedure to execute FortiGate CLI command

proc fgt_cmd cmd {

puts -nonewline [exec "$cmd\n" "# "]

}

#procedure to set a series of key-value pairs

proc set_kv kv_list {

foreach kv $kv_list {

set len [llength $kv]

if {$len == 0} {

continue

} elseif {$len == 1} {

fgt_cmd "unset [lindex $kv 0]"

} else {

fgt_cmd "set [lindex $kv 0] [lindex $kv 1]"

} } }

#configure log syslogd setting---begin

fgt_cmd "config global"

fgt_cmd "config log syslogd$syslogd_no setting"

set_kv $setting_list

fgt_cmd "end"

#configure log syslogd setting---end

#configure log syslogd filter---begin

fgt_cmd "config log syslogd$syslogd_no filter"

set_kv $filter_list

fgt_cmd "end"

#configure log syslogd filter---end

Output:

Starting log (Run on device)

FortiGate-VM64 # config global

FortiGate-VM64 (global) # config log syslogd2 setting

FortiGate-VM64 (setting) # set status enable

FortiGate-VM64 (setting) # set csv enable

FortiGate-VM64 (setting) # set facility alert

FortiGate-VM64 (setting) # unset port

FortiGate-VM64 (setting) # set server 1.1.1.2

FortiGate-VM64 (setting) # end

FortiGate-VM64 (global) # config log syslogd2 filter

FortiGate-VM64 (filter) # set attack enable

FortiGate-VM64 (filter) # set email enable

FortiGate-VM64 (filter) # unset severity

FortiGate-VM64 (filter) # set traffic enable

FortiGate-VM64 (filter) # set virus disable

FortiGate-VM64 (filter) # set web enable

FortiGate-VM64 (filter) # end

FortiGate-VM64 (global) #

------- The end of log ----------

Example: Configure the FortiGate device to communicate with a FortiAnalyzer unit:

Script:

#!

#This script will configure the FortiGate device to

#communicate with a FortiAnalyzer unit

#Enter the following key-value pairs for 'config

#system fortianalyzer'

set status enable

set enc-algorithm high

#localid will be set as the hostname automatically

#later

puts [exec "# This is an example Tcl script to configure the FortiGate to communicate with a FortiAnalyzer\n" "# " 15 ]

set server 1.1.1.1

#for fortianalyzer, fortianalyzer2 or

#fortianalyzer3, enter the corresponding value "",

#"2", "3"

set faz_no ""

#keys used for 'config system fortianalyzer', if you

#do not want to change the value of a key, do not put

#it in the list

set key_list {status enc-algorithm localid server }

##procedure to get system status from a FortiGate

proc get_sys_status aname {

upvar $aname a

set input [split [exec "get system status\n" "# "] \n]

foreach line $input {

if {![regexp {([^:]+):(.*)} $line dummy key value]} continue

set a([string trim $key]) [string trim $value]

}

}

#procedure to execute FortiGate command

proc fgt_cmd cmd {

puts -nonewline [exec "$cmd\n" "# "]

}

#set the localid as the FortiGate's hostname

get_sys_status sys_status

set localid $sys_status(Hostname)

#config system fortianalyzer---begin

fgt_cmd "config global"

fgt_cmd "config log fortianalyzer$faz_no setting"

foreach key $key_list {

if [info exists $key] {

fgt_cmd "set $key [set $key]"

} else {

fgt_cmd "unset $key"

}

}

fgt_cmd "end"

fgt_cmd "end"

#config system fortianalyzer---end

Output:

Starting log (Run on device)

FortiGate-VM64 # config global

FortiGate-VM64 (global) # config log fortianalyzer setting

FortiGate-VM64 (setting) # set status enable

FortiGate-VM64 (setting) # set enc-algorithm high

FortiGate-VM64 (setting) # set localid FortiGate-VM64

FortiGate-VM64 (setting) # set server 1.1.1.1

FortiGate-VM64 (setting) # end

FortiGate-VM64 (global) # end

FortiGate-VM64 #

------- The end of log ---------

Example: Create custom IPS signatures and add them to a custom group.

Script:

#!

#Run on FortiOS v5.00

#This script will create custom ips signatures and

#change the settings for the custom ips signatures

puts [exec "# This is an example Tcl script to create custom ips signatures and change the settings for the custom ips signatures on a FortiGate\n" "# " 15 ]

#Enter custom ips signatures, signature names are the

#names of array elements

set custom_sig(c1) {"F-SBID(--protocol icmp;--icmp_type 10; )"}

set custom_sig(c2) {"F-SBID(--protocol icmp;--icmp_type 0; )"}

#Enter custom ips settings

set custom_rule(c1) {{status enable} {action block} {log enable} {log-packet} {severity high}}

set custom_rule(c2) {{status enable} {action pass} {log} {log-packet disable} {severity low}}

#procedure to execute FortiGate command

proc fgt_cmd cmd {

puts -nonewline [exec "$cmd\n" "# "]

}

#procedure to set a series of key-value pairs

proc set_kv kv_list {

foreach kv $kv_list {

set len [llength $kv]

if {$len == 0} {

continue

} elseif {$len == 1} {

fgt_cmd "unset [lindex $kv 0]"

} else {

fgt_cmd "set [lindex $kv 0] [lindex $kv 1]"

}

} }

#config ips custom---begin

fgt_cmd "config vdom"

fgt_cmd "edit root"

fgt_cmd "config ips custom"

foreach sig_name [array names custom_sig] {

fgt_cmd "edit $sig_name"

fgt_cmd "set signature $custom_sig($sig_name)"

fgt_cmd "next"

}

fgt_cmd "end"

#config ips custom settings---begin

foreach rule_name [array names custom_rule] {

fgt_cmd "config ips custom"

fgt_cmd "edit $rule_name"

set_kv $custom_rule($rule_name)

fgt_cmd "end"

}

fgt_cmd "end"

#config ips custom settings---end

Output:

Starting log (Run on device)

FortiGate-VM64 # config vdom

FortiGate-VM64 (vdom) # edit root

current vf=root:0

FortiGate-VM64 (root) # config ips custom

FortiGate-VM64 (custom) # edit c1

set signature "F-SBID(--protocol icmp;--icmp_type 10; )"

FortiGate-VM64 (c1) # set signature "F-SBID(--protocol icmp;--icmp_type 10; )"

FortiGate-VM64 (c1) # next

FortiGate-VM64 (custom) # edit c2

FortiGate-VM64 (c2) # set signature "F-SBID(--protocol icmp;--icmp_type 0; )"

FortiGate-VM64 (c2) # next

FortiGate-VM64 (custom) # end

FortiGate-VM64 (root) # config ips custom

FortiGate-VM64 (custom) # edit c1

FortiGate-VM64 (c1) # set status enable

FortiGate-VM64 (c1) # set action block

FortiGate-VM64 (c1) # set log enable

FortiGate-VM64 (c1) # unset log-packet

FortiGate-VM64 (c1) # set severity high

FortiGate-VM64 (c1) # end

FortiGate-VM64 (root) # config ips custom

FortiGate-VM64 (custom) # edit c2

FortiGate-VM64 (c2) # set status enable

FortiGate-VM64 (c2) # set action pass

FortiGate-VM64 (c2) # unset log

FortiGate-VM64 (c2) # set log-packet disable

FortiGate-VM64 (c2) # set severity low

FortiGate-VM64 (c2) # end

FortiGate-VM64 (root) # end

FortiGate-VM64 #

------- The end of log ----------

Variations:

None.

Tcl file IO

You can write to and read from files using Tcl scripts. For security reasons there is only one directory on the FortiManager where scripts can access files. For this reason, there is no reason to include the directory in the file name you are accessing. For example “/var/temp/myfile” or “~/myfile” will cause an error, but “myfile” or “/myfile” is OK.

The Tcl commands that are supported for file IO are: file, open, gets, read, tell, seek, eof, flush, close, fcopy, fconfigure, and fileevent.

The Tcl file command only supports delete subcommand, and does not support the -force option.

There is 10MB of diskspace allocated for Tcl scripts. An error will be reported if this size is exceeded.

These files will be reset when the following CLI commands are run: exec format, exec reset partition, or exec reset all. The files will not be reset when the firmware is updated unless otherwise specified.

To write to a file:

Script

#!

set somefile [open “tcl_test” w]

puts $somefile "Hello, world!"

close $somefile

To read from a file:

Script

#!

set otherfile [open “tcl_test” r]

while {[gets $otherfile line] >= 0} {

puts [string length $line]

}

close $otherfile

Output

Hello, world!

These two short scripts write a file called tcl_test and then read it back.

Line 3 in both scripts opens the file either for reading (r) or writing (w) and assigns it to a filehandle (somefile or otherfile). Later in the script when you see these filehandles, its input or output passing to the open file.

When reading from the file, lines 4 and 5 loop through the file line by line until it reaches the end of the file. Each line that is read is put to the screen.

Both scripts close the file before they exit.

Troubleshooting Tips

This section includes suggestions to help you find and fix problems you may be having with your scripts.

  • Make sure the commands you are trying to execute are valid for the version of FortiOS running on your target FortiGate device.
  • You should always use braces when evaluating code that may contain user input, to avoid possible security breaches. To illustrate the danger, consider this interactive session:

    % set userinput {[puts DANGER!]}

    [puts DANGER!]

    % expr $userinput == 1

    DANGER!

    0

    % expr {$userinput == 1}

    0

    In the first example, the code contained in the user-supplied input is evaluated, whereas in the second the braces prevent this potential danger. As a general rule, always surround expressions with braces, whether using expr directly or some other command that takes an expression.

  • A number that includes a leading zero or zeros, such as 0500 or 0011, is interpreted as an octal number, not a decimal number. So 0500 is actually 320 in decimal, and 0011 is 9 in decimal.
  • There is a limit to the number of scripts allowed on the FortiManager unit. Try removing an old script before trying to save your current one.
  • Using the Tcl command “catch” you can add custom error messages in your script to alert you to problems during the script execution. When catch encounters an error it will return 1, but if there is no error it will return 0. For example:

    if { [catch {open $someFile w} fid] } {

    puts stderr "Could not open $someFile for writing\n$fid"

    exit 1 ;# error opening the file!

    } else {

    # put the rest of your script here

    }