weave 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.