blower 3.4 → 4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/blow +14 -5
- data/lib/blower.rb +7 -0
- data/lib/blower/context.rb +239 -184
- data/lib/blower/errors.rb +17 -0
- data/lib/blower/host.rb +62 -44
- data/lib/blower/logger.rb +68 -66
- data/lib/blower/util.rb +28 -0
- data/lib/blower/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 347f6e63c590bd170c7e942416850924fd0a10f9
|
4
|
+
data.tar.gz: 8389ddabdcc2e0bb4144806433b82b9a0a1f3eb0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5dfd5196f69a28330ab5b560c3c2a94379b3c6aabedabe81e17aacfd27ffec190bd62bedd33aaa0969f379f61a6c34324fac39903b52465aa43970e9be3ceb39
|
7
|
+
data.tar.gz: 9e0c0317df8fecb554f59a088d5d298ee4025678434f0feea924cb66d81f9f94b34b0c6775ba3371e4b29e2aa8e404af24b38a58059d1891c512fc5ac2d6d10a
|
data/bin/blow
CHANGED
@@ -3,7 +3,7 @@ require 'blower'
|
|
3
3
|
require 'pathname'
|
4
4
|
require 'optparse'
|
5
5
|
|
6
|
-
|
6
|
+
path = []
|
7
7
|
|
8
8
|
OptionParser.new do |opts|
|
9
9
|
opts.banner = "Usage: blow [options] task..."
|
@@ -11,18 +11,27 @@ OptionParser.new do |opts|
|
|
11
11
|
Dir.chdir v
|
12
12
|
end
|
13
13
|
opts.on "-l LEVEL", "Minimal log level" do |v|
|
14
|
-
|
14
|
+
Blower::Logger.level = v.downcase.to_sym
|
15
15
|
end
|
16
16
|
opts.on "-I DIR", "Add directory to search path" do |v|
|
17
|
-
|
17
|
+
path << v
|
18
18
|
end
|
19
19
|
opts.on "-v", "Show version and exit" do |v|
|
20
|
-
puts Blower::VERSION
|
20
|
+
puts "blow (blower) #{Blower::VERSION}"
|
21
21
|
exit
|
22
22
|
end
|
23
23
|
end.order!
|
24
24
|
|
25
|
-
|
25
|
+
$: << File.join(Dir.pwd, "lib")
|
26
|
+
|
27
|
+
path.unshift Dir.pwd
|
28
|
+
|
29
|
+
context = Blower::Context.new(path)
|
30
|
+
|
31
|
+
if File.directory?(File.join(Dir.pwd, "lib"))
|
32
|
+
context.path << File.join(Dir.pwd, "lib")
|
33
|
+
end
|
34
|
+
|
26
35
|
context.run "Blowfile", optional: true
|
27
36
|
begin
|
28
37
|
until ARGV.empty?
|
data/lib/blower.rb
CHANGED
data/lib/blower/context.rb
CHANGED
@@ -1,251 +1,306 @@
|
|
1
1
|
require 'erb'
|
2
|
+
require 'json'
|
2
3
|
require 'forwardable'
|
3
4
|
|
4
5
|
module Blower
|
5
6
|
|
6
7
|
# Blower tasks are executed within a context.
|
7
8
|
#
|
8
|
-
# The context can be used to share information between tasks
|
9
|
+
# The context can be used to share information between tasks by storing it in instance variables.
|
9
10
|
#
|
10
11
|
class Context
|
11
12
|
extend Forwardable
|
12
13
|
|
14
|
+
# YARD documentation macros
|
15
|
+
|
16
|
+
# @macro [new] onable
|
17
|
+
# @param [Array] on The hosts to operate on. Defaults to the context's current host list.
|
18
|
+
|
19
|
+
# @macro [new] asable
|
20
|
+
# @param [#to_s] as The remote user to operate as. Defaults to the context's current user if that is not nil, and the host's configured user otherwise.
|
21
|
+
|
22
|
+
# @macro [new] quietable
|
23
|
+
# @param [Boolean] quiet Whether to suppress log messages.
|
24
|
+
|
25
|
+
# @macro [new] onceable
|
26
|
+
# @param [String] once If non-nil, only perform the operation if it hasn't been done before as per #once.
|
27
|
+
|
13
28
|
# Search path for tasks.
|
14
29
|
attr_accessor :path
|
15
30
|
|
16
|
-
# The currently running task.
|
17
|
-
attr_accessor :task
|
18
|
-
|
19
31
|
# The target hosts.
|
20
32
|
attr_accessor :hosts
|
21
33
|
|
34
|
+
# Username override. If not-nil, this user is used for all remote accesses.
|
35
|
+
attr_accessor :user
|
36
|
+
|
37
|
+
# The file name of the currently running task.
|
38
|
+
# Context#cp interprets relative file names relative to this file name's directory component.
|
39
|
+
attr_accessor :file
|
40
|
+
|
41
|
+
# Create a new Context.
|
42
|
+
# @param [Array] path The search path for tasks.
|
22
43
|
def initialize (path)
|
23
44
|
@path = path
|
24
|
-
@hosts = []
|
25
|
-
@have_seen = {}
|
26
45
|
@data = {}
|
27
|
-
@
|
46
|
+
@hosts = []
|
28
47
|
end
|
29
48
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
block.()
|
36
|
-
ensure
|
37
|
-
@data = old_data
|
38
|
-
end
|
39
|
-
else
|
40
|
-
@data.merge! hash
|
41
|
-
hash.each do |var, val|
|
42
|
-
define_singleton_method(var) { @data[var] }
|
43
|
-
end
|
44
|
-
end
|
49
|
+
# Return a context variable.
|
50
|
+
# @param name The name of the variable.
|
51
|
+
# @param default The value to return if the variable is not set.
|
52
|
+
def get (name, default = nil)
|
53
|
+
@data.fetch(name, default)
|
45
54
|
end
|
46
55
|
|
47
|
-
|
48
|
-
|
56
|
+
# Remove context variables
|
57
|
+
# @param names The names to remove from the variables.
|
58
|
+
def unset (*names)
|
59
|
+
names.each do |name|
|
60
|
+
@data.delete name
|
61
|
+
end
|
49
62
|
end
|
50
63
|
|
51
|
-
|
52
|
-
|
64
|
+
# Merge the hash into the context variables.
|
65
|
+
# @param [Hash] hash The values to merge into the context variables.
|
66
|
+
def set (hash)
|
67
|
+
@data.merge! hash
|
53
68
|
end
|
54
69
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
70
|
+
# Yield with the hash temporary merged into the context variables.
|
71
|
+
# Only variables specifically named in +hash+ will be reset when the yield returns.
|
72
|
+
# @param [Hash] hash The values to merge into the context variables.
|
73
|
+
# @return Whatever the yielded-to block returns.
|
74
|
+
# @macro quietable
|
75
|
+
def with (hash, quiet: false)
|
76
|
+
old_values = data.values_at(hash.keys)
|
77
|
+
log.debug "with #{hash}", quiet: quiet do
|
78
|
+
set hash
|
79
|
+
yield
|
80
|
+
end
|
81
|
+
ensure
|
82
|
+
hash.keys.each.with_index do |key, i|
|
83
|
+
@data[key] = old_values[i]
|
84
|
+
end
|
59
85
|
end
|
60
86
|
|
61
|
-
#
|
62
|
-
|
63
|
-
|
64
|
-
hosts.
|
87
|
+
# Yield with a temporary host list
|
88
|
+
# @macro quietable
|
89
|
+
def on (*hosts, quiet: false)
|
90
|
+
let :@hosts => hosts.flatten do
|
91
|
+
log.info "on #{@hosts.map(&:name).join(", ")}", quiet: quiet do
|
92
|
+
yield
|
93
|
+
end
|
94
|
+
end
|
65
95
|
end
|
66
96
|
|
67
|
-
#
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
97
|
+
# Yield with a temporary username override
|
98
|
+
# @macro quietable
|
99
|
+
def as (user, quiet: false)
|
100
|
+
let :@user => user do
|
101
|
+
log.info "as #{user}", quiet: quiet do
|
102
|
+
yield
|
103
|
+
end
|
72
104
|
end
|
73
|
-
ctx.instance_exec(&block)
|
74
105
|
end
|
75
106
|
|
76
|
-
|
77
|
-
|
78
|
-
|
107
|
+
# Find and execute a task. For each entry in the search path, it checks for +path/task.rb+, +path/task/blow.rb+,
|
108
|
+
# and finally for bare +path/task+. The search stops at the first match.
|
109
|
+
# If found, the task is executed within the context, with +@file+ bound to the task's file name.
|
110
|
+
# @param [String] task The name of the task.
|
111
|
+
# @macro quietable
|
112
|
+
# @macro onceable
|
113
|
+
# @raise [TaskNotFound] If no task is found.
|
114
|
+
# @raise Whatever the task itself raises.
|
115
|
+
# @return The result of evaluating the task file.
|
116
|
+
def run (task, optional: false, quiet: false, once: nil)
|
117
|
+
once once, quiet: quiet do
|
118
|
+
log.info "run #{task}", quiet: quiet do
|
119
|
+
file = find_task(task)
|
120
|
+
code = File.read(file)
|
121
|
+
let :@file => file do
|
122
|
+
instance_eval(code, file)
|
123
|
+
end
|
124
|
+
end
|
79
125
|
end
|
126
|
+
rescue TaskNotFound => e
|
127
|
+
return if optional
|
128
|
+
raise e
|
80
129
|
end
|
81
130
|
|
82
|
-
# Execute
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
131
|
+
# Execute a shell command on each host
|
132
|
+
# @macro onable
|
133
|
+
# @macro asable
|
134
|
+
# @macro quietable
|
135
|
+
# @macro onceable
|
136
|
+
def sh (command, as: user, on: hosts, quiet: false, once: nil)
|
137
|
+
self.once once, quiet: quiet do
|
138
|
+
log.info "sh #{command}", quiet: quiet do
|
139
|
+
hash_map(hosts) do |host|
|
140
|
+
host.sh command, as: as, quiet: quiet
|
90
141
|
end
|
91
142
|
end
|
92
|
-
threads.each(&:join)
|
93
143
|
end
|
94
|
-
result
|
95
144
|
end
|
96
145
|
|
97
|
-
#
|
98
|
-
# @
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
log.info "sh: #{command}" unless quiet
|
121
|
-
if f = get(:sh_command)
|
122
|
-
command = f.(command)
|
123
|
-
end
|
124
|
-
win = true
|
125
|
-
hashmap do |host|
|
126
|
-
out = ""
|
127
|
-
status = host.sh(command, out, user: @user)
|
128
|
-
if status != 0
|
129
|
-
fail host, "#{command}: exit status #{status}"
|
130
|
-
nil
|
131
|
-
else
|
132
|
-
out
|
146
|
+
# Copy a file or readable to the host filesystems.
|
147
|
+
# @overload cp(readable, to, as: user, on: hosts, quiet: false)
|
148
|
+
# @param [#read] from An object from which to read the contents of the new file.
|
149
|
+
# @param [String] to The file name to write the string to.
|
150
|
+
# @macro onable
|
151
|
+
# @macro asable
|
152
|
+
# @macro quietable
|
153
|
+
# @macro onceable
|
154
|
+
# @overload cp(filename, to, as: user, on: hosts, quiet: false)
|
155
|
+
# @param [String] from The name of the local file to copy.
|
156
|
+
# @param [String] to The file name to write the string to.
|
157
|
+
# @macro onable
|
158
|
+
# @macro asable
|
159
|
+
# @macro quietable
|
160
|
+
# @macro onceable
|
161
|
+
def cp (from, to, as: user, on: hosts, quiet: false, once: nil)
|
162
|
+
self.once once, quiet: quiet do
|
163
|
+
log.info "cp: #{from} -> #{to}", quiet: quiet do
|
164
|
+
Dir.chdir File.dirname(file) do
|
165
|
+
hash_map(hosts) do |host|
|
166
|
+
host.cp from, to, as: as, quiet: quiet
|
167
|
+
end
|
168
|
+
end
|
133
169
|
end
|
134
170
|
end
|
135
171
|
end
|
136
172
|
|
137
|
-
#
|
138
|
-
# @
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
173
|
+
# Reads a remote file from each host.
|
174
|
+
# @param [String] filename The file to read.
|
175
|
+
# @return [Hash] A hash of +Host+ objects to +Strings+ of the file contents.
|
176
|
+
# @macro onable
|
177
|
+
# @macro asable
|
178
|
+
# @macro quietable
|
179
|
+
def read (filename, as: user, on: hosts, quiet: false)
|
180
|
+
log.info "read: #{filename}", quiet: quiet do
|
181
|
+
hash_map(hosts) do |host|
|
182
|
+
host.read filename, as: as
|
183
|
+
end
|
146
184
|
end
|
147
185
|
end
|
148
186
|
|
149
|
-
#
|
150
|
-
# @
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
187
|
+
# Writes a string to a file on the host filesystems.
|
188
|
+
# @param [String] string The string to write.
|
189
|
+
# @param [String] to The file name to write the string to.
|
190
|
+
# @macro onable
|
191
|
+
# @macro asable
|
192
|
+
# @macro quietable
|
193
|
+
# @macro onceable
|
194
|
+
def write (string, to, as: user, on: hosts, quiet: false, once: nil)
|
195
|
+
self.once once, quiet: quiet do
|
196
|
+
log.info "write: #{string.bytesize} bytes -> #{to}", quiet: quiet do
|
197
|
+
hash_map(hosts) do |host|
|
198
|
+
host.write string, to, as: as, quiet: quiet
|
199
|
+
end
|
200
|
+
end
|
156
201
|
end
|
157
|
-
win
|
158
202
|
end
|
159
203
|
|
160
|
-
|
161
|
-
|
204
|
+
# Renders and installs files from ERB templates. Files are under +from+ in ERB format. +from/foo/bar.conf.erb+ is
|
205
|
+
# rendered and written to +to/foo/bar.conf+. Non-ERB files are ignored.
|
206
|
+
# @param [String] from The directory to search for .erb files.
|
207
|
+
# @param [String] to The remote directory to put files in.
|
208
|
+
# @macro onable
|
209
|
+
# @macro asable
|
210
|
+
# @macro quietable
|
211
|
+
# @macro onceable
|
212
|
+
def render (from, to, as: user, on: hosts, quiet: false, once: nil)
|
213
|
+
self.once once, quiet: quiet do
|
214
|
+
Dir.chdir File.dirname(file) do
|
215
|
+
(Dir["#{from}**/*.erb"] + Dir["#{from}**/.*.erb"]).each do |path|
|
216
|
+
template = ERB.new(File.read(path))
|
217
|
+
to_path = to + path[from.length..-5]
|
218
|
+
log.info "render: #{path} -> #{to_path}", quiet: quiet do
|
219
|
+
hash_map(hosts) do |host|
|
220
|
+
host.cp StringIO.new(template.result(binding)), to_path, as: as, quiet: quiet
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
162
226
|
end
|
163
227
|
|
164
|
-
|
165
|
-
|
228
|
+
# Ping each host by trying to connect to port 22
|
229
|
+
# @macro onable
|
230
|
+
# @macro quietable
|
231
|
+
def ping (on: hosts, quiet: false)
|
232
|
+
log.info "ping", quiet: quiet do
|
233
|
+
hash_map(hosts) do |host|
|
234
|
+
host.ping
|
235
|
+
end
|
236
|
+
end
|
166
237
|
end
|
167
238
|
|
168
|
-
#
|
169
|
-
#
|
170
|
-
#
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
239
|
+
# Execute a block only once per host.
|
240
|
+
# It is usually preferable to make tasks idempotent, but when that isn't
|
241
|
+
# possible, +once+ will only execute the block on hosts where a block
|
242
|
+
# with the same key hasn't previously been successfully executed.
|
243
|
+
# @param [String] key Uniquely identifies the block.
|
244
|
+
# @param [String] store File to store +once+'s state in.
|
245
|
+
# @macro quietable
|
246
|
+
def once (key, store: "/var/cache/blower.json", quiet: false)
|
247
|
+
return yield unless key
|
248
|
+
log.info "once: #{key}", quiet: quiet do
|
249
|
+
hash_map(hosts) do |host|
|
250
|
+
done = begin
|
251
|
+
JSON.parse(host.read(store, quiet: true))
|
252
|
+
rescue => e
|
253
|
+
{}
|
254
|
+
end
|
255
|
+
unless done[key]
|
256
|
+
on [host] do
|
257
|
+
yield
|
258
|
+
end
|
259
|
+
done[key] = true
|
260
|
+
host.write(done.to_json, store, quiet: true)
|
261
|
+
end
|
262
|
+
end
|
176
263
|
end
|
177
264
|
end
|
178
265
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
# Writes a string to a file on the host filesystem.
|
187
|
-
# @param string The string to write.
|
188
|
-
# @param to A string.
|
189
|
-
def write (string, to)
|
190
|
-
log.info "write: -> #{to}"
|
191
|
-
each do |host|
|
192
|
-
host.cp(StringIO.new(string), to, user: @user, sh_command: get(:sh_command))
|
193
|
-
end
|
194
|
-
end
|
195
|
-
|
196
|
-
def render (from, to)
|
197
|
-
from = rebase_path(from)
|
198
|
-
(Dir["#{from}**/*.erb"] + Dir["#{from}**/.*.erb"]).each do |path|
|
199
|
-
template = ERB.new(File.read(path))
|
200
|
-
to_path = to + path[from.length..-5]
|
201
|
-
log.info "render: #{path} -> #{to_path}"
|
202
|
-
each do |host|
|
203
|
-
host.cp StringIO.new(template.result(binding)), to_path, user: @user, sh_command: get(:sh_command)
|
204
|
-
end
|
205
|
-
end
|
206
|
-
end
|
207
|
-
|
208
|
-
# Run a task.
|
209
|
-
# @param task (String) The name of the task
|
210
|
-
def run (task, optional: false)
|
211
|
-
files = []
|
212
|
-
@path.each do |dir|
|
213
|
-
name = File.join(dir, task)
|
214
|
-
name += ".rb" unless File.exist?(name)
|
215
|
-
if File.directory?(name)
|
216
|
-
dirtask = File.join(name, @basename)
|
217
|
-
dirtask += ".rb" unless File.exist?(dirtask)
|
218
|
-
name = dirtask
|
219
|
-
blowfile = File.join(name, "Blowfile")
|
220
|
-
files << blowfile if File.exist?(blowfile) && !@have_seen[blowfile]
|
266
|
+
private
|
267
|
+
|
268
|
+
def hash_map (hosts = self.hosts)
|
269
|
+
{}.tap do |result|
|
270
|
+
each(hosts) do |host|
|
271
|
+
result[host] = yield(host)
|
221
272
|
end
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def each (hosts = self.hosts)
|
277
|
+
fail "No hosts" if hosts.empty?
|
278
|
+
[hosts].flatten.each do |host|
|
279
|
+
begin
|
280
|
+
yield host
|
281
|
+
rescue => e
|
282
|
+
host.log.error e.message
|
283
|
+
hosts.delete host
|
230
284
|
end
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
285
|
+
end
|
286
|
+
fail "No hosts remaining" if hosts.empty?
|
287
|
+
end
|
288
|
+
|
289
|
+
def find_task (name)
|
290
|
+
log.debug "Searching for task #{name}" do
|
291
|
+
path.each do |path|
|
292
|
+
log.trace "checking #{File.join(path, name + ".rb")}"
|
293
|
+
file = File.join(path, name + ".rb")
|
294
|
+
return file if File.exists?(file)
|
295
|
+
log.trace "checking #{File.join(path, name, "/blow.rb")}"
|
296
|
+
file = File.join(path, name, "/blow.rb")
|
297
|
+
return file if File.exists?(file)
|
298
|
+
log.trace "checking #{File.join(path, name)}"
|
299
|
+
file = File.join(path, name)
|
300
|
+
return file if File.exists?(file)
|
247
301
|
end
|
248
302
|
end
|
303
|
+
fail TaskNotFound, "Task not found: #{name}"
|
249
304
|
end
|
250
305
|
|
251
306
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
|
2
|
+
module Blower
|
3
|
+
|
4
|
+
# Raised when a task isn't found.
|
5
|
+
class TaskNotFound < RuntimeError; end
|
6
|
+
|
7
|
+
# Raised when a command returns a non-zero exit status.
|
8
|
+
class FailedCommand < RuntimeError; end
|
9
|
+
|
10
|
+
class ExecuteError < RuntimeError
|
11
|
+
attr_accessor :status
|
12
|
+
def initialize (status)
|
13
|
+
@status = status
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
data/lib/blower/host.rb
CHANGED
@@ -10,46 +10,56 @@ module Blower
|
|
10
10
|
include MonitorMixin
|
11
11
|
extend Forwardable
|
12
12
|
|
13
|
-
|
13
|
+
# The default remote user.
|
14
14
|
attr_accessor :user
|
15
|
+
|
16
|
+
# The host adress.
|
15
17
|
attr_accessor :address
|
16
18
|
|
17
19
|
def_delegators :data, :[], :[]=
|
18
20
|
|
19
|
-
|
20
|
-
|
21
|
-
def initialize (status)
|
22
|
-
@status = status
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
def initialize (name, data: {}, user: "root")
|
27
|
-
@address = @name = name
|
21
|
+
def initialize (address, data: {}, user: "root")
|
22
|
+
@address = address
|
28
23
|
@user = user
|
29
24
|
@data = data
|
30
25
|
super()
|
31
26
|
end
|
32
27
|
|
33
|
-
def
|
28
|
+
def name
|
29
|
+
to_s
|
30
|
+
end
|
31
|
+
|
32
|
+
# Represent the host as a string.
|
33
|
+
def to_s
|
34
|
+
"#{@user}@#{@address}"
|
35
|
+
end
|
36
|
+
|
37
|
+
# Attempt to connect to port 22 on the host.
|
38
|
+
# @return +true+
|
39
|
+
# @raise If it doesn't connect within 1 second.
|
40
|
+
# @api private
|
41
|
+
def ping ()
|
42
|
+
log.debug "Pinging"
|
34
43
|
Timeout.timeout(1) do
|
35
44
|
TCPSocket.new(address, 22).close
|
36
45
|
end
|
37
46
|
true
|
38
47
|
rescue Timeout::Error, Errno::ECONNREFUSED
|
39
|
-
|
40
|
-
retries -= 1
|
41
|
-
retry
|
48
|
+
fail "Failed to ping #{self}"
|
42
49
|
end
|
43
50
|
|
44
|
-
|
51
|
+
# Copy files or directories to the host.
|
52
|
+
# @api private
|
53
|
+
def cp (froms, to, as: nil, quiet: false)
|
54
|
+
as ||= @user
|
55
|
+
output = ""
|
45
56
|
synchronize do
|
46
57
|
[froms].flatten.each do |from|
|
47
58
|
if from.is_a?(String)
|
48
59
|
to += "/" if to[-1] != "/" && from.is_a?(Array)
|
49
60
|
command = ["rsync", "-e", "ssh -oStrictHostKeyChecking=no", "-r"]
|
50
|
-
command += ["
|
51
|
-
command
|
52
|
-
log.trace command.shelljoin
|
61
|
+
command += [*from, "#{as}@#{@address}:#{to}"]
|
62
|
+
log.trace command.shelljoin, quiet: quiet
|
53
63
|
IO.popen(command, in: :close, err: %i(child out)) do |io|
|
54
64
|
until io.eof?
|
55
65
|
begin
|
@@ -61,15 +71,14 @@ module Blower
|
|
61
71
|
end
|
62
72
|
io.close
|
63
73
|
if !$?.success?
|
64
|
-
log.fatal "exit status #{$?.exitstatus}: #{command}"
|
65
|
-
log.fatal output
|
74
|
+
log.fatal "exit status #{$?.exitstatus}: #{command}", quiet: quiet
|
75
|
+
log.fatal output, quiet: quiet
|
66
76
|
fail "failed to copy files"
|
67
77
|
end
|
68
78
|
end
|
69
79
|
elsif from.respond_to?(:read)
|
70
80
|
cmd = "echo #{from.read.shellescape} > #{to.shellescape}"
|
71
|
-
|
72
|
-
sh cmd
|
81
|
+
sh cmd, quiet: quiet
|
73
82
|
else
|
74
83
|
fail "Don't know how to copy a #{from.class}: #{from}"
|
75
84
|
end
|
@@ -78,61 +87,70 @@ module Blower
|
|
78
87
|
true
|
79
88
|
end
|
80
89
|
|
81
|
-
|
90
|
+
# Write a string to a host file.
|
91
|
+
# @api private
|
92
|
+
def write (string, to, as: nil, quiet: false)
|
93
|
+
cp StringIO.new(string), to, as: as, quiet: quiet
|
94
|
+
end
|
95
|
+
|
96
|
+
# Read a host file.
|
97
|
+
# @api private
|
98
|
+
def read (filename, as: nil, quiet: false)
|
99
|
+
sh "cat #{filename.shellescape}", as: as, quiet: quiet
|
100
|
+
end
|
101
|
+
|
102
|
+
# Execute a command on the host and return its output.
|
103
|
+
# @api private
|
104
|
+
def sh (command, as: nil, quiet: false)
|
105
|
+
as ||= @user
|
106
|
+
output = ""
|
82
107
|
synchronize do
|
83
|
-
log.debug command
|
108
|
+
log.debug "sh #{command}", quiet: quiet
|
84
109
|
result = nil
|
85
|
-
ch = ssh(
|
110
|
+
ch = ssh(as).open_channel do |ch|
|
86
111
|
ch.request_pty do |ch, success|
|
87
112
|
"failed to acquire pty" unless success unless success
|
88
113
|
ch.exec(command) do |_, success|
|
89
114
|
fail "failed to execute command" unless success
|
90
115
|
ch.on_data do |_, data|
|
91
|
-
log.trace "received #{data.bytesize} bytes stdout"
|
116
|
+
log.trace "received #{data.bytesize} bytes stdout", quiet: quiet
|
92
117
|
output << data
|
93
118
|
end
|
94
119
|
ch.on_extended_data do |_, _, data|
|
95
|
-
log.trace "received #{data.bytesize} bytes stderr"
|
120
|
+
log.trace "received #{data.bytesize} bytes stderr", quiet: quiet
|
96
121
|
output << data.colorize(:red)
|
97
122
|
end
|
98
123
|
ch.on_request("exit-status") do |_, data|
|
99
124
|
result = data.read_long
|
100
|
-
log.trace "received exit-status #{result}"
|
125
|
+
log.trace "received exit-status #{result}", quiet: quiet
|
101
126
|
end
|
102
127
|
end
|
103
128
|
end
|
104
129
|
end
|
105
130
|
ch.wait
|
106
|
-
if result != 0
|
107
|
-
|
108
|
-
log.fatal output
|
109
|
-
end
|
110
|
-
result
|
131
|
+
fail FailedCommand, output if result != 0
|
132
|
+
output
|
111
133
|
end
|
112
134
|
end
|
113
135
|
|
114
|
-
#
|
115
|
-
#
|
116
|
-
|
117
|
-
|
118
|
-
|
136
|
+
# Produce a Logger prefixed with the host name.
|
137
|
+
# @api private
|
138
|
+
def log
|
139
|
+
@log ||= Logger.instance.with_prefix("on #{self}: ")
|
140
|
+
end
|
119
141
|
|
120
142
|
private
|
121
143
|
|
122
144
|
attr_accessor :data
|
123
145
|
|
124
|
-
def log
|
125
|
-
Logger.instance.with_prefix("(on #{name})")
|
126
|
-
end
|
127
|
-
|
128
146
|
def ssh (user)
|
129
147
|
@sessions ||= {}
|
130
148
|
if @sessions[user] && @sessions[user].closed?
|
131
|
-
log.
|
149
|
+
log.warn "Discovered the connection to ssh:#{self} was lost"
|
132
150
|
@sessions[user] = nil
|
133
151
|
end
|
134
152
|
@sessions[user] ||= begin
|
135
|
-
log.
|
153
|
+
log.debug "Connecting to ssh:#{self}"
|
136
154
|
Net::SSH.start(address, user)
|
137
155
|
end
|
138
156
|
end
|
data/lib/blower/logger.rb
CHANGED
@@ -12,92 +12,94 @@ module Blower
|
|
12
12
|
include MonitorMixin
|
13
13
|
include Singleton
|
14
14
|
|
15
|
+
# Logging levels in ascending order of severity.
|
16
|
+
LEVELS = %i(all trace debug info warn error fatal none)
|
17
|
+
|
18
|
+
# Colorize specifications for log levels.
|
15
19
|
COLORS = {
|
16
|
-
trace: {color: :light_black},
|
17
|
-
debug: {color: :default},
|
18
|
-
info: {color: :blue},
|
19
|
-
warn: {color: :yellow},
|
20
|
-
error: {color: :red},
|
21
|
-
fatal: {color: :light_white, background: :red},
|
20
|
+
trace: { color: :light_black },
|
21
|
+
debug: { color: :default },
|
22
|
+
info: { color: :blue },
|
23
|
+
warn: { color: :yellow },
|
24
|
+
error: { color: :red },
|
25
|
+
fatal: { color: :light_white, background: :red },
|
22
26
|
}
|
23
27
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
}
|
28
|
+
class << self
|
29
|
+
# The minimum severity level for which messages will be displayed.
|
30
|
+
attr_accessor :level
|
31
|
+
|
32
|
+
# The current indentation level.
|
33
|
+
attr_accessor :indent
|
34
|
+
end
|
35
|
+
|
36
|
+
self.indent = 0
|
34
37
|
|
35
|
-
|
38
|
+
self.level = :info
|
36
39
|
|
37
|
-
def initialize (prefix =
|
40
|
+
def initialize (prefix = "")
|
38
41
|
@prefix = prefix
|
39
42
|
super()
|
40
43
|
end
|
41
44
|
|
45
|
+
# Return a logger with the specified prefix
|
42
46
|
def with_prefix (string)
|
43
|
-
|
47
|
+
Logger.send(:new, string)
|
44
48
|
end
|
45
49
|
|
46
|
-
#
|
47
|
-
def
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
# Log a info level event
|
53
|
-
def info (a=nil, *b, &c); log(a, :info, *b, &c); end
|
54
|
-
|
55
|
-
# Log a warn level event
|
56
|
-
def warn (a=nil, *b, &c); log(a, :warn, *b, &c); end
|
57
|
-
|
58
|
-
# Log a error level event
|
59
|
-
def error (a=nil, *b, &c); log(a, :error, *b, &c); end
|
60
|
-
|
61
|
-
# Log a fatal level event
|
62
|
-
def fatal (a=nil, *b, &c); log(a, :fatal, *b, &c); end
|
63
|
-
|
64
|
-
def nest (&c)
|
65
|
-
log(nil, nil, &c)
|
50
|
+
# Yield with a temporarily incremented indent counter
|
51
|
+
def with_indent ()
|
52
|
+
Logger.indent += 1
|
53
|
+
yield
|
54
|
+
ensure
|
55
|
+
Logger.indent -= 1
|
66
56
|
end
|
67
57
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
58
|
+
# Display a log message. The block, if specified, is executed in an indented region after the log message is shown.
|
59
|
+
# @api private
|
60
|
+
# @param [Symbol] level the severity level
|
61
|
+
# @param [#to_s] message the message to display
|
62
|
+
# @param block a block to execute with an indent after the message is displayed
|
63
|
+
# @return the value of block, or nil
|
64
|
+
def log (level, message, quiet: false, &block)
|
65
|
+
if !quiet && (LEVELS.index(level) >= LEVELS.index(Logger.level))
|
66
|
+
synchronize do
|
67
|
+
message = message.to_s.colorize(COLORS[level]) if level
|
68
|
+
message.split("\n").each do |line|
|
69
|
+
STDERR.puts " " * Logger.indent + @prefix + line
|
70
|
+
end
|
71
|
+
end
|
72
|
+
with_indent(&block) if block
|
73
|
+
elsif block
|
74
|
+
block.()
|
75
75
|
end
|
76
|
-
puts "OK".colorize({color: :green})
|
77
|
-
rescue => e
|
78
|
-
puts "ERROR".colorize({color: :red})
|
79
|
-
raise e
|
80
76
|
end
|
81
77
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
block.() if block
|
93
|
-
ensure
|
94
|
-
@@indent -= 1
|
95
|
-
end
|
96
|
-
else
|
97
|
-
block.() if block
|
78
|
+
# Define a helper method for a given severity level.
|
79
|
+
# @!macro [attach] log_helper
|
80
|
+
# @!method $1(message, &block)
|
81
|
+
# Display a $1 log message, as if by calling log directly.
|
82
|
+
# @param [#to_s] message the message to display
|
83
|
+
# @param block a block to execute with an indent after the message is displayed
|
84
|
+
# @return the value of block, or nil
|
85
|
+
def self.define_helper (level)
|
86
|
+
define_method(level) do |*args, **kwargs, &block|
|
87
|
+
log(level, *args, **kwargs, &block)
|
98
88
|
end
|
99
89
|
end
|
100
90
|
|
91
|
+
define_helper :trace
|
92
|
+
define_helper :debug
|
93
|
+
define_helper :info
|
94
|
+
define_helper :warn
|
95
|
+
define_helper :error
|
96
|
+
define_helper :fatal
|
97
|
+
|
101
98
|
end
|
102
99
|
|
103
100
|
end
|
101
|
+
|
102
|
+
# Return the logger instance.
|
103
|
+
def log
|
104
|
+
Blower::Logger.instance
|
105
|
+
end
|
data/lib/blower/util.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
|
2
|
+
class Object
|
3
|
+
|
4
|
+
# Yield with temporary instance variable assignments.
|
5
|
+
# @param [Hash] bindings A hash of instance variable names to their temporary values.
|
6
|
+
# @return Whatever yielding returns
|
7
|
+
# @example Time
|
8
|
+
# @foo = 1
|
9
|
+
# let :@foo => 2 do
|
10
|
+
# puts @foo #=> outputs 2
|
11
|
+
# end
|
12
|
+
# puts @foo #=> outputs 1
|
13
|
+
def let (bindings)
|
14
|
+
old_values = bindings.keys.map do |key|
|
15
|
+
instance_variable_get(key)
|
16
|
+
end
|
17
|
+
bindings.each do |key, value|
|
18
|
+
instance_variable_set(key, value)
|
19
|
+
end
|
20
|
+
yield
|
21
|
+
ensure
|
22
|
+
return unless old_values
|
23
|
+
bindings.keys.each.with_index do |key, i|
|
24
|
+
instance_variable_set(key, old_values[i])
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
data/lib/blower/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: blower
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '
|
4
|
+
version: '4.0'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nathan Baum
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-06-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: net-ssh
|
@@ -76,8 +76,10 @@ files:
|
|
76
76
|
- bin/blow
|
77
77
|
- lib/blower.rb
|
78
78
|
- lib/blower/context.rb
|
79
|
+
- lib/blower/errors.rb
|
79
80
|
- lib/blower/host.rb
|
80
81
|
- lib/blower/logger.rb
|
82
|
+
- lib/blower/util.rb
|
81
83
|
- lib/blower/version.rb
|
82
84
|
homepage: http://www.github.org/nbaum/blower
|
83
85
|
licenses:
|