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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9cfbaaa18cbeed95710091e851f7869055d0adb3
4
- data.tar.gz: 5dba28a766c20b2c622a814687fe6defdc825982
3
+ metadata.gz: 326c39aae83c23c79f60751abeabe1a8eea19870
4
+ data.tar.gz: e3fa37856f16d53884cc4f79ff7bc762dacc0633
5
5
  SHA512:
6
- metadata.gz: 720985c8137a5283d7b9909c767b39342d06b87ed8d83866f595ac44f18ccd86b6ce7286aca31bc4a54ba3246f148d5ddf63fc43b6351510d3d5100df84aa668
7
- data.tar.gz: f6b2a4d82dc3ffc7ff803a082a8f1374e52b20f7f95c843aa8265197613f63dbec6d89d20962354da9441535bf5cd5458acab48d68408bbd5646fc426f621bec
6
+ metadata.gz: 1705364adfc1bcfa70d7b12cdeccae35d6ba391b2f08192ab1e1380f29bd2495e4945634268237486f8ac250c843752876de2c7574b30ba34634fe4e3b22e3fd
7
+ data.tar.gz: d091fe3d25f24af89309b01cb716e97961d9165528c9abbe5f3441dc2b6343096eb25ecc9babb5562ad0b28633c5e99d34eecb7ef6c29f2eec49459399d7e2e7
@@ -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
- Enumerator.new do |y|
38
- hosts.each do |host|
39
- y << host
40
- end
41
- end.each(&block)
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
- each hosts do |host|
49
- threads << Thread.new do
50
- result[host] = block.(host)
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
- each do
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 "Waiting for server to go away..."
66
- sleep 0.1 while ping(true)
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 "Waiting for server to come back..."
70
- sleep 1.0 until ping(true)
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 = false)
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 = false)
138
+ def sh? (command, quiet: false)
93
139
  log.info "sh?: #{command}" unless quiet
94
- win = true
95
- each do |host|
96
- status = host.sh(command)
97
- win = false if status != 0
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 = false)
105
- log.info "ping" unless quiet
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 "upload data to #{to}", :debug
132
- target.cp(StringIO.new(string), to)
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
- fail "can't find #{task}"
228
+ raise "can't find #{task}"
157
229
  end
158
230
  else
159
- log.info "Running #{task}" do
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
- instance_eval(File.read(file), file)
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 = "root")
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(name, 22).close
35
+ TCPSocket.new(address, 22).close
35
36
  end
36
37
  true
37
- rescue Timeout::ExitException
38
+ rescue Timeout::Error
38
39
  false
39
40
  rescue Errno::ECONNREFUSED
40
41
  false
41
42
  end
42
43
 
43
- def cp (from, to, output = "")
44
+ def cp (froms, to, output = "", user: nil, rsync_command: nil, sh_command: nil)
44
45
  synchronize do
45
- if from.is_a?(String) || from.is_a?(Array)
46
- to += "/" if to[-1] != "/" && from.is_a?(Array)
47
- command = ["rsync", "-e", "ssh -oStrictHostKeyChecking=no", "-r", "--progress", *from,
48
- "#{@user}@#{@name}:#{to}"]
49
- log.trace command.shelljoin
50
- IO.popen(command, in: :close, err: %i(child out)) do |io|
51
- until io.eof?
52
- begin
53
- output << io.read_nonblock(100)
54
- rescue IO::WaitReadable
55
- IO.select([io])
56
- retry
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
- io.close
60
- if !$?.success?
61
- log.fatal "exit status #{$?.exitstatus}: #{command}"
62
- log.raw output
63
- end
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.exec(command) do |_, success|
80
- fail "failed to execute command" unless success
81
- ch.on_data do |_, data|
82
- log.trace "received #{data.bytesize} bytes stdout"
83
- output << data
84
- end
85
- ch.on_extended_data do |_, _, data|
86
- log.trace "received #{data.bytesize} bytes stderr"
87
- output << data.colorize(:red)
88
- end
89
- ch.on_request("exit-status") do |_, data|
90
- result = data.read_long
91
- log.trace "received exit-status #{result}"
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.raw output
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
- block.(self)
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
- if @ssh && @ssh.closed?
120
- log.trace "Discovered the connection to ssh:#{name}@#{user} was lost"
121
- @ssh = nil
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
- @ssh ||= begin
124
- log.trace "Connecting to ssh:#{name}@#{user}"
125
- Net::SSH.start(name, user)
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
- # Log a level-less event
64
- # @deprecated
65
- def raw (a=nil, *b, &c); log(a, nil, *b, &c); end
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 " " * @indent + (@prefix ? @prefix + " " : "") + message
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
@@ -1,3 +1,3 @@
1
1
  module Blower
2
- VERSION = "2.2.0"
2
+ VERSION = "3.1"
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: 2.2.0
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-02-07 00:00:00.000000000 Z
11
+ date: 2016-04-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: net-ssh