blower 4.7 → 6
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 +4 -0
- data/lib/blower.rb +2 -0
- data/lib/blower/context.rb +84 -25
- data/lib/blower/host.rb +3 -2
- data/lib/blower/local.rb +48 -0
- data/lib/blower/logger.rb +10 -5
- data/lib/blower/target.rb +106 -0
- data/lib/blower/util.rb +6 -0
- data/lib/blower/version.rb +1 -1
- metadata +36 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b43c163f31a76c68c8327d1c696bda453f2c6fefcfc673fdcf338f8c840e3a6c
|
4
|
+
data.tar.gz: e33fde4f43223527937dd57d11da5609bb2f124a66dc21dfa71d2f79a119a765
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 11ee37db482dad0ea13f9cdf42cb29007d6018a995f404bc06fb27f971ba3c6c2073f0a44165d0790c75f8cd931d37cd197ffda7d59e2f43368d9ebad14957a1
|
7
|
+
data.tar.gz: '0860054ed01d01203c7a616ae03fbc0653e0e3519e3516786530a88163207aef206f3da6e4be1ddcf78642314ddbde62d4cd427c1aecbfa64e0b73dc9f7465a9'
|
data/bin/blow
CHANGED
@@ -2,6 +2,9 @@
|
|
2
2
|
require 'blower'
|
3
3
|
require 'pathname'
|
4
4
|
require 'optparse'
|
5
|
+
require 'dotenv'
|
6
|
+
|
7
|
+
Dotenv.load
|
5
8
|
|
6
9
|
path = []
|
7
10
|
|
@@ -30,6 +33,7 @@ if File.directory?(File.join(Dir.pwd, "lib"))
|
|
30
33
|
end
|
31
34
|
|
32
35
|
context.run "Blowfile", optional: true
|
36
|
+
|
33
37
|
begin
|
34
38
|
until ARGV.empty?
|
35
39
|
context.run ARGV.shift
|
data/lib/blower.rb
CHANGED
data/lib/blower/context.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'colorize'
|
1
2
|
require 'find'
|
2
3
|
require 'erb'
|
3
4
|
require 'json'
|
@@ -32,6 +33,9 @@ module Blower
|
|
32
33
|
# The target hosts.
|
33
34
|
attr_accessor :hosts
|
34
35
|
|
36
|
+
# The failed hosts.
|
37
|
+
attr_accessor :failures
|
38
|
+
|
35
39
|
# Username override. If not-nil, this user is used for all remote accesses.
|
36
40
|
attr_accessor :user
|
37
41
|
|
@@ -45,6 +49,7 @@ module Blower
|
|
45
49
|
@path = path
|
46
50
|
@data = {}
|
47
51
|
@hosts = []
|
52
|
+
@failures = []
|
48
53
|
end
|
49
54
|
|
50
55
|
# Return a context variable.
|
@@ -90,7 +95,7 @@ module Blower
|
|
90
95
|
def on (*hosts, quiet: false)
|
91
96
|
let :@hosts => hosts.flatten do
|
92
97
|
log.info "on #{@hosts.map(&:name).join(", ")}", quiet: quiet do
|
93
|
-
yield
|
98
|
+
yield *hosts
|
94
99
|
end
|
95
100
|
end
|
96
101
|
end
|
@@ -115,12 +120,20 @@ module Blower
|
|
115
120
|
# @raise Whatever the task itself raises.
|
116
121
|
# @return The result of evaluating the task file.
|
117
122
|
def run (task, optional: false, quiet: false, once: nil)
|
123
|
+
@run_cache ||= {}
|
118
124
|
once once, quiet: quiet do
|
119
125
|
log.info "run #{task}", quiet: quiet do
|
120
126
|
file = find_task(task)
|
121
|
-
|
122
|
-
|
123
|
-
|
127
|
+
if @run_cache.has_key? file
|
128
|
+
log.info "*cached*"
|
129
|
+
@run_cache[file]
|
130
|
+
else
|
131
|
+
@run_cache[file] = begin
|
132
|
+
code = File.read(file)
|
133
|
+
let :@file => file do
|
134
|
+
instance_eval(code, file)
|
135
|
+
end
|
136
|
+
end
|
124
137
|
end
|
125
138
|
end
|
126
139
|
end
|
@@ -129,6 +142,12 @@ module Blower
|
|
129
142
|
raise e
|
130
143
|
end
|
131
144
|
|
145
|
+
def sh? (command, as: user, on: hosts, quiet: false, once: nil)
|
146
|
+
sh command, as: as, on: on, quiet: quiet, once: once
|
147
|
+
rescue
|
148
|
+
nil
|
149
|
+
end
|
150
|
+
|
132
151
|
# Execute a shell command on each host
|
133
152
|
# @macro onable
|
134
153
|
# @macro asable
|
@@ -136,7 +155,7 @@ module Blower
|
|
136
155
|
# @macro onceable
|
137
156
|
def sh (command, as: user, on: hosts, quiet: false, once: nil)
|
138
157
|
self.once once, quiet: quiet do
|
139
|
-
log.info "sh #{command}", quiet: quiet do
|
158
|
+
log.info "sh: #{command}", quiet: quiet do
|
140
159
|
hash_map(hosts) do |host|
|
141
160
|
host.sh command, as: as, quiet: quiet
|
142
161
|
end
|
@@ -282,7 +301,7 @@ module Blower
|
|
282
301
|
|
283
302
|
def to_s
|
284
303
|
map do |host, data|
|
285
|
-
"#{host.name.blue}
|
304
|
+
"#{host.name.blue}\n" + data.strip.to_s.gsub(/^/, " ")
|
286
305
|
end.join("\n")
|
287
306
|
end
|
288
307
|
|
@@ -302,40 +321,80 @@ module Blower
|
|
302
321
|
end
|
303
322
|
|
304
323
|
def hash_map (hosts = self.hosts)
|
305
|
-
HostHash.new.tap do |result|
|
306
|
-
each(hosts) do |host|
|
307
|
-
result[host] = yield(host)
|
324
|
+
hh = HostHash.new.tap do |result|
|
325
|
+
each(hosts) do |host, i|
|
326
|
+
result[host] = yield(host, i)
|
327
|
+
end
|
328
|
+
end
|
329
|
+
if @singular
|
330
|
+
hh.values.first
|
331
|
+
else
|
332
|
+
hh
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
def singularly (flag = true)
|
337
|
+
was, @singular = @singular, flag
|
338
|
+
yield
|
339
|
+
ensure
|
340
|
+
@singular = was
|
341
|
+
end
|
342
|
+
|
343
|
+
def on_one (host = self.hosts.sample, serial: true)
|
344
|
+
ret = nil
|
345
|
+
each([host], serial: serial) do |host, i|
|
346
|
+
on host do
|
347
|
+
singularly do
|
348
|
+
ret = yield host, i
|
349
|
+
end
|
308
350
|
end
|
309
351
|
end
|
352
|
+
ret
|
310
353
|
end
|
311
354
|
|
312
355
|
def on_each (hosts = self.hosts, serial: true)
|
313
|
-
each(hosts, serial: serial) do |host|
|
356
|
+
each(hosts.dup, serial: serial) do |host, i|
|
314
357
|
on host do
|
315
|
-
|
358
|
+
singularly do
|
359
|
+
yield host, i
|
360
|
+
end
|
316
361
|
end
|
317
362
|
end
|
318
363
|
end
|
319
364
|
|
365
|
+
def locally (&block)
|
366
|
+
on Local.new("<local>") do
|
367
|
+
singularly &block
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
320
371
|
def each (hosts = self.hosts, serial: false)
|
321
372
|
fail "No hosts" if hosts.empty?
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
373
|
+
q = (@threads || serial) && Queue.new
|
374
|
+
if q && serial
|
375
|
+
q.push nil
|
376
|
+
elsif q
|
377
|
+
@threads.times { q.push nil }
|
378
|
+
end
|
379
|
+
indent = Thread.current[:indent]
|
380
|
+
i = -1
|
381
|
+
threads = [hosts].flatten.map.with_index do |host|
|
382
|
+
Thread.new do
|
383
|
+
Thread.current[:indent] = indent
|
384
|
+
begin
|
385
|
+
q.pop if q
|
386
|
+
yield host, i += 1
|
387
|
+
rescue => e
|
388
|
+
host.log.error e.message
|
389
|
+
hosts.delete host
|
390
|
+
@failures |= [host]
|
391
|
+
ensure
|
392
|
+
q.push nil if q
|
393
|
+
sleep @delay if @delay
|
335
394
|
end
|
336
395
|
end
|
337
|
-
threads.each(&:join)
|
338
396
|
end
|
397
|
+
threads.each(&:join)
|
339
398
|
fail "No hosts remaining" if hosts.empty?
|
340
399
|
end
|
341
400
|
|
data/lib/blower/host.rb
CHANGED
@@ -2,7 +2,6 @@ require 'net/ssh'
|
|
2
2
|
require 'net/ssh/gateway'
|
3
3
|
require 'net/scp'
|
4
4
|
require 'monitor'
|
5
|
-
require 'colorize'
|
6
5
|
require 'base64'
|
7
6
|
require 'timeout'
|
8
7
|
|
@@ -56,6 +55,7 @@ module Blower
|
|
56
55
|
# Copy files or directories to the host.
|
57
56
|
# @api private
|
58
57
|
def cp (froms, to, as: nil, quiet: false, delete: false)
|
58
|
+
sleep DELAY if defined?(DELAY)
|
59
59
|
as ||= @user
|
60
60
|
output = ""
|
61
61
|
synchronize do
|
@@ -111,6 +111,7 @@ module Blower
|
|
111
111
|
# Execute a command on the host and return its output.
|
112
112
|
# @api private
|
113
113
|
def sh (command, as: nil, quiet: false)
|
114
|
+
sleep DELAY if defined?(DELAY)
|
114
115
|
as ||= @user
|
115
116
|
output = ""
|
116
117
|
synchronize do
|
@@ -127,7 +128,7 @@ module Blower
|
|
127
128
|
end
|
128
129
|
ch.on_extended_data do |_, _, data|
|
129
130
|
log.trace "received #{data.bytesize} bytes stderr", quiet: quiet
|
130
|
-
output << data
|
131
|
+
output << data
|
131
132
|
end
|
132
133
|
ch.on_request("exit-status") do |_, data|
|
133
134
|
result = data.read_long
|
data/lib/blower/local.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'net/ssh'
|
2
|
+
require 'net/ssh/gateway'
|
3
|
+
require 'net/scp'
|
4
|
+
require 'monitor'
|
5
|
+
require 'base64'
|
6
|
+
require 'timeout'
|
7
|
+
|
8
|
+
module Blower
|
9
|
+
|
10
|
+
class Local
|
11
|
+
include MonitorMixin
|
12
|
+
extend Forwardable
|
13
|
+
|
14
|
+
attr_reader :name, :data
|
15
|
+
|
16
|
+
def_delegators :data, :[], :[]=
|
17
|
+
|
18
|
+
def initialize (name, proxy: nil)
|
19
|
+
@name, @proxy = name, proxy
|
20
|
+
@data = {}
|
21
|
+
end
|
22
|
+
|
23
|
+
# Represent the host as a string.
|
24
|
+
def to_s
|
25
|
+
@name
|
26
|
+
end
|
27
|
+
|
28
|
+
def sh (command, as: nil, quiet: false)
|
29
|
+
command = "#{@proxy} #{command.shellescape}" if @proxy
|
30
|
+
result = IO.popen(command) do |io|
|
31
|
+
io.read
|
32
|
+
end
|
33
|
+
if $?.success?
|
34
|
+
result
|
35
|
+
else
|
36
|
+
raise "Command failed"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Produce a Logger prefixed with the host name.
|
41
|
+
# @api private
|
42
|
+
def log
|
43
|
+
@log ||= Logger.instance.with_prefix("on #{name}: ")
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
data/lib/blower/logger.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'colorize'
|
1
2
|
require "singleton"
|
2
3
|
|
3
4
|
module Blower
|
@@ -33,12 +34,11 @@ module Blower
|
|
33
34
|
attr_accessor :indent
|
34
35
|
end
|
35
36
|
|
36
|
-
self.indent = 0
|
37
|
-
|
38
37
|
self.level = :info
|
39
38
|
|
40
39
|
def initialize (prefix = "")
|
41
40
|
@prefix = prefix
|
41
|
+
thread[:indent] = 0
|
42
42
|
super()
|
43
43
|
end
|
44
44
|
|
@@ -49,10 +49,10 @@ module Blower
|
|
49
49
|
|
50
50
|
# Yield with a temporarily incremented indent counter
|
51
51
|
def with_indent ()
|
52
|
-
|
52
|
+
thread[:indent] += 1
|
53
53
|
yield
|
54
54
|
ensure
|
55
|
-
|
55
|
+
thread[:indent] -= 1
|
56
56
|
end
|
57
57
|
|
58
58
|
# Display a log message. The block, if specified, is executed in an indented region after the log message is shown.
|
@@ -64,9 +64,10 @@ module Blower
|
|
64
64
|
def log (level, message, quiet: false, &block)
|
65
65
|
if !quiet && (LEVELS.index(level) >= LEVELS.index(Logger.level))
|
66
66
|
synchronize do
|
67
|
+
message = message.to_s.colorize(COLORS[level]) if level
|
67
68
|
message = message.to_s.colorize(COLORS[level]) if level
|
68
69
|
message.split("\n").each do |line|
|
69
|
-
STDERR.puts " " *
|
70
|
+
STDERR.puts " " * thread[:indent] + @prefix + line
|
70
71
|
end
|
71
72
|
end
|
72
73
|
with_indent(&block) if block
|
@@ -95,6 +96,10 @@ module Blower
|
|
95
96
|
define_helper :error
|
96
97
|
define_helper :fatal
|
97
98
|
|
99
|
+
def thread
|
100
|
+
Thread.current
|
101
|
+
end
|
102
|
+
|
98
103
|
end
|
99
104
|
|
100
105
|
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'monitor'
|
2
|
+
require 'base64'
|
3
|
+
require 'timeout'
|
4
|
+
require 'open3'
|
5
|
+
|
6
|
+
module Blower
|
7
|
+
|
8
|
+
class Target
|
9
|
+
include MonitorMixin
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
attr_reader :name, :data
|
13
|
+
|
14
|
+
def_delegators :data, :[], :[]=
|
15
|
+
|
16
|
+
def initialize (name, ssh: "ssh", scp: "scp")
|
17
|
+
@name, @ssh, @scp = name, ssh, scp
|
18
|
+
@data = {}
|
19
|
+
super()
|
20
|
+
end
|
21
|
+
|
22
|
+
# Represent the host as a string.
|
23
|
+
def to_s
|
24
|
+
@name
|
25
|
+
end
|
26
|
+
|
27
|
+
# Copy files or directories to the host.
|
28
|
+
# @api private
|
29
|
+
def cp (froms, to, as: nil, quiet: false, delete: false)
|
30
|
+
as ||= @user
|
31
|
+
output = ""
|
32
|
+
synchronize do
|
33
|
+
[froms].flatten.each do |from|
|
34
|
+
if from.is_a?(String)
|
35
|
+
to += "/" if to[-1] != "/" && from.is_a?(Array)
|
36
|
+
command = ["rsync", "-e", @ssh, "-r"]
|
37
|
+
if File.exist?(".blowignore")
|
38
|
+
command += ["--exclude-from", ".blowignore"]
|
39
|
+
end
|
40
|
+
command += ["--delete"] if delete
|
41
|
+
command += [*from, ":#{to}"]
|
42
|
+
log.trace command.shelljoin, quiet: quiet
|
43
|
+
IO.popen(command, in: :close, err: %i(child out)) do |io|
|
44
|
+
until io.eof?
|
45
|
+
begin
|
46
|
+
output << io.read_nonblock(100)
|
47
|
+
rescue IO::WaitReadable
|
48
|
+
IO.select([io])
|
49
|
+
retry
|
50
|
+
end
|
51
|
+
end
|
52
|
+
io.close
|
53
|
+
if !$?.success?
|
54
|
+
log.fatal "exit status #{$?.exitstatus}: #{command}", quiet: quiet
|
55
|
+
log.fatal output, quiet: quiet
|
56
|
+
fail "failed to copy files"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
elsif from.respond_to?(:read)
|
60
|
+
cmd = "echo #{Base64.strict_encode64(from.read).shellescape} | base64 -d > #{to.shellescape}"
|
61
|
+
sh cmd, quiet: quiet
|
62
|
+
else
|
63
|
+
fail "Don't know how to copy a #{from.class}: #{from}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
true
|
68
|
+
end
|
69
|
+
|
70
|
+
def sh (command, as: nil, quiet: false)
|
71
|
+
marker, output = SecureRandom.hex(32), nil
|
72
|
+
ssh do |i, o, _|
|
73
|
+
i.puts "echo #{marker}"
|
74
|
+
i.puts "sh -c #{command.shellescape} 2>&1"
|
75
|
+
i.puts "STATUS_#{marker}=$?"
|
76
|
+
i.puts "echo #{marker}"
|
77
|
+
i.flush
|
78
|
+
o.readline("#{marker}\n")
|
79
|
+
output = o.readline("#{marker}\n")[0..-(marker.length + 2)]
|
80
|
+
i.puts "echo $STATUS_#{marker}"
|
81
|
+
status = o.readline.to_i
|
82
|
+
if status != 0
|
83
|
+
fail FailedCommand, output
|
84
|
+
end
|
85
|
+
output
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Produce a Logger prefixed with the host name.
|
90
|
+
# @api private
|
91
|
+
def log
|
92
|
+
@log ||= Logger.instance.with_prefix("on #{name}: ")
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def ssh
|
98
|
+
unless @wait
|
99
|
+
@stdin, @stdout, @stderr, @wait = Open3.popen3(@ssh)
|
100
|
+
end
|
101
|
+
yield @stdin, @stdout, @stderr
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
data/lib/blower/util.rb
CHANGED
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: '6'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nathan Baum
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-06-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: net-ssh
|
@@ -94,6 +94,34 @@ dependencies:
|
|
94
94
|
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: ed25519
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: dotenv
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
97
125
|
- !ruby/object:Gem::Dependency
|
98
126
|
name: yard
|
99
127
|
requirement: !ruby/object:Gem::Requirement
|
@@ -120,15 +148,17 @@ files:
|
|
120
148
|
- lib/blower/context.rb
|
121
149
|
- lib/blower/errors.rb
|
122
150
|
- lib/blower/host.rb
|
151
|
+
- lib/blower/local.rb
|
123
152
|
- lib/blower/logger.rb
|
124
153
|
- lib/blower/plugin.rb
|
154
|
+
- lib/blower/target.rb
|
125
155
|
- lib/blower/util.rb
|
126
156
|
- lib/blower/version.rb
|
127
157
|
homepage: http://www.github.org/nbaum/blower
|
128
158
|
licenses:
|
129
159
|
- MIT
|
130
160
|
metadata: {}
|
131
|
-
post_install_message:
|
161
|
+
post_install_message:
|
132
162
|
rdoc_options: []
|
133
163
|
require_paths:
|
134
164
|
- lib
|
@@ -143,9 +173,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
143
173
|
- !ruby/object:Gem::Version
|
144
174
|
version: '0'
|
145
175
|
requirements: []
|
146
|
-
|
147
|
-
|
148
|
-
signing_key:
|
176
|
+
rubygems_version: 3.2.3
|
177
|
+
signing_key:
|
149
178
|
specification_version: 4
|
150
179
|
summary: Really simple server orchestration
|
151
180
|
test_files: []
|