sshotgun 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.rdoc +523 -0
- data/lib/sshotgun.rb +475 -0
- metadata +76 -0
data/README.rdoc
ADDED
@@ -0,0 +1,523 @@
|
|
1
|
+
== SSHotgun - Utility library for writing scripts to administer and provision multiple servers or server farms.
|
2
|
+
|
3
|
+
The SSHotgun project homepage is http://sshotgun.rubyforge.org
|
4
|
+
|
5
|
+
To get help, post your questions on forums on RubyForge at http://rubyforge.org/forum/?group_id=6867
|
6
|
+
|
7
|
+
The author of SSHotgun is Vick Perry (vick.perry @nospam@ gmail.com)
|
8
|
+
|
9
|
+
== DESCRIPTION:
|
10
|
+
|
11
|
+
SShotgun is a utility library for writing Unix/Linux server management and
|
12
|
+
provisioning scripts in Ruby. I use it for remotely managing machines in server
|
13
|
+
clusters/farms. SShotgun uses your locally installed SSH client and expects
|
14
|
+
that you have your SSH public key installed on the servers.
|
15
|
+
|
16
|
+
== FEATURES:
|
17
|
+
|
18
|
+
* Uses OpenSSH compatible SSH client and keys already installed on your local machine
|
19
|
+
* Optional use of a gateway machine for access to servers behind a firewall
|
20
|
+
* Logs in to remote machines via your public key already installed on all remote machines
|
21
|
+
* Automatically logs to the console and to a timestamped log file
|
22
|
+
* stdout and stderr from each remote machine is displayed and logged
|
23
|
+
* Can run sudo commands (your sudo password must be same on all remote machines)
|
24
|
+
* Multithreaded with configurable thread pool to service multiple machines simultaneously
|
25
|
+
* At end of processing run, a status report is displayed indicating successes, failures, timeouts, incompletes, etc.
|
26
|
+
|
27
|
+
== EXAMPLE:
|
28
|
+
|
29
|
+
You don't need to know much about Ruby to write powerful SSHotgun scripts. Just
|
30
|
+
copy the sample script below and edit the hostnames and the doProcessing method
|
31
|
+
as necessary.
|
32
|
+
|
33
|
+
The steps for writing a SSHotgun Ruby script are:
|
34
|
+
|
35
|
+
1. Create an array of fully qualified hostnames (fqdn).
|
36
|
+
2. Write your own custom processing class and doProcess method that will be
|
37
|
+
invoked on all remote servers. You can copy the example for this.
|
38
|
+
3. Instantiate a SSHotgun object in the script.
|
39
|
+
4. Run the script. A Ruby script can be invoked two different ways:
|
40
|
+
|
41
|
+
Run Ruby and specify the script
|
42
|
+
|
43
|
+
ruby ./myrubyscript.rb
|
44
|
+
|
45
|
+
or
|
46
|
+
|
47
|
+
include this line as the first line of the script
|
48
|
+
|
49
|
+
#!/usr/bin/env ruby
|
50
|
+
|
51
|
+
then make the script executable
|
52
|
+
|
53
|
+
chmod 755 myrubyscript.rb
|
54
|
+
|
55
|
+
and run it directly from the command line
|
56
|
+
|
57
|
+
./myrubyscript.rb
|
58
|
+
|
59
|
+
This is a simple example of a SSHotgun Ruby script:
|
60
|
+
|
61
|
+
#!/usr/bin/env ruby
|
62
|
+
|
63
|
+
# Required libraries
|
64
|
+
require "highline/import"
|
65
|
+
require 'sshotgun'
|
66
|
+
|
67
|
+
# Create an array to contain hosts for processing
|
68
|
+
hostlist = Array::new()
|
69
|
+
|
70
|
+
# Create a host definition
|
71
|
+
host = HostDefinition.new("localhost")
|
72
|
+
|
73
|
+
# Add the host definition to the array of hosts
|
74
|
+
hostlist << host
|
75
|
+
|
76
|
+
# Create and add more hosts...
|
77
|
+
host = HostDefinition.new("another.host.junk")
|
78
|
+
hostlist << host
|
79
|
+
|
80
|
+
# Create your own custom processing class that subclasses
|
81
|
+
# BaseProcessingClass. In this class, define your own doProcessing method
|
82
|
+
# where you write commands to execute on each remote server.
|
83
|
+
class MyProcessingClass < BaseProcessingClass
|
84
|
+
def doProcessing
|
85
|
+
# Execute a command on all remote servers. The run command has a return
|
86
|
+
# object from which you can obtain the output and exit status. See the
|
87
|
+
# user guide for more details.
|
88
|
+
run "ls -aF"
|
89
|
+
|
90
|
+
# There are other sshotgun commands available for copying files, running
|
91
|
+
# sudo, invoking local commands, etc.
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Create an instance of SSHotgun and pass it the list of hosts and your
|
96
|
+
# custom processing class.
|
97
|
+
sshotgun = SSHotgun.new(hostlist, MyProcessingClass)
|
98
|
+
|
99
|
+
# Start processing. The start call should be the very last line in a SSHotgun
|
100
|
+
# script.
|
101
|
+
sshotgun.start
|
102
|
+
|
103
|
+
== REQUIREMENTS:
|
104
|
+
|
105
|
+
* You must have an OpenSSH-compatible SSH client installed on your local machine.
|
106
|
+
* You must have correctly installed your SSH public keys on all remote servers that you access.
|
107
|
+
* For most advanced administrative tasks, you'll also need sudo privleges on the remote servers.
|
108
|
+
* You'll need write permissions in your present working directory (pwd) since SSHotgun will need to write its logs.
|
109
|
+
|
110
|
+
== INSTALL:
|
111
|
+
|
112
|
+
Use gem to install SSHotgun. Note that you probably need to run this command with sudo or as root.
|
113
|
+
|
114
|
+
gem install sshotgun
|
115
|
+
|
116
|
+
== USER GUIDE:
|
117
|
+
|
118
|
+
Here are snippets of SSHotgun Ruby code to illustrate SSHotgun's capabilities.
|
119
|
+
|
120
|
+
At a minimum, a host definition contains the fully qualified domain name (fqdn)
|
121
|
+
of a machine. As a best practice, DNS aliases are not generally referenced -
|
122
|
+
only the fqdn. There are also several optional fields in a host definition that
|
123
|
+
can be used for more selectivity when running a script. For example, you may
|
124
|
+
wish to do some additional configuration for those machines categorized as
|
125
|
+
"ldap" or "proxy"
|
126
|
+
|
127
|
+
A host definition has the following fields: hostname, category, os, desc
|
128
|
+
|
129
|
+
* hostname is the primary hostname or fqdn and NOT an alias.
|
130
|
+
* category is a freeform identifier for the type of the host. e.g. "wiki", "ldap" (optional)
|
131
|
+
* os is a freeform string indicating os name + server/workstation + version. e.g. ubuntu_server_7.10 (optional)
|
132
|
+
* desc is a one sentence string describing main purpose of the host (optional)
|
133
|
+
|
134
|
+
An example of a complete host definition is:
|
135
|
+
|
136
|
+
host = HostDefinition.new("localhost", "test", "ubuntu_server_7.10", "test vm")
|
137
|
+
hostlist << host
|
138
|
+
|
139
|
+
You can share a list of hosts among several SSHotgun scripts. To do this,
|
140
|
+
create a Ruby module (not a class) in a separate file. The module will contain
|
141
|
+
a method that returns an array of host definitions. The module is referenced by
|
142
|
+
any script that needs it.
|
143
|
+
|
144
|
+
# When you create your own modules, name the filename and module name with a similar name.
|
145
|
+
# Note: This module filename is 'listofhosts.rb' - this is the reference in the require statement in the calling script.
|
146
|
+
module ListOfHosts
|
147
|
+
|
148
|
+
# Create a method named <modulename>.getHostlist
|
149
|
+
def ListOfHosts.getHostlist
|
150
|
+
# Define, load and return the hostlist array
|
151
|
+
hostlist = []
|
152
|
+
|
153
|
+
host = HostDefinition.new("localhost", "test", "ubuntu_server_7.10", "test vm")
|
154
|
+
hostlist << host
|
155
|
+
|
156
|
+
host = HostDefinition.new("other.host.junk", "test", "ubuntu_server_7.10", "test vm")
|
157
|
+
hostlist << host
|
158
|
+
|
159
|
+
# return the array
|
160
|
+
return hostlist
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
164
|
+
|
165
|
+
|
166
|
+
It is a good practice to name the module file name similar or identical to the
|
167
|
+
module name.
|
168
|
+
|
169
|
+
Here is a SSHotgun Ruby script that uses the list of hosts:
|
170
|
+
|
171
|
+
#!/usr/bin/env ruby
|
172
|
+
|
173
|
+
# Required libraries
|
174
|
+
require "highline/import"
|
175
|
+
require 'sshotgun'
|
176
|
+
|
177
|
+
# Create array to contain hosts for processing
|
178
|
+
hostlist = Array::new()
|
179
|
+
|
180
|
+
# Read hosts from an external Ruby module.
|
181
|
+
require 'listofhosts'
|
182
|
+
|
183
|
+
# In the listofhosts.rb file, the ListOfHosts getHostlist method returns an
|
184
|
+
# array of hosts. Concatenate all of the multiple arrays into a single array of
|
185
|
+
# hosts.
|
186
|
+
hostlist = hostlist + ListOfHosts.getHostlist
|
187
|
+
|
188
|
+
class MyProcessingClass < BaseProcessingClass
|
189
|
+
def doProcessing
|
190
|
+
run "ls -aF"
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
sshotgun = SSHotgun.new(hostlist, MyProcessingClass)
|
195
|
+
sshotgun.start
|
196
|
+
|
197
|
+
Here's how to prompt for the sudo password (Don't hardcode a password into your
|
198
|
+
script). See the advanced example for details.
|
199
|
+
|
200
|
+
sshotgun.sudopassword = ask(">>> Enter your sudo password: ") { |q| q.echo = "*" } # or q.echo = false
|
201
|
+
|
202
|
+
Below is an advanced example that illustrates additional capabilities.
|
203
|
+
|
204
|
+
#!/usr/bin/env ruby
|
205
|
+
|
206
|
+
# Required libraries
|
207
|
+
require "highline/import"
|
208
|
+
require 'sshotgun'
|
209
|
+
|
210
|
+
# Create array to contain hosts for processing
|
211
|
+
hostlist = Array::new()
|
212
|
+
host = HostDefinition.new("someserver.junk", "test", "ubuntu_server_7.10", "test vm")
|
213
|
+
hostlist << host
|
214
|
+
|
215
|
+
# Import a list of host definitions
|
216
|
+
require 'listofhosts'
|
217
|
+
hostlist = hostlist + ListOfHosts.getHostlist
|
218
|
+
|
219
|
+
class MyProcessingClass < BaseProcessingClass
|
220
|
+
|
221
|
+
# You can add your own Ruby instance variables. These are useful if you
|
222
|
+
# want to share data across your own custom methods.
|
223
|
+
attr_accessor :myInstanceVariable
|
224
|
+
|
225
|
+
# You MUST define a doProcessing method. Note that to make the doProcessing
|
226
|
+
# method less cluttered, you can also create your own methods that are
|
227
|
+
# called from within the doProcessing method. You can use your own accessor
|
228
|
+
# variables (see above) for data sharing between methods. The doProcess method
|
229
|
+
# is the only method called by the SSHotgun framework for each host.
|
230
|
+
def doProcessing
|
231
|
+
# Log the beginning of this method. This marker is useful for debugging problems.
|
232
|
+
log "[" + @hostdef.hostname + "] info: " + "Processing started"
|
233
|
+
|
234
|
+
# Execute a command on all remote servers. Remote output is always written
|
235
|
+
# to this script's stdout.
|
236
|
+
# run <command>
|
237
|
+
run "ls -aF | grep -i .bash"
|
238
|
+
|
239
|
+
# The runstatus object is returned from all run and runSudo calls.
|
240
|
+
# runstatus contains information about exit status, stdout and stderr from
|
241
|
+
# the remote server.
|
242
|
+
runstatus = run "ls -aF"
|
243
|
+
|
244
|
+
# Use the log method to write to the log (the console). Don't use the Ruby
|
245
|
+
# 'puts' method to write to the console because it isn't threadsafe and may
|
246
|
+
# mix and garble simultaneous output by multiple threads.
|
247
|
+
log "[" + @hostdef.hostname + "] info: " + "The exit status of the run command is " + runstatus.exitstatus.to_s
|
248
|
+
|
249
|
+
# Remote stderr and stdout are captured and stored in the stdoutstr and
|
250
|
+
# stderrstr variables. Learn about Ruby's regular expressions for examining
|
251
|
+
# the output from a remote command.
|
252
|
+
runstatus = run "id rumplestiltskin"
|
253
|
+
|
254
|
+
# Check for "No such user" contained in stdoutstr. That's what is displayed
|
255
|
+
# when the unix "id" command can't find the specified user. Note that the
|
256
|
+
# the exit status of a failed 'id' command is not equal to zero either (see
|
257
|
+
# below). This is also a Ruby regex comparison.
|
258
|
+
if /No such user/ =~ runstatus.stdoutstr
|
259
|
+
log "I failed to find user rumplestiltskin on this server"
|
260
|
+
end
|
261
|
+
|
262
|
+
# In some cases a remote command fails and you must stop processing for
|
263
|
+
# that one server. Stop the execution of the doProcessing method by calling
|
264
|
+
# Ruby's "return" statement. Don't call Ruby's "exit" statement because
|
265
|
+
# that will halt all running threads.
|
266
|
+
# runstatus = run "id rumplestiltskin"
|
267
|
+
# if runstatus.exitstatus > 0
|
268
|
+
# log "The exit code was not 0, therefore I failed to find user rumplestiltskin"
|
269
|
+
# return # stop processing for this host
|
270
|
+
# end
|
271
|
+
# log "You'll never get here...unless you have a user named rumplestiltskin"
|
272
|
+
|
273
|
+
# The @hostdef instance variable contains the host definition for the
|
274
|
+
# current host being processed. If you set category, os and desc to
|
275
|
+
# meaningful information then you can do fancier branching logic in your
|
276
|
+
# script. The @hostdef.hostname is very useful in log statements. The other
|
277
|
+
# @hostdef fields are useful for treating a category or os differently than
|
278
|
+
# the others.
|
279
|
+
log "The current host is: " + @hostdef.hostname
|
280
|
+
log "The current host's category is: " + @hostdef.category
|
281
|
+
log "The current host's os is: " + @hostdef.os
|
282
|
+
log "The current host's description is: " + @hostdef.desc
|
283
|
+
|
284
|
+
# Target a specified host. This is a Ruby string comparison.
|
285
|
+
if @hostdef.hostname == "192.168.1.224"
|
286
|
+
log "[" + @hostdef.hostname + "] info: " + "Special processing for this host"
|
287
|
+
end
|
288
|
+
|
289
|
+
# If necessary, escape any quotes within the command. that must be passed
|
290
|
+
# through to the remote server. This isn't a great example but you'll need
|
291
|
+
# to do this for tricky shell scripting.
|
292
|
+
run "ls -aF | grep -i \".bash\""
|
293
|
+
|
294
|
+
# If you are running any runSudo commands then the SSHotgun.sudoPassword
|
295
|
+
# must be set - uncomment the user prompt and setting of the sudoPassword
|
296
|
+
# at the bottom of this script. Don't set up your servers to permit a
|
297
|
+
# password-less sudo - it is a security risk. Watch out, the "runSudo"
|
298
|
+
# command and Ruby in general, are case sensitive.
|
299
|
+
# runSudo <command>
|
300
|
+
# runSudo "touch /usr/local/bin/mytest.txt"
|
301
|
+
# run "ls -alF /usr/local/bin/"
|
302
|
+
# runSudo "rm /usr/local/bin/mytest.txt"
|
303
|
+
|
304
|
+
# Run a command locally (on your local machine). The runstatus variable is
|
305
|
+
# returned if you need the data - same as run or runSudo.
|
306
|
+
# runLocal <command>
|
307
|
+
# runLocal "ls -alF"
|
308
|
+
|
309
|
+
# Run a sudo command locally. The runstatus variable is returned if you
|
310
|
+
# need data from it. Set a sudoPassword if you are calling runLocalSudo.
|
311
|
+
# runLocalSudo <command>
|
312
|
+
# runLocalSudo "ls -alF"
|
313
|
+
|
314
|
+
# Create a remote file from a string parameter.
|
315
|
+
# createRemoteFile <content string>, <remotefile>
|
316
|
+
# createRemoteFile "this is your content here", "/home/vickp/testfile.txt"
|
317
|
+
|
318
|
+
# Create a remote file and set mode
|
319
|
+
# createRemoteFile <content string>, <remotefile>, <mode string>
|
320
|
+
# createRemoteFile "this is your content here", "/home/vickp/testfile.sh", "0755"
|
321
|
+
|
322
|
+
# Copy a remote file to the local machine. Note that you should vary the
|
323
|
+
# name of the incoming local file else it will be overwritten when each
|
324
|
+
# host's file is copied.
|
325
|
+
# copyRemoteFileToLocal <remotefile>, <localfile>
|
326
|
+
# copyRemoteFileToLocal "/home/vickp/testfile.txt", "/home/vickp/temp/" + @hostdef.hostname + "_testfile.txt"
|
327
|
+
|
328
|
+
# Copy a remote file to the local machine and set the mode of local file.
|
329
|
+
# copyRemoteFileToLocal <remotefile>, <localfile>, <mode string>
|
330
|
+
# copyRemoteFileToLocal "/home/vickp/testfile.txt", "/home/vickp/temp/" + @hostdef.hostname + "_testfile.txt", "0644"
|
331
|
+
|
332
|
+
# Copy a local file to the remote machine.
|
333
|
+
# copyRemoteFileToLocal <localfile>, <remotefile>
|
334
|
+
# copyLocalFileToRemote "/home/vickp/temp/testfile.txt", "/home/vickp/testfile2.txt"
|
335
|
+
|
336
|
+
# Copy a local file to the remote machine and set the mode of remote file.
|
337
|
+
# copyRemoteFileToLocal <localfile>, <remotefile>, <mode string>
|
338
|
+
# copyLocalFileToRemote "/home/vickp/temp/testfile.txt", "/home/vickp/testfile2.txt", "0644"
|
339
|
+
|
340
|
+
# Other helpful SSHotgun and Ruby tips below
|
341
|
+
# ===========================================
|
342
|
+
|
343
|
+
# Ruby allows you to concatenate multiple command lines
|
344
|
+
# Note use of shell command separator ';'
|
345
|
+
# cmd = "ls -alF;"
|
346
|
+
# cmd << "ls -alF | grep -i .bash;"
|
347
|
+
# cmd << "ls -alF | grep -i .profile"
|
348
|
+
# run cmd
|
349
|
+
|
350
|
+
# Delete a file on the remote server.
|
351
|
+
# Note use of -f with rm command
|
352
|
+
# run "rm -f /path/to/remote/file"
|
353
|
+
|
354
|
+
# Call your own methods to unclutter and better organize your doProcess
|
355
|
+
# method code. In most of my SSHotgun scripts the doProcess method is
|
356
|
+
# fairly sparse - most of the work is done is various custom methods that
|
357
|
+
# are called within the doProcess method
|
358
|
+
# myOwnMethod
|
359
|
+
|
360
|
+
# Sometimes I either install unsigned packages of my own (shell and ruby
|
361
|
+
# scripts) or need to force the installation of files (overwrite) that
|
362
|
+
# belong to another package. Aptitude doesn't yet support forcing yes for
|
363
|
+
# this sort of thing but apt-get does...
|
364
|
+
# runSudo "apt-get install -y --force-yes myUntrustedPackage -o DPkg::options::='--force-overwrite'"
|
365
|
+
|
366
|
+
# Ruby's ENV contains the current user (you)
|
367
|
+
currentUser = ENV["USER"].to_s
|
368
|
+
|
369
|
+
# For double quoted strings, Ruby will do inline substitution for a
|
370
|
+
# variable enbedded within "#{myvariablehere}". This is useful when you
|
371
|
+
# want a variable substituted inside of double quotes
|
372
|
+
# createRemoteFile "deb http://debrepo/global ./", "/home/#{currentUser}/sources.list", "0644"
|
373
|
+
|
374
|
+
# Create a global variable if you need to prompt for additional information
|
375
|
+
# (such as userid, date range, etc) and want to use that information in
|
376
|
+
# your doProcessing method. Global variables begin with '$'. See bottom of
|
377
|
+
# script where the variable is created. Note that "id" returns with an exit
|
378
|
+
# status of 1 if the user does not exist.
|
379
|
+
runstatus = run "id #{$userid}"
|
380
|
+
if runstatus.exitstatus == 0
|
381
|
+
log "[" + @hostdef.hostname + "] info: " + "Found user " + $userid
|
382
|
+
else
|
383
|
+
log "[" + @hostdef.hostname + "] info: " + "DID NOT find user " + $userid
|
384
|
+
end
|
385
|
+
|
386
|
+
# To pass escaped characters in strings, tell Ruby not to interpolate them
|
387
|
+
# by using single quotes or the %q() delimiter to enclose the escaped
|
388
|
+
# string. This means that Ruby will not attempt to resolve the escaped
|
389
|
+
# string but merely pass it on to the ssh client.
|
390
|
+
# run %q("mycommand \"some\ string\ with\ escapes\" more")
|
391
|
+
|
392
|
+
# Example of how to update a debian/ubuntu sources.list package repository file
|
393
|
+
# Also see use of case statement in the method body.
|
394
|
+
# installSourcesListAndUpdate
|
395
|
+
|
396
|
+
# Log the end of this method.
|
397
|
+
log "[" + @hostdef.hostname + "] info: " + "Processing completed"
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
# Your own custom methods can be called from within the doProcess method
|
402
|
+
def myOwnMethod
|
403
|
+
log "[" + @hostdef.hostname + "] info: " + "Calling my own method"
|
404
|
+
end
|
405
|
+
|
406
|
+
# Update a debian/ubuntu /etc/apt/sources.list
|
407
|
+
def installSourcesListAndUpdate
|
408
|
+
# WARNING: This only works if you fill in the category field in your hostdefs.
|
409
|
+
#
|
410
|
+
# Copy production sources.list into your home directory, then copy it into
|
411
|
+
# place. Note this is a two step operation because you can't create the
|
412
|
+
# remote file in a place where you don't have perms. The second step is to
|
413
|
+
# use sudo and copy the file into place.
|
414
|
+
s = ""
|
415
|
+
case @hostdef.category
|
416
|
+
when "production"
|
417
|
+
f = File.new("/usr/local/bin/adminscripts_templates/production_ubuntuserver710i386.sources.list")
|
418
|
+
when "devint"
|
419
|
+
f = File.new("/usr/local/bin/adminscripts_templates/devint_ubuntuserver710i386.sources.list")
|
420
|
+
when "qaint"
|
421
|
+
f = File.new("/usr/local/bin/adminscripts_templates/qaint_ubuntuserver710i386.sources.list")
|
422
|
+
when "staging"
|
423
|
+
f = File.new("/usr/local/bin/adminscripts_templates/staging_ubuntuserver710i386.sources.list")
|
424
|
+
else
|
425
|
+
log "[" + @hostdef.hostname + "] ERROR: " + "Unknown hostdef category = " + @hostdef.category
|
426
|
+
return
|
427
|
+
end
|
428
|
+
# read the file and concatenate it into a single string
|
429
|
+
f.each_line do |line|
|
430
|
+
s << line
|
431
|
+
end
|
432
|
+
createRemoteFile s, "/home/#{$currentUser}/sources.list", "0644"
|
433
|
+
runSudo "cp /home/#{$currentUser}/sources.list /etc/apt/sources.list"
|
434
|
+
run "rm /home/#{$currentUser}/sources.list"
|
435
|
+
|
436
|
+
# after changing sources.list, update package manager to refresh package list
|
437
|
+
runSudo "aptitude update"
|
438
|
+
end
|
439
|
+
|
440
|
+
# Create an instance of SSHotgun and pass it the list of hosts and your custom
|
441
|
+
# processing class.
|
442
|
+
sshotgun = SSHotgun.new(hostlist, MyProcessingClass)
|
443
|
+
|
444
|
+
# If you are calling a sudo command then prompt the user for the sudo password.
|
445
|
+
# I recommend that you NEVER HARDCODE A PASSWORD IN A SCRIPT! Configure the
|
446
|
+
# ask command to hide the password as the user types it. Note that the you
|
447
|
+
# must have the highline gem package installed for the "ask" command
|
448
|
+
#sshotgun.sudopassword = ask(">>> Enter your sudo password: ") { |q| q.echo = "*" } # or q.echo = false
|
449
|
+
|
450
|
+
# If you need to forward all calls via a gateway ssh server, set it here. Once
|
451
|
+
# set, all ssh connections go through the gateway machine
|
452
|
+
#sshotgun.gatewayhost = "someserver.somedomain.com"
|
453
|
+
|
454
|
+
# You can also use the 'ask' command to prompt the user for any other
|
455
|
+
# information. Configure the ask command to display the string as the user
|
456
|
+
# types it. Be sure to test password-less login from your gateway server to
|
457
|
+
# clear out any prompts asking you to verify the authenticity of the remote
|
458
|
+
# host.
|
459
|
+
#gatewayhost = ask(">>> Enter the hostname of the gateway server: ") { |q| q.echo = true }
|
460
|
+
#sshotgun.gatewayhost = gatewayhost
|
461
|
+
|
462
|
+
# Create and use a global variable to get additional data into your doProcessing method.
|
463
|
+
$userid = ask(">>> Enter the userid to find: ") { |q| q.echo = true }
|
464
|
+
|
465
|
+
# Number of simultaneous processing threads (hosts) to run. In terms of load to
|
466
|
+
# your local machine, each processing thread equates to about one ssh client
|
467
|
+
# running. Default is 50
|
468
|
+
# sshotgun.maxThreads = 30
|
469
|
+
|
470
|
+
# A processing thread will be killed after this amount of time. Default is 30 minutes.
|
471
|
+
# sshotgun.threadTimeoutInSeconds = 900
|
472
|
+
|
473
|
+
# Period for displaying status information about processing threads that
|
474
|
+
# are still running. Default is 30
|
475
|
+
# sshotgun.monitorPollIntervalInSeconds = 20
|
476
|
+
|
477
|
+
# Any output is also written to a file logger. You can disable the file logging
|
478
|
+
# by setting isFileLogging to false.
|
479
|
+
# sshotgun.isFileLogging = false.
|
480
|
+
|
481
|
+
# What should the behavior be when a command returns a non-zero exit status? A
|
482
|
+
# non-zero exit status usually indicates failure of the command. Some sshotgun
|
483
|
+
# users wish to immediately stop the processing for that server and stop it's
|
484
|
+
# thread. Other people will check the exit status after each command and decide
|
485
|
+
# whether to continue or return from the processing method. Note that if
|
486
|
+
# isStopOnNonZeroExit is true then the processing thread exits immediately and
|
487
|
+
# you never have the opportunity to check the exit status in your processing
|
488
|
+
# method code. The default is false (don't stop on non-zero exit).
|
489
|
+
# sshotgun.isStopOnNonZeroExit = true
|
490
|
+
|
491
|
+
# Start processing. The start call should be the very last line in a SSHotgun
|
492
|
+
# script.
|
493
|
+
sshotgun.start
|
494
|
+
|
495
|
+
== LICENSE:
|
496
|
+
|
497
|
+
(Simplified BSD License)
|
498
|
+
|
499
|
+
Copyright (c) 2008, Vick Perry
|
500
|
+
All rights reserved.
|
501
|
+
|
502
|
+
Redistribution and use in source and binary forms, with or without
|
503
|
+
modification, are permitted provided that the following conditions are met:
|
504
|
+
|
505
|
+
* Redistributions of source code must retain the above copyright notice, this
|
506
|
+
list of conditions and the following disclaimer.
|
507
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
508
|
+
this list of conditions and the following disclaimer in the documentation
|
509
|
+
and/or other materials provided with the distribution.
|
510
|
+
* Neither the name of the <ORGANIZATION> nor the names of its contributors may
|
511
|
+
be used to endorse or promote products derived from this software without
|
512
|
+
specific prior written permission.
|
513
|
+
|
514
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
515
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
516
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
517
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
518
|
+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
519
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
520
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
521
|
+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
522
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
523
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/lib/sshotgun.rb
ADDED
@@ -0,0 +1,475 @@
|
|
1
|
+
#:title: SSHotgun
|
2
|
+
#Author:: Vick Perry (vick.perry @nospam@ gmail.com)
|
3
|
+
#Copyright:: Copyright (c) 2008, Vick Perry
|
4
|
+
#License:: Simplified BSD License.
|
5
|
+
#
|
6
|
+
require 'open4'
|
7
|
+
require 'logger'
|
8
|
+
|
9
|
+
# Class to contain information about each server
|
10
|
+
#
|
11
|
+
class HostDefinition
|
12
|
+
# hostname is the primary hostname or fqdn (NOT an alias). It is better to only refer to the primary DNS entry for a machine or VM.
|
13
|
+
attr_accessor :hostname
|
14
|
+
|
15
|
+
# category is a freeform identifier for the type of the host. e.g. "production", "devint", "qaint", "staging" (optional)
|
16
|
+
attr_accessor :category
|
17
|
+
|
18
|
+
# os is a freeform string indicating os name + server/workstation + version. e.g. ubuntu_server_7.10 (optional)
|
19
|
+
attr_accessor :os
|
20
|
+
|
21
|
+
# desc is a one sentence string describing main purpose of the host (optional)
|
22
|
+
attr_accessor :desc
|
23
|
+
|
24
|
+
# For internal use. True if processing was done for this server.
|
25
|
+
attr_accessor :startedProcess
|
26
|
+
|
27
|
+
# For internal use. True if processing was finished for this server. This means that doProcess exited
|
28
|
+
# normally with no exceptions.
|
29
|
+
attr_accessor :finishedProcess
|
30
|
+
|
31
|
+
# For internal use. True if one or more commands on a server returned with a non-zero exit status
|
32
|
+
attr_accessor :hadNonZeroExitStatus
|
33
|
+
|
34
|
+
def initialize(hostname, category = "", os = "", desc = "")
|
35
|
+
@hostname = hostname
|
36
|
+
@category = category
|
37
|
+
@os = os
|
38
|
+
@desc = desc
|
39
|
+
@startedProcess = false
|
40
|
+
@finishedProcess = false
|
41
|
+
@hadNonZeroExitStatus = false
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# This class contains the returned information from commands such as run, runSudo, runLocal, runLocalSudo
|
46
|
+
#
|
47
|
+
class RunStatus
|
48
|
+
# The exit status of the command
|
49
|
+
attr_accessor :exitstatus
|
50
|
+
|
51
|
+
# The stdout stream data from the command stored in a string
|
52
|
+
attr_accessor :stdoutstr
|
53
|
+
|
54
|
+
# The stderr stream data from the command stored in a string
|
55
|
+
attr_accessor :stderrstr
|
56
|
+
|
57
|
+
def initialize
|
58
|
+
@exitstatus = 0
|
59
|
+
@stdoutstr = ""
|
60
|
+
@stderrstr = ""
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# This is the base processing class. Your custom processing class MUST inherit from this base class.
|
65
|
+
#
|
66
|
+
class BaseProcessingClass
|
67
|
+
# A SSHotgun object
|
68
|
+
attr_accessor :sshotgun
|
69
|
+
|
70
|
+
# The current HostDefinition object (represents the remote server for processing)
|
71
|
+
attr_accessor :hostdef
|
72
|
+
|
73
|
+
# Default constructor. Don't create a constructor in your custom processing
|
74
|
+
# class so that this constructor will be called.
|
75
|
+
#
|
76
|
+
def initialize(sshotgun, hostdef)
|
77
|
+
@sshotgun = sshotgun
|
78
|
+
@hostdef = hostdef
|
79
|
+
end
|
80
|
+
|
81
|
+
# You must override the doProcessing method in your own custom processing class
|
82
|
+
#
|
83
|
+
def doProcessing
|
84
|
+
log "[" + @hostdef.hostname + "] info: " + "The doProcessing method must be created in your custom processing class"
|
85
|
+
end
|
86
|
+
|
87
|
+
# Run a command on a remote server
|
88
|
+
#
|
89
|
+
# cmd is a string containing the command to run on a remote server. e.g. "ls -alF"
|
90
|
+
#
|
91
|
+
# Returns a RunStatus object
|
92
|
+
#
|
93
|
+
def run(cmd)
|
94
|
+
log "[" + @hostdef.hostname + "] run: " + cmd
|
95
|
+
if @sshotgun.gatewayhost
|
96
|
+
cmdline = "ssh -A -t -x " + @sshotgun.gatewayhost + " 'ssh -A -x " + @hostdef.hostname + " \"" + cmd + "\"'"
|
97
|
+
else
|
98
|
+
cmdline = "ssh -A -t -x " + @hostdef.hostname + " \"" + cmd + "\""
|
99
|
+
end
|
100
|
+
runLocal cmdline, false
|
101
|
+
end
|
102
|
+
|
103
|
+
# Sudo a command on a remote server
|
104
|
+
#
|
105
|
+
# cmd is a string containing the sudo command to run on a remote server. e.g. "ls -alF"
|
106
|
+
#
|
107
|
+
# Returns a RunStatus object
|
108
|
+
#
|
109
|
+
def runSudo(cmd)
|
110
|
+
log "[" + @hostdef.hostname + "] runSudo: " + cmd
|
111
|
+
if @sshotgun.gatewayhost
|
112
|
+
cmdline = "ssh -A -t -x " + @sshotgun.gatewayhost + " 'ssh -A -x " + @hostdef.hostname + " sudo -S \"" + cmd + "\"'"
|
113
|
+
else
|
114
|
+
cmdline = "ssh -A -t -x " + @hostdef.hostname + " sudo -S \"" + cmd + "\""
|
115
|
+
end
|
116
|
+
runLocal cmdline, true
|
117
|
+
end
|
118
|
+
|
119
|
+
# Copy a remote file to local
|
120
|
+
#
|
121
|
+
# remotefile is the remote file to fetch. e.g. "/usr/local/bin/myscript.sh"
|
122
|
+
#
|
123
|
+
# localfile is the name of the destination local file. e.g. "/home/vickp/temp/myscript.sh"
|
124
|
+
#
|
125
|
+
# modestr is the string containing parameters for the chmod operation.
|
126
|
+
# The string must be in the NNNN or ugo format. e.g. "0755" or "u=rwx,g=rw,o=r"
|
127
|
+
#
|
128
|
+
def copyRemoteFileToLocal(remotefile, localfile, modestr = "0644")
|
129
|
+
log "[" + @hostdef.hostname + "] copyRemoteFileToLocal: " + @hostdef.hostname + ":" + remotefile + " " + localfile
|
130
|
+
if @sshotgun.gatewayhost
|
131
|
+
cmdline = "ssh -A -t -x " + @sshotgun.gatewayhost + " 'ssh -A -x " + @hostdef.hostname + " \"cat " + remotefile + " | gzip - \"' | gzip -d - > " + localfile + "; chmod " + modestr + " " + localfile
|
132
|
+
else
|
133
|
+
cmdline = "ssh -A -t -x " + @hostdef.hostname + " \"cat " + remotefile + " | gzip - \" | gzip -d - > " + localfile + "; chmod " + modestr + " " + localfile
|
134
|
+
end
|
135
|
+
runLocal cmdline, false
|
136
|
+
end
|
137
|
+
|
138
|
+
# Copy a local file to remote
|
139
|
+
#
|
140
|
+
# localfile is the local file to copy. e.g. "/home/vickp/temp/myscript.sh"
|
141
|
+
#
|
142
|
+
# remotefile is the name of the destination remote file. e.g. "/usr/local/bin/myscript.sh"
|
143
|
+
#
|
144
|
+
# modestr is the string containing parameters for the chmod operation.
|
145
|
+
# The string must be in the NNNN or ugo format. e.g. "0755" or "u=rwx,g=rw,o=r"
|
146
|
+
#
|
147
|
+
def copyLocalFileToRemote(localfile, remotefile, modestr = "0644")
|
148
|
+
log "[" + @hostdef.hostname + "] copyLocalFileToRemote: " + localfile + " " + @hostdef.hostname + ":" + remotefile
|
149
|
+
if @sshotgun.gatewayhost
|
150
|
+
cmdline = "cat " + localfile + " | gzip - | ssh -A -t -x " + @sshotgun.gatewayhost + " 'ssh -A -x " + @hostdef.hostname + " \"cat | gzip -d > " + remotefile + "; chmod " + modestr + " " + remotefile + "\"'"
|
151
|
+
else
|
152
|
+
cmdline = "cat " + localfile + " | gzip - | ssh -A -t -x " + @hostdef.hostname + " \"cat | gzip -d > " + remotefile + "; chmod " + modestr + " " + remotefile + "\""
|
153
|
+
end
|
154
|
+
runLocal cmdline, false
|
155
|
+
end
|
156
|
+
|
157
|
+
# Create a remote text file from a content string.
|
158
|
+
#
|
159
|
+
# contentstr is the content to write to the file.
|
160
|
+
#
|
161
|
+
# remotefile is the name of the destination remote file. e.g. "/usr/local/bin/myscript.sh"
|
162
|
+
#
|
163
|
+
# modestr is the string containing parameters for the chmod operation.
|
164
|
+
# The string must be in the NNNN or ugo format. e.g. "0755" or "u=rwx,g=rw,o=r"
|
165
|
+
#
|
166
|
+
def createRemoteFile(contentstr, remotefile, modestr = "0644")
|
167
|
+
log "[" + @hostdef.hostname + "] createRemoteFile: " + @hostdef.hostname + ":" + remotefile
|
168
|
+
if @sshotgun.gatewayhost
|
169
|
+
cmdline = "echo \"" + contentstr + "\" | gzip - | ssh -A -t -x " + @sshotgun.gatewayhost + " 'ssh -A -x " + @hostdef.hostname + " \"cat | gzip -d > " + remotefile + "; chmod " + modestr + " " + remotefile + "\"'"
|
170
|
+
else
|
171
|
+
cmdline = "echo \"" + contentstr + "\" | gzip - | ssh -A -t -x " + @hostdef.hostname + " \"cat | gzip -d > " + remotefile + "; chmod " + modestr + " " + remotefile + "\""
|
172
|
+
end
|
173
|
+
runLocal cmdline, false
|
174
|
+
end
|
175
|
+
|
176
|
+
# Execute a command locally without launching ssh. Note that this will be executed for EACH host.
|
177
|
+
#
|
178
|
+
# cmdline is the command line to run
|
179
|
+
#
|
180
|
+
# isSudo is the flag to launch as a sudo command
|
181
|
+
#
|
182
|
+
def runLocal (cmdline, isSudo=false)
|
183
|
+
#log "[" + @hostdef.hostname + "] debug: " + cmdline
|
184
|
+
currentThread = Thread.current
|
185
|
+
runstatus = RunStatus.new
|
186
|
+
retval = Open4::popen4( cmdline ) do |pid, stdin, stdout, stderr|
|
187
|
+
# isSudo and if sudo password is defined, insert the sudo password via stdin
|
188
|
+
if isSudo
|
189
|
+
if @sshotgun.sudopassword
|
190
|
+
stdin.puts @sshotgun.sudopassword
|
191
|
+
end
|
192
|
+
end
|
193
|
+
runstatus.stdoutstr = stdout.read.strip
|
194
|
+
runstatus.stdoutstr.each do |line|
|
195
|
+
log "[" + @hostdef.hostname + "] stdout: " + line
|
196
|
+
end
|
197
|
+
runstatus.stderrstr = stderr.read.strip
|
198
|
+
runstatus.stderrstr.each do |line|
|
199
|
+
log "[" + @hostdef.hostname + "] stderr: " + line
|
200
|
+
end
|
201
|
+
end
|
202
|
+
runstatus.exitstatus = retval.exitstatus
|
203
|
+
|
204
|
+
if runstatus.exitstatus != 0
|
205
|
+
# set flag to indicate a non-zero exit status
|
206
|
+
# this flag is needed by the status report at the end of the script run
|
207
|
+
@hostdef.hadNonZeroExitStatus = true
|
208
|
+
|
209
|
+
# am I supposed to stop thread?
|
210
|
+
if @sshotgun.isStopOnNonZeroExit
|
211
|
+
log "[" + @hostdef.hostname + "] stderr: Non-zero exit status - Stop processing for this server (" + runstatus.exitstatus.to_s + ")"
|
212
|
+
currentThread.exit
|
213
|
+
else
|
214
|
+
# did not exit thread so just log that a non-zero exit occured
|
215
|
+
log "[" + @hostdef.hostname + "] stderr: Non-zero exit status (" + runstatus.exitstatus.to_s + ")"
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
# return runstatus to caller
|
220
|
+
return runstatus
|
221
|
+
end
|
222
|
+
|
223
|
+
# Execute a sudo command locally without launching ssh. Note that this will be executed for EACH host.
|
224
|
+
#
|
225
|
+
# cmdline is the command line to run
|
226
|
+
#
|
227
|
+
def runLocalSudo (cmdline)
|
228
|
+
runLocal(cmdline, true)
|
229
|
+
end
|
230
|
+
|
231
|
+
# Log a string to the console.
|
232
|
+
#
|
233
|
+
# s is the string to log
|
234
|
+
#
|
235
|
+
def log(s)
|
236
|
+
@sshotgun.log s
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
# This is the main class of SSHotgun. This class stores the configuration and launches the processing threads.
|
241
|
+
#
|
242
|
+
class SSHotgun
|
243
|
+
|
244
|
+
# Array containing all of the HostDefinitions (hosts to process)
|
245
|
+
attr_accessor :hostlist
|
246
|
+
|
247
|
+
# Your custom processing class
|
248
|
+
attr_accessor :processingclass
|
249
|
+
|
250
|
+
# The sudo password, if needed. (optional)
|
251
|
+
attr_accessor :sudopassword
|
252
|
+
|
253
|
+
# The gateway host for use as an ssh relay. (optional)
|
254
|
+
attr_accessor :gatewayhost
|
255
|
+
|
256
|
+
# The maximum number of processing threads to run simultaneously. Each
|
257
|
+
# processing thread roughly equates to one ssh client running on your local
|
258
|
+
# machine. Default is 30. (optional)
|
259
|
+
attr_accessor :maxThreads
|
260
|
+
|
261
|
+
# Timeout for processing threads. Default is 30 minutes. (optional)
|
262
|
+
attr_accessor :threadTimeoutInSeconds
|
263
|
+
|
264
|
+
# Interval time to periodically display thread status information. Default is 30 sec. (optional)
|
265
|
+
attr_accessor :monitorPollIntervalInSeconds
|
266
|
+
|
267
|
+
# For internal use. Mutex for writing to the log
|
268
|
+
attr_reader :logmutex
|
269
|
+
|
270
|
+
# For internal use. Mutex for launching processing threads
|
271
|
+
attr_reader :threadStartMutex
|
272
|
+
|
273
|
+
# File logger. Set your own if desired. (optional)
|
274
|
+
attr_accessor :filelogger
|
275
|
+
|
276
|
+
# Turn on/off file logging. Default is on. (optional)
|
277
|
+
attr_accessor :isFileLogging
|
278
|
+
|
279
|
+
# For internal use. Script start time
|
280
|
+
attr_reader :startTime
|
281
|
+
|
282
|
+
# Turn on/off behavior where processing for a server is stopped if any
|
283
|
+
# command (run, runSudo, etc) returns with a non-zero exit code. Default is
|
284
|
+
# off (don't stop). Note that if isStopOnNonZeroExit is true then the
|
285
|
+
# processing thread exits immediately, you never have the opportunity to
|
286
|
+
# check the exit status in your processing code.
|
287
|
+
attr_accessor :isStopOnNonZeroExit
|
288
|
+
|
289
|
+
def initialize(hostlist, processingclass)
|
290
|
+
@hostlist = hostlist
|
291
|
+
@processingclass = processingclass
|
292
|
+
@logmutex = Mutex.new
|
293
|
+
@threadStartMutex = Mutex.new
|
294
|
+
@maxThreads = 30 # allow NN processing threads to run at any time
|
295
|
+
@threadTimeoutInSeconds = 1800 # timeout after 30 minutes
|
296
|
+
@monitorPollIntervalInSeconds = 30 # period for displaying thread monitor
|
297
|
+
timestr = Time.now.strftime("%Y%m%d%H%M%S")
|
298
|
+
logfilename = File.basename($0) + ".#{timestr}.log"
|
299
|
+
@filelogger = Logger.new(logfilename)
|
300
|
+
@isFileLogging = true
|
301
|
+
@isStopOnNonZeroExit = false
|
302
|
+
end
|
303
|
+
|
304
|
+
# Start the processing. In most cases, this call will be the last line in
|
305
|
+
# your SSHotgun processing script.
|
306
|
+
#
|
307
|
+
def start
|
308
|
+
# trap ctrl-c, show stats and exit
|
309
|
+
trap("INT") {
|
310
|
+
log "[_sshotgun_] info: Script terminated early by ctrl-c"
|
311
|
+
showRunStats
|
312
|
+
exit
|
313
|
+
}
|
314
|
+
@startTime = Time.now
|
315
|
+
log "[_sshotgun_] info: Started script " + $0 + " at " + @startTime.to_s
|
316
|
+
|
317
|
+
# Display configuration info
|
318
|
+
log "[_sshotgun_] info: User=" + ENV["USER"].to_s
|
319
|
+
log "[_sshotgun_] info: Gateway host=" + @gatewayhost.to_s
|
320
|
+
log "[_sshotgun_] info: Thread timeout=" + @threadTimeoutInSeconds.to_s + "s"
|
321
|
+
log "[_sshotgun_] info: Max concurrent threads=" + @maxThreads.to_s
|
322
|
+
log "[_sshotgun_] info: Monitor poll interval=" + @monitorPollIntervalInSeconds.to_s + "s"
|
323
|
+
log "[_sshotgun_] info: File logging is=" + @isFileLogging.to_s
|
324
|
+
log "[_sshotgun_] info: Will stop thread upon non-zero exit=" + @isStopOnNonZeroExit.to_s
|
325
|
+
|
326
|
+
hostarr = []
|
327
|
+
@hostlist.each do |hostdef|
|
328
|
+
hostarr << hostdef.hostname
|
329
|
+
end
|
330
|
+
log "[_sshotgun_] info: Host list=" + hostarr.join(", ")
|
331
|
+
|
332
|
+
currentHostIndex = 0
|
333
|
+
processingThreads = []
|
334
|
+
checkRunningThreadsCounter = 0
|
335
|
+
while true
|
336
|
+
|
337
|
+
# how many active threads now?
|
338
|
+
numThreadsAlive = 0
|
339
|
+
processingThreads.each { |aThread|
|
340
|
+
if aThread.alive?
|
341
|
+
numThreadsAlive += 1
|
342
|
+
end
|
343
|
+
}
|
344
|
+
|
345
|
+
# calc number of threads to launch with this
|
346
|
+
# pass through the loop
|
347
|
+
numThreadsToLaunch = @maxThreads - numThreadsAlive
|
348
|
+
numThreadsLeftToLaunch = @hostlist.length - currentHostIndex
|
349
|
+
if numThreadsToLaunch > numThreadsLeftToLaunch
|
350
|
+
numThreadsToLaunch = numThreadsLeftToLaunch
|
351
|
+
end
|
352
|
+
|
353
|
+
# from current index in hosts array loop and launch threads up to maxThreads running
|
354
|
+
i = 0
|
355
|
+
while i < numThreadsToLaunch do
|
356
|
+
# I hate to admit it but I don't know why i needed to synchronize this block
|
357
|
+
# Without it, some threads seemed to freeze at startup
|
358
|
+
@threadStartMutex.synchronize {
|
359
|
+
hostdef = @hostlist[currentHostIndex]
|
360
|
+
processingThreads << Thread.new {
|
361
|
+
aThread = Thread.current
|
362
|
+
aThread["threadstarttime"] = Time.now # store start time in threadlocal variable
|
363
|
+
aThread["hostname"] = hostdef.hostname # store hostname time in threadlocal variable
|
364
|
+
log "[_sshotgun_] info: Started thread for host = " + hostdef.hostname + " at " + aThread["threadstarttime"].to_s
|
365
|
+
begin
|
366
|
+
hostdef.startedProcess = true
|
367
|
+
o = @processingclass.new(self, hostdef)
|
368
|
+
o.doProcessing
|
369
|
+
hostdef.finishedProcess = true
|
370
|
+
rescue => ex
|
371
|
+
log "[_sshotgun_] stderr: Thread blew up for host = " + hostdef.hostname + ". ex = " + ex.inspect + "\n" + ex.backtrace.join("\n")
|
372
|
+
end
|
373
|
+
}
|
374
|
+
|
375
|
+
# bump loop counter
|
376
|
+
i += 1
|
377
|
+
currentHostIndex += 1
|
378
|
+
}
|
379
|
+
end
|
380
|
+
|
381
|
+
# kill expired threads
|
382
|
+
processingThreads.each { |aThread|
|
383
|
+
if aThread.alive?
|
384
|
+
# ensure that the threadlocal var is actually set before
|
385
|
+
# doing a calc with it. This is race condition where you
|
386
|
+
# get here so fast and threadstarttime is nil
|
387
|
+
if aThread["threadstarttime"]
|
388
|
+
if Time.now - aThread["threadstarttime"] > @threadTimeoutInSeconds
|
389
|
+
log "[_sshotgun_] info: Killed thread for host = " + aThread["hostname"]
|
390
|
+
aThread.kill
|
391
|
+
end
|
392
|
+
end
|
393
|
+
end
|
394
|
+
}
|
395
|
+
|
396
|
+
# display list of running threads as visual indicator to user
|
397
|
+
checkRunningThreadsCounter += 1
|
398
|
+
if checkRunningThreadsCounter > @monitorPollIntervalInSeconds
|
399
|
+
# display running threads
|
400
|
+
processingThreads.each { |aThread|
|
401
|
+
if aThread.alive?
|
402
|
+
log "[_sshotgun_] monitor: Thread still running for host = " + aThread["hostname"] + ". Elapsed time is " + (Time.now - aThread["threadstarttime"] ).to_s + "s"
|
403
|
+
end
|
404
|
+
}
|
405
|
+
checkRunningThreadsCounter = 0
|
406
|
+
end
|
407
|
+
|
408
|
+
# any processing threads still alive?
|
409
|
+
aThreadIsAlive = false
|
410
|
+
processingThreads.each { |aThread|
|
411
|
+
if aThread.alive?
|
412
|
+
aThreadIsAlive = true
|
413
|
+
break
|
414
|
+
end
|
415
|
+
}
|
416
|
+
|
417
|
+
# break out of this main loop if no processing threads are alive
|
418
|
+
if aThreadIsAlive
|
419
|
+
sleep 1
|
420
|
+
else
|
421
|
+
break
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
# ended normally, show stats
|
426
|
+
log "[_sshotgun_] info: Script terminated normally"
|
427
|
+
showRunStats
|
428
|
+
end
|
429
|
+
|
430
|
+
# Display these status at end of script or when control-c is pressed
|
431
|
+
def showRunStats
|
432
|
+
# Display script ending message
|
433
|
+
stopTime = Time.now
|
434
|
+
log "[_sshotgun_] info: Ended script " + $0 + " at " + stopTime.to_s
|
435
|
+
log "[_sshotgun_] info: Elapsed script " + $0 + " time " + (stopTime - @startTime).to_s + "s"
|
436
|
+
|
437
|
+
hostarr = []
|
438
|
+
@hostlist.each do |hostdef|
|
439
|
+
if hostdef.startedProcess == false
|
440
|
+
# did not start processing
|
441
|
+
hostarr << (hostdef.hostname + "(np)")
|
442
|
+
else
|
443
|
+
# started processing
|
444
|
+
if hostdef.hadNonZeroExitStatus
|
445
|
+
# one or more exit status were non-zero
|
446
|
+
hostarr << (hostdef.hostname + "(*)")
|
447
|
+
else
|
448
|
+
if hostdef.finishedProcess
|
449
|
+
# finished with no errors during processing
|
450
|
+
hostarr << hostdef.hostname
|
451
|
+
else
|
452
|
+
# Incomplete processing. This usually means that the method threw
|
453
|
+
# an exception
|
454
|
+
hostarr << (hostdef.hostname + "(i)")
|
455
|
+
end
|
456
|
+
end
|
457
|
+
end
|
458
|
+
end
|
459
|
+
log "[_sshotgun_] info: Status codes are '*'=had one|more non-zero exit status, 'i'=incomplete, 'np'=not started, blank=finished"
|
460
|
+
log "[_sshotgun_] info: Hosts summary = " + hostarr.join(", ")
|
461
|
+
end
|
462
|
+
|
463
|
+
# Log a string to the console and to the file logger
|
464
|
+
#
|
465
|
+
# s is the string to log
|
466
|
+
#
|
467
|
+
def log(s)
|
468
|
+
@logmutex.synchronize {
|
469
|
+
puts s.chomp
|
470
|
+
if @isFileLogging
|
471
|
+
@filelogger.info s.chomp
|
472
|
+
end
|
473
|
+
}
|
474
|
+
end
|
475
|
+
end
|
metadata
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sshotgun
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.4
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Vick Perry
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-09-08 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: open4
|
17
|
+
version_requirement:
|
18
|
+
version_requirements: !ruby/object:Gem::Requirement
|
19
|
+
requirements:
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 0.9.6
|
23
|
+
version:
|
24
|
+
- !ruby/object:Gem::Dependency
|
25
|
+
name: highline
|
26
|
+
version_requirement:
|
27
|
+
version_requirements: !ruby/object:Gem::Requirement
|
28
|
+
requirements:
|
29
|
+
- - ">="
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: 1.4.0
|
32
|
+
version:
|
33
|
+
description:
|
34
|
+
email: vick.perry @nospam@ gmail.com
|
35
|
+
executables: []
|
36
|
+
|
37
|
+
extensions: []
|
38
|
+
|
39
|
+
extra_rdoc_files:
|
40
|
+
- README.rdoc
|
41
|
+
files:
|
42
|
+
- lib/sshotgun.rb
|
43
|
+
- README.rdoc
|
44
|
+
has_rdoc: true
|
45
|
+
homepage: http://sshotgun.rubyforge.org
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options:
|
48
|
+
- --line-numbers
|
49
|
+
- --inline-source
|
50
|
+
- --title
|
51
|
+
- sshotgun
|
52
|
+
- --main
|
53
|
+
- README.rdoc
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: "0"
|
61
|
+
version:
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: "0"
|
67
|
+
version:
|
68
|
+
requirements: []
|
69
|
+
|
70
|
+
rubyforge_project: sshotgun
|
71
|
+
rubygems_version: 1.1.1
|
72
|
+
signing_key:
|
73
|
+
specification_version: 2
|
74
|
+
summary: A lib for writing server management scripts in Ruby. Useful for managing lots of servers.
|
75
|
+
test_files: []
|
76
|
+
|