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.
Files changed (3) hide show
  1. data/README.rdoc +164 -99
  2. data/lib/sshotgun.rb +104 -60
  3. 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. SShotgun uses your locally installed SSH client and expects
14
- that you have your SSH public key installed on the servers.
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 a list of host definitions
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
- # 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
-
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
- # 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
-
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
- # runLocal "ls -alF"
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
- # runLocalSudo "ls -alF"
313
-
356
+ #runLocalSudo "ls -alF"
357
+
314
358
  # Create a remote file from a string parameter.
315
359
  # createRemoteFile <content string>, <remotefile>
316
- # createRemoteFile "this is your content here", "/home/vickp/testfile.txt"
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
- # createRemoteFile "this is your content here", "/home/vickp/testfile.sh", "0755"
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
- # copyRemoteFileToLocal "/home/vickp/testfile.txt", "/home/vickp/temp/" + @hostdef.hostname + "_testfile.txt"
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
- # copyRemoteFileToLocal "/home/vickp/testfile.txt", "/home/vickp/temp/" + @hostdef.hostname + "_testfile.txt", "0644"
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
- # copyLocalFileToRemote "/home/vickp/temp/testfile.txt", "/home/vickp/testfile2.txt"
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
- # copyLocalFileToRemote "/home/vickp/temp/testfile.txt", "/home/vickp/testfile2.txt", "0644"
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
- # cmd = "ls -alF;"
346
- # cmd << "ls -alF | grep -i .bash;"
347
- # cmd << "ls -alF | grep -i .profile"
348
- # run cmd
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
- # run "rm -f /path/to/remote/file"
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
- # 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
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
- # 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
-
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
- # createRemoteFile "deb http://debrepo/global ./", "/home/#{currentUser}/sources.list", "0644"
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
- # 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
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
- # installSourcesListAndUpdate
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
- # 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
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
- @startedProcess = false
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 @sshotgun.gatewayhost
96
- cmdline = "ssh -A -t -x " + @sshotgun.gatewayhost + " 'ssh -A -x " + @hostdef.hostname + " \"" + cmd + "\"'"
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
- cmdline = "ssh -A -t -x " + @hostdef.hostname + " \"" + cmd + "\""
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 @sshotgun.gatewayhost
112
- cmdline = "ssh -A -t -x " + @sshotgun.gatewayhost + " 'ssh -A -x " + @hostdef.hostname + " sudo -S \"" + cmd + "\"'"
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
- cmdline = "ssh -A -t -x " + @hostdef.hostname + " sudo -S \"" + cmd + "\""
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
- #log "[" + @hostdef.hostname + "] debug: " + cmdline
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
- showRunStats
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
- showRunStats
485
+ showRunStatsAndExit
428
486
  end
429
487
 
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
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
- 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(", ")
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
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-08 00:00:00 -07:00
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.1.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.