rutty 2.3.2 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
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.4.0'
12
- gem "thoughtbot-shoulda"
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
- end
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
- gemcutter (0.5.0)
8
- json_pure
8
+ fastthread (1.0.7)
9
9
  git (1.2.5)
10
10
  highline (1.6.1)
11
- jeweler (1.4.0)
12
- gemcutter (>= 0.1.0)
11
+ jeweler (1.5.1)
12
+ bundler (~> 1.0.0)
13
13
  git (>= 1.2.5)
14
- rubyforge (>= 2.0.0)
15
- json_pure (1.4.3)
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
- rubyforge (2.0.4)
21
- json_pure (>= 1.1.7)
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
- jeweler (~> 1.4.0)
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 --tags example,test
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 `--tags` or the `-a` flag, which will run the command
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
- * Cleanup dsh action code
82
- * Implement delete_node command
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
- begin
5
- require 'jeweler'
6
- Jeweler::Tasks.new do |gem|
7
- gem.name = "rutty"
8
- gem.summary = %Q{A DSH implementation in Ruby}
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
- gem.email = "josh@cloudspace.com"
15
- gem.homepage = "http://github.com/jlindsey/rutty"
16
- gem.authors = ["Josh Lindsey"]
17
- gem.add_development_dependency "bundler", ">= 1.0.0"
18
- gem.add_development_dependency "jeweler", ">= 1.4.0"
19
- gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
20
- gem.add_development_dependency "xml-simple", ">= 1.0.12"
21
- gem.add_dependency "commander", ">= 4.0.3"
22
- gem.add_dependency "terminal-table", ">= 1.4.2"
23
- gem.add_dependency "net-ssh", ">= 2.0.23"
24
- gem.add_dependency "net-scp", ">= 1.0.4"
25
- gem.add_dependency "builder", ">= 2.1.2"
26
- gem.add_dependency "treetop", ">= 1.4.8"
27
- end
28
- Jeweler::GemcutterTasks.new
29
- rescue LoadError
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 = true
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.3.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('-t', '--tags TAG1[,TAG2,...]', Array, 'Comma-separated list of tags')
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('-t', '--tags TAG1[,TAG2,...]', String, 'Comma-separated list of tags to run the command on')
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
- # TODO: Make tag searching AND or OR, not just OR
70
- c.syntax = "rutty list_nodes [options]"
71
- c.summary = "List nodes according to [options]."
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.dsh args, options
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
- Dir.mkdir dir
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
- connections = []
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
- abort "FAILED: couldn't execute command (ssh.channel.exec failure)"
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
- exit_code = data.read_long
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).each do |node|
193
- @returns[node.host] = { :exit => 0, :out => '' }
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
- output = case self.output_format
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
- require 'logger'
283
- require 'net/ssh'
284
- require 'net/scp'
285
- require 'pp'
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
- self.nodes.filter(options).each do |node|
293
- begin
294
- connections << Net::SSH.start(node.host, node.user, :port => node.port, :paranoid => false,
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