sshwrap 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in sshwrap.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Jason Heiss
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # SSHwrap
2
+
3
+ This provides a wrapper for ssh that execute a command on multiple machines,
4
+ optionally in parallel, and handles any sudo prompts that result.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ gem 'sshwrap'
11
+
12
+ And then execute:
13
+
14
+ $ bundle
15
+
16
+ Or install it yourself as:
17
+
18
+ $ gem install sshwrap
19
+
20
+ ## Usage
21
+
22
+ Usage: sshwrap [options]
23
+ -c, --command, --cmd=CMD Command to run
24
+ -u, --user=USER SSH as specified user
25
+ -k, --ssh-key=KEY Use the specified SSH private key
26
+ --abort-on-failure Abort if connection or command fails with any target
27
+ --max-workers=NUM Use specified number of parallel connections
28
+ --debug Enable debugging
29
+
30
+ ## Contributing
31
+
32
+ 1. Fork it
33
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
34
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
35
+ 4. Push to the branch (`git push origin my-new-feature`)
36
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env rake
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
+
6
+ Rake::TestTask.new do |t|
7
+ # t.verbose = true
8
+ end
data/TODO ADDED
@@ -0,0 +1,11 @@
1
+ Options for displaying the output as the command runs on the various targets:
2
+
3
+ * Immediate (lines from various targets intermingled)
4
+ * Immediate, prefaced by hostname
5
+ * Captured (lines for a target displayed together once command completes)
6
+
7
+ Other ways of specifying targets:
8
+
9
+ * stdin
10
+ * groups, with user-supplied group expansion script
11
+ * read from file
data/bin/sshwrap ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'sshwrap'
4
+ require 'optparse'
5
+
6
+ @options = {}
7
+ @cmd = nil
8
+
9
+ opts = OptionParser.new(nil, 24, ' ')
10
+ opts.banner = 'Usage: sshwrap [options]'
11
+ opts.on('--command', '--cmd', '-c', '=CMD', 'Command to run') do |opt|
12
+ @cmd = opt
13
+ end
14
+ opts.on('--user', '-u', '=USER', 'SSH as specified user') do |opt|
15
+ @options[:user] = opt
16
+ end
17
+ opts.on('--ssh-key', '-k', '=KEY', 'Use the specified SSH private key') do |opt|
18
+ @options[:ssh_key] = opt
19
+ end
20
+ opts.on('--abort-on-failure', 'Abort if connection or command fails with any target') do |opt|
21
+ options[:abort_on_failure] = opt
22
+ end
23
+ opts.on('--max-workers', '=NUM', 'Use specified number of parallel connections') do |opt|
24
+ options[:max_workers] = opt
25
+ end
26
+ opts.on('--debug', 'Enable debugging') do |opt|
27
+ @options[:debug] = opt
28
+ end
29
+
30
+ leftovers = nil
31
+ begin
32
+ leftovers = opts.parse(ARGV)
33
+ rescue OptionParser::ParseError => e
34
+ $stderr.puts "Error parsing arguments, try --help"
35
+ $stderr.puts e.message
36
+ exit 1
37
+ end
38
+ @targets = leftovers
39
+
40
+ sshwrap = SSHwrap::Main.new(@options)
41
+ sshwrap.sshwrap(@cmd, @targets)
@@ -0,0 +1,151 @@
1
+ require 'net/ssh'
2
+ require 'highline/import'
3
+
4
+ class SSHwrap::Main
5
+ def initialize(options={})
6
+ @mutex = Mutex.new
7
+ @max_workers = options[:max_workers] || 1
8
+ @abort_on_failure = options[:abort_on_failure]
9
+ @user = options[:user] || Etc.getlogin
10
+ @ssh_key = options[:ssh_key]
11
+ @debug = options[:debug]
12
+ @password = nil
13
+ end
14
+
15
+ def get_password
16
+ @mutex.synchronize do
17
+ if !@password
18
+ @password = ask("Password for #{@user}: ") { |q| q.echo = "x" }
19
+ end
20
+ end
21
+ @password
22
+ end
23
+
24
+ def ssh_execute(cmd, target)
25
+ exitstatus = nil
26
+ stdout = []
27
+ stderr = []
28
+
29
+ params = {}
30
+ if @ssh_key
31
+ params[:keys] = [@ssh_key]
32
+ end
33
+ using_password = false
34
+ if @password
35
+ using_password = true
36
+ params[:password] = @password
37
+ end
38
+
39
+ begin
40
+ Net::SSH.start(target, @user, params) do |ssh|
41
+ puts "Connecting to #{target}" if @debug
42
+ ch = ssh.open_channel do |channel|
43
+ # Now we request a "pty" (i.e. interactive) session so we can send
44
+ # data back and forth if needed. It WILL NOT WORK without this,
45
+ # and it has to be done before any call to exec.
46
+ channel.request_pty do |ch_pty, success|
47
+ if !success
48
+ raise "Could not obtain pty (interactive ssh session) on #{target}"
49
+ end
50
+ end
51
+
52
+ channel.exec(cmd) do |ch_exec, success|
53
+ puts "Executing '#{cmd}' on #{target}" if @debug
54
+ # 'success' isn't related to process exit codes or anything, but
55
+ # more about ssh internals. Not sure why it would fail at such
56
+ # a basic level, but it seems smart to do something about it.
57
+ if !success
58
+ raise "SSH unable to execute command on #{target}"
59
+ end
60
+
61
+ # on_data is a hook that fires when ssh returns output data. This
62
+ # is what we've been doing all this for; now we can check to see
63
+ # if it's a password prompt, and interactively return data if so
64
+ # (see request_pty above).
65
+ channel.on_data do |ch_data, data|
66
+ # This is the standard sudo password prompt
67
+ if data =~ /Password:/
68
+ channel.send_data "#{get_password}\n"
69
+ else
70
+ stdout << data unless (data.nil? or data.empty?)
71
+ end
72
+ end
73
+
74
+ channel.on_extended_data do |ch_onextdata, type, data|
75
+ stderr << data unless (data.nil? or data.empty?)
76
+ end
77
+
78
+ channel.on_request "exit-status" do |ch_onreq, data|
79
+ exitstatus = data.read_long
80
+ end
81
+ end
82
+ end
83
+ ch.wait
84
+ ssh.loop
85
+ end
86
+ rescue Net::SSH::AuthenticationFailed
87
+ if !using_password
88
+ get_password
89
+ return ssh_execute(cmd, target)
90
+ else
91
+ stderr << "Authentication failed to #{target}"
92
+ end
93
+ rescue Exception => e
94
+ stderr << "SSH connection error: #{e.message}"
95
+ end
96
+
97
+ [exitstatus, stdout, stderr]
98
+ end
99
+
100
+ # cmd is a string or array of strings containing the command and arguments
101
+ # targets is an array of remote system hostnames
102
+ def sshwrap(cmd, targets)
103
+ cmdstring = nil
104
+ if cmd.kind_of?(Array)
105
+ cmdstring = cmd.join(' ')
106
+ else
107
+ cmdstring = cmd.to_s
108
+ end
109
+
110
+ statuses = {}
111
+
112
+ threads = (1..@max_workers).map do |i|
113
+ Thread.new("worker#{i}") do |tname|
114
+ while true
115
+ target = nil
116
+ @mutex.synchronize do
117
+ target = targets.shift
118
+ end
119
+ if !target
120
+ break
121
+ end
122
+ puts "Thread #{tname} processing target #{target}" if @debug
123
+
124
+ exitstatus, stdout, stderr = ssh_execute(cmdstring, target)
125
+ statuses[target] = exitstatus
126
+
127
+ @mutex.synchronize do
128
+ puts '=================================================='
129
+ if !stdout.empty?
130
+ puts "Output from #{target}:"
131
+ puts stdout.join
132
+ end
133
+ if !stderr.empty?
134
+ puts "Error from #{target}:"
135
+ puts stderr.join
136
+ end
137
+ puts "Exit status from #{target}: #{exitstatus}"
138
+ end
139
+
140
+ if @abort_on_failure && exitstatus != 0
141
+ exit exitstatus
142
+ end
143
+ end
144
+ end
145
+ end
146
+
147
+ threads.each(&:join)
148
+
149
+ statuses
150
+ end
151
+ end
@@ -0,0 +1,3 @@
1
+ module SSHwrap
2
+ VERSION = "0.0.1"
3
+ end
data/lib/sshwrap.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'sshwrap/version'
2
+ require 'sshwrap/main'
3
+
4
+ module SSHwrap
5
+ end
data/sshwrap.gemspec ADDED
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/sshwrap/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ['Jason Heiss']
6
+ gem.email = ['jheiss@aput.net']
7
+ gem.description = File.read('README.md')
8
+ gem.summary = 'Perform batch SSH operations, handling sudo prompts'
9
+ gem.homepage = 'https://github.com/jheiss/sshwrap'
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = 'sshwrap'
15
+ gem.require_paths = ['lib']
16
+ gem.version = SSHwrap::VERSION
17
+ gem.add_dependency('net-ssh')
18
+ gem.add_dependency('highline')
19
+ end
@@ -0,0 +1,72 @@
1
+ #
2
+ # Test sshwrap command line options
3
+ #
4
+
5
+ require 'test/unit'
6
+ require 'open4'
7
+ require 'rbconfig'
8
+
9
+ RUBY = File.join(*RbConfig::CONFIG.values_at("bindir", "ruby_install_name")) +
10
+ RbConfig::CONFIG["EXEEXT"]
11
+ LIBDIR = File.join(File.dirname(File.dirname(__FILE__)), 'lib')
12
+ SSHWRAP = File.expand_path('../bin/sshwrap', File.dirname(__FILE__))
13
+
14
+ class SSHwrapOptionTests < Test::Unit::TestCase
15
+ def test_help
16
+ output = nil
17
+ IO.popen("#{RUBY} -I #{LIBDIR} #{SSHWRAP} --help") do |pipe|
18
+ output = pipe.readlines
19
+ end
20
+ # Make sure at least something resembling help output is there
21
+ assert(output.any? {|line| line.include?('Usage: sshwrap [options]')}, 'help output content')
22
+ # Make sure it fits on the screen
23
+ assert(output.all? {|line| line.length <= 80}, 'help output columns')
24
+ assert(output.size <= 23, 'help output lines')
25
+ end
26
+
27
+ def test_command_arg_required
28
+ end
29
+ def test_command
30
+ end
31
+
32
+ def test_user_arg_required
33
+ end
34
+ def test_user
35
+ end
36
+
37
+ def test_ssh_key_arg_required
38
+ output = nil
39
+ error = nil
40
+ status = Open4.popen4("#{RUBY} -I #{LIBDIR} #{SSHWRAP} --ssh-key") do |pid, stdin, stdout, stderr|
41
+ stdin.close
42
+ output = stdout.readlines
43
+ error = stderr.readlines
44
+ end
45
+ assert_equal(1, status.exitstatus, "-ssh-key arg required exitstatus")
46
+ # Make sure the expected lines are there
47
+ assert(error.any? {|line| line.include?('missing argument: --ssh-key')})
48
+ end
49
+ def test_ssh_key_bogus_file
50
+ # error = nil
51
+ # status = Open4.popen4("#{RUBY} -I #{LIBDIR} #{SSHWRAP} --ssh-key bogus") do |pid, stdin, stdout, stderr|
52
+ # stdin.close
53
+ # error = stderr.readlines
54
+ # end
55
+ # # Make sure the expected lines are there
56
+ # assert(error.any? {|line| line.include?('Unable to read ssh key from bogus')})
57
+ end
58
+ def test_ssh_key
59
+ end
60
+
61
+ def test_abort_on_failure
62
+ end
63
+
64
+ def test_max_workers_arg_required
65
+ end
66
+ def test_max_workers
67
+ end
68
+
69
+ def test_debug
70
+ end
71
+ end
72
+
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sshwrap
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jason Heiss
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-09-23 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: net-ssh
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: highline
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: ! "# SSHwrap\n\nThis provides a wrapper for ssh that execute a command
47
+ on multiple machines,\noptionally in parallel, and handles any sudo prompts that
48
+ result.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n gem
49
+ 'sshwrap'\n\nAnd then execute:\n\n $ bundle\n\nOr install it yourself as:\n\n
50
+ \ $ gem install sshwrap\n\n## Usage\n\n Usage: sshwrap [options]\n -c,
51
+ --command, --cmd=CMD Command to run\n -u, --user=USER SSH as specified
52
+ user\n -k, --ssh-key=KEY Use the specified SSH private key\n --abort-on-failure
53
+ \ Abort if connection or command fails with any target\n --max-workers=NUM
54
+ \ Use specified number of parallel connections\n --debug Enable
55
+ debugging\n\n## Contributing\n\n1. Fork it\n2. Create your feature branch (`git
56
+ checkout -b my-new-feature`)\n3. Commit your changes (`git commit -am 'Added some
57
+ feature'`)\n4. Push to the branch (`git push origin my-new-feature`)\n5. Create
58
+ new Pull Request\n"
59
+ email:
60
+ - jheiss@aput.net
61
+ executables:
62
+ - sshwrap
63
+ extensions: []
64
+ extra_rdoc_files: []
65
+ files:
66
+ - .gitignore
67
+ - Gemfile
68
+ - LICENSE
69
+ - README.md
70
+ - Rakefile
71
+ - TODO
72
+ - bin/sshwrap
73
+ - lib/sshwrap.rb
74
+ - lib/sshwrap/main.rb
75
+ - lib/sshwrap/version.rb
76
+ - sshwrap.gemspec
77
+ - test/test_options.rb
78
+ homepage: https://github.com/jheiss/sshwrap
79
+ licenses: []
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ! '>='
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ requirements: []
97
+ rubyforge_project:
98
+ rubygems_version: 1.8.24
99
+ signing_key:
100
+ specification_version: 3
101
+ summary: Perform batch SSH operations, handling sudo prompts
102
+ test_files:
103
+ - test/test_options.rb
104
+ has_rdoc: