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