blower 2.0.0 → 2.1.2

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: 43a7308c9eaa1ad35d5a1c3942fad1850b32f3dc
4
- data.tar.gz: 0d43517ed6d42111fd7aa0eabe3f30275ee1d227
3
+ metadata.gz: 1346720ababac71f881dae28ba20012eaa110361
4
+ data.tar.gz: 567937a547591d6bcaaafc55f10d3c515fd78969
5
5
  SHA512:
6
- metadata.gz: dd7de8fa74c568387e02af7afde83fa52d719a9ccb8658eae7a26ef9fa483b924654bb7bf9fe5f0b97d08b595a7652f72c4994810424b833058d14feb3dbed89
7
- data.tar.gz: 200326faec848b91ea49c5422f8fb74843c17de9ac622559fd927e19099628a1a5de6d10573655ae5b4bd67056c9cca459b97758a4d3c160867bec3ba486a422
6
+ metadata.gz: e94f4c1b1074a20ff18fd7bf416af1fe52436e473eba52c7a53ea642c46fdbae5dc9a48f2b3e30c4458f49ccab4c69010426494978590be4751824708571dd06
7
+ data.tar.gz: 7013297bf513abbb9b364dc429f78fa71d21916bb6c773816eeb1ba71aa23fa183aee0bf885ca6ace9968475dee576b2b9bc5cf44f4db5aeb264f7523d8c2b73
data/bin/blow CHANGED
@@ -3,18 +3,25 @@ require 'blower'
3
3
  require 'pathname'
4
4
  require 'optparse'
5
5
 
6
+ $LOGLEVEL = :info
7
+
6
8
  OptionParser.new do |opts|
7
9
  opts.banner = "Usage: blow [options] task..."
8
10
  opts.on "-d DIR", "Change directory" do |v|
9
11
  Dir.chdir v
10
12
  end
13
+ opts.on "-l LEVEL", "Minimal log level" do |l|
14
+ $LOGLEVEL = l.downcase.to_sym
15
+ end
11
16
  end.order!
12
17
 
18
+ context = Blower::Context.new([".", Dir.pwd])
19
+ context.run "Blowfile", optional: true
13
20
  begin
14
- context = Blower::Context.new([".", Dir.pwd])
15
- context.target = nil
16
- context.run "Blowfile"
17
- ARGV.each do |arg|
18
- context.run arg
21
+ until ARGV.empty?
22
+ context.run ARGV.shift
19
23
  end
24
+ rescue RuntimeError => e
25
+ puts e.message.colorize(:red)
26
+ exit 1
20
27
  end
@@ -2,77 +2,138 @@ require 'forwardable'
2
2
 
3
3
  module Blower
4
4
 
5
+ # Blower tasks are executed within a context.
6
+ #
7
+ # The context can be used to share information between tasks, by storing it in instance variables.
8
+ #
5
9
  class Context
6
10
  extend Forwardable
7
11
 
12
+ # Search path for tasks.
8
13
  attr_accessor :path
9
- attr_accessor :location
10
- attr_accessor :target
14
+
15
+ # The target hosts.
16
+ attr_accessor :hosts
11
17
 
12
18
  def initialize (path)
13
19
  @path = path
20
+ @hosts = []
14
21
  @have_seen = {}
15
22
  end
16
23
 
17
- def log (message, level, &block)
18
- message = "(on #{target.name}) " + message if target.respond_to?(:name)
19
- Logger.instance.log(message, level, &block)
24
+ def log
25
+ Logger.instance
26
+ end
27
+
28
+ def add_host (spec)
29
+ host = Host.new(*spec) unless spec.is_a?(Host)
30
+ @hosts << host
31
+ end
32
+
33
+ # Execute the block on one host.
34
+ # @param host Host to use. If nil, a random host is picked.
35
+ def one_host (host = nil, &block)
36
+ map [host || target.hosts.sample], &block
20
37
  end
21
38
 
22
- def stage (message, &block)
23
- log message, :info, &block
39
+ # Execute the block once for each host.
40
+ # Each block executes in a copy of the context.
41
+ def each (hosts = @hosts, &block)
42
+ map(hosts, &block)
43
+ nil
24
44
  end
25
45
 
26
- def one_host (name = nil, &block)
27
- each_host [name || target.hosts.sample], &block
46
+ # Execute the block once for each host.
47
+ # Each block executes in a copy of the context.
48
+ def map (hosts = @hosts, &block)
49
+ Kernel.fail "No hosts left" if hosts.empty?
50
+ hosts.map do |host|
51
+ Thread.new do
52
+ block.(host)
53
+ end
54
+ end.map(&:join)
28
55
  end
29
56
 
30
- def each_host (hosts = target, parallel: true, &block)
31
- hosts.each do |host|
32
- ctx = dup
33
- ctx.target = host
34
- ctx.instance_exec(&block)
57
+ # Reboot each host and waits for them to come back up.
58
+ # @param command The reboot command. A string.
59
+ def reboot (command = "reboot")
60
+ each do
61
+ begin
62
+ sh command
63
+ rescue IOError
64
+ end
65
+ log.debug "Waiting for server to go away..."
66
+ sleep 0.1 while ping(true)
67
+ log.debug "Waiting for server to come back..."
68
+ sleep 1.0 until ping(true)
35
69
  end
36
70
  end
37
71
 
38
- def reboot
39
- begin
40
- sh "shutdown -r now"
41
- rescue IOError
42
- sleep 0.1 while ping
43
- sleep 1.0 until ping
72
+ # Execute a shell command on each host.
73
+ def sh (command, quiet = false)
74
+ log.info "sh: #{command}" unless quiet
75
+ map do |host|
76
+ status = host.sh(command)
77
+ fail host, "#{command}: exit status #{status}" if status != 0
44
78
  end
45
79
  end
46
80
 
47
- def sh (command)
48
- log "execute #{command}", :debug
49
- target.sh(command)
81
+ # Execute a command on the remote host.
82
+ # @return false if the command exits with a non-zero status
83
+ def sh? (command, quiet = false)
84
+ log.info "sh?: #{command}" unless quiet
85
+ win = true
86
+ map do |host|
87
+ status = host.sh(command)
88
+ win = false if status != 0
89
+ end
90
+ win
50
91
  end
51
92
 
93
+ # Execute a command on the remote host.
94
+ # @return false if the command exits with a non-zero status
95
+ def ping (quiet = false)
96
+ log.info "ping" unless quiet
97
+ win = true
98
+ map do |host|
99
+ win &&= host.ping
100
+ end
101
+ win
102
+ end
103
+
104
+ def fail (host, message)
105
+ @hosts -= [host]
106
+ end
107
+
108
+ # Copy a file or readable to the host filesystem.
109
+ # @param from An object that responds to read, or a string which names a file, or an array of either.
110
+ # @param to A string.
52
111
  def cp (from, to)
53
- log "upload #{Array(from).join(", ")} -> #{to}", :debug
54
- target.cp(from, to)
112
+ log.info "cp: #{from} -> #{to}"
113
+ map do |host|
114
+ host.cp(from, to)
115
+ end
55
116
  end
56
117
 
118
+ # Writes a string to a file on the host filesystem.
119
+ # @param string The string to write.
120
+ # @param to A string.
57
121
  def write (string, to)
58
122
  log "upload data to #{to}", :debug
59
123
  target.cp(StringIO.new(string), to)
60
124
  end
61
125
 
62
- def sh? (command)
63
- log "execute #{command}", :debug
64
- target.sh(command)
65
- rescue Blower::Host::ExecuteError
66
- false
67
- end
68
-
126
+ # Capture the output a command on the remote host.
127
+ # @return (String) The combined stdout and stderr of the command.
69
128
  def capture (command)
70
129
  stdout = ""
71
130
  target.sh(command, stdout)
72
131
  stdout
73
132
  end
74
133
 
75
- def run (task)
134
+ # Run a task.
135
+ # @param task (String) The name of the task
136
+ def run (task, optional: false)
76
137
  files = []
77
138
  @path.each do |dir|
78
139
  name = File.join(dir, task)
@@ -88,16 +149,22 @@ module Blower
88
149
  break unless files.empty?
89
150
  end
90
151
  if files.empty?
91
- fail "can't find #{task}"
152
+ if optional
153
+ return
154
+ else
155
+ fail "can't find #{task}"
156
+ end
92
157
  else
93
- begin
94
- old_task, @task = @task, task
95
- files.each do |file|
96
- @have_seen[file] = true
97
- instance_eval(File.read(file), file)
158
+ log.info "Running #{task}" do
159
+ begin
160
+ old_task, @task = @task, task
161
+ files.each do |file|
162
+ @have_seen[file] = true
163
+ instance_eval(File.read(file), file)
164
+ end
165
+ ensure
166
+ @task = old_task
98
167
  end
99
- ensure
100
- @task = old_task
101
168
  end
102
169
  end
103
170
  end
data/lib/blower/host.rb CHANGED
@@ -2,6 +2,7 @@ require 'net/ssh'
2
2
  require 'net/scp'
3
3
  require 'monitor'
4
4
  require 'colorize'
5
+ require 'timeout'
5
6
 
6
7
  module Blower
7
8
 
@@ -11,7 +12,6 @@ module Blower
11
12
 
12
13
  attr_accessor :name
13
14
  attr_accessor :user
14
- attr_accessor :data
15
15
 
16
16
  def_delegators :data, :[], :[]=
17
17
 
@@ -29,10 +29,6 @@ module Blower
29
29
  super()
30
30
  end
31
31
 
32
- def log
33
- Logger.instance
34
- end
35
-
36
32
  def ping
37
33
  Timeout.timeout(1) do
38
34
  TCPSocket.new(name, 22).close
@@ -48,8 +44,10 @@ module Blower
48
44
  synchronize do
49
45
  if from.is_a?(String) || from.is_a?(Array)
50
46
  to += "/" if to[-1] != "/" && from.is_a?(Array)
51
- IO.popen(["rsync", "-z", "-r", "--progress", *from, "#{@user}@#{@name}:#{to}"],
52
- in: :close, err: %i(child out)) do |io|
47
+ command = ["rsync", "-e", "ssh -oStrictHostKeyChecking=no", "-zz", "-r", "--progress", *from,
48
+ "#{@user}@#{@name}:#{to}"]
49
+ log.trace command.shelljoin
50
+ IO.popen(command, in: :close, err: %i(child out)) do |io|
53
51
  until io.eof?
54
52
  begin
55
53
  output << io.read_nonblock(100)
@@ -58,53 +56,74 @@ module Blower
58
56
  retry
59
57
  end
60
58
  end
59
+ io.close
60
+ if !$?.success?
61
+ log.fatal "exit status #{$?.exitstatus}: #{command}"
62
+ log.raw output
63
+ end
61
64
  end
62
- elsif from.is_a?(StringIO) or from.is_a?(IO)
63
- log.info "string -> #{to}" unless quiet
65
+ elsif from.respond_to?(:read)
64
66
  ssh.scp.upload!(from, to)
65
67
  else
66
68
  fail "Don't know how to copy a #{from.class}: #{from}"
67
69
  end
68
70
  end
69
71
  true
70
- rescue => e
71
- false
72
72
  end
73
73
 
74
74
  def sh (command, output = "")
75
75
  synchronize do
76
+ log.debug command
76
77
  result = nil
77
78
  ch = ssh.open_channel do |ch|
78
79
  ch.exec(command) do |_, success|
79
80
  fail "failed to execute command" unless success
80
81
  ch.on_data do |_, data|
82
+ log.trace "received #{data.bytesize} bytes stdout"
81
83
  output << data
82
84
  end
83
85
  ch.on_extended_data do |_, _, data|
86
+ log.trace "received #{data.bytesize} bytes stderr"
84
87
  output << data.colorize(:red)
85
88
  end
86
- ch.on_request("exit-status") { |_, data| result = data.read_long }
89
+ ch.on_request("exit-status") do |_, data|
90
+ result = data.read_long
91
+ log.trace "received exit-status #{result}"
92
+ end
87
93
  end
88
94
  end
89
95
  ch.wait
90
96
  if result != 0
91
- log.fatal "failed on #{name}"
97
+ log.fatal "exit status #{result}: #{command}"
92
98
  log.raw output
93
- exit 1
94
99
  end
95
100
  result
96
101
  end
97
102
  end
98
103
 
104
+ # Execute the block with self as a parameter.
105
+ # Exists to confirm with the HostGroup interface.
99
106
  def each (&block)
100
107
  block.(self)
101
108
  end
102
109
 
103
110
  private
104
111
 
112
+ attr_accessor :data
113
+
114
+ def log
115
+ Logger.instance.with_prefix("(on #{name})")
116
+ end
117
+
105
118
  def ssh
106
- @ssh = nil if @ssh && @ssh.closed?
107
- @ssh ||= Net::SSH.start(name, user)
119
+ if @ssh && @ssh.closed?
120
+ log.trace "Discovered the connection to ssh:#{name}@#{user} was lost"
121
+ @ssh = nil
122
+ end
123
+ @ssh ||= begin
124
+ log.trace "Connecting to ssh:#{name}@#{user}"
125
+ Net::SSH.start(name, user)
126
+ end
108
127
  end
109
128
 
110
129
  end
data/lib/blower/logger.rb CHANGED
@@ -2,37 +2,75 @@ require "singleton"
2
2
 
3
3
  module Blower
4
4
 
5
+ # Colorized logger.
6
+ #
7
+ # Prints messages to STDOUT, colorizing them according to the specified log level.
8
+ #
9
+ # The logging methods accept an optional block. Inside the block, log messages will
10
+ # be indented by two spaces. This works recursively.
5
11
  class Logger
6
12
  include MonitorMixin
7
13
  include Singleton
8
14
 
9
15
  COLORS = {
10
- trace: :light_black,
11
- debug: :light_black,
12
- info: :blue,
13
- warn: :yellow,
14
- error: :red,
15
- fatal: :magenta,
16
+ trace: {color: :light_black},
17
+ debug: {color: :default},
18
+ info: {color: :blue},
19
+ warn: {color: :yellow},
20
+ error: {color: :red},
21
+ fatal: {color: :light_white, background: :red},
16
22
  }
17
23
 
18
- def initialize
24
+ RANKS = {
25
+ all: 100,
26
+ trace: 60,
27
+ debug: 50,
28
+ info: 40,
29
+ warn: 30,
30
+ error: 20,
31
+ fatal: 10,
32
+ off: 0,
33
+ }
34
+
35
+ def initialize (prefix = nil)
19
36
  @indent = 0
37
+ @prefix = prefix
20
38
  super()
21
39
  end
22
40
 
41
+ def with_prefix (string)
42
+ self.class.send(:new, "#{@prefix}#{string}")
43
+ end
44
+
45
+ # Log a trace level event
23
46
  def trace (a=nil, *b, &c); log(a, :trace, *b, &c); end
47
+
48
+ # Log a debug level event
24
49
  def debug (a=nil, *b, &c); log(a, :debug, *b, &c); end
50
+
51
+ # Log a info level event
25
52
  def info (a=nil, *b, &c); log(a, :info, *b, &c); end
53
+
54
+ # Log a warn level event
26
55
  def warn (a=nil, *b, &c); log(a, :warn, *b, &c); end
56
+
57
+ # Log a error level event
27
58
  def error (a=nil, *b, &c); log(a, :error, *b, &c); end
59
+
60
+ # Log a fatal level event
28
61
  def fatal (a=nil, *b, &c); log(a, :fatal, *b, &c); end
62
+
63
+ # Log a level-less event
64
+ # @deprecated
29
65
  def raw (a=nil, *b, &c); log(a, nil, *b, &c); end
30
66
 
67
+ private
68
+
31
69
  def log (message = nil, level = :info, &block)
32
- if message
33
- synchronize do
70
+ if message && (level.nil? || RANKS[level] <= RANKS[$LOGLEVEL])
71
+ Logger.instance.synchronize do
34
72
  message = message.colorize(COLORS[level]) if level
35
- puts " " * @indent + message
73
+ puts " " * @indent + (@prefix ? @prefix + " " : "") + message
36
74
  end
37
75
  end
38
76
  begin
@@ -45,8 +83,4 @@ module Blower
45
83
 
46
84
  end
47
85
 
48
- def self.log (*args, &block)
49
- Logger.instance.log(*args, &block)
50
- end
51
-
52
86
  end
@@ -1,3 +1,3 @@
1
1
  module Blower
2
- VERSION = "2.0.0"
2
+ VERSION = "2.1.2"
3
3
  end
data/lib/blower.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  require 'blower/logger'
2
2
  require 'blower/context'
3
3
  require 'blower/host'
4
- require 'blower/host_group'
5
4
  require 'blower/mock_host'
6
5
  require 'blower/version'
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.0.0
4
+ version: 2.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan Baum
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-11-05 00:00:00.000000000 Z
11
+ date: 2015-12-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: net-ssh
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0.7'
55
+ - !ruby/object:Gem::Dependency
56
+ name: yard
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  description: Really simple server orchestration
56
70
  email: n@p12a.org.uk
57
71
  executables:
@@ -63,7 +77,6 @@ files:
63
77
  - lib/blower.rb
64
78
  - lib/blower/context.rb
65
79
  - lib/blower/host.rb
66
- - lib/blower/host_group.rb
67
80
  - lib/blower/logger.rb
68
81
  - lib/blower/mock_host.rb
69
82
  - lib/blower/version.rb
@@ -92,3 +105,4 @@ signing_key:
92
105
  specification_version: 4
93
106
  summary: Really simple server orchestration
94
107
  test_files: []
108
+ has_rdoc:
@@ -1,36 +0,0 @@
1
- module Blower
2
-
3
- class HostGroup
4
-
5
- attr_accessor :hosts
6
- attr_accessor :root
7
- attr_accessor :location
8
-
9
- def initialize (hosts)
10
- @hosts = hosts
11
- end
12
-
13
- def sh (command = nil, *args, &block)
14
- each do |host|
15
- command = block.() if block
16
- host.sh(command)
17
- end
18
- end
19
-
20
- def cp (from, to)
21
- each do |host|
22
- host.cp(from, to)
23
- end
24
- end
25
-
26
- def each (&block)
27
- hosts.map do |host|
28
- Thread.new do
29
- block.(host)
30
- end
31
- end.map(&:join)
32
- end
33
-
34
- end
35
-
36
- end