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.
- 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.
|