sshotgun 1.0.4 → 1.0.5
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 +164 -99
- data/lib/sshotgun.rb +104 -60
- metadata +5 -3
data/README.rdoc
CHANGED
@@ -10,8 +10,12 @@ The author of SSHotgun is Vick Perry (vick.perry @nospam@ gmail.com)
|
|
10
10
|
|
11
11
|
SShotgun is a utility library for writing Unix/Linux server management and
|
12
12
|
provisioning scripts in Ruby. I use it for remotely managing machines in server
|
13
|
-
clusters/farms.
|
14
|
-
|
13
|
+
clusters/farms.
|
14
|
+
|
15
|
+
SShotgun calls your locally installed SSH client. Before you do anything with
|
16
|
+
SSHotgun, insure that your SSH public keys are installed correctly, the
|
17
|
+
ssh-agent is running, that you've entered your passphrase via ssh-add and you
|
18
|
+
can successfully log into the remote servers without getting a password prompt.
|
15
19
|
|
16
20
|
== FEATURES:
|
17
21
|
|
@@ -202,26 +206,26 @@ script). See the advanced example for details.
|
|
202
206
|
Below is an advanced example that illustrates additional capabilities.
|
203
207
|
|
204
208
|
#!/usr/bin/env ruby
|
205
|
-
|
209
|
+
|
206
210
|
# Required libraries
|
207
211
|
require "highline/import"
|
208
212
|
require 'sshotgun'
|
209
|
-
|
213
|
+
|
210
214
|
# Create array to contain hosts for processing
|
211
215
|
hostlist = Array::new()
|
212
216
|
host = HostDefinition.new("someserver.junk", "test", "ubuntu_server_7.10", "test vm")
|
213
217
|
hostlist << host
|
214
|
-
|
215
|
-
# Import
|
218
|
+
|
219
|
+
# Import an external (shared) list of host definitions
|
216
220
|
require 'listofhosts'
|
217
221
|
hostlist = hostlist + ListOfHosts.getHostlist
|
218
|
-
|
222
|
+
|
219
223
|
class MyProcessingClass < BaseProcessingClass
|
220
|
-
|
224
|
+
|
221
225
|
# You can add your own Ruby instance variables. These are useful if you
|
222
226
|
# want to share data across your own custom methods.
|
223
227
|
attr_accessor :myInstanceVariable
|
224
|
-
|
228
|
+
|
225
229
|
# You MUST define a doProcessing method. Note that to make the doProcessing
|
226
230
|
# method less cluttered, you can also create your own methods that are
|
227
231
|
# called from within the doProcessing method. You can use your own accessor
|
@@ -230,27 +234,81 @@ Below is an advanced example that illustrates additional capabilities.
|
|
230
234
|
def doProcessing
|
231
235
|
# Log the beginning of this method. This marker is useful for debugging problems.
|
232
236
|
log "[" + @hostdef.hostname + "] info: " + "Processing started"
|
233
|
-
|
234
|
-
#
|
235
|
-
# to
|
236
|
-
#
|
237
|
-
|
238
|
-
|
237
|
+
|
238
|
+
# Sometimes a remote server is unavailable. I like to include a check at
|
239
|
+
# top of the doProcessing method to stop processing for that server. This
|
240
|
+
# snippet works because if a server is down, unreachable or non-responsive
|
241
|
+
# then the id lookup for yourself will fail - when this happens call
|
242
|
+
# "return" to drop out of this method.
|
243
|
+
# Ruby's ENV contains the current user (you)
|
244
|
+
currentUser = ENV["USER"].to_s
|
245
|
+
runstatus = run "id " + currentUser
|
246
|
+
if runstatus.exitstatus != 0
|
247
|
+
log "[" + @hostdef.hostname + "] info: " + "Server not available or you cannot log in. Stopping processing for this server"
|
248
|
+
|
249
|
+
# Set the status field for this host to indicate an abort. This status
|
250
|
+
# will be displayed upon termination of the script
|
251
|
+
@hostdef.status = "aborted"
|
252
|
+
|
253
|
+
# return will end the processing of this host
|
254
|
+
return
|
255
|
+
else
|
256
|
+
# set status to indicate that processing for this host has started
|
257
|
+
@hostdef.status = "started"
|
258
|
+
end
|
259
|
+
|
260
|
+
# Compound commands in many shells may be created via the ";" semicolon
|
261
|
+
# separator.
|
262
|
+
run "ls -alF; printenv"
|
263
|
+
|
264
|
+
# Escape any quotes (single or double) that are included within the command
|
265
|
+
# string. Surround the shell command string with single quotes or %q() in
|
266
|
+
# the sshotgun script so that Ruby won't interpolate the escaped characters
|
267
|
+
# before passing them to the your local ssh client.
|
268
|
+
run 'ls -aF | grep -i \".bash*\"'
|
269
|
+
run %q(ls -aF | grep -i \".bash*\")
|
270
|
+
|
271
|
+
# Run the command under sudo on the remote machine. If you run runSudo
|
272
|
+
# commands then the SSHotgun.sudoPassword must be set - uncomment the user
|
273
|
+
# prompt that sets the sudoPassword at the bottom of this script. See the
|
274
|
+
# gotcha below when running compound commands via sudo.
|
275
|
+
#runSudo "somecommand"
|
276
|
+
|
277
|
+
# A gotcha with sudo in unix is that is that security is tightened by
|
278
|
+
# restricting the process environment. This means that compound commands
|
279
|
+
# that use built-in shell functions such as "cd" or "pushd" will fail. To
|
280
|
+
# overcome this, first spawn a new shell, then pass it the commands to run.
|
281
|
+
# Note that because the command string contains escaped characters, you
|
282
|
+
# must enclose the entire command string in single quotes.
|
283
|
+
#runSudo 'bash -c \"hostname;pushd /tmp;hostname;ls -alf;popd\"'
|
284
|
+
|
285
|
+
# THIS WON'T WORK - SUDO can't find pushd and popd unless you spawn a shell first
|
286
|
+
#runSudo "hostname;pushd /tmp;hostname;ls -alf;popd"
|
287
|
+
|
288
|
+
# On the remote server, the run and runSudo commands are executed in a
|
289
|
+
# non-interactive shell. This means that many of the environment variables
|
290
|
+
# that are set when you log in interactively will be missing. For debugging
|
291
|
+
# purposes, call 'printenv' to view the environment variables.
|
292
|
+
#run "printenv"
|
293
|
+
|
294
|
+
# A useful technique is set environment variables via a compound command.
|
295
|
+
#run "export http_proxy=aaa.bbb.com/8080/;wget www.mysite.junk/mypage"
|
296
|
+
|
239
297
|
# The runstatus object is returned from all run and runSudo calls.
|
240
298
|
# runstatus contains information about exit status, stdout and stderr from
|
241
299
|
# the remote server.
|
242
|
-
runstatus = run "ls -aF"
|
243
|
-
|
300
|
+
runstatus = run "ls -aF .bash*"
|
301
|
+
|
244
302
|
# Use the log method to write to the log (the console). Don't use the Ruby
|
245
303
|
# 'puts' method to write to the console because it isn't threadsafe and may
|
246
304
|
# mix and garble simultaneous output by multiple threads.
|
247
305
|
log "[" + @hostdef.hostname + "] info: " + "The exit status of the run command is " + runstatus.exitstatus.to_s
|
248
|
-
|
306
|
+
|
249
307
|
# Remote stderr and stdout are captured and stored in the stdoutstr and
|
250
308
|
# stderrstr variables. Learn about Ruby's regular expressions for examining
|
251
309
|
# the output from a remote command.
|
252
310
|
runstatus = run "id rumplestiltskin"
|
253
|
-
|
311
|
+
|
254
312
|
# Check for "No such user" contained in stdoutstr. That's what is displayed
|
255
313
|
# when the unix "id" command can't find the specified user. Note that the
|
256
314
|
# the exit status of a failed 'id' command is not equal to zero either (see
|
@@ -258,18 +316,18 @@ Below is an advanced example that illustrates additional capabilities.
|
|
258
316
|
if /No such user/ =~ runstatus.stdoutstr
|
259
317
|
log "I failed to find user rumplestiltskin on this server"
|
260
318
|
end
|
261
|
-
|
319
|
+
|
262
320
|
# In some cases a remote command fails and you must stop processing for
|
263
321
|
# that one server. Stop the execution of the doProcessing method by calling
|
264
322
|
# Ruby's "return" statement. Don't call Ruby's "exit" statement because
|
265
323
|
# that will halt all running threads.
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
324
|
+
#runstatus = run "id rumplestiltskin"
|
325
|
+
#if runstatus.exitstatus > 0
|
326
|
+
# log "The exit code was not 0, therefore I failed to find user rumplestiltskin"
|
327
|
+
# return # stop processing for this host
|
328
|
+
#end
|
329
|
+
#log "You'll never get here...unless you have a user named rumplestiltskin"
|
330
|
+
|
273
331
|
# The @hostdef instance variable contains the host definition for the
|
274
332
|
# current host being processed. If you set category, os and desc to
|
275
333
|
# meaningful information then you can do fancier branching logic in your
|
@@ -280,97 +338,99 @@ Below is an advanced example that illustrates additional capabilities.
|
|
280
338
|
log "The current host's category is: " + @hostdef.category
|
281
339
|
log "The current host's os is: " + @hostdef.os
|
282
340
|
log "The current host's description is: " + @hostdef.desc
|
283
|
-
|
341
|
+
log "The current host's status is: " + @hostdef.status
|
342
|
+
|
284
343
|
# Target a specified host. This is a Ruby string comparison.
|
285
344
|
if @hostdef.hostname == "192.168.1.224"
|
286
345
|
log "[" + @hostdef.hostname + "] info: " + "Special processing for this host"
|
287
346
|
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
|
-
|
347
|
+
|
304
348
|
# Run a command locally (on your local machine). The runstatus variable is
|
305
349
|
# returned if you need the data - same as run or runSudo.
|
306
350
|
# runLocal <command>
|
307
|
-
|
308
|
-
|
351
|
+
#runLocal "ls -alF"
|
352
|
+
|
309
353
|
# Run a sudo command locally. The runstatus variable is returned if you
|
310
354
|
# need data from it. Set a sudoPassword if you are calling runLocalSudo.
|
311
355
|
# runLocalSudo <command>
|
312
|
-
|
313
|
-
|
356
|
+
#runLocalSudo "ls -alF"
|
357
|
+
|
314
358
|
# Create a remote file from a string parameter.
|
315
359
|
# createRemoteFile <content string>, <remotefile>
|
316
|
-
|
317
|
-
|
360
|
+
#createRemoteFile "this is your content here", "/home/vickp/testfile.txt"
|
361
|
+
|
318
362
|
# Create a remote file and set mode
|
319
363
|
# createRemoteFile <content string>, <remotefile>, <mode string>
|
320
|
-
|
364
|
+
#createRemoteFile "this is your content here", "/home/vickp/testfile.sh", "0755"
|
321
365
|
|
322
366
|
# Copy a remote file to the local machine. Note that you should vary the
|
323
367
|
# name of the incoming local file else it will be overwritten when each
|
324
368
|
# host's file is copied.
|
325
369
|
# copyRemoteFileToLocal <remotefile>, <localfile>
|
326
|
-
|
370
|
+
#copyRemoteFileToLocal "/home/vickp/testfile.txt", "/home/vickp/temp/" + @hostdef.hostname + "_testfile.txt"
|
327
371
|
|
328
372
|
# Copy a remote file to the local machine and set the mode of local file.
|
329
373
|
# copyRemoteFileToLocal <remotefile>, <localfile>, <mode string>
|
330
|
-
|
374
|
+
#copyRemoteFileToLocal "/home/vickp/testfile.txt", "/home/vickp/temp/" + @hostdef.hostname + "_testfile.txt", "0644"
|
331
375
|
|
332
376
|
# Copy a local file to the remote machine.
|
333
377
|
# copyRemoteFileToLocal <localfile>, <remotefile>
|
334
|
-
|
378
|
+
#copyLocalFileToRemote "/home/vickp/temp/testfile.txt", "/home/vickp/testfile2.txt"
|
335
379
|
|
336
380
|
# Copy a local file to the remote machine and set the mode of remote file.
|
337
381
|
# copyRemoteFileToLocal <localfile>, <remotefile>, <mode string>
|
338
|
-
|
339
|
-
|
382
|
+
#copyLocalFileToRemote "/home/vickp/temp/testfile.txt", "/home/vickp/testfile2.txt", "0644"
|
383
|
+
|
340
384
|
# Other helpful SSHotgun and Ruby tips below
|
341
385
|
# ===========================================
|
342
|
-
|
386
|
+
|
343
387
|
# Ruby allows you to concatenate multiple command lines
|
344
388
|
# Note use of shell command separator ';'
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
389
|
+
cmd = "hostname;"
|
390
|
+
cmd << "ls -alF | grep -i .bash;"
|
391
|
+
cmd << "ls -alF | grep -i .profile"
|
392
|
+
run cmd
|
393
|
+
|
350
394
|
# Delete a file on the remote server.
|
351
395
|
# Note use of -f with rm command
|
352
|
-
|
353
|
-
|
396
|
+
#run "rm -f /path/to/remote/file"
|
397
|
+
|
398
|
+
# Determine if a directory exists on the remote server
|
399
|
+
runstatus = run 'test -d \"/tmp\"'
|
400
|
+
if runstatus.exitstatus > 0
|
401
|
+
log "[" + @hostdef.hostname + "] info: " + "The /tmp directory does NOT exist"
|
402
|
+
else
|
403
|
+
log "[" + @hostdef.hostname + "] info: " + "The /tmp directory exists"
|
404
|
+
end
|
405
|
+
|
406
|
+
# Determine if a file exists on the remote server
|
407
|
+
runstatus = run 'test -e \".profile\"'
|
408
|
+
if runstatus.exitstatus > 0
|
409
|
+
log "[" + @hostdef.hostname + "] info: " + ".profile does NOT exist"
|
410
|
+
else
|
411
|
+
log "[" + @hostdef.hostname + "] info: " + ".profile exists"
|
412
|
+
end
|
413
|
+
|
354
414
|
# Call your own methods to unclutter and better organize your doProcess
|
355
415
|
# method code. In most of my SSHotgun scripts the doProcess method is
|
356
416
|
# fairly sparse - most of the work is done is various custom methods that
|
357
417
|
# are called within the doProcess method
|
358
|
-
|
359
|
-
|
360
|
-
#
|
361
|
-
|
418
|
+
#myOwnMethod
|
419
|
+
|
420
|
+
# The SSHotgun instance variables are visible in this method.
|
421
|
+
log "[" + @hostdef.hostname + "] info: " + "The script started at: " + @sshotgun.startTime.to_s
|
422
|
+
|
423
|
+
# Sometimes I either install unsigned debian packages of my own
|
424
|
+
# or need to force the installation of files (overwrite) that
|
362
425
|
# belong to another package. Aptitude doesn't yet support forcing yes for
|
363
426
|
# this sort of thing but apt-get does...
|
364
|
-
|
365
|
-
|
366
|
-
# Ruby's ENV contains the current user (you)
|
367
|
-
currentUser = ENV["USER"].to_s
|
368
|
-
|
427
|
+
#runSudo "apt-get install -y --force-yes myUntrustedPackage -o DPkg::options::='--force-overwrite'"
|
428
|
+
|
369
429
|
# For double quoted strings, Ruby will do inline substitution for a
|
370
430
|
# variable enbedded within "#{myvariablehere}". This is useful when you
|
371
431
|
# want a variable substituted inside of double quotes
|
372
|
-
|
373
|
-
|
432
|
+
#createRemoteFile "deb http://debrepo/global ./", "/home/#{currentUser}/sources.list", "0644"
|
433
|
+
|
374
434
|
# Create a global variable if you need to prompt for additional information
|
375
435
|
# (such as userid, date range, etc) and want to use that information in
|
376
436
|
# your doProcessing method. Global variables begin with '$'. See bottom of
|
@@ -382,27 +442,27 @@ Below is an advanced example that illustrates additional capabilities.
|
|
382
442
|
else
|
383
443
|
log "[" + @hostdef.hostname + "] info: " + "DID NOT find user " + $userid
|
384
444
|
end
|
385
|
-
|
386
|
-
#
|
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
|
445
|
+
|
446
|
+
# See example of how to update a debian/ubuntu sources.list package repository file
|
393
447
|
# Also see use of case statement in the method body.
|
394
|
-
|
395
|
-
|
448
|
+
#installSourcesListAndUpdate
|
449
|
+
|
450
|
+
# You can set the exit code of this script
|
451
|
+
#@sshotgun.exitCode = 3
|
452
|
+
|
396
453
|
# Log the end of this method.
|
397
454
|
log "[" + @hostdef.hostname + "] info: " + "Processing completed"
|
455
|
+
|
456
|
+
# Set the status field for this host to indicate successful processing.
|
457
|
+
@hostdef.status = "finished"
|
398
458
|
end
|
399
459
|
end
|
400
|
-
|
460
|
+
|
401
461
|
# Your own custom methods can be called from within the doProcess method
|
402
462
|
def myOwnMethod
|
403
463
|
log "[" + @hostdef.hostname + "] info: " + "Calling my own method"
|
404
464
|
end
|
405
|
-
|
465
|
+
|
406
466
|
# Update a debian/ubuntu /etc/apt/sources.list
|
407
467
|
def installSourcesListAndUpdate
|
408
468
|
# WARNING: This only works if you fill in the category field in your hostdefs.
|
@@ -432,25 +492,25 @@ Below is an advanced example that illustrates additional capabilities.
|
|
432
492
|
createRemoteFile s, "/home/#{$currentUser}/sources.list", "0644"
|
433
493
|
runSudo "cp /home/#{$currentUser}/sources.list /etc/apt/sources.list"
|
434
494
|
run "rm /home/#{$currentUser}/sources.list"
|
435
|
-
|
495
|
+
|
436
496
|
# after changing sources.list, update package manager to refresh package list
|
437
497
|
runSudo "aptitude update"
|
438
498
|
end
|
439
|
-
|
499
|
+
|
440
500
|
# Create an instance of SSHotgun and pass it the list of hosts and your custom
|
441
501
|
# processing class.
|
442
502
|
sshotgun = SSHotgun.new(hostlist, MyProcessingClass)
|
443
|
-
|
503
|
+
|
444
504
|
# If you are calling a sudo command then prompt the user for the sudo password.
|
445
505
|
# I recommend that you NEVER HARDCODE A PASSWORD IN A SCRIPT! Configure the
|
446
506
|
# ask command to hide the password as the user types it. Note that the you
|
447
507
|
# must have the highline gem package installed for the "ask" command
|
448
508
|
#sshotgun.sudopassword = ask(">>> Enter your sudo password: ") { |q| q.echo = "*" } # or q.echo = false
|
449
|
-
|
509
|
+
|
450
510
|
# If you need to forward all calls via a gateway ssh server, set it here. Once
|
451
511
|
# set, all ssh connections go through the gateway machine
|
452
512
|
#sshotgun.gatewayhost = "someserver.somedomain.com"
|
453
|
-
|
513
|
+
|
454
514
|
# You can also use the 'ask' command to prompt the user for any other
|
455
515
|
# information. Configure the ask command to display the string as the user
|
456
516
|
# types it. Be sure to test password-less login from your gateway server to
|
@@ -458,26 +518,30 @@ Below is an advanced example that illustrates additional capabilities.
|
|
458
518
|
# host.
|
459
519
|
#gatewayhost = ask(">>> Enter the hostname of the gateway server: ") { |q| q.echo = true }
|
460
520
|
#sshotgun.gatewayhost = gatewayhost
|
461
|
-
|
521
|
+
#sshotgun.gatewayhost = "mygateway.hostname.junk"
|
522
|
+
|
462
523
|
# Create and use a global variable to get additional data into your doProcessing method.
|
463
524
|
$userid = ask(">>> Enter the userid to find: ") { |q| q.echo = true }
|
464
|
-
|
525
|
+
|
465
526
|
# Number of simultaneous processing threads (hosts) to run. In terms of load to
|
466
527
|
# your local machine, each processing thread equates to about one ssh client
|
467
528
|
# running. Default is 50
|
468
529
|
# sshotgun.maxThreads = 30
|
469
|
-
|
530
|
+
|
470
531
|
# A processing thread will be killed after this amount of time. Default is 30 minutes.
|
471
532
|
# sshotgun.threadTimeoutInSeconds = 900
|
472
|
-
|
533
|
+
|
473
534
|
# Period for displaying status information about processing threads that
|
474
535
|
# are still running. Default is 30
|
475
536
|
# sshotgun.monitorPollIntervalInSeconds = 20
|
476
|
-
|
537
|
+
|
477
538
|
# Any output is also written to a file logger. You can disable the file logging
|
478
539
|
# by setting isFileLogging to false.
|
479
540
|
# sshotgun.isFileLogging = false.
|
480
|
-
|
541
|
+
|
542
|
+
# Turn on debug mode
|
543
|
+
# sshotgun.isDebug = true
|
544
|
+
|
481
545
|
# What should the behavior be when a command returns a non-zero exit status? A
|
482
546
|
# non-zero exit status usually indicates failure of the command. Some sshotgun
|
483
547
|
# users wish to immediately stop the processing for that server and stop it's
|
@@ -487,10 +551,11 @@ Below is an advanced example that illustrates additional capabilities.
|
|
487
551
|
# you never have the opportunity to check the exit status in your processing
|
488
552
|
# method code. The default is false (don't stop on non-zero exit).
|
489
553
|
# sshotgun.isStopOnNonZeroExit = true
|
490
|
-
|
554
|
+
|
491
555
|
# Start processing. The start call should be the very last line in a SSHotgun
|
492
556
|
# script.
|
493
557
|
sshotgun.start
|
558
|
+
|
494
559
|
|
495
560
|
== LICENSE:
|
496
561
|
|
data/lib/sshotgun.rb
CHANGED
@@ -3,6 +3,19 @@
|
|
3
3
|
#Copyright:: Copyright (c) 2008, Vick Perry
|
4
4
|
#License:: Simplified BSD License.
|
5
5
|
#
|
6
|
+
# vickp 9/24/2008, Added HostDefinition.status freeform text field. Upon
|
7
|
+
# termination of the script, the status field is displayed for each host.
|
8
|
+
# Typically set by the script programmer to indicate state of processing. E.g.
|
9
|
+
# "started", "aborted", "finished", etc. Removed original status flags.
|
10
|
+
#
|
11
|
+
# vickp 9/23/2008, Added SSHotgun.exitCode (fixnum). So that programmer can set
|
12
|
+
# the script's exit code. Exit code is returned to calling process upon termination.
|
13
|
+
#
|
14
|
+
# vickp 9/23/2008, Added check for unescaped quotes in command strings. Added
|
15
|
+
# isDebug flag. Updated documents with clarification about escaping quotes in
|
16
|
+
# command strings and tips about how to deal with a restricted unix environment
|
17
|
+
# when using sudo.
|
18
|
+
#
|
6
19
|
require 'open4'
|
7
20
|
require 'logger'
|
8
21
|
|
@@ -21,24 +34,15 @@ class HostDefinition
|
|
21
34
|
# desc is a one sentence string describing main purpose of the host (optional)
|
22
35
|
attr_accessor :desc
|
23
36
|
|
24
|
-
#
|
25
|
-
attr_accessor :
|
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
|
37
|
+
# status is freeform text field that the script programmer can set. Generally set to "started", "finished", "aborted", etc.
|
38
|
+
attr_accessor :status
|
33
39
|
|
34
40
|
def initialize(hostname, category = "", os = "", desc = "")
|
35
41
|
@hostname = hostname
|
36
42
|
@category = category
|
37
43
|
@os = os
|
38
44
|
@desc = desc
|
39
|
-
@
|
40
|
-
@finishedProcess = false
|
41
|
-
@hadNonZeroExitStatus = false
|
45
|
+
@status = ""
|
42
46
|
end
|
43
47
|
end
|
44
48
|
|
@@ -92,12 +96,19 @@ class BaseProcessingClass
|
|
92
96
|
#
|
93
97
|
def run(cmd)
|
94
98
|
log "[" + @hostdef.hostname + "] run: " + cmd
|
95
|
-
if
|
96
|
-
|
99
|
+
if !hasUnescapedQuotes(cmd)
|
100
|
+
if @sshotgun.gatewayhost
|
101
|
+
cmdline = "ssh -A -t -x " + @sshotgun.gatewayhost + " 'ssh -A -x " + @hostdef.hostname + " \"" + cmd + "\"'"
|
102
|
+
else
|
103
|
+
cmdline = "ssh -A -t -x " + @hostdef.hostname + " \"" + cmd + "\""
|
104
|
+
end
|
105
|
+
runLocal cmdline, false
|
97
106
|
else
|
98
|
-
|
107
|
+
log "[_sshotgun_] fatal: Command string cannot contain unescaped quotes. Surround command string"
|
108
|
+
log "[_sshotgun_] fatal: with single quotes or %q() so that Ruby does not interpolate string first."
|
109
|
+
@sshotgun.exitCode = 1
|
110
|
+
@sshotgun.showRunStatsAndExit
|
99
111
|
end
|
100
|
-
runLocal cmdline, false
|
101
112
|
end
|
102
113
|
|
103
114
|
# Sudo a command on a remote server
|
@@ -108,12 +119,19 @@ class BaseProcessingClass
|
|
108
119
|
#
|
109
120
|
def runSudo(cmd)
|
110
121
|
log "[" + @hostdef.hostname + "] runSudo: " + cmd
|
111
|
-
if
|
112
|
-
|
122
|
+
if !hasUnescapedQuotes(cmd)
|
123
|
+
if @sshotgun.gatewayhost
|
124
|
+
cmdline = "ssh -A -t -x " + @sshotgun.gatewayhost + " 'ssh -A -x " + @hostdef.hostname + " sudo -S \"" + cmd + "\"'"
|
125
|
+
else
|
126
|
+
cmdline = "ssh -A -t -x " + @hostdef.hostname + " sudo -S \"" + cmd + "\""
|
127
|
+
end
|
128
|
+
runLocal cmdline, true
|
113
129
|
else
|
114
|
-
|
130
|
+
log "[_sshotgun_] fatal: Command string cannot contain unescaped quotes. Surround command string"
|
131
|
+
log "[_sshotgun_] fatal: with single quotes or %q() so that Ruby does not interpolate string first."
|
132
|
+
@sshotgun.exitCode = 1
|
133
|
+
@sshotgun.showRunStatsAndExit
|
115
134
|
end
|
116
|
-
runLocal cmdline, true
|
117
135
|
end
|
118
136
|
|
119
137
|
# Copy a remote file to local
|
@@ -180,7 +198,10 @@ class BaseProcessingClass
|
|
180
198
|
# isSudo is the flag to launch as a sudo command
|
181
199
|
#
|
182
200
|
def runLocal (cmdline, isSudo=false)
|
183
|
-
|
201
|
+
if @sshotgun.isDebug
|
202
|
+
# display & log the actual command line sent to ssh
|
203
|
+
log "[" + @hostdef.hostname + "] debug: raw command line to local ssh client: " + cmdline
|
204
|
+
end
|
184
205
|
currentThread = Thread.current
|
185
206
|
runstatus = RunStatus.new
|
186
207
|
retval = Open4::popen4( cmdline ) do |pid, stdin, stdout, stderr|
|
@@ -188,6 +209,8 @@ class BaseProcessingClass
|
|
188
209
|
if isSudo
|
189
210
|
if @sshotgun.sudopassword
|
190
211
|
stdin.puts @sshotgun.sudopassword
|
212
|
+
else
|
213
|
+
log "[" + @hostdef.hostname + "] info: sudo invoked but sudo password is blank"
|
191
214
|
end
|
192
215
|
end
|
193
216
|
runstatus.stdoutstr = stdout.read.strip
|
@@ -202,10 +225,6 @@ class BaseProcessingClass
|
|
202
225
|
runstatus.exitstatus = retval.exitstatus
|
203
226
|
|
204
227
|
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
228
|
# am I supposed to stop thread?
|
210
229
|
if @sshotgun.isStopOnNonZeroExit
|
211
230
|
log "[" + @hostdef.hostname + "] stderr: Non-zero exit status - Stop processing for this server (" + runstatus.exitstatus.to_s + ")"
|
@@ -235,6 +254,30 @@ class BaseProcessingClass
|
|
235
254
|
def log(s)
|
236
255
|
@sshotgun.log s
|
237
256
|
end
|
257
|
+
|
258
|
+
# Does the string contain unescaped quotes? This is a safety measure. If
|
259
|
+
# there are unescaped quotes (double or single) contained in a command
|
260
|
+
# string, it may be interpolated differently than as is expected. For example
|
261
|
+
# a compound command may execute the first part on a local or gateway server
|
262
|
+
# and the last part on the remote server.
|
263
|
+
#
|
264
|
+
# s is the string to check
|
265
|
+
#
|
266
|
+
def hasUnescapedQuotes(s)
|
267
|
+
retval = false
|
268
|
+
previousChar = ""
|
269
|
+
sArray = s.split(//)
|
270
|
+
sArray.each do |currentChar|
|
271
|
+
#log currentChar
|
272
|
+
if (currentChar == "\"" || currentChar == "\'") && previousChar != "\\"
|
273
|
+
retval = true
|
274
|
+
break
|
275
|
+
end
|
276
|
+
previousChar = currentChar
|
277
|
+
end
|
278
|
+
return retval
|
279
|
+
end
|
280
|
+
|
238
281
|
end
|
239
282
|
|
240
283
|
# This is the main class of SSHotgun. This class stores the configuration and launches the processing threads.
|
@@ -270,15 +313,24 @@ class SSHotgun
|
|
270
313
|
# For internal use. Mutex for launching processing threads
|
271
314
|
attr_reader :threadStartMutex
|
272
315
|
|
316
|
+
# For internal use. Mutex for exiting
|
317
|
+
attr_reader :threadEndMutex
|
318
|
+
|
273
319
|
# File logger. Set your own if desired. (optional)
|
274
320
|
attr_accessor :filelogger
|
275
321
|
|
276
322
|
# Turn on/off file logging. Default is on. (optional)
|
277
323
|
attr_accessor :isFileLogging
|
278
324
|
|
325
|
+
# Turn on/off debugging. Default is off. (optional)
|
326
|
+
attr_accessor :isDebug
|
327
|
+
|
279
328
|
# For internal use. Script start time
|
280
329
|
attr_reader :startTime
|
281
330
|
|
331
|
+
# For internal use. sshotgun version
|
332
|
+
attr_reader :versionStr
|
333
|
+
|
282
334
|
# Turn on/off behavior where processing for a server is stopped if any
|
283
335
|
# command (run, runSudo, etc) returns with a non-zero exit code. Default is
|
284
336
|
# off (don't stop). Note that if isStopOnNonZeroExit is true then the
|
@@ -286,11 +338,16 @@ class SSHotgun
|
|
286
338
|
# check the exit status in your processing code.
|
287
339
|
attr_accessor :isStopOnNonZeroExit
|
288
340
|
|
341
|
+
# Exit code for this script, default is zero. Settable by the script programmer.
|
342
|
+
attr_accessor :exitCode
|
343
|
+
|
289
344
|
def initialize(hostlist, processingclass)
|
345
|
+
@versionStr = "1.0.5"
|
290
346
|
@hostlist = hostlist
|
291
347
|
@processingclass = processingclass
|
292
348
|
@logmutex = Mutex.new
|
293
349
|
@threadStartMutex = Mutex.new
|
350
|
+
@threadEndMutex = Mutex.new
|
294
351
|
@maxThreads = 30 # allow NN processing threads to run at any time
|
295
352
|
@threadTimeoutInSeconds = 1800 # timeout after 30 minutes
|
296
353
|
@monitorPollIntervalInSeconds = 30 # period for displaying thread monitor
|
@@ -298,7 +355,9 @@ class SSHotgun
|
|
298
355
|
logfilename = File.basename($0) + ".#{timestr}.log"
|
299
356
|
@filelogger = Logger.new(logfilename)
|
300
357
|
@isFileLogging = true
|
358
|
+
@isDebug = false
|
301
359
|
@isStopOnNonZeroExit = false
|
360
|
+
@exitCode = 0
|
302
361
|
end
|
303
362
|
|
304
363
|
# Start the processing. In most cases, this call will be the last line in
|
@@ -308,10 +367,11 @@ class SSHotgun
|
|
308
367
|
# trap ctrl-c, show stats and exit
|
309
368
|
trap("INT") {
|
310
369
|
log "[_sshotgun_] info: Script terminated early by ctrl-c"
|
311
|
-
|
312
|
-
exit
|
370
|
+
showRunStatsAndExit
|
313
371
|
}
|
372
|
+
|
314
373
|
@startTime = Time.now
|
374
|
+
log "[_sshotgun_] info: SSHotgun version = " + @versionStr
|
315
375
|
log "[_sshotgun_] info: Started script " + $0 + " at " + @startTime.to_s
|
316
376
|
|
317
377
|
# Display configuration info
|
@@ -363,10 +423,8 @@ class SSHotgun
|
|
363
423
|
aThread["hostname"] = hostdef.hostname # store hostname time in threadlocal variable
|
364
424
|
log "[_sshotgun_] info: Started thread for host = " + hostdef.hostname + " at " + aThread["threadstarttime"].to_s
|
365
425
|
begin
|
366
|
-
hostdef.startedProcess = true
|
367
426
|
o = @processingclass.new(self, hostdef)
|
368
427
|
o.doProcessing
|
369
|
-
hostdef.finishedProcess = true
|
370
428
|
rescue => ex
|
371
429
|
log "[_sshotgun_] stderr: Thread blew up for host = " + hostdef.hostname + ". ex = " + ex.inspect + "\n" + ex.backtrace.join("\n")
|
372
430
|
end
|
@@ -424,42 +482,27 @@ class SSHotgun
|
|
424
482
|
|
425
483
|
# ended normally, show stats
|
426
484
|
log "[_sshotgun_] info: Script terminated normally"
|
427
|
-
|
485
|
+
showRunStatsAndExit
|
428
486
|
end
|
429
487
|
|
430
|
-
# Display
|
431
|
-
def
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
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
|
488
|
+
# Display status and exit. May be called by pressing ctrl-c, fatal error or normal termination
|
489
|
+
def showRunStatsAndExit
|
490
|
+
@threadEndMutex.synchronize {
|
491
|
+
# Display script ending message
|
492
|
+
stopTime = Time.now
|
493
|
+
log "[_sshotgun_] info: Ended script " + $0 + " at " + stopTime.to_s
|
494
|
+
log "[_sshotgun_] info: Elapsed script " + $0 + " time " + (stopTime - @startTime).to_s + "s"
|
495
|
+
log "[_sshotgun_] info: Script exit code is = " + @exitCode.to_s
|
496
|
+
|
497
|
+
# Display status of each host
|
498
|
+
@hostlist.each do |hostdef|
|
499
|
+
log "[_sshotgun_] status: " + hostdef.hostname + " = " + hostdef.status
|
457
500
|
end
|
458
|
-
|
459
|
-
|
460
|
-
log "[_sshotgun_] info: Hosts summary = " + hostarr.join(", ")
|
501
|
+
exit @exitCode
|
502
|
+
}
|
461
503
|
end
|
462
504
|
|
505
|
+
|
463
506
|
# Log a string to the console and to the file logger
|
464
507
|
#
|
465
508
|
# s is the string to log
|
@@ -472,4 +515,5 @@ class SSHotgun
|
|
472
515
|
end
|
473
516
|
}
|
474
517
|
end
|
518
|
+
|
475
519
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sshotgun
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vick Perry
|
@@ -9,11 +9,12 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2008-09-
|
12
|
+
date: 2008-09-26 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: open4
|
17
|
+
type: :runtime
|
17
18
|
version_requirement:
|
18
19
|
version_requirements: !ruby/object:Gem::Requirement
|
19
20
|
requirements:
|
@@ -23,6 +24,7 @@ dependencies:
|
|
23
24
|
version:
|
24
25
|
- !ruby/object:Gem::Dependency
|
25
26
|
name: highline
|
27
|
+
type: :runtime
|
26
28
|
version_requirement:
|
27
29
|
version_requirements: !ruby/object:Gem::Requirement
|
28
30
|
requirements:
|
@@ -68,7 +70,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
68
70
|
requirements: []
|
69
71
|
|
70
72
|
rubyforge_project: sshotgun
|
71
|
-
rubygems_version: 1.
|
73
|
+
rubygems_version: 1.2.0
|
72
74
|
signing_key:
|
73
75
|
specification_version: 2
|
74
76
|
summary: A lib for writing server management scripts in Ruby. Useful for managing lots of servers.
|