blower 4.7 → 6

Sign up to get free protection for your applications and to get access to all the features.
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: []