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 +4 -4
- data/bin/blow +12 -5
- data/lib/blower/context.rb +108 -41
- data/lib/blower/host.rb +35 -16
- data/lib/blower/logger.rb +48 -14
- data/lib/blower/version.rb +1 -1
- data/lib/blower.rb +0 -1
- metadata +17 -3
- data/lib/blower/host_group.rb +0 -36
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1346720ababac71f881dae28ba20012eaa110361
|
4
|
+
data.tar.gz: 567937a547591d6bcaaafc55f10d3c515fd78969
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
15
|
-
|
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
|
data/lib/blower/context.rb
CHANGED
@@ -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
|
-
|
10
|
-
|
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
|
18
|
-
|
19
|
-
|
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
|
-
|
23
|
-
|
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
|
-
|
27
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
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 "
|
54
|
-
|
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
|
-
|
63
|
-
|
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
|
-
|
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
|
-
|
152
|
+
if optional
|
153
|
+
return
|
154
|
+
else
|
155
|
+
fail "can't find #{task}"
|
156
|
+
end
|
92
157
|
else
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
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
|
-
|
52
|
-
|
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.
|
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")
|
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 "
|
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
|
-
|
107
|
-
|
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: :
|
12
|
-
info: :blue,
|
13
|
-
warn: :yellow,
|
14
|
-
error: :red,
|
15
|
-
fatal: :
|
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
|
-
|
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
|
data/lib/blower/version.rb
CHANGED
data/lib/blower.rb
CHANGED
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.
|
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
|
+
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:
|
data/lib/blower/host_group.rb
DELETED
@@ -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
|