blower 3.4 → 4.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.
- 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:
|