sshotgun 1.0.4 → 1.0.5
Sign up to get free protection for your applications and to get access to all the features.
- 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.
|