blower 2.2.0 → 3.1
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/lib/blower/context.rb +109 -32
- data/lib/blower/host.rb +66 -55
- data/lib/blower/logger.rb +28 -11
- data/lib/blower/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 326c39aae83c23c79f60751abeabe1a8eea19870
|
4
|
+
data.tar.gz: e3fa37856f16d53884cc4f79ff7bc762dacc0633
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1705364adfc1bcfa70d7b12cdeccae35d6ba391b2f08192ab1e1380f29bd2495e4945634268237486f8ac250c843752876de2c7574b30ba34634fe4e3b22e3fd
|
7
|
+
data.tar.gz: d091fe3d25f24af89309b01cb716e97961d9165528c9abbe5f3441dc2b6343096eb25ecc9babb5562ad0b28633c5e99d34eecb7ef6c29f2eec49459399d7e2e7
|
data/lib/blower/context.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'erb'
|
1
2
|
require 'forwardable'
|
2
3
|
|
3
4
|
module Blower
|
@@ -12,6 +13,9 @@ module Blower
|
|
12
13
|
# Search path for tasks.
|
13
14
|
attr_accessor :path
|
14
15
|
|
16
|
+
# The currently running task.
|
17
|
+
attr_accessor :task
|
18
|
+
|
15
19
|
# The target hosts.
|
16
20
|
attr_accessor :hosts
|
17
21
|
|
@@ -19,6 +23,28 @@ module Blower
|
|
19
23
|
@path = path
|
20
24
|
@hosts = []
|
21
25
|
@have_seen = {}
|
26
|
+
@data = {}
|
27
|
+
end
|
28
|
+
|
29
|
+
def set (hash, &block)
|
30
|
+
if block
|
31
|
+
begin
|
32
|
+
old_data, @data = @data, @data.dup
|
33
|
+
set(hash)
|
34
|
+
block.()
|
35
|
+
ensure
|
36
|
+
@data = old_data
|
37
|
+
end
|
38
|
+
else
|
39
|
+
@data.merge! hash
|
40
|
+
hash.each do |var, val|
|
41
|
+
define_singleton_method(var) { @data[var] }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def get (var)
|
47
|
+
@data[var]
|
22
48
|
end
|
23
49
|
|
24
50
|
def log
|
@@ -34,50 +60,70 @@ module Blower
|
|
34
60
|
# Execute the block once for each host and return nil.
|
35
61
|
def each (hosts = @hosts, &block)
|
36
62
|
Kernel.fail "No hosts left" if hosts.empty?
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
63
|
+
hosts.each(&block)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Execute the block once for each host and return nil.
|
67
|
+
def with (hosts = @hosts, &block)
|
68
|
+
ctx = clone
|
69
|
+
ctx.hosts = hosts.map do |spec|
|
70
|
+
spec.is_a?(Host) ? spec : Host.new(*spec)
|
71
|
+
end
|
72
|
+
ctx.instance_exec(&block)
|
73
|
+
end
|
74
|
+
|
75
|
+
def with_each (hosts = @hosts, &block)
|
76
|
+
each do |host|
|
77
|
+
with host, &block
|
78
|
+
end
|
42
79
|
end
|
43
80
|
|
44
81
|
# Execute the block once for each host and return a hash of the results.
|
45
82
|
def hashmap (hosts = @hosts, &block)
|
46
83
|
result = {}
|
47
84
|
threads = []
|
48
|
-
|
49
|
-
|
50
|
-
|
85
|
+
log.nest do
|
86
|
+
each hosts do |host|
|
87
|
+
threads << Thread.new do
|
88
|
+
result[host] = block.(host)
|
89
|
+
end
|
51
90
|
end
|
91
|
+
threads.each(&:join)
|
52
92
|
end
|
53
|
-
threads.each(&:join)
|
54
93
|
result
|
55
94
|
end
|
56
95
|
|
57
96
|
# Reboot each host and waits for them to come back up.
|
58
97
|
# @param command The reboot command. A string.
|
59
98
|
def reboot (command = "reboot")
|
60
|
-
|
99
|
+
if f = get(:sh_command)
|
100
|
+
command = f.(command)
|
101
|
+
end
|
102
|
+
each do |host|
|
103
|
+
log.info "rebooting: #{host.name}"
|
61
104
|
begin
|
62
|
-
sh command
|
105
|
+
host.sh command, user: @user
|
63
106
|
rescue IOError
|
64
107
|
end
|
65
|
-
log.debug "
|
66
|
-
sleep 0.1 while ping
|
108
|
+
log.debug "rebooting: #{host.name}: waiting for shutdown..."
|
109
|
+
sleep 0.1 while host.ping
|
67
110
|
end
|
68
|
-
each do
|
69
|
-
log.debug "
|
70
|
-
sleep 1.0 until ping
|
111
|
+
each do |host|
|
112
|
+
log.debug "rebooting: #{host.name}: waiting for return..."
|
113
|
+
sleep 1.0 until host.ping
|
71
114
|
end
|
72
115
|
end
|
73
116
|
|
74
117
|
# Execute a shell command on each host.
|
75
|
-
def sh (command, quiet
|
118
|
+
def sh (command, quiet: false)
|
76
119
|
log.info "sh: #{command}" unless quiet
|
120
|
+
if f = get(:sh_command)
|
121
|
+
command = f.(command)
|
122
|
+
end
|
77
123
|
win = true
|
78
124
|
hashmap do |host|
|
79
125
|
out = ""
|
80
|
-
status = host.sh(command, out)
|
126
|
+
status = host.sh(command, out, user: @user)
|
81
127
|
if status != 0
|
82
128
|
fail host, "#{command}: exit status #{status}"
|
83
129
|
nil
|
@@ -89,20 +135,20 @@ module Blower
|
|
89
135
|
|
90
136
|
# Execute a command on the remote host.
|
91
137
|
# @return false if the command exits with a non-zero status
|
92
|
-
def sh? (command, quiet
|
138
|
+
def sh? (command, quiet: false)
|
93
139
|
log.info "sh?: #{command}" unless quiet
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
140
|
+
if f = get(:sh_command)
|
141
|
+
command = f.(command)
|
142
|
+
end
|
143
|
+
hashmap do |host|
|
144
|
+
host.sh(command, user: @user) == 0
|
98
145
|
end
|
99
|
-
win
|
100
146
|
end
|
101
147
|
|
102
148
|
# Execute a command on the remote host.
|
103
149
|
# @return false if the command exits with a non-zero status
|
104
|
-
def ping (quiet
|
105
|
-
log.
|
150
|
+
def ping (quiet: false)
|
151
|
+
log.debug "ping" unless quiet
|
106
152
|
win = true
|
107
153
|
each do |host|
|
108
154
|
win &&= host.ping
|
@@ -114,22 +160,48 @@ module Blower
|
|
114
160
|
@hosts -= [host]
|
115
161
|
end
|
116
162
|
|
163
|
+
def rebase_path (path)
|
164
|
+
path.gsub(/^(?!\/)/, File.dirname(@file))
|
165
|
+
end
|
166
|
+
|
117
167
|
# Copy a file or readable to the host filesystem.
|
118
168
|
# @param from An object that responds to read, or a string which names a file, or an array of either.
|
119
169
|
# @param to A string.
|
120
170
|
def cp (from, to)
|
121
171
|
log.info "cp: #{from} -> #{to}"
|
172
|
+
from = rebase_path(from)
|
122
173
|
each do |host|
|
123
|
-
host.cp(from, to)
|
174
|
+
host.cp(from, to, rsync_command: get(:rsync_command))
|
124
175
|
end
|
125
176
|
end
|
126
177
|
|
178
|
+
def as (user)
|
179
|
+
old_user, @user = @user, user
|
180
|
+
yield
|
181
|
+
ensure
|
182
|
+
@user = old_user
|
183
|
+
end
|
184
|
+
|
127
185
|
# Writes a string to a file on the host filesystem.
|
128
186
|
# @param string The string to write.
|
129
187
|
# @param to A string.
|
130
188
|
def write (string, to)
|
131
|
-
log "
|
132
|
-
|
189
|
+
log.info "write: -> #{to}"
|
190
|
+
each do |host|
|
191
|
+
host.cp(StringIO.new(string), to, user: @user, sh_command: get(:sh_command))
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def render (from, to)
|
196
|
+
from = rebase_path(from)
|
197
|
+
(Dir["#{from}**/*.erb"] + Dir["#{from}**/.*.erb"]).each do |path|
|
198
|
+
template = ERB.new(File.read(path))
|
199
|
+
to_path = to + path[from.length..-5]
|
200
|
+
log.info "render: #{path} -> #{to_path}"
|
201
|
+
each do |host|
|
202
|
+
host.cp StringIO.new(template.result(binding)), to_path, user: @user, sh_command: get(:sh_command)
|
203
|
+
end
|
204
|
+
end
|
133
205
|
end
|
134
206
|
|
135
207
|
# Run a task.
|
@@ -153,15 +225,20 @@ module Blower
|
|
153
225
|
if optional
|
154
226
|
return
|
155
227
|
else
|
156
|
-
|
228
|
+
raise "can't find #{task}"
|
157
229
|
end
|
158
230
|
else
|
159
|
-
log.
|
231
|
+
log.debug "Running #{task}" do
|
160
232
|
begin
|
161
233
|
old_task, @task = @task, task
|
162
234
|
files.each do |file|
|
163
235
|
@have_seen[file] = true
|
164
|
-
|
236
|
+
begin
|
237
|
+
old_file, @file = @file, file
|
238
|
+
instance_eval(File.read(file), file)
|
239
|
+
ensure
|
240
|
+
@file = old_file
|
241
|
+
end
|
165
242
|
end
|
166
243
|
ensure
|
167
244
|
@task = old_task
|
data/lib/blower/host.rb
CHANGED
@@ -12,6 +12,7 @@ module Blower
|
|
12
12
|
|
13
13
|
attr_accessor :name
|
14
14
|
attr_accessor :user
|
15
|
+
attr_accessor :address
|
15
16
|
|
16
17
|
def_delegators :data, :[], :[]=
|
17
18
|
|
@@ -22,90 +23,99 @@ module Blower
|
|
22
23
|
end
|
23
24
|
end
|
24
25
|
|
25
|
-
def initialize (name, user
|
26
|
-
@name = name
|
26
|
+
def initialize (name, data: {}, user: "root")
|
27
|
+
@address = @name = name
|
27
28
|
@user = user
|
28
|
-
@data =
|
29
|
+
@data = data
|
29
30
|
super()
|
30
31
|
end
|
31
32
|
|
32
33
|
def ping
|
33
34
|
Timeout.timeout(1) do
|
34
|
-
TCPSocket.new(
|
35
|
+
TCPSocket.new(address, 22).close
|
35
36
|
end
|
36
37
|
true
|
37
|
-
rescue Timeout::
|
38
|
+
rescue Timeout::Error
|
38
39
|
false
|
39
40
|
rescue Errno::ECONNREFUSED
|
40
41
|
false
|
41
42
|
end
|
42
43
|
|
43
|
-
def cp (
|
44
|
+
def cp (froms, to, output = "", user: nil, rsync_command: nil, sh_command: nil)
|
44
45
|
synchronize do
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
46
|
+
[froms].flatten.each do |from|
|
47
|
+
if from.is_a?(String)
|
48
|
+
to += "/" if to[-1] != "/" && from.is_a?(Array)
|
49
|
+
command = ["rsync", "-e", "ssh -oStrictHostKeyChecking=no", "-r"]
|
50
|
+
command += ["--rsync-path=#{rsync_command.()}"] if rsync_command
|
51
|
+
command += [*from, "#{user || @user}@#{@address}:#{to}"]
|
52
|
+
log.trace command.shelljoin
|
53
|
+
IO.popen(command, in: :close, err: %i(child out)) do |io|
|
54
|
+
until io.eof?
|
55
|
+
begin
|
56
|
+
output << io.read_nonblock(100)
|
57
|
+
rescue IO::WaitReadable
|
58
|
+
IO.select([io])
|
59
|
+
retry
|
60
|
+
end
|
61
|
+
end
|
62
|
+
io.close
|
63
|
+
if !$?.success?
|
64
|
+
log.fatal "exit status #{$?.exitstatus}: #{command}"
|
65
|
+
log.fatal output
|
66
|
+
fail "failed to copy files"
|
57
67
|
end
|
58
68
|
end
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
69
|
+
elsif from.respond_to?(:read)
|
70
|
+
cmd = "echo #{from.read.shellescape} > #{to.shellescape}"
|
71
|
+
cmd = sh_command.(cmd) if sh_command
|
72
|
+
sh cmd
|
73
|
+
else
|
74
|
+
fail "Don't know how to copy a #{from.class}: #{from}"
|
64
75
|
end
|
65
|
-
elsif from.respond_to?(:read)
|
66
|
-
ssh.scp.upload!(from, to)
|
67
|
-
else
|
68
|
-
fail "Don't know how to copy a #{from.class}: #{from}"
|
69
76
|
end
|
70
77
|
end
|
71
78
|
true
|
72
79
|
end
|
73
80
|
|
74
|
-
def sh (command, output = "")
|
81
|
+
def sh (command, output = "", user: nil)
|
75
82
|
synchronize do
|
76
83
|
log.debug command
|
77
84
|
result = nil
|
78
|
-
ch = ssh.open_channel do |ch|
|
79
|
-
ch.
|
80
|
-
|
81
|
-
ch.
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
85
|
+
ch = ssh(user || @user).open_channel do |ch|
|
86
|
+
ch.request_pty do |ch, success|
|
87
|
+
"failed to acquire pty" unless success unless success
|
88
|
+
ch.exec(command) do |_, success|
|
89
|
+
fail "failed to execute command" unless success
|
90
|
+
ch.on_data do |_, data|
|
91
|
+
log.trace "received #{data.bytesize} bytes stdout"
|
92
|
+
output << data
|
93
|
+
end
|
94
|
+
ch.on_extended_data do |_, _, data|
|
95
|
+
log.trace "received #{data.bytesize} bytes stderr"
|
96
|
+
output << data.colorize(:red)
|
97
|
+
end
|
98
|
+
ch.on_request("exit-status") do |_, data|
|
99
|
+
result = data.read_long
|
100
|
+
log.trace "received exit-status #{result}"
|
101
|
+
end
|
92
102
|
end
|
93
103
|
end
|
94
104
|
end
|
95
105
|
ch.wait
|
96
106
|
if result != 0
|
97
107
|
log.fatal "exit status #{result}: #{command}"
|
98
|
-
log.
|
108
|
+
log.fatal output
|
99
109
|
end
|
100
110
|
result
|
101
111
|
end
|
102
112
|
end
|
103
113
|
|
104
|
-
# Execute the block with self as a parameter.
|
105
|
-
# Exists to conform with the HostGroup interface.
|
106
|
-
def each (&block)
|
107
|
-
|
108
|
-
end
|
114
|
+
# # Execute the block with self as a parameter.
|
115
|
+
# # Exists to conform with the HostGroup interface.
|
116
|
+
# def each (&block)
|
117
|
+
# block.(self)
|
118
|
+
# end
|
109
119
|
|
110
120
|
private
|
111
121
|
|
@@ -115,14 +125,15 @@ module Blower
|
|
115
125
|
Logger.instance.with_prefix("(on #{name})")
|
116
126
|
end
|
117
127
|
|
118
|
-
def ssh
|
119
|
-
|
120
|
-
|
121
|
-
|
128
|
+
def ssh (user)
|
129
|
+
@sessions ||= {}
|
130
|
+
if @sessions[user] && @sessions[user].closed?
|
131
|
+
log.trace "Discovered the connection to ssh:#{user}@#{name} was lost"
|
132
|
+
@sessions[user] = nil
|
122
133
|
end
|
123
|
-
@
|
124
|
-
log.trace "Connecting to ssh:#{
|
125
|
-
Net::SSH.start(
|
134
|
+
@sessions[user] ||= begin
|
135
|
+
log.trace "Connecting to ssh:#{user}@#{name}"
|
136
|
+
Net::SSH.start(address, user)
|
126
137
|
end
|
127
138
|
end
|
128
139
|
|
data/lib/blower/logger.rb
CHANGED
@@ -32,8 +32,9 @@ module Blower
|
|
32
32
|
off: 0,
|
33
33
|
}
|
34
34
|
|
35
|
+
@@indent = 0
|
36
|
+
|
35
37
|
def initialize (prefix = nil)
|
36
|
-
@indent = 0
|
37
38
|
@prefix = prefix
|
38
39
|
super()
|
39
40
|
end
|
@@ -60,9 +61,23 @@ module Blower
|
|
60
61
|
# Log a fatal level event
|
61
62
|
def fatal (a=nil, *b, &c); log(a, :fatal, *b, &c); end
|
62
63
|
|
63
|
-
|
64
|
-
|
65
|
-
|
64
|
+
def nest (&c)
|
65
|
+
log(nil, nil, &c)
|
66
|
+
end
|
67
|
+
|
68
|
+
def task (message, &block)
|
69
|
+
STDOUT.write " " * @@indent + (@prefix ? @prefix + " " : "") + message + "... "
|
70
|
+
begin
|
71
|
+
@@indent += 1
|
72
|
+
block.() if block
|
73
|
+
ensure
|
74
|
+
@@indent -= 1
|
75
|
+
end
|
76
|
+
puts "OK".colorize({color: :green})
|
77
|
+
rescue => e
|
78
|
+
puts "ERROR".colorize({color: :red})
|
79
|
+
raise e
|
80
|
+
end
|
66
81
|
|
67
82
|
private
|
68
83
|
|
@@ -70,15 +85,17 @@ module Blower
|
|
70
85
|
if message && (level.nil? || RANKS[level] <= RANKS[$LOGLEVEL])
|
71
86
|
Logger.instance.synchronize do
|
72
87
|
message = message.colorize(COLORS[level]) if level
|
73
|
-
puts " " *
|
88
|
+
puts " " * @@indent + (@prefix ? @prefix + " " : "") + message
|
89
|
+
end
|
90
|
+
begin
|
91
|
+
@@indent += 1
|
92
|
+
block.() if block
|
93
|
+
ensure
|
94
|
+
@@indent -= 1
|
74
95
|
end
|
96
|
+
else
|
97
|
+
block.() if block
|
75
98
|
end
|
76
|
-
begin
|
77
|
-
@indent += 1
|
78
|
-
block.()
|
79
|
-
ensure
|
80
|
-
@indent -= 1
|
81
|
-
end if block
|
82
99
|
end
|
83
100
|
|
84
101
|
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: '3.1'
|
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-04-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: net-ssh
|