blower 4.8 → 5.0a1

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: 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