sshwrap 0.0.1

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