ssh_voodoo 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +23 -0
- data/bin/ssh_voodoo +49 -0
- data/lib/ssh_voodoo.rb +192 -0
- data/lib/thread_pool.rb +103 -0
- data/ssh_voodoo.gemspec +23 -0
- metadata +100 -0
data/README.md
ADDED
@@ -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
|
+
```
|
data/bin/ssh_voodoo
ADDED
@@ -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)
|
data/lib/ssh_voodoo.rb
ADDED
@@ -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
|
data/lib/thread_pool.rb
ADDED
@@ -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
|
data/ssh_voodoo.gemspec
ADDED
@@ -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:
|