TMSH Scripting in v10.1
With the release of v10.1, tmsh has added a scripting language to accompany the shell introduced in v10. To steal Yoda's linguistic skills...Powerful, this will be. So powerful in fact that we here at DevCentral have created a wiki namespace and a forum for tmsh related content and questions.
Last Spring when v10 was released, I wrote an article on how to use the new traffic management shell to configure GTM. In this article, we’ll build the same configuration from a questionnaire generated by a tmsh script. A couple caveats before getting started:
- Error checking is lacking here. The script is for demonstration purposes. It would need serious work for use in a production environment.
- There are plenty of features not highlighted or optional in the script. Those that are used outside of pool and member ratios are hardcoded.
Getting Started
You can launch the script editor in tmsh by entering “edit cli script <scriptname>.tcl”. Once in the editor, if it’s a new script, you’ll see a create script <scriptname.tcl { } wrapper with four procedures:
- script::init – optional. Can initialize global variables here.
- script::run – required. This is the main program loop.
- script::help – optional. Hitting the “?” key will provide context sensitive help.
- script::tabc – optional. Hitting the tab key will provide context sensitive help.
Our script will create a procedure of our own and utilize the script::run procedure, so you can remove the others for this effort.
Working with User Input
When I first started this script effort, there was an awful lot of repetition with regards to stdout. With that in mind, I created a proc to handle the user feedback:
1: proc getFeedback { question } {
2: puts -nonewline $question
3: flush stdout
4: return [gets stdin]
5: }
6:
7: script::run {
8: set dc_count [getFeedback "How many datacenters? "]
9: }
Each time we need to get feedback, now instead of repeating the puts <string> and the stdout flush, we just set the variable to the returned data from the procedure.
GTM Configuration Tasks
To get to a resolving system, several tasks need to be accomplished:
- Create a datacenter
- Create a server, and if applicable, virtual servers. Since the server is a BIG-IP, we’ll create a few virtual servers.
- Create a pool
- Create a wideIP
- Create a listener (already accomplished in our case)
In tmsh, the commands for these tasks are all under the gtm module, and we’ll need the syntax for the commands for our script.
Creating the Datacenters
This one’s really easy. The only required information for the datacenter is the name. In the tmsh shell, this would be create gtm datacenter <dc name>. In script, we’ll use the tmsh::create command to achieve the same result.
1: # Enable stateless so existing objects can be overwritten
2: tmsh::stateless enabled
3:
4: #Build Datacenters
5: set dc_count [getFeedback "How many datacenters do you wish to create? "]
6: for {set x 0} {$x<$dc_count} {incr x} {
7: lappend dc_names [getFeedback "Datacenter [expr $x +1] name? "]
8: }
9: tmsh::create /gtm datacenter $dc_names
10: puts "\nDatacenters created...\n\n"
Here we grab the number of datacenters desired, loop through the count number and append the datacenter names to a variable we’ll use to create the datacenters with the tmsh::create command. One other note, since I’m using this script for demonstration purposes, running through it repeatedly would error out because these objects already exist. Rather than deleting them all after each iteration of the script, I’m using the tmsh::stateless command to overwrite any objects already in place.
Creating the Servers
This one is slightly more difficult as there are many different options that can be applied at the server level. To keep this short, we’ll hard code the monitor and ignore the other options. In the tmsh shell, we’d enter create gtm server <server name> addresses add { <ip> } monitor bigip datacenter <dc name> virtual-servers add { <virtual server ip:port> }. In script, we’ll approach it this way:
1: #Build Servers
2: set srv_count [getFeedback "How many servers are you adding? "]
3: for {set x 0} {$x<$srv_count} {incr x} {
4: set srv_name [getFeedback "Server [expr $x +1] Name? "]
5: set srv_ip [getFeedback "Server [expr $x +1] IP? "]
6: set srv_dc_loc [getFeedback "Datacenter server belongs to (tmsh::get_config /gtm datacenter)? "]
7: set v_count [getFeedback "How mnay virtuals for $srv_name? "]
8: for {set y 0} {$y<$v_count} {incr y} {
9: lappend vmembers($srv_name) [getFeedback "Virtual Server [expr $y +1] IP:Port? "]
10: }
11: tmsh::create /gtm server $srv_name addresses add \{ \
12: $srv_ip \} monitor bigip datacenter $srv_dc_loc \
13: virtual-servers add \{ $vmembers($srv_name) \}
14: }
15: puts "\nServers created...\n\n"
This requires nested for loops, one to create the server, and the other to create the virtual servers within each server.
Creating the Pools
This code block is very similar to the previous one, with a couple exceptions. Because the tmsh create pool command expects the ratio to be set (if being set) within the context of the { ip:port { ratio x } ip:port { ratio x} } etc, I needed to make sure that part of the string was accounted for when iterating through. The foreach block takes each argument of the array and builds a single string so that the format is appropriate for the gtm pool command.
1: #Build Pools
2: set pl_count [getFeedback "How many pools are you adding? "]
3: for {set x 0} {$x<$pl_count} {incr x} {
4: set pl_name [getFeedback "Pool [expr $x +1] name? "]
5: set pm_count [getFeedback "How many pool members? "]
6: for {set y 0} {$y<$pm_count} {incr y} {
7: set pm_raw [getFeedback "Pool member [expr $y +1] IP:Port and ratio (Ex. 1.1.1.1:80 1)? "]
8: set pm_value "[lindex [split $pm_raw " "] 0] \{ ratio [lindex [split $pm_raw " "] 1] \}"
9: lappend pmembers($pl_name) $pm_value
10: }
11: foreach z $pmembers($pl_name) { append pmmod "$z " }
12: tmsh::create /gtm pool $pl_name members add \{ $pmmod \} \
13: load-balancing-mode ratio verify-member-availability disabled
14: }
15: puts "\nPools created...\n\n"
Creating the WideIP
To create the WideIP, the block looks nearly identical to creating the pools, except for the elimination of the nested for loop and the different tmsh command in use.
1: #Build WideIP
2: set wip_name [getFeedback "WideIP Name? "]
3: set wip_pl_count [getFeedback "How many pools? "]
4: for {set x 0} {$x<$wip_pl_count} {incr x} {
5: set pl_raw [getFeedback "Pool [expr $x +1] name a ratio (Ex. pool1 1)? "]
6: set pl_value "[lindex [split $pl_raw " "] 0] \{ ratio [lindex [split $pl_raw " "] 1] \}"
7: lappend wip_pools($wip_name) $pl_value
8: }
9: foreach z $wip_pools($wip_name) { append wipplmod "$z " }
10: tmsh::create /gtm wideip $wip_name pool-lb-mode ratio pools add \{ $wipplmod \} \
11: persistence enabled ttl-persistence 300 rules add \{ testwip-rule \}
12:
13: puts "\nWideIP created, configuration is complete.\n\n"
And that’s a wrap for the script! Now let’s run it.
Running the Script
root@golgotha(Active)(tmos)# run cli script test1.tcl
How many datacenters do you wish to create? 2
Datacenter 1 name? dc1
Datacenter 2 name? dc2Datacenters created...
How many servers are you adding? 2
Server 1 Name? ltm1
Server 1 IP? 10.10.100.1
Datacenter server belongs to (tmsh::get_config /gtm datacenter)? dc1
How mnay virtuals for ltm1? 3
Virtual Server 1 IP:Port? 10.10.100.10:80
Virtual Server 2 IP:Port? 10.10.100.11:80
Virtual Server 3 IP:Port? 10.10.100.12:80
Server 2 Name? ltm2
Server 2 IP? 10.10.200.1
Datacenter server belongs to (tmsh::get_config /gtm datacenter)? dc2
How mnay virtuals for ltm2? 3
Virtual Server 1 IP:Port? 10.10.200.10:80
Virtual Server 2 IP:Port? 10.10.200.11:80
Virtual Server 3 IP:Port? 10.10.200.12:80Servers created...
How many pools are you adding? 2
Pool 1 name? gpool1
How many pool members? 3
Pool member 1 IP:Port and ratio (Ex. 1.1.1.1:80 1)? 10.10.100.10:80 1
Pool member 2 IP:Port and ratio (Ex. 1.1.1.1:80 1)? 10.10.100.11:80 2
Pool member 3 IP:Port and ratio (Ex. 1.1.1.1:80 1)? 10.10.100.12:80 3
Pool 2 name? gpool2
How many pool members? 3
Pool member 1 IP:Port and ratio (Ex. 1.1.1.1:80 1)? 10.10.200.10:80 1
Pool member 2 IP:Port and ratio (Ex. 1.1.1.1:80 1)? 10.10.200.11:80 2
Pool member 3 IP:Port and ratio (Ex. 1.1.1.1:80 1)? 10.10.200.12:80 3Pools created...
WideIP Name? test.wip.com
How many pools? 2
Pool 1 name a ratio (Ex. pool1 1)? gpool1 1
Pool 2 name a ratio (Ex. pool1 1)? gpool2 2
WideIP created, configuration is complete.
Obviously, since there’s no error checking in this script, typos will kill, so I’d encourage you to include error checking on the data entry. So now that our configuration is complete, I’ve modified this entry from the codeshare to watch our new pools as I run some test traffic against the new WideIP:
1: proc script::init {} {
2: set ::pool_ids ""
3: }
4:
5: proc get_stats { resultsArray } {
6:
7: upvar $resultsArray results
8:
9: set idx 0
10: set objs [tmsh::get_status gtm pool $::pool_ids raw]
11: set count [llength $objs]
12:
13: while { $idx < $count } {
14:
15: set obj [lindex $objs $idx]
16: set pool [tmsh::get_name $obj]
17:
18: lappend results($pool) preferred
19: lappend results($pool) \
20: [tmsh::get_field_value $obj "preferred"]
21:
22: lappend results($pool) alternate
23: lappend results($pool) \
24: [tmsh::get_field_value $obj "alternate"]
25:
26: lappend results($pool) dropped
27: lappend results($pool) \
28: [tmsh::get_field_value $obj "dropped"]
29:
30: incr idx
31: }
32: }
33:
34: proc script::run {} {
35: for {set idx 1} {$idx < $tmsh::argc} {incr idx} {
36: lappend ::pool_ids [lindex $tmsh::argv $idx]
37: }
38:
39: array set r1 {}
40: array set r2 {}
41:
42: set interval 2
43: set delay [expr $interval * 1000]
44:
45: get_stats r1
46:
47: while { true } {
48: after $delay
49: get_stats r2
50: tmsh::clear_screen
51:
52: foreach { pool } [lsort [array names r1]] {
53:
54: if { [string length [array names r2 -exact $pool]] == 0 } {
55: puts "$pool: no sample"
56: continue
57: }
58:
59: set line [format "%-20s" $pool]
60:
61: set s1 $r1($pool)
62: set s2 $r2($pool)
63:
64: set idx 0
65: set count [llength $s1]
66: while { $idx < $count } {
67: append line "[lindex $s1 $idx] "
68: incr idx
69:
70: set stat \
71: [expr ([lindex $s2 $idx] - [lindex $s1 $idx]) / $interval]
72: append line "[format "%-12s" $stat]"
73: incr idx
74: }
75: puts $line
76: }
77:
78: # use the most recent results as the next previous results
79: array set r1 [array get r2]
80: array unset r2
81: }
82: }
83:
84: proc script::help {} {
85: tmsh::add_help "enter zero or more pool names"
86: }
87:
88: proc script::tabc {} {
89: foreach {pool} [tmsh::get_config /gtm pool] {
90: tmsh::add_tabc [tmsh::get_name $pool]
91: }
92: }
The output from the script above looks like this when running “run cli script watch_gtmPools.tcl”:
gpool1 preferred 5 alternate 0 dropped 0
gpool2 preferred 9 alternate 0 dropped 0
The complete script for the gtm configuration example above can be found here in the codeshare. The GTM pool monitor script is also in the codeshare. Happy scripting!