weave 0.0.2 → 0.1.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.
@@ -5,7 +5,7 @@ require "weave"
5
5
 
6
6
  usage = <<-EOS
7
7
  Usage:
8
- $ ./parallel-ssh user@host1 user@host2 ...
8
+ $ ruby parallel-console.rb user@host1 user@host2 ...
9
9
  EOS
10
10
 
11
11
  abort usage if ARGV.empty?
@@ -13,12 +13,20 @@ abort usage if ARGV.empty?
13
13
  $stty_state = `stty -g`.chomp
14
14
  $pool = Weave.connect(ARGV)
15
15
 
16
- while command = Readline.readline(">>> ", true)
16
+ prompt = ">>> "
17
+ while command = Readline.readline(prompt, true)
18
+ prompt = ">>> "
17
19
  break unless command # ctrl-D
18
20
  command.chomp!
19
21
  next if command.empty?
20
22
  break if ["exit", "quit"].include? command
21
- $pool.execute { run command }
23
+ bad_exit = false
24
+ $pool.execute do
25
+ result = run(command, :continue_on_failure => true)
26
+ bad_exit = result[:exit_code] && result[:exit_code] != 0
27
+ bad_exit ||= result[:exit_signal]
28
+ end
29
+ prompt = "!!! " if bad_exit
22
30
  end
23
31
 
24
32
  $pool.disconnect!
@@ -4,6 +4,9 @@ require "thread"
4
4
  module Weave
5
5
  DEFAULT_THREAD_POOL_SIZE = 10
6
6
 
7
+ # A Weave error.
8
+ class Error < StandardError; end
9
+
7
10
  # @private
8
11
  COLORS = { :red => 1, :green => 2 }
9
12
 
@@ -14,6 +17,9 @@ module Weave
14
17
  #
15
18
  # @see ConnectionPool#execute
16
19
  def self.connect(host_list, options = {}, &block)
20
+ unless host_list.is_a? Array
21
+ raise Weave::Error, "Must pass an array for host_list. Received: #{host_list.inspect}"
22
+ end
17
23
  pool = ConnectionPool.new(host_list)
18
24
  if block_given?
19
25
  pool.execute(options, &block)
@@ -60,24 +66,26 @@ module Weave
60
66
  # Run a command over the connection pool. The block is evaluated in the context of LazyConnection.
61
67
  #
62
68
  # @param [Hash] options the various knobs
69
+ # @option options [Array] :args the arguments to pass through to the block when it runs.
63
70
  # @option options [Fixnum] :num_threads the number of concurrent threads to use to process this command.
64
71
  # Defaults to `DEFAULT_THREAD_POOL_SIZE`.
65
72
  # @option options [Boolean] :serial whether to process the command for each connection one at a time.
66
73
  # @option options [Fixnum] :batch_by if set, group the connections into batches of no more than this value
67
74
  # and fully process each batch before starting the next one.
68
75
  def execute(options = {}, &block)
76
+ args = options[:args] || []
69
77
  options[:num_threads] ||= DEFAULT_THREAD_POOL_SIZE
70
78
  if options[:serial]
71
- @connections.each_key { |host| @connections[host].self_eval &block }
79
+ @connections.each_key { |host| @connections[host].self_eval args, &block }
72
80
  elsif options[:batch_by]
73
81
  @connections.each_key.each_slice(options[:batch_by]) do |batch|
74
82
  Weave.with_thread_pool(batch, options[:num_threads]) do |host, mutex|
75
- @connections[host].self_eval mutex, &block
83
+ @connections[host].self_eval args, mutex, &block
76
84
  end
77
85
  end
78
86
  else
79
87
  Weave.with_thread_pool(@connections.keys, options[:num_threads]) do |host, mutex|
80
- @connections[host].self_eval mutex, &block
88
+ @connections[host].self_eval args, mutex, &block
81
89
  end
82
90
  end
83
91
  end
@@ -106,37 +114,84 @@ module Weave
106
114
  @mutex = NilMutex
107
115
  end
108
116
 
117
+ # A thread-safe wrapper around Kernel.puts.
118
+ def puts(*args) @mutex.synchronize { Kernel.puts(*args) } end
119
+
109
120
  # Run a command on this connection. This will open a connection if it's not already connected. The way the
110
121
  # output is presented is determined by the option `:output`. The default, `:output => :pretty`, prints
111
122
  # each line of output with the name of the host and whether the output is stderr or stdout. If `:output =>
112
123
  # :raw`, then the output will be passed as is directly back to `STDERR` or `STDOUT` as appropriate. If
113
- # `:output => :capture`, then this method returns the output in a hash of the form
114
- # `{ :stdout => [...], :stderr => [...] }`.
124
+ # `:output => :capture`, then this method puts the output into the result hash as
125
+ # `{ :stdout => "...", :stderr => "..." }`.
126
+ #
127
+ # The result of this method is a hash containing either `:exit_code` (if the command exited normally) or
128
+ # `:exit_signal` (if the command exited due to a signal). It also has `:stdout` and `:stderr` strings, if
129
+ # `option[:output]` was set to `:capture`.
130
+ #
131
+ # If the option `:continue_on_failure` is set to true, then this method will continue as normal if the
132
+ # command terminated via a signal or with a non-zero exit status. Otherwise (the default), these will
133
+ # cause a `Weave::Error` to be raised.
115
134
  #
116
135
  # @param [Hash] options the output options
117
136
  # @option options [Symbol] :output the output format
118
137
  def run(command, options = {})
119
138
  options[:output] ||= :pretty
120
139
  @connection ||= Net::SSH.start(@host, @user)
121
- output = { :stderr => [], :stdout => [] }
122
- @connection.exec(command) do |channel, stream, data|
123
- case options[:output]
124
- when :capture
125
- output[stream] << data
126
- when :raw
127
- out_stream = stream == :stdout ? STDOUT : STDERR
128
- out_stream.print data
129
- else
130
- stream_colored = case stream
131
- when :stdout then Weave.color_string("out", :green)
132
- when :stderr then Weave.color_string("err", :red)
133
- end
134
- lines = data.split("\n").map { |line| "[#{stream_colored}|#{host}] #{line}" }.join("\n")
135
- @mutex.synchronize { puts lines }
140
+ result = options[:output] == :capture ? { :stdout => "", :stderr => "" } : {}
141
+ @connection.open_channel do |channel|
142
+ channel.exec(command) do |_, success|
143
+ unless success
144
+ raise Error, "Could not run ssh command: #{command}"
145
+ end
146
+
147
+ channel.on_data do |_, data|
148
+ append_or_print_output(result, data, :stdout, options)
149
+ end
150
+
151
+ channel.on_extended_data do |_, type, data|
152
+ next unless type == 1
153
+ append_or_print_output(result, data, :stderr, options)
154
+ end
155
+
156
+ channel.on_request("exit-status") do |_, data|
157
+ code = data.read_long
158
+ unless code.zero? || options[:continue_on_failure]
159
+ raise Error, "[#{@host}] command finished with exit status #{code}: #{command}"
160
+ end
161
+ result[:exit_code] = code
162
+ end
163
+
164
+ channel.on_request("exit-signal") do |_, data|
165
+ signal = data.read_long
166
+ unless options[:continue_on_failure]
167
+ signal_name = Signal.list.invert[signal]
168
+ signal_message = signal_name ? "#{signal} (#{signal_name})" : "#{signal}"
169
+ raise Error, "[#{@host}] command received signal #{signal_message}: #{command}"
170
+ end
171
+ result[:exit_signal] = signal
172
+ end
136
173
  end
137
174
  end
138
- @connection.loop(0.1)
139
- (options[:output] == :capture) ? output : nil
175
+ @connection.loop(0.05)
176
+ result
177
+ end
178
+
179
+ # @private
180
+ def append_or_print_output(result, data, stream, options)
181
+ case options[:output]
182
+ when :capture
183
+ result[stream] << data
184
+ when :raw
185
+ out_stream = stream == :stdout ? STDOUT : STDERR
186
+ out_stream.print data
187
+ else
188
+ stream_colored = case stream
189
+ when :stdout then Weave.color_string("out", :green)
190
+ when :stderr then Weave.color_string("err", :red)
191
+ end
192
+ lines = data.split("\n").map { |line| "[#{stream_colored}|#{host}] #{line}" }.join("\n")
193
+ puts lines
194
+ end
140
195
  end
141
196
 
142
197
  # @private
@@ -149,9 +204,9 @@ module Weave
149
204
  end
150
205
 
151
206
  # @private
152
- def self_eval(mutex = nil, &block)
207
+ def self_eval(args, mutex = nil, &block)
153
208
  @mutex = mutex || NilMutex
154
- instance_eval &block
209
+ instance_exec(*args, &block)
155
210
  @mutex = NilMutex
156
211
  end
157
212
 
@@ -1,3 +1,3 @@
1
1
  module Weave
2
- VERSION = "0.0.2"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -6,6 +6,7 @@ require "weave"
6
6
  class SanityTest < Scope::TestCase
7
7
  TEST_HOSTS = [1, 2].map { |i| "weave#{i}" }
8
8
  ROOT_AT_TEST_HOSTS = TEST_HOSTS.map { |host| "root@#{host}" }
9
+ SINGLE_TEST_HOST = ["root@#{TEST_HOSTS[0]}"]
9
10
 
10
11
  setup_once do
11
12
  # Make sure the machines are up.
@@ -36,6 +37,18 @@ class SanityTest < Scope::TestCase
36
37
  end
37
38
  end
38
39
 
40
+ should "raise an exception when a command exits with non-zero exit status." do
41
+ assert_raises(Weave::Error) do
42
+ Weave.connect(SINGLE_TEST_HOST) { run("cd noexist", :output => :capture) }
43
+ end
44
+
45
+ results = {}
46
+ Weave.connect(SINGLE_TEST_HOST) do
47
+ results = run("exit 123", :output => :capture, :continue_on_failure => true)
48
+ end
49
+ assert_equal 123, results[:exit_code]
50
+ end
51
+
39
52
  context "in serial" do
40
53
  should "run some commands in the expected order" do
41
54
  output = []
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: weave
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-11-17 00:00:00.000000000 Z
12
+ date: 2013-02-10 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: net-ssh
16
- requirement: !ruby/object:Gem::Requirement
16
+ requirement: &70237392577060 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,15 +21,10 @@ dependencies:
21
21
  version: 2.2.0
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
- requirements:
27
- - - ! '>='
28
- - !ruby/object:Gem::Version
29
- version: 2.2.0
24
+ version_requirements: *70237392577060
30
25
  - !ruby/object:Gem::Dependency
31
26
  name: vagrant
32
- requirement: !ruby/object:Gem::Requirement
27
+ requirement: &70237392576560 !ruby/object:Gem::Requirement
33
28
  none: false
34
29
  requirements:
35
30
  - - ~>
@@ -37,15 +32,10 @@ dependencies:
37
32
  version: 1.0.5
38
33
  type: :development
39
34
  prerelease: false
40
- version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
- requirements:
43
- - - ~>
44
- - !ruby/object:Gem::Version
45
- version: 1.0.5
35
+ version_requirements: *70237392576560
46
36
  - !ruby/object:Gem::Dependency
47
37
  name: scope
48
- requirement: !ruby/object:Gem::Requirement
38
+ requirement: &70237392576180 !ruby/object:Gem::Requirement
49
39
  none: false
50
40
  requirements:
51
41
  - - ! '>='
@@ -53,15 +43,10 @@ dependencies:
53
43
  version: '0'
54
44
  type: :development
55
45
  prerelease: false
56
- version_requirements: !ruby/object:Gem::Requirement
57
- none: false
58
- requirements:
59
- - - ! '>='
60
- - !ruby/object:Gem::Version
61
- version: '0'
46
+ version_requirements: *70237392576180
62
47
  - !ruby/object:Gem::Dependency
63
48
  name: rake
64
- requirement: !ruby/object:Gem::Requirement
49
+ requirement: &70237392575720 !ruby/object:Gem::Requirement
65
50
  none: false
66
51
  requirements:
67
52
  - - ! '>='
@@ -69,15 +54,10 @@ dependencies:
69
54
  version: '0'
70
55
  type: :development
71
56
  prerelease: false
72
- version_requirements: !ruby/object:Gem::Requirement
73
- none: false
74
- requirements:
75
- - - ! '>='
76
- - !ruby/object:Gem::Version
77
- version: '0'
57
+ version_requirements: *70237392575720
78
58
  - !ruby/object:Gem::Dependency
79
59
  name: yard
80
- requirement: !ruby/object:Gem::Requirement
60
+ requirement: &70237392575300 !ruby/object:Gem::Requirement
81
61
  none: false
82
62
  requirements:
83
63
  - - ! '>='
@@ -85,15 +65,10 @@ dependencies:
85
65
  version: '0'
86
66
  type: :development
87
67
  prerelease: false
88
- version_requirements: !ruby/object:Gem::Requirement
89
- none: false
90
- requirements:
91
- - - ! '>='
92
- - !ruby/object:Gem::Version
93
- version: '0'
68
+ version_requirements: *70237392575300
94
69
  - !ruby/object:Gem::Dependency
95
70
  name: redcarpet
96
- requirement: !ruby/object:Gem::Requirement
71
+ requirement: &70237392574880 !ruby/object:Gem::Requirement
97
72
  none: false
98
73
  requirements:
99
74
  - - ! '>='
@@ -101,12 +76,7 @@ dependencies:
101
76
  version: '0'
102
77
  type: :development
103
78
  prerelease: false
104
- version_requirements: !ruby/object:Gem::Requirement
105
- none: false
106
- requirements:
107
- - - ! '>='
108
- - !ruby/object:Gem::Version
109
- version: '0'
79
+ version_requirements: *70237392574880
110
80
  description: Simple parallel ssh.
111
81
  email:
112
82
  - cespare@gmail.com
@@ -148,7 +118,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
148
118
  version: '0'
149
119
  requirements: []
150
120
  rubyforge_project:
151
- rubygems_version: 1.8.23
121
+ rubygems_version: 1.8.10
152
122
  signing_key:
153
123
  specification_version: 3
154
124
  summary: Simple parallel ssh.