TCL programming

A reusable expect dispatcher I kept around for running the same kind of operation across a list of servers — untar an index, restart a service, patch a config file. The trick is that the script reads the first command-line argument and dispatches to a same-named proc, so one script holds many small ops:

1
2
3
./script.exp UntarLuceneindexes
./script.exp stopLuceneServices
./script.exp startLuceneServices

That’s the $functionName line at the bottom — it literally calls whatever proc name you passed in. Crude, but very handy when you’re iterating on a runbook.

The script below assumes you’ve set up SSH keys so ssh root@host connects without a password prompt. That keeps secrets out of the script and the shell history.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
#!/usr/bin/expect -f
# Expect dispatcher. Beware: whitespace matters in Tcl.

# Multi-line comments use a set / curly-brace trick:

# set comment {
#     your multi-line comment

# }

# ---------------------------------------------------

# Function definitions
# ---------------------------------------------------

if {[llength $argv] == 0} {
    send_user "Usage: script.exp FUNCTION_NAME\n"
    send_user "Example: script.exp UntarLuceneindexes\n"
    exit 1
}
set functionName [lindex $argv 0]

# Servers used by the Lucene-related procs.

set aLuceneServers {your_server_1 your_server_2 your_server_3}

proc printPass {} {
    send_user "\n\[PASS\]\n"
}

proc printFail {} {
    send_user "\n\[FAIL\]\n"
    exit 1
}

proc simpleTest {} {
    set aProductionServers {your_server_1 your_server_2 your_server_3 your_server_4 your_server_5 your_server_6}
    foreach host $aProductionServers {
        send_user "Processing ... '$host'\n"
        set timeout 60
        spawn ssh root@$host

        send "hostname\r"
        expect $host

        send "uptime\r"
        expect "load average"

        send "exit\r"
    }
}

proc UntarLuceneindexes {} {
    global aLuceneServers
    set sCurrentLuceneTar "index2_20151009.tar.bz2"
    foreach host $aLuceneServers {
        send_user "\n================================================\n"
        send_user "Processing ... '$host'\n"
        send_user "================================================\n"
        set timeout 60
        spawn ssh root@$host

        send "hostname\r"
        expect $host

        send "su - path\r"
        send "pwd\r"
        expect "/home/path"

        send "ls $sCurrentLuceneTar | wc -l\r"
        expect "1"

        send "tar -xjvf $sCurrentLuceneTar &\r"
        send "sleep 2\r"
        send "ps aux | grep index2 | grep tar | wc -l\r"
        send "disown %1\r"
        expect "1"

        send_user "Done processing ... '$host'\n"
    }
}

proc updateDBRef {} {
    set apathServers {your_server_6}
    set newDB "some_db_20150925"
    foreach host $apathServers {
        send_user "\n================================================\n"
        send_user "Processing ... '$host'\n"
        send_user "================================================\n"
        set timeout 60
        spawn ssh root@$host

        send "hostname\r"
        expect $host

        send "su - path\r"
        send "pwd\r"
        expect "/home/path"

        set propsFile "/home/path/path-current/SomeConfig.properties"
        send "grep -v 'com.somecompany.setting.server.db.pathdb2.url=' $propsFile > $propsFile.new\r"
        send "echo 'com.somecompany.setting.server.db.pathdb2.url=jdbc:postgresql://127.0.0.1:5432/$newDB' >> $propsFile.new\r"
        send "mv $propsFile.new $propsFile\r"

        send_user "Done processing ... '$host'\n"
    }
}

proc stopLuceneServices {} {
    global aLuceneServers
    foreach host $aLuceneServers {
        send_user "\n================================================\n"
        send_user "Processing ... '$host'\n"
        send_user "================================================\n"
        set timeout 60
        spawn ssh root@$host

        send "hostname\r"
        expect $host

        send "service lucene stop\r"
        expect {
            "stopped PID"           { printPass }
            "lucene is not running" { printPass }
            timeout                 { printFail }
        }

        send_user "Done processing ... '$host'\n"
    }
}

proc startLuceneServices {} {
    global aLuceneServers
    foreach host $aLuceneServers {
        send_user "\n================================================\n"
        send_user "Processing ... '$host'\n"
        send_user "================================================\n"
        set timeout 60
        spawn ssh root@$host

        send "hostname\r"
        expect $host

        send "service lucene start\r"
        expect {
            "started PID" { printPass }
            timeout       { printFail }
        }

        send_user "Done processing ... '$host'\n"
    }
}

# ---------------------------------------------------

# Run
# ---------------------------------------------------

set timeout 60
log_file -noappend expect.log  ;# default is append; -noappend overwrites

$functionName

Tcl or Expect? The post title says Tcl, but almost every interesting line above — spawn, expect, send, send_user, log_file, the #!/usr/bin/expect -f shebang — comes from Expect, an extension that sits on top of Tcl and lets you drive interactive programs (SSH, telnet, vendor CLIs) by pattern-matching on their output. Pure Tcl is the language; Expect is what makes a script like this one possible. 🤖

Use SSH keys, not expect-and-send-password. The original version of this script had blocks like expect “*assword: “ followed by send “YOUR_SERVER_PASSWORD\r”. That works, but it puts a plaintext password in your script and your shell history. The cleaner answer is SSH keypair auth — the script above assumes that’s already set up, so the password block disappears entirely. Reserve the expect-and-send-password pattern for the cases where it’s truly unavoidable: old network gear, vendor CLIs, devices that don’t accept keys.

You don’t always need Expect. If all you need is to run the same shell command across a list of servers, plain SSH in a loop is simpler and easier to debug:

1
2
3
for host in your_server_1 your_server_2 your_server_3; do
  ssh "$host" "service lucene stop"
done

For larger fleets, parallel-ssh (pssh) or ansible -m shell -a “…” mygroup are usually saner. Expect earns its keep when you have to navigate an interactive prompt that doesn’t accept piped commands — confirm dialogs, paged output, vendor consoles. 💡

This entry was posted in Linux, TCL/Expect. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *


7 × = twenty eight