blower 2.0.0 → 2.1.2

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