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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd4ef69511d05d0f1ee58cd814ddc58a85102c467fe695a0520cf02345dbad69
4
- data.tar.gz: 82aeb0b7c6ead150c17d78f50513c300b5aa326cd7ccf4c1932eb84017e712d2
3
+ metadata.gz: b43c163f31a76c68c8327d1c696bda453f2c6fefcfc673fdcf338f8c840e3a6c
4
+ data.tar.gz: e33fde4f43223527937dd57d11da5609bb2f124a66dc21dfa71d2f79a119a765
5
5
  SHA512:
6
- metadata.gz: e932789921c9553fbefd425a38db2d0d3db063681a660065ad9e642b0335d364e09a392fd34535fb64ba57d05d87da40f8a8f9420d7749f53a8ff5a8851efc79
7
- data.tar.gz: 40bf21b8b30968869d6a3f425155d66a4cb562fc1f1241e40f14c5987ac1b670713e10a4bbc148373e2b3012c376ada391f6157da79df24e947790bf3aa767f1
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
@@ -8,5 +8,7 @@ require 'blower/logger'
8
8
  require 'blower/errors'
9
9
  require 'blower/context'
10
10
  require 'blower/host'
11
+ require 'blower/local'
12
+ require 'blower/target'
11
13
  require 'blower/plugin'
12
14
  require 'blower/version'
@@ -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
- code = File.read(file)
122
- let :@file => file do
123
- instance_eval(code, file)
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} (#{host.address.green})\n" + data.strip.to_s.gsub(/^/, " ")
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
- yield host
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
- if false
323
- [hosts].flatten.each do |host|
324
- yield host
325
- end
326
- else
327
- threads = [hosts].flatten.map do |host|
328
- Thread.new do
329
- begin
330
- yield host
331
- rescue => e
332
- host.log.error e.full_message
333
- hosts.delete host
334
- end
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.colorize(:red)
131
+ output << data
131
132
  end
132
133
  ch.on_request("exit-status") do |_, data|
133
134
  result = data.read_long
@@ -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
- Logger.indent += 1
52
+ thread[:indent] += 1
53
53
  yield
54
54
  ensure
55
- Logger.indent -= 1
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 " " * Logger.indent + @prefix + line
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
@@ -26,3 +26,9 @@ class Object
26
26
  end
27
27
 
28
28
  end
29
+
30
+ def try (err = nil, &block)
31
+ block.()
32
+ rescue
33
+ err
34
+ end
@@ -1,3 +1,3 @@
1
1
  module Blower
2
- VERSION = "4.7"
2
+ VERSION = "6"
3
3
  end
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.7'
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: 2018-10-27 00:00:00.000000000 Z
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
- rubyforge_project:
147
- rubygems_version: 2.7.7
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: []