sshotgun 1.0.4 → 1.0.5

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