ssh_voodoo 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,23 @@
1
+ # ssh_voodoo
2
+ ssh_voodo is a Ruby script to help with the task of running commands on remote machines via ssh. It allows you to run commands remotely in parallel and helps cache your password (including sudo) so you don't have to keep on entering your password. There's also an option to use ssh key.
3
+
4
+ ## Installation
5
+ It's hosted on rubygems.org
6
+
7
+ sudo gem install ssh_voodoo
8
+
9
+ ## Usage
10
+
11
+ ```
12
+ ssh_voodoo -h
13
+ Usage: ssh_voodoo [options]
14
+ -s, --servers=SERVERS Servers to apply the actions to
15
+ --debug Print lots of messages
16
+ --use-ssh-key [FILE] Use ssh key instead of password
17
+ -c, --command=STRING What command to run on the remote server
18
+ --username=USERNAME What username to use for connecting to remote servers
19
+ --dw=INTEGER Number of workers for parallel ssh connections
20
+ --connectiontimeout=INTEGER
21
+ Connection timeout
22
+ -h, --help Show this message
23
+ ```
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env ruby
2
+ $:.unshift File.join(File.dirname(__FILE__), "..", "lib")
3
+
4
+ require 'ssh_voodoo'
5
+ require 'optparse'
6
+
7
+ options = {} # options for how to running ssh_voodoo
8
+ servers = nil
9
+ cmd = nil
10
+
11
+ opts = OptionParser.new(nil, 24, ' ')
12
+ opts.banner = 'Usage: ssh_voodoo [options]'
13
+ opts.on('--servers', '-s', '=SERVERS', Array, 'Servers to apply the actions to') do |opt|
14
+ servers = opt
15
+ end
16
+ opts.on('--debug', 'Print lots of messages') do |opt|
17
+ options["debug"] = opt
18
+ end
19
+ opts.on('--use-ssh-key [FILE]', 'Use ssh key instead of password') do |opt|
20
+ options["use-ssh-key"] = true
21
+ options["ssh-key"] = opt
22
+ end
23
+
24
+ opts.on('--command', '-c', '=STRING', 'What command to run on the remote server') do |opt|
25
+ cmd = opt
26
+ end
27
+ opts.on('--username', '=USERNAME', 'What username to use for connecting to remote servers') do |opt|
28
+ options["username"] = opt
29
+ end
30
+ opts.on('--dw', '=INTEGER', 'Number of workers for parallel ssh connections') do |opt|
31
+ options["max-worker"] = opt.to_i
32
+ end
33
+ opts.on('--connectiontimeout', '=INTEGER', 'Connection timeout') do |opt|
34
+ options["connectiontimeout"] = opt.to_i
35
+ end
36
+ opts.on_tail("-h", "--help", "Show this message") do
37
+ puts opts
38
+ exit
39
+ end
40
+
41
+ leftovers = opts.parse(ARGV)
42
+
43
+ if cmd.nil? or servers.nil?
44
+ puts opts
45
+ exit
46
+ end
47
+
48
+ ssh_voodoo = SshVoodoo.new(options)
49
+ ssh_voodoo.perform_magic(cmd, servers)
@@ -0,0 +1,192 @@
1
+ require 'thread_pool'
2
+ require 'rubygems'
3
+ require 'net/ssh'
4
+ require 'thread'
5
+
6
+ require 'etc'
7
+
8
+ class SshVoodoo
9
+
10
+ def initialize(options = nil)
11
+ @sudo_pw = nil
12
+ @pw_prompts = {}
13
+ @mutex = Mutex.new
14
+ @max_worker = 4
15
+ @abort_on_failure = false
16
+ @use_ssh_key = false
17
+ @user = Etc.getlogin
18
+ @password = nil
19
+ @connectiontimeout = options["connectiontimeout"]
20
+ @debug = false
21
+ unless options.nil? or options.empty?
22
+ @user = options["username"] unless options["username"].nil?
23
+ @password = options["deploy-password"] unless options["deploy-password"].nil?
24
+ @max_worker = options["max-worker"] unless options["max-worker"].nil?
25
+ @abort_on_failure = options["abort-on-failure"] unless options["abort-on-failure"].nil?
26
+ @use_ssh_key = options["use-ssh-key"] unless options["use-ssh-key"].nil?
27
+ @ssh_key = options["ssh-key"] unless options["ssh-key"].nil?
28
+ @debug = options["debug"] unless options["debug"].nil?
29
+ end
30
+ end
31
+
32
+ def prompt_username
33
+ ask("Username: ")
34
+ end
35
+
36
+ def prompt_password
37
+ ask("SSH Password (leave blank if using ssh key): ", true)
38
+ end
39
+
40
+ def ask(str,mask=false)
41
+ begin
42
+ print str
43
+ system 'stty -echo;' if mask
44
+ input = STDIN.gets.chomp
45
+ ensure
46
+ system 'stty echo; echo ""'
47
+ end
48
+ return input
49
+ end
50
+
51
+ def get_sudo_pw
52
+ @mutex.synchronize {
53
+ if @sudo_pw.nil?
54
+ @sudo_pw = ask("Sudo password: ", true)
55
+ else
56
+ return @sudo_pw
57
+ end
58
+ }
59
+ end
60
+
61
+ # Prompt user for input and cache it. If in the future, we see
62
+ # the same prompt again, we can reuse the existing inputs. This saves
63
+ # the users from having to type in a bunch of inputs (such as password)
64
+ def get_input_for_pw_prompt(prompt)
65
+ @mutex.synchronize {
66
+ if @pw_prompts[prompt].nil?
67
+ @pw_prompts[prompt] = ask(prompt, true)
68
+ end
69
+ return @pw_prompts[prompt]
70
+ }
71
+ end
72
+
73
+ # Return a block that can be used for executing a cmd on the remote server
74
+ def ssh_execute(server, username, password, key, cmd)
75
+ return lambda {
76
+ exit_status = 0
77
+ result = []
78
+
79
+ params = {}
80
+ params[:password] = password if password
81
+ params[:keys] = [key] if key
82
+ params[:timeout] = @connectiontimeout if @connectiontimeout
83
+
84
+ begin
85
+ Net::SSH.start(server, username, params) do |ssh|
86
+ puts "Connecting to #{server}"
87
+ ch = ssh.open_channel do |channel|
88
+ # now we request a "pty" (i.e. interactive) session so we can send data
89
+ # back and forth if needed. it WILL NOT WORK without this, and it has to
90
+ # be done before any call to exec.
91
+
92
+ channel.request_pty do |ch, success|
93
+ raise "Could not obtain pty (i.e. an interactive ssh session)" if !success
94
+ end
95
+
96
+ channel.exec(cmd) do |ch, success|
97
+ puts "Executing #{cmd} on #{server}" if @debug
98
+ # 'success' isn't related to bash exit codes or anything, but more
99
+ # about ssh internals (i think... not bash related anyways).
100
+ # not sure why it would fail at such a basic level, but it seems smart
101
+ # to do something about it.
102
+ abort "could not execute command" unless success
103
+
104
+ # on_data is a hook that fires when the loop that this block is fired
105
+ # in (see below) returns data. This is what we've been doing all this
106
+ # for; now we can check to see if it's a password prompt, and
107
+ # interactively return data if so (see request_pty above).
108
+ channel.on_data do |ch, data|
109
+ if data =~ /Password:/
110
+ password = get_sudo_pw unless !password.nil? && password != ""
111
+ channel.send_data "#{password}\n"
112
+ elsif data =~ /password/i or data =~ /passphrase/i or
113
+ data =~ /pass phrase/i or data =~ /incorrect passphrase/i
114
+ input = get_input_for_pw_prompt(data)
115
+ channel.send_data "#{input}\n"
116
+ else
117
+ result << data unless data.nil? or data.empty?
118
+ end
119
+ end
120
+
121
+ channel.on_extended_data do |ch, type, data|
122
+ print "SSH command returned on stderr: #{data}"
123
+ end
124
+
125
+ channel.on_request "exit-status" do |ch, data|
126
+ exit_status = data.read_long
127
+ end
128
+ end
129
+ end
130
+ ch.wait
131
+ ssh.loop
132
+ end
133
+ puts "==================================================\nResult from #{server}:"
134
+ puts result.join
135
+ puts "=================================================="
136
+
137
+ rescue Net::SSH::AuthenticationFailed
138
+ exit_status = 1
139
+ puts "Bad username/password combination for host #{server}"
140
+ rescue Exception => e
141
+ exit_status = 1
142
+ puts e.inspect if @debug
143
+ puts e.backtrace if @debug
144
+ puts "Can't connect to #{server}"
145
+ end
146
+
147
+ return exit_status
148
+ }
149
+ end
150
+
151
+ # servers is an array, a filename or a callback that list the remote servers where we want to ssh to
152
+ def perform_magic(cmd, servers)
153
+ user = @user
154
+
155
+ if @user.nil? && !@use_ssh_key
156
+ @user = prompt_username
157
+ end
158
+
159
+ if @password.nil? && !@use_ssh_key
160
+ @password = prompt_password
161
+ end
162
+
163
+ tp = ThreadPool.new(@max_worker)
164
+ statuses = {}
165
+ ssh_to = []
166
+ if servers.kind_of?(Proc)
167
+ ssh_to = servers.call
168
+ elsif servers.size == 1 && File.exists?(servers[0])
169
+ puts "Reading server list from file #{servers[0]}"
170
+ File.open(servers[0], 'r') do |f|
171
+ while line = f.gets
172
+ ssh_to << line.chomp.split(",")
173
+ end
174
+ end
175
+ ssh_to.flatten!
176
+ else
177
+ ssh_to = servers
178
+ end
179
+
180
+ ssh_to.each do | server |
181
+ tp.process(server) do
182
+ status = ssh_execute(server, @user, @password, @ssh_key, cmd).call
183
+ statuses[server] = status
184
+ end
185
+ end
186
+ tp.shutdown
187
+ puts "Exit statuses: "
188
+ puts statuses.inspect
189
+
190
+ return statuses
191
+ end
192
+ end
@@ -0,0 +1,103 @@
1
+ require 'thread'
2
+
3
+ class ThreadPool
4
+ class Worker
5
+ def initialize(thread_queue)
6
+ @block = nil
7
+ @mutex = Mutex.new
8
+ @cv = ConditionVariable.new
9
+ @queue = thread_queue
10
+ @running = true
11
+ @thread = Thread.new do
12
+ @mutex.synchronize do
13
+ while @running
14
+ @cv.wait(@mutex)
15
+ block = get_block
16
+ if block
17
+ @mutex.unlock
18
+ block.call
19
+ @mutex.lock
20
+ reset_block
21
+ end
22
+ @queue << self
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ def name
29
+ @thread.inspect
30
+ end
31
+
32
+ def get_block
33
+ @block
34
+ end
35
+
36
+ def set_block(block)
37
+ @mutex.synchronize do
38
+ raise RuntimeError, "Thread already busy." if @block
39
+ @block = block
40
+ # Signal the thread in this class, that there's a job to be done
41
+ @cv.signal
42
+ end
43
+ end
44
+
45
+ def reset_block
46
+ @block = nil
47
+ end
48
+
49
+ def busy?
50
+ @mutex.synchronize { !@block.nil? }
51
+ end
52
+
53
+ def stop
54
+ @mutex.synchronize do
55
+ @running = false
56
+ @cv.signal
57
+ end
58
+ @thread.join
59
+ end
60
+ end
61
+
62
+ attr_accessor :max_size
63
+
64
+ def initialize(max_size = 10)
65
+ @max_size = max_size
66
+ @queue = Queue.new
67
+ @workers = []
68
+ end
69
+
70
+ def size
71
+ @workers.size
72
+ end
73
+
74
+ def busy?
75
+ @queue.size < @workers.size
76
+ end
77
+
78
+ def shutdown
79
+ @workers.each { |w| w.stop }
80
+ @workers = []
81
+ end
82
+
83
+ alias :join :shutdown
84
+
85
+ def process(block=nil,&blk)
86
+ block = blk if block_given?
87
+ worker = get_worker
88
+ worker.set_block(block)
89
+ end
90
+
91
+ private
92
+
93
+ def get_worker
94
+ if !@queue.empty? or @workers.size == @max_size
95
+ return @queue.pop
96
+ else
97
+ worker = Worker.new(@queue)
98
+ @workers << worker
99
+ worker
100
+ end
101
+ end
102
+
103
+ end
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ $LOAD_PATH.push File.expand_path("../lib", __FILE__)
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "ssh_voodoo"
7
+ s.version = "0.0.1"
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Darren Dao"]
10
+ s.email = ["darrendao@gmail.com"]
11
+ s.homepage = "https://github.com/darrendao/ssh_voodoo"
12
+ s.summary = %q{Ruby script to help with the task of running commands on remote machines via ssh.}
13
+ s.description = %q{Ruby script to help with the task of running commands on remote machines via ssh. It supports password caching and ssh keys. You can specify the remote hosts on the cmd option, or via a file.}
14
+
15
+ s.add_dependency 'net-ssh', '~> 2.3'
16
+ s.add_development_dependency 'yard', '~> 0.7'
17
+
18
+ s.files = `git ls-files`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+ s.extra_rdoc_files = ["README.md"]
22
+ end
23
+
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ssh_voodoo
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Darren Dao
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-02-14 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: net-ssh
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ hash: 5
29
+ segments:
30
+ - 2
31
+ - 3
32
+ version: "2.3"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: yard
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ hash: 5
44
+ segments:
45
+ - 0
46
+ - 7
47
+ version: "0.7"
48
+ type: :development
49
+ version_requirements: *id002
50
+ description: Ruby script to help with the task of running commands on remote machines via ssh. It supports password caching and ssh keys. You can specify the remote hosts on the cmd option, or via a file.
51
+ email:
52
+ - darrendao@gmail.com
53
+ executables:
54
+ - ssh_voodoo
55
+ extensions: []
56
+
57
+ extra_rdoc_files:
58
+ - README.md
59
+ files:
60
+ - README.md
61
+ - bin/ssh_voodoo
62
+ - lib/ssh_voodoo.rb
63
+ - lib/thread_pool.rb
64
+ - ssh_voodoo.gemspec
65
+ homepage: https://github.com/darrendao/ssh_voodoo
66
+ licenses: []
67
+
68
+ post_install_message:
69
+ rdoc_options: []
70
+
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ hash: 3
79
+ segments:
80
+ - 0
81
+ version: "0"
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ hash: 3
88
+ segments:
89
+ - 0
90
+ version: "0"
91
+ requirements: []
92
+
93
+ rubyforge_project:
94
+ rubygems_version: 1.8.11
95
+ signing_key:
96
+ specification_version: 3
97
+ summary: Ruby script to help with the task of running commands on remote machines via ssh.
98
+ test_files: []
99
+
100
+ has_rdoc: