rutty 2.3.2 → 2.4.0
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/Gemfile +8 -3
- data/Gemfile.lock +22 -11
- data/README.md +62 -5
- data/Rakefile +33 -27
- data/VERSION +1 -1
- data/bin/rutty +8 -8
- data/lib/rutty/actions.rb +171 -98
- data/lib/rutty/helpers.rb +33 -3
- data/lib/rutty/node.rb +13 -1
- data/lib/rutty/thread_pool/pool.rb +80 -0
- data/lib/rutty/thread_pool/worker.rb +103 -0
- data/lib/rutty/version.rb +2 -2
- data/rutty.gemspec +105 -54
- data/test/helper.rb +20 -4
- data/test/test_action_add_node.rb +2 -5
- data/test/test_action_dsh.rb +20 -19
- data/test/test_action_init.rb +4 -8
- data/test/test_action_list_nodes.rb +1 -4
- data/test/test_action_scp.rb +50 -0
- data/test/test_node.rb +13 -0
- data/test/test_nodes.rb +10 -1
- metadata +303 -50
- data/.gitignore +0 -24
data/Gemfile
CHANGED
@@ -2,13 +2,18 @@ source :gemcutter
|
|
2
2
|
|
3
3
|
gem "commander", '~> 4.0.3'
|
4
4
|
gem "terminal-table", '~> 1.4.2'
|
5
|
+
gem "json", '~> 1.4.6'
|
5
6
|
gem "net-ssh", '~> 2.0.23'
|
6
7
|
gem "net-scp", '~> 1.0.4'
|
7
8
|
gem "builder", '~> 2.1.2'
|
8
9
|
gem "treetop", '~> 1.4.8'
|
10
|
+
gem "fastthread", '~> 1.0.7'
|
9
11
|
|
10
12
|
group :development do
|
11
|
-
gem "jeweler", '~> 1.
|
12
|
-
gem "
|
13
|
+
gem "jeweler", '~> 1.5.1'
|
14
|
+
gem "bundler", '~> 1.0.0'
|
15
|
+
gem "shoulda", ">= 0"
|
16
|
+
gem "rcov", ">= 0"
|
13
17
|
gem "xml-simple", '~> 1.0.12'
|
14
|
-
|
18
|
+
gem "ruby-debug", ">= 0"
|
19
|
+
end
|
data/Gemfile.lock
CHANGED
@@ -2,25 +2,31 @@ GEM
|
|
2
2
|
remote: http://rubygems.org/
|
3
3
|
specs:
|
4
4
|
builder (2.1.2)
|
5
|
+
columnize (0.3.1)
|
5
6
|
commander (4.0.3)
|
6
7
|
highline (>= 1.5.0)
|
7
|
-
|
8
|
-
json_pure
|
8
|
+
fastthread (1.0.7)
|
9
9
|
git (1.2.5)
|
10
10
|
highline (1.6.1)
|
11
|
-
jeweler (1.
|
12
|
-
|
11
|
+
jeweler (1.5.1)
|
12
|
+
bundler (~> 1.0.0)
|
13
13
|
git (>= 1.2.5)
|
14
|
-
|
15
|
-
|
14
|
+
rake
|
15
|
+
json (1.4.6)
|
16
|
+
linecache (0.43)
|
16
17
|
net-scp (1.0.4)
|
17
18
|
net-ssh (>= 1.99.1)
|
18
19
|
net-ssh (2.0.23)
|
19
20
|
polyglot (0.3.1)
|
20
|
-
|
21
|
-
|
21
|
+
rake (0.8.7)
|
22
|
+
rcov (0.9.9)
|
23
|
+
ruby-debug (0.10.3)
|
24
|
+
columnize (>= 0.1)
|
25
|
+
ruby-debug-base (~> 0.10.3.0)
|
26
|
+
ruby-debug-base (0.10.3)
|
27
|
+
linecache (>= 0.3)
|
28
|
+
shoulda (2.11.3)
|
22
29
|
terminal-table (1.4.2)
|
23
|
-
thoughtbot-shoulda (2.11.1)
|
24
30
|
treetop (1.4.8)
|
25
31
|
polyglot (>= 0.3.1)
|
26
32
|
xml-simple (1.0.12)
|
@@ -30,11 +36,16 @@ PLATFORMS
|
|
30
36
|
|
31
37
|
DEPENDENCIES
|
32
38
|
builder (~> 2.1.2)
|
39
|
+
bundler (~> 1.0.0)
|
33
40
|
commander (~> 4.0.3)
|
34
|
-
|
41
|
+
fastthread (~> 1.0.7)
|
42
|
+
jeweler (~> 1.5.1)
|
43
|
+
json (~> 1.4.6)
|
35
44
|
net-scp (~> 1.0.4)
|
36
45
|
net-ssh (~> 2.0.23)
|
46
|
+
rcov
|
47
|
+
ruby-debug
|
48
|
+
shoulda
|
37
49
|
terminal-table (~> 1.4.2)
|
38
|
-
thoughtbot-shoulda
|
39
50
|
treetop (~> 1.4.8)
|
40
51
|
xml-simple (~> 1.0.12)
|
data/README.md
CHANGED
@@ -16,6 +16,16 @@ Requirements
|
|
16
16
|
|
17
17
|
* Bundler >= 1.0.0
|
18
18
|
|
19
|
+
###A note on running tests###
|
20
|
+
|
21
|
+
Since RuTTY is essentially a wrapper around SSH connections, a system that is capable of recieving SSH
|
22
|
+
connections must be available to fully test this tool. Currently, I have the tests setup to attempt to
|
23
|
+
log into the current system via the loopback interface (localhost) as the user who ran the tests (as
|
24
|
+
detected by `ENV['USER']`). It attempts to do this on port 22 using the public key found in
|
25
|
+
`#{ENV['HOME']}/.ssh/id_rsa`.
|
26
|
+
|
27
|
+
Note that unless all these conditions are met on your system, tests will not be fully successful.
|
28
|
+
|
19
29
|
Installation
|
20
30
|
------------
|
21
31
|
|
@@ -45,7 +55,7 @@ After initialization, you must add nodes to the RuTTY config. This is done with
|
|
45
55
|
Invoking `rutty help add_node` will give you a list of all the options to pass into it. Any options you don't pass
|
46
56
|
will be filled in from the defaults at `$RUTTY_HOME/defaults.yaml`.
|
47
57
|
|
48
|
-
$ rutty add_node example.com -u root -k /Users/jlindsey/.ssh/id_rsa
|
58
|
+
$ rutty add_node example.com -u root -k /Users/jlindsey/.ssh/id_rsa -g example,test
|
49
59
|
|
50
60
|
The above will add a node to the RuTTY config that looks like this (in YAML):
|
51
61
|
|
@@ -68,20 +78,67 @@ can be omitted. That is to say, the following two commands are identical:
|
|
68
78
|
$ rutty dsh -a uptime
|
69
79
|
$ rutty -a uptime
|
70
80
|
|
71
|
-
The `dsh` action can accept either a list of tags passed via
|
81
|
+
The `dsh` action can accept either a list of tags passed via `-g` or the `-a` flag, which will run the command
|
72
82
|
on all defined nodes regardless of tags.
|
73
83
|
|
74
84
|
Note that any command that has any whitespace in it must be enclosed in quotes.
|
75
85
|
|
76
86
|
$ rutty -a "free -m"
|
87
|
+
|
88
|
+
###Tags###
|
89
|
+
|
90
|
+
The `rutty dsh` and `rutty scp` commands both allow the `-g` flag. The usage of this flag on these two commands
|
91
|
+
is different than on `add_node`, where it is simply a list of tags to apply to the new node. On the remote commands,
|
92
|
+
it essentially has three "modes": single tag, multiple comma-separated tags, and pseudo-SQL tag query.
|
93
|
+
|
94
|
+
The single tag mode is simple. The command
|
95
|
+
|
96
|
+
$ rutty dsh -g foobar uptime
|
97
|
+
|
98
|
+
will run the `uptime` command on every node that is tagged with "foobar".
|
99
|
+
|
100
|
+
---
|
101
|
+
|
102
|
+
The multiple tags mode is essentially an OR query. It will run the command on any nodes that have **ANY** of the
|
103
|
+
specified tags. For example:
|
104
|
+
|
105
|
+
$ rutty dsh -g foo,baz,bar uptime
|
106
|
+
|
107
|
+
will run the `uptime` command on any nodes that are tagged with "foo" **OR** "baz" **OR** "bar".
|
108
|
+
|
109
|
+
---
|
110
|
+
|
111
|
+
The pseudo-SQL tag query mode provides the most control over your tags. This allows you to pass in a string that looks
|
112
|
+
a bit like SQL, letting you specify complex rules for your tag lookup. For example:
|
113
|
+
|
114
|
+
$ rutty dsh -g "'foo' AND 'bar'" uptime
|
115
|
+
|
116
|
+
will run `uptime` on any node that has **BOTH** "foo" and "bar" tags. The command
|
117
|
+
|
118
|
+
$ rutty dsh -g "'foo' OR ('baz' AND 'bar')" uptime
|
119
|
+
|
120
|
+
will run `uptime` on any node that has a "foo" tag **OR** any node that has **BOTH** "baz" and "bar" tags.
|
77
121
|
|
78
122
|
TODO
|
79
123
|
----
|
80
124
|
|
81
|
-
*
|
82
|
-
|
125
|
+
* Refactor defaults config YAML to allow for a broader range of
|
126
|
+
configuration options (max number of threads, default output format, etc)
|
127
|
+
* Implement `rutty upgrade` action, which will upgrade your config files to the latest version
|
128
|
+
|
129
|
+
Note on Patches/Pull Requests
|
130
|
+
-----------------------------
|
131
|
+
|
132
|
+
* Fork the project.
|
133
|
+
* Make your feature addition or bug fix.
|
134
|
+
* Add tests for it. This is important so I don't break it in a
|
135
|
+
future version unintentionally.
|
136
|
+
* Commit, do not mess with rakefile, version, or history.
|
137
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
138
|
+
* Send me a pull request. Bonus points for topic branches.
|
83
139
|
|
84
140
|
Copyright
|
85
141
|
---------
|
86
142
|
|
87
|
-
Copyright (c) 2010 Josh Lindsey at Cloudspace. See LICENSE for details.
|
143
|
+
Copyright (c) 2010 Josh Lindsey at Cloudspace. See LICENSE for details.
|
144
|
+
|
data/Rakefile
CHANGED
@@ -1,40 +1,47 @@
|
|
1
1
|
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
2
10
|
require 'rake'
|
3
11
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
gem.description = %Q{
|
12
|
+
require 'jeweler'
|
13
|
+
Jeweler::Tasks.new do |gem|
|
14
|
+
gem.name = "rutty"
|
15
|
+
gem.summary = %Q{A DSH implementation in Ruby}
|
16
|
+
gem.description = %Q{
|
10
17
|
RuTTY is a DSH (distributed / dancer's shell) written in Ruby. It's used to run commands
|
11
18
|
on multiple remote servers at once, based on a tagging system. Also allows for multiple
|
12
19
|
SCP-style uploads.
|
13
20
|
}
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
21
|
+
gem.email = "josh@cloudspace.com"
|
22
|
+
gem.homepage = "http://github.com/jlindsey/rutty"
|
23
|
+
gem.license = "MIT"
|
24
|
+
gem.authors = ["Josh Lindsey"]
|
25
|
+
gem.add_development_dependency "bundler", ">= 1.0.0"
|
26
|
+
gem.add_development_dependency "jeweler", ">= 1.5.1"
|
27
|
+
gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
|
28
|
+
gem.add_development_dependency "xml-simple", ">= 1.0.12"
|
29
|
+
gem.add_runtime_dependency "commander", ">= 4.0.3"
|
30
|
+
gem.add_runtime_dependency "terminal-table", ">= 1.4.2"
|
31
|
+
gem.add_runtime_dependency "json", ">= 1.4.6"
|
32
|
+
gem.add_runtime_dependency "net-ssh", ">= 2.0.23"
|
33
|
+
gem.add_runtime_dependency "net-scp", ">= 1.0.4"
|
34
|
+
gem.add_runtime_dependency "builder", ">= 2.1.2"
|
35
|
+
gem.add_runtime_dependency "treetop", ">= 1.4.8"
|
36
|
+
gem.add_runtime_dependency "fastthread", ">= 1.0.7"
|
31
37
|
end
|
38
|
+
Jeweler::RubygemsDotOrgTasks.new
|
32
39
|
|
33
40
|
require 'rake/testtask'
|
34
41
|
Rake::TestTask.new(:test) do |test|
|
35
42
|
test.libs << 'lib' << 'test'
|
36
43
|
test.pattern = 'test/**/test_*.rb'
|
37
|
-
test.verbose =
|
44
|
+
test.verbose = false
|
38
45
|
end
|
39
46
|
|
40
47
|
begin
|
@@ -42,7 +49,7 @@ begin
|
|
42
49
|
Rcov::RcovTask.new do |test|
|
43
50
|
test.libs << 'test'
|
44
51
|
test.pattern = 'test/**/test_*.rb'
|
45
|
-
test.verbose = true
|
52
|
+
test.verbose = true
|
46
53
|
end
|
47
54
|
rescue LoadError
|
48
55
|
task :rcov do
|
@@ -50,6 +57,5 @@ rescue LoadError
|
|
50
57
|
end
|
51
58
|
end
|
52
59
|
|
53
|
-
task :test => :check_dependencies
|
54
|
-
|
55
60
|
task :default => :test
|
61
|
+
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
1
|
+
2.4.0
|
data/bin/rutty
CHANGED
@@ -12,11 +12,11 @@ add_filter_options = lambda { |c|
|
|
12
12
|
c.option('-k', '--keypath PATH', String, 'Path to a private key')
|
13
13
|
c.option('-u', '--user NAME', String, 'User login')
|
14
14
|
c.option('-p', '--port NUMBER', Integer, 'SSH port number')
|
15
|
-
c.option('-
|
15
|
+
c.option('-g', '--tags TAG1[,TAG2,...]', Array, 'Comma-separated list of tags')
|
16
16
|
}
|
17
17
|
|
18
18
|
add_remote_options = lambda { |c|
|
19
|
-
c.option('-
|
19
|
+
c.option('-g', '--tags TAG1[,TAG2,...]', String, 'Comma-separated list of tags to run the command on')
|
20
20
|
c.option('-a', 'Run the command on ALL nodes')
|
21
21
|
c.option('-d', '--debug', 'Enable debug output')
|
22
22
|
}
|
@@ -40,7 +40,7 @@ end
|
|
40
40
|
# Commander commands
|
41
41
|
command :init do |c|
|
42
42
|
c.syntax = "rutty init [DIR]"
|
43
|
-
c.summary = "Creates the default file structure for rutty"
|
43
|
+
c.summary = "Creates the default file structure for rutty."
|
44
44
|
c.description = "#{c.summary} in the specified DIR. If DIR is not given, defaults to ~/.rutty"
|
45
45
|
c.when_called do |args, options|
|
46
46
|
args.push Rutty::Consts::CONF_DIR if args.empty?
|
@@ -66,9 +66,9 @@ command :add_node do |c|
|
|
66
66
|
end
|
67
67
|
|
68
68
|
command :list_nodes do |c|
|
69
|
-
|
70
|
-
c.
|
71
|
-
c.
|
69
|
+
c.syntax = "rutty list_nodes [-o FORMAT]"
|
70
|
+
c.summary = "List all currently defined nodes."
|
71
|
+
c.description = "#{c.summary} Defaults to human-readable ASCII table format. Optionally specify either json or xml."
|
72
72
|
|
73
73
|
c.when_called do |args, options|
|
74
74
|
$r.list_nodes args, options
|
@@ -77,7 +77,7 @@ end
|
|
77
77
|
|
78
78
|
command :dsh do |c|
|
79
79
|
c.syntax = "rutty [dsh] [options] COMMAND"
|
80
|
-
c.summary = "Runs the specified COMMAND across all nodes that match [options]"
|
80
|
+
c.summary = "Runs the specified COMMAND across all nodes that match [options]."
|
81
81
|
|
82
82
|
c.example "Get a list of all users logged in to all your web and app nodes", "rutty --tags web,app w"
|
83
83
|
c.example "See all your nodes' current memory footprint", "rutty -a \"free -m\""
|
@@ -97,7 +97,7 @@ command :scp do |c|
|
|
97
97
|
add_remote_options.call(c)
|
98
98
|
|
99
99
|
c.when_called do |args, options|
|
100
|
-
$r.
|
100
|
+
$r.scp args, options
|
101
101
|
end
|
102
102
|
end
|
103
103
|
|
data/lib/rutty/actions.rb
CHANGED
@@ -25,7 +25,7 @@ module Rutty
|
|
25
25
|
log "\t<%= color('exists', :cyan) %>", dir
|
26
26
|
else
|
27
27
|
log "\t<%= color('create', :green) %>", dir
|
28
|
-
|
28
|
+
FileUtils.mkdir_p dir
|
29
29
|
end
|
30
30
|
|
31
31
|
if File.exists? general_file
|
@@ -137,6 +137,8 @@ module Rutty
|
|
137
137
|
# @see (see #add_node)
|
138
138
|
# @param (see #add_node)
|
139
139
|
def dsh args, options
|
140
|
+
@start_time = Time.now
|
141
|
+
|
140
142
|
check_installed!
|
141
143
|
raise Rutty::BadUsage.new "Must supply a command to run. See `rutty help dsh' for usage" if args.empty?
|
142
144
|
raise Rutty::BadUsage.new "One of -a or --tags must be passed" if options.a.nil? and options.tags.nil?
|
@@ -147,24 +149,18 @@ module Rutty
|
|
147
149
|
say "<%= color('No nodes defined', :yellow) %>"
|
148
150
|
exit
|
149
151
|
end
|
150
|
-
|
151
|
-
HighLine.color_scheme = HighLine::SampleColorScheme.new
|
152
152
|
|
153
153
|
com_str = args.pop
|
154
154
|
|
155
|
-
require 'logger'
|
156
|
-
require 'net/ssh'
|
157
|
-
|
158
155
|
@returns = {}
|
159
|
-
|
160
|
-
|
156
|
+
|
161
157
|
# This is necessary in order to capture exit codes and/or signals,
|
162
158
|
# which are't passed through when using just the ssh.exec!() semantics.
|
163
159
|
exec_command = lambda { |ssh|
|
164
160
|
ssh.open_channel do |channel|
|
165
161
|
channel.exec(com_str) do |ch, success|
|
166
162
|
unless success
|
167
|
-
|
163
|
+
@returns[ssh.host][:out] = "FAILED: couldn't execute command (ssh.channel.exec failure)"
|
168
164
|
end
|
169
165
|
|
170
166
|
channel.on_data do |ch, data| # stdout
|
@@ -177,8 +173,7 @@ module Rutty
|
|
177
173
|
end
|
178
174
|
|
179
175
|
channel.on_request("exit-status") do |ch, data|
|
180
|
-
|
181
|
-
@returns[ssh.host][:exit] = exit_code
|
176
|
+
@returns[ssh.host][:exit] = data.read_long
|
182
177
|
end
|
183
178
|
|
184
179
|
channel.on_request("exit-signal") do |ch, data|
|
@@ -188,25 +183,9 @@ module Rutty
|
|
188
183
|
end
|
189
184
|
ssh.loop
|
190
185
|
}
|
191
|
-
|
192
|
-
self.nodes.filter(options)
|
193
|
-
|
194
|
-
begin
|
195
|
-
connections << Net::SSH.start(node.host, node.user, :port => node.port, :paranoid => false,
|
196
|
-
:user_known_hosts_file => '/dev/null', :keys => [node.keypath], :timeout => 5,
|
197
|
-
:logger => Logger.new(options.debug.nil? ? $stderr : $stdout),
|
198
|
-
:verbose => (options.debug.nil? ? Logger::FATAL : Logger::DEBUG))
|
199
|
-
rescue Errno::ECONNREFUSED
|
200
|
-
@returns[node.host][:out] = "ERROR: Connection refused"
|
201
|
-
@returns[node.host][:exit] = 10000
|
202
|
-
rescue SocketError
|
203
|
-
@returns[node.host][:out] = "ERROR: no nodename nor servname provided, or not known"
|
204
|
-
@returns[node.host][:exit] = 20000
|
205
|
-
rescue Timeout::Error
|
206
|
-
@returns[node.host][:out] = "ERROR: Connection timeout"
|
207
|
-
@returns[node.host][:exit] = 30000
|
208
|
-
end
|
209
|
-
end
|
186
|
+
|
187
|
+
@filtered_nodes = self.nodes.filter(options)
|
188
|
+
connections = connect_to_nodes @filtered_nodes, options.debug.nil?
|
210
189
|
|
211
190
|
connections.each { |ssh| exec_command.call(ssh) }
|
212
191
|
|
@@ -215,56 +194,7 @@ module Rutty
|
|
215
194
|
break if connections.empty?
|
216
195
|
end
|
217
196
|
|
218
|
-
|
219
|
-
when 'human-readable'
|
220
|
-
min_width = 0
|
221
|
-
@returns.each do |host, hash|
|
222
|
-
min_width = host.length if host.length > min_width
|
223
|
-
end
|
224
|
-
|
225
|
-
buffer = ''
|
226
|
-
@returns.each do |host, hash|
|
227
|
-
padded_host = host.dup
|
228
|
-
|
229
|
-
if hash[:exit] >= 10000
|
230
|
-
padded_host = "<%= color('#{padded_host}', :critical) %>"
|
231
|
-
hash[:out] = "<%= color('#{hash[:out]}', :red) %>"
|
232
|
-
elsif hash[:exit] > 0
|
233
|
-
padded_host = "<%= color('#{padded_host}', :error) %>"
|
234
|
-
else
|
235
|
-
padded_host = "<%= color('#{padded_host}', :green) %>"
|
236
|
-
end
|
237
|
-
|
238
|
-
padded_host << (" " * (min_width - host.length)) if host.length < min_width
|
239
|
-
buffer << padded_host << "\t\t"
|
240
|
-
|
241
|
-
buffer << hash[:out].lstrip
|
242
|
-
end
|
243
|
-
|
244
|
-
buffer
|
245
|
-
|
246
|
-
when 'json'
|
247
|
-
require 'json'
|
248
|
-
JSON.dump @returns
|
249
|
-
|
250
|
-
when 'xml'
|
251
|
-
require 'builder'
|
252
|
-
|
253
|
-
xml = Builder::XmlMarkup.new(:indent => 2)
|
254
|
-
|
255
|
-
xml.instruct!
|
256
|
-
xml.nodes do
|
257
|
-
@returns.each do |host, hash|
|
258
|
-
xml.node do
|
259
|
-
xml.host host
|
260
|
-
xml.exit hash[:exit]
|
261
|
-
xml.out hash[:out].strip
|
262
|
-
end
|
263
|
-
end
|
264
|
-
end
|
265
|
-
end
|
266
|
-
|
267
|
-
say output
|
197
|
+
say generate_output
|
268
198
|
end
|
269
199
|
|
270
200
|
##
|
@@ -274,33 +204,24 @@ module Rutty
|
|
274
204
|
# @see (see #add_node)
|
275
205
|
# @param (see #add_node)
|
276
206
|
def scp args, options
|
207
|
+
@start_time = Time.now
|
208
|
+
|
277
209
|
check_installed!
|
278
210
|
raise Rutty::BadUsage.new "Must supply a local path and a remote path" unless args.length == 2
|
279
211
|
raise Rutty::BadUsage.new "One of -a or --tags must be passed" if options.a.nil? and options.tags.nil?
|
280
212
|
raise Rutty::BadUsage.new "Use either -a or --tags, not both" if !options.a.nil? and !options.tags.nil?
|
281
213
|
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
connections = []
|
214
|
+
if self.nodes.empty?
|
215
|
+
say "<%= color('No nodes defined', :yellow) %>"
|
216
|
+
exit
|
217
|
+
end
|
288
218
|
|
289
219
|
remote_path = args.pop
|
290
220
|
local_path = args.pop
|
291
221
|
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
:user_known_hosts_file => '/dev/null', :keys => [node.keypath],
|
296
|
-
:logger => Logger.new(options.debug.nil? ? $stderr : $stdout),
|
297
|
-
:verbose => (options.debug.nil? ? Logger::FATAL : Logger::DEBUG))
|
298
|
-
rescue Errno::ECONNREFUSED
|
299
|
-
$stderr.puts "ERROR: Connection refused on #{node.host}"
|
300
|
-
rescue SocketError
|
301
|
-
$stderr.puts "ERROR: nodename nor servname provided, or not known for #{node.host}"
|
302
|
-
end
|
303
|
-
end
|
222
|
+
@returns = {}
|
223
|
+
@filtered_nodes = self.nodes.filter(options)
|
224
|
+
connections = connect_to_nodes @filtered_nodes, options.debug.nil?
|
304
225
|
|
305
226
|
connections.each { |ssh| ssh.scp.upload! local_path, remote_path }
|
306
227
|
|
@@ -308,6 +229,158 @@ module Rutty
|
|
308
229
|
connections.delete_if { |ssh| !ssh.process(0.1) { |s| s.busy? } }
|
309
230
|
break if connections.empty?
|
310
231
|
end
|
232
|
+
|
233
|
+
say generate_output
|
234
|
+
end
|
235
|
+
|
236
|
+
private
|
237
|
+
|
238
|
+
##
|
239
|
+
# Sets up the Net::SSH connections given a filled {Nodes} object.
|
240
|
+
#
|
241
|
+
# @since 2.4.0
|
242
|
+
#
|
243
|
+
# @see #dsh
|
244
|
+
# @see #scp
|
245
|
+
#
|
246
|
+
# @param [Nodes] nodes The {Node} objects to connect to
|
247
|
+
# @return [Array<Net:SSH::Connection>] The array of live connections
|
248
|
+
def connect_to_nodes nodes, debug = false
|
249
|
+
require 'logger'
|
250
|
+
require 'net/ssh'
|
251
|
+
require 'net/scp'
|
252
|
+
require 'fastthread'
|
253
|
+
require 'rutty/thread_pool/pool'
|
254
|
+
|
255
|
+
connections = []
|
256
|
+
cons_lock = Mutex.new
|
257
|
+
returns_lock = Mutex.new
|
258
|
+
|
259
|
+
pool = Rutty::ThreadPool.new 10
|
260
|
+
|
261
|
+
nodes.each do |node|
|
262
|
+
pool.process do
|
263
|
+
returns_lock.synchronize {
|
264
|
+
@returns[node.host] = { :exit => 0, :out => '' }
|
265
|
+
}
|
266
|
+
begin
|
267
|
+
cons_lock.synchronize {
|
268
|
+
connections << Net::SSH.start(node.host, node.user, :port => node.port, :paranoid => false,
|
269
|
+
:user_known_hosts_file => '/dev/null', :keys => [node.keypath], :timeout => 5,
|
270
|
+
:logger => Logger.new(debug ? $stderr : $stdout),
|
271
|
+
:verbose => (debug ? Logger::FATAL : Logger::DEBUG))
|
272
|
+
}
|
273
|
+
rescue Errno::ECONNREFUSED
|
274
|
+
returns_lock.synchronize {
|
275
|
+
@returns[node.host][:out] = "ERROR: Connection refused"
|
276
|
+
@returns[node.host][:exit] = 10000
|
277
|
+
}
|
278
|
+
|
279
|
+
cons_lock.synchronize { connections << nil }
|
280
|
+
rescue SocketError
|
281
|
+
returns_lock.synchronize {
|
282
|
+
@returns[node.host][:out] = "ERROR: no nodename nor servname provided, or not known"
|
283
|
+
@returns[node.host][:exit] = 20000
|
284
|
+
}
|
285
|
+
|
286
|
+
cons_lock.synchronize { connections << nil }
|
287
|
+
rescue Timeout::Error
|
288
|
+
returns_lock.synchronize {
|
289
|
+
@returns[node.host][:out] = "ERROR: Connection timeout"
|
290
|
+
@returns[node.host][:exit] = 30000
|
291
|
+
}
|
292
|
+
|
293
|
+
cons_lock.synchronize { connections << nil }
|
294
|
+
rescue Net::SSH::AuthenticationFailed => e
|
295
|
+
returns_lock.synchronize {
|
296
|
+
@returns[node.host][:out] = "ERROR: Authentication failure (#{e.to_s})"
|
297
|
+
@returns[node.host][:exit] = 40000
|
298
|
+
}
|
299
|
+
|
300
|
+
cons_lock.synchronize { connections << nil }
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
loop do
|
306
|
+
break if nodes.length == connections.length
|
307
|
+
sleep 0.2
|
308
|
+
end
|
309
|
+
|
310
|
+
pool.shutdown
|
311
|
+
connections.compact
|
312
|
+
end
|
313
|
+
|
314
|
+
##
|
315
|
+
# Generates the output of the remote actions, based on the value of {Rutty::Runner#output_format}.
|
316
|
+
#
|
317
|
+
# @since 2.4.0
|
318
|
+
#
|
319
|
+
# @see #dsh
|
320
|
+
# @see #scp
|
321
|
+
#
|
322
|
+
# @return [String] The formatted string to output
|
323
|
+
def generate_output
|
324
|
+
case self.output_format
|
325
|
+
when 'human-readable'
|
326
|
+
HighLine.color_scheme = HighLine::SampleColorScheme.new
|
327
|
+
|
328
|
+
min_width = 0
|
329
|
+
@returns.each do |host, hash|
|
330
|
+
min_width = host.length if host.length > min_width
|
331
|
+
end
|
332
|
+
|
333
|
+
buffer = ''
|
334
|
+
@returns.each do |host, hash|
|
335
|
+
padded_host = host.dup
|
336
|
+
|
337
|
+
if hash[:exit] >= 10000
|
338
|
+
padded_host = "<%= color('#{padded_host}', :critical) %>"
|
339
|
+
hash[:out] = "<%= color('#{hash[:out]}', :red) %>"
|
340
|
+
elsif hash[:exit] > 0
|
341
|
+
padded_host = "<%= color('#{padded_host}', :error) %>"
|
342
|
+
else
|
343
|
+
padded_host = "<%= color('#{padded_host}', :green) %>"
|
344
|
+
end
|
345
|
+
|
346
|
+
padded_host << (" " * (min_width - host.length)) if host.length < min_width
|
347
|
+
buffer << padded_host << "\t\t"
|
348
|
+
|
349
|
+
buffer << hash[:out].lstrip
|
350
|
+
end
|
351
|
+
|
352
|
+
@end_time = Time.now
|
353
|
+
|
354
|
+
total_nodes = @filtered_nodes.length
|
355
|
+
total_error_nodes = @returns.reject { |host, hash| hash[:exit] == 0 }.length
|
356
|
+
total_time = @end_time - @start_time
|
357
|
+
|
358
|
+
buffer.rstrip!
|
359
|
+
buffer << "\n\n" << "<%= color('#{total_nodes} host(s), #{total_error_nodes} error(s), #{seconds_in_words total_time}', "
|
360
|
+
buffer << (total_error_nodes > 0 ? ":red" : ":green") << ") %>"
|
361
|
+
|
362
|
+
buffer
|
363
|
+
|
364
|
+
when 'json'
|
365
|
+
require 'json'
|
366
|
+
JSON.dump @returns
|
367
|
+
|
368
|
+
when 'xml'
|
369
|
+
require 'builder'
|
370
|
+
|
371
|
+
xml = Builder::XmlMarkup.new(:indent => 2)
|
372
|
+
|
373
|
+
xml.instruct!
|
374
|
+
xml.nodes do
|
375
|
+
@returns.each do |host, hash|
|
376
|
+
xml.node do
|
377
|
+
xml.host host
|
378
|
+
xml.exit hash[:exit]
|
379
|
+
xml.out hash[:out].strip
|
380
|
+
end
|
381
|
+
end
|
382
|
+
end
|
383
|
+
end
|
311
384
|
end
|
312
385
|
end
|
313
386
|
end
|