weave 0.0.2 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/examples/parallel-console.rb +11 -3
- data/lib/weave.rb +79 -24
- data/lib/weave/version.rb +1 -1
- data/test/integrations/sanity_test.rb +13 -0
- metadata +15 -45
@@ -5,7 +5,7 @@ require "weave"
|
|
5
5
|
|
6
6
|
usage = <<-EOS
|
7
7
|
Usage:
|
8
|
-
$
|
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
|
-
|
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
|
-
|
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!
|
data/lib/weave.rb
CHANGED
@@ -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
|
114
|
-
# `{ :stdout =>
|
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
|
-
|
122
|
-
@connection.
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
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.
|
139
|
-
|
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
|
-
|
209
|
+
instance_exec(*args, &block)
|
155
210
|
@mutex = NilMutex
|
156
211
|
end
|
157
212
|
|
data/lib/weave/version.rb
CHANGED
@@ -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
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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.
|
121
|
+
rubygems_version: 1.8.10
|
152
122
|
signing_key:
|
153
123
|
specification_version: 3
|
154
124
|
summary: Simple parallel ssh.
|