blower 4.8 → 5.0a1

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: 64719d3d0e7dadbea696e04130f8a96141c3567c195eb456a42e2c0805924f78
4
- data.tar.gz: b73b17da21d56ff6a7ed17da0d64906ae8639201d29136a1c8f0f90ed03f04a6
3
+ metadata.gz: 7e86119a63baca69801f4556a37de41ace91bcb5a877d3c82e6fe15cb569f512
4
+ data.tar.gz: ef8affaeb03b9eaf5cba800f18bf43a80bcb2a86d59de734744926eb9441e7c8
5
5
  SHA512:
6
- metadata.gz: 8f3f6f1b20a32c90e11a8ef22ef5f01454b876342ccd4a82a063be3d9fcd6e3dcb1be1fc52634022305e604b6e868b3d4a2f15d8366b14fb7f903f3930a9d692
7
- data.tar.gz: '0970f7c8a32e89e0f549a318542b22969264d03d638560bfec66c748b240cef36746aecb5bf07045e3067f43e6ccda2db9ffa407a52e1622f0daaf286e2fc680'
6
+ metadata.gz: 27bba3e972d839c4f1b0a0469e0abff4a6ad007cfc6d87e03596604e2a4025db007ccd2b3e679901440134ad598d09312af448b1f880a7ae50e282ce498b45ee
7
+ data.tar.gz: b3e698f5285034a22b44e66dd1bfff3b987dbae6af6bc5f840a43fe1bef1cafc3cc1644b19fd69a8c1b211ec979f308e6fa1135905830ed796970d9cd3ba5909
data/bin/blow CHANGED
@@ -30,6 +30,7 @@ if File.directory?(File.join(Dir.pwd, "lib"))
30
30
  end
31
31
 
32
32
  context.run "Blowfile", optional: true
33
+
33
34
  begin
34
35
  until ARGV.empty?
35
36
  context.run ARGV.shift
@@ -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.
@@ -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
@@ -136,7 +149,7 @@ module Blower
136
149
  # @macro onceable
137
150
  def sh (command, as: user, on: hosts, quiet: false, once: nil)
138
151
  self.once once, quiet: quiet do
139
- log.info "sh #{command}", quiet: quiet do
152
+ log.info "sh: #{command}", quiet: quiet do
140
153
  hash_map(hosts) do |host|
141
154
  host.sh command, as: as, quiet: quiet
142
155
  end
@@ -282,7 +295,7 @@ module Blower
282
295
 
283
296
  def to_s
284
297
  map do |host, data|
285
- "#{host.name.blue} (#{host.address.green})\n" + data.strip.to_s.gsub(/^/, " ")
298
+ "#{host.name.blue}\n" + data.strip.to_s.gsub(/^/, " ")
286
299
  end.join("\n")
287
300
  end
288
301
 
@@ -302,40 +315,80 @@ module Blower
302
315
  end
303
316
 
304
317
  def hash_map (hosts = self.hosts)
305
- HostHash.new.tap do |result|
306
- each(hosts) do |host|
307
- result[host] = yield(host)
318
+ hh = HostHash.new.tap do |result|
319
+ each(hosts) do |host, i|
320
+ result[host] = yield(host, i)
308
321
  end
309
322
  end
323
+ if @singular
324
+ hh.values.first
325
+ else
326
+ hh
327
+ end
328
+ end
329
+
330
+ def singularly (flag = true)
331
+ was, @singular = @singular, flag
332
+ yield
333
+ ensure
334
+ @singular = was
335
+ end
336
+
337
+ def on_one (host = self.hosts.sample, serial: true)
338
+ ret = nil
339
+ each([host], serial: serial) do |host, i|
340
+ on host do
341
+ singularly do
342
+ ret = yield host, i
343
+ end
344
+ end
345
+ end
346
+ ret
310
347
  end
311
348
 
312
349
  def on_each (hosts = self.hosts, serial: true)
313
- each(hosts, serial: serial) do |host|
350
+ each(hosts, serial: serial) do |host, i|
314
351
  on host do
315
- yield host
352
+ singularly do
353
+ yield host, i
354
+ end
316
355
  end
317
356
  end
318
357
  end
319
358
 
359
+ def locally (&block)
360
+ on Local.new("<local>") do
361
+ singularly &block
362
+ end
363
+ end
364
+
320
365
  def each (hosts = self.hosts, serial: false)
321
366
  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
367
+ q = (@threads || serial) && Queue.new
368
+ if q && serial
369
+ q.push nil
370
+ elsif q
371
+ @threads.times { q.push nil }
372
+ end
373
+ indent = Thread.current[:indent]
374
+ i = -1
375
+ threads = [hosts].flatten.map.with_index do |host|
376
+ Thread.new do
377
+ Thread.current[:indent] = indent
378
+ begin
379
+ q.pop if q
380
+ yield host, i += 1
381
+ rescue => e
382
+ host.log.error e.message
383
+ hosts.delete host
384
+ @failures |= [host]
385
+ ensure
386
+ q.push nil if q
387
+ sleep @delay if @delay
335
388
  end
336
389
  end
337
- threads.each(&:join)
338
390
  end
391
+ threads.each(&:join)
339
392
  fail "No hosts remaining" if hosts.empty?
340
393
  end
341
394
 
@@ -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
 
@@ -127,7 +126,7 @@ module Blower
127
126
  end
128
127
  ch.on_extended_data do |_, _, data|
129
128
  log.trace "received #{data.bytesize} bytes stderr", quiet: quiet
130
- output << data.colorize(:red)
129
+ output << data
131
130
  end
132
131
  ch.on_request("exit-status") do |_, data|
133
132
  result = data.read_long
@@ -0,0 +1,41 @@
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
+ IO.popen(command).read
31
+ end
32
+
33
+ # Produce a Logger prefixed with the host name.
34
+ # @api private
35
+ def log
36
+ @log ||= Logger.instance.with_prefix("on #{name}: ")
37
+ end
38
+
39
+ end
40
+
41
+ end
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Blower
2
- VERSION = "4.8"
2
+ VERSION = "5.0a1"
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.8'
4
+ version: 5.0a1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan Baum
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-10-27 00:00:00.000000000 Z
11
+ date: 2019-02-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: net-ssh
@@ -120,8 +120,10 @@ files:
120
120
  - lib/blower/context.rb
121
121
  - lib/blower/errors.rb
122
122
  - lib/blower/host.rb
123
+ - lib/blower/local.rb
123
124
  - lib/blower/logger.rb
124
125
  - lib/blower/plugin.rb
126
+ - lib/blower/target.rb
125
127
  - lib/blower/util.rb
126
128
  - lib/blower/version.rb
127
129
  homepage: http://www.github.org/nbaum/blower
@@ -139,12 +141,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
139
141
  version: '0'
140
142
  required_rubygems_version: !ruby/object:Gem::Requirement
141
143
  requirements:
142
- - - ">="
144
+ - - ">"
143
145
  - !ruby/object:Gem::Version
144
- version: '0'
146
+ version: 1.3.1
145
147
  requirements: []
146
- rubyforge_project:
147
- rubygems_version: 2.7.7
148
+ rubygems_version: 3.0.2
148
149
  signing_key:
149
150
  specification_version: 4
150
151
  summary: Really simple server orchestration