hadouken 0.1.4.pre
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +32 -0
- data/LICENSE.txt +20 -0
- data/README.md +55 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/hadouken.gemspec +84 -0
- data/lib/hadouken.rb +29 -0
- data/lib/hadouken/executor.rb +236 -0
- data/lib/hadouken/ext/net_ssh_multi_session_actions.rb +31 -0
- data/lib/hadouken/group.rb +32 -0
- data/lib/hadouken/groups.rb +42 -0
- data/lib/hadouken/host.rb +120 -0
- data/lib/hadouken/plan.rb +48 -0
- data/lib/hadouken/runner.rb +84 -0
- data/lib/hadouken/strategy/base.rb +15 -0
- data/lib/hadouken/strategy/by_group.rb +15 -0
- data/lib/hadouken/strategy/by_group_parallel.rb +34 -0
- data/lib/hadouken/strategy/by_host.rb +14 -0
- data/lib/hadouken/task.rb +58 -0
- data/lib/hadouken/tasks.rb +17 -0
- data/test/helper.rb +21 -0
- data/test/test_hadouken.rb +118 -0
- metadata +164 -0
@@ -0,0 +1,31 @@
|
|
1
|
+
module Net::SSH::Multi::SessionActions
|
2
|
+
|
3
|
+
def hadouken_exec(command)
|
4
|
+
open_channel do |channel|
|
5
|
+
|
6
|
+
channel.exec(command) do |ch, success|
|
7
|
+
raise "could not execute command: #{command.inspect} (#{ch[:host]})" unless success
|
8
|
+
channel.on_data do |ch, data|
|
9
|
+
ch[:stdout] = []
|
10
|
+
data.chomp.each_line do |line|
|
11
|
+
ch[:stdout] << line
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
channel.on_extended_data do |ch, type, data|
|
17
|
+
ch[:stderr] = []
|
18
|
+
data.chomp.each_line do |line|
|
19
|
+
ch[:stderr] << line
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
channel.on_request("exit-status") do |ch, data|
|
24
|
+
ch[:exit_status] = data.read_long
|
25
|
+
end
|
26
|
+
|
27
|
+
end #open_channel
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class Hadouken::Group
|
2
|
+
attr_reader :name
|
3
|
+
attr_reader :range
|
4
|
+
attr_reader :pattern
|
5
|
+
|
6
|
+
def initialize(opts)
|
7
|
+
@name = opts[:name]
|
8
|
+
@range = opts[:range]
|
9
|
+
@pattern = opts[:pattern]
|
10
|
+
end
|
11
|
+
|
12
|
+
def count
|
13
|
+
@count ||= hosts.size
|
14
|
+
end
|
15
|
+
alias :size :count
|
16
|
+
|
17
|
+
def hosts
|
18
|
+
@hosts ||= @range.map{|idx| "#{@pattern}" % [ idx ] }.map do |hostname|
|
19
|
+
Hadouken::Hosts.add({:name => hostname})
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def has_host?(name)
|
24
|
+
@hosts_by_name ||= hosts.inject({}){|h,host| h[host]=true; h}
|
25
|
+
@hosts_by_name.has_key?(name)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.create!(name, opts)
|
29
|
+
new(opts.merge(:name => name))
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
class Hadouken::Groups
|
2
|
+
include Enumerable
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
@groups = {}
|
6
|
+
@order = []
|
7
|
+
end
|
8
|
+
|
9
|
+
def count
|
10
|
+
@groups.values.size
|
11
|
+
end
|
12
|
+
alias :size :count
|
13
|
+
|
14
|
+
def hosts
|
15
|
+
@groups.values.map{|group| group.hosts}.flatten
|
16
|
+
end
|
17
|
+
|
18
|
+
def each
|
19
|
+
@order.uniq!
|
20
|
+
@order.each do |name|
|
21
|
+
yield @groups[name]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def [](name)
|
26
|
+
fetch name
|
27
|
+
end
|
28
|
+
|
29
|
+
def fetch (name)
|
30
|
+
@groups[ name ]
|
31
|
+
end
|
32
|
+
|
33
|
+
def add (name, opts)
|
34
|
+
store(Hadouken::Group.create!(name, opts))
|
35
|
+
end
|
36
|
+
|
37
|
+
def store (group)
|
38
|
+
raise ArgumentError.new("8==D~") unless group.is_a?(Hadouken::Group)
|
39
|
+
@groups[ group.name ] = group
|
40
|
+
@order << group.name
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module Hadouken::Hosts
|
2
|
+
|
3
|
+
class << self
|
4
|
+
attr_accessor :history_filepath
|
5
|
+
end
|
6
|
+
|
7
|
+
@@hosts = {}
|
8
|
+
|
9
|
+
def self.add(opts={})
|
10
|
+
@@hosts[opts[:name]] ||= Hadouken::Host.new(opts)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.get(hostname)
|
14
|
+
@@hosts[hostname]
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.exists?(hostname)
|
18
|
+
@@hosts.exists?(hostname)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.count
|
22
|
+
@@hosts.keys.count
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.any?
|
26
|
+
@@hosts.keys.any?
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.each
|
30
|
+
@@hosts.each do |hostname, host|
|
31
|
+
yield host
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.disable_all!
|
36
|
+
each do |host|
|
37
|
+
host.disable! if host.enabled?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class Hadouken::Host
|
43
|
+
attr_reader :name
|
44
|
+
attr_reader :history
|
45
|
+
|
46
|
+
# used to store the net-ssh server object
|
47
|
+
attr_accessor :server
|
48
|
+
|
49
|
+
def initialize(opts={})
|
50
|
+
@name = opts[:name]
|
51
|
+
@enabled = true
|
52
|
+
@history = History.new(self)
|
53
|
+
end
|
54
|
+
|
55
|
+
def disable!
|
56
|
+
history.add :disabled, :noop
|
57
|
+
@enabled = false
|
58
|
+
end
|
59
|
+
|
60
|
+
def enable!
|
61
|
+
history.add :enabled, :noop
|
62
|
+
@enabled = true
|
63
|
+
end
|
64
|
+
|
65
|
+
def enabled?
|
66
|
+
@enabled
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_s
|
70
|
+
name
|
71
|
+
end
|
72
|
+
|
73
|
+
def history_filepath
|
74
|
+
File.join(Hadouken::Hosts.history_filepath, "#{name}.log")
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
class History
|
79
|
+
include Enumerable
|
80
|
+
|
81
|
+
def initialize(host)
|
82
|
+
@host = host
|
83
|
+
@history = []
|
84
|
+
end
|
85
|
+
|
86
|
+
def add(command, status, stdout=nil, stderr=nil, epoch=Time.now.to_f)
|
87
|
+
stdoutJoined = stdout ? stdout.join("\n") : nil
|
88
|
+
stderrJoined = stderr ? stderr.join("\n") : nil
|
89
|
+
@history << [command, status, epoch, stdoutJoined, stderrJoined]
|
90
|
+
File.open(@host.history_filepath, 'a') do |history_file|
|
91
|
+
history_file.write(Yajl::Encoder.encode(command_to_hash(command, status, epoch, stdoutJoined, stderrJoined)))
|
92
|
+
history_file.write("\n")
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def each
|
97
|
+
@history.each do |command, status, epoch, stdout, stderr|
|
98
|
+
yield command, status, epoch, stdout, stderr
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def to_json
|
103
|
+
Yajl::Encoder.encode self.map do |command, status, epoch, stdout, stderr|
|
104
|
+
command_to_hash(command, status, epoch, stdout, stderr)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def command_to_hash(command, status, epoch, stdout, stderr)
|
111
|
+
return {
|
112
|
+
:command => command,
|
113
|
+
:status => status,
|
114
|
+
:time => (epoch * 1000).round,
|
115
|
+
:stdout => stdout,
|
116
|
+
:stderr => stderr
|
117
|
+
}
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
class Hadouken::Plan
|
2
|
+
attr_accessor :name
|
3
|
+
attr_accessor :root
|
4
|
+
attr_accessor :user
|
5
|
+
|
6
|
+
attr_accessor :environment
|
7
|
+
attr_accessor :dry_run
|
8
|
+
attr_accessor :interactive
|
9
|
+
|
10
|
+
attr_accessor :history_path
|
11
|
+
attr_accessor :planfile
|
12
|
+
attr_accessor :artifact
|
13
|
+
|
14
|
+
attr_reader :timestamp
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@tasks = Hadouken::Tasks.new
|
18
|
+
@groups = Hadouken::Groups.new
|
19
|
+
@timestamp = Time.now.utc.strftime("%Y%m%d%H%M%S")
|
20
|
+
end
|
21
|
+
|
22
|
+
def groups
|
23
|
+
@groups
|
24
|
+
end
|
25
|
+
|
26
|
+
def tasks
|
27
|
+
@tasks
|
28
|
+
end
|
29
|
+
|
30
|
+
def dry_run?
|
31
|
+
!!@dry_run
|
32
|
+
end
|
33
|
+
|
34
|
+
def interactive?
|
35
|
+
!!@interactive
|
36
|
+
end
|
37
|
+
|
38
|
+
def env
|
39
|
+
environment
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
def logger
|
44
|
+
Hadouken.logger
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
@@ -0,0 +1,84 @@
|
|
1
|
+
class Hadouken::Runner
|
2
|
+
|
3
|
+
attr_accessor :args
|
4
|
+
attr_accessor :plan
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@args = self.class.optparse
|
8
|
+
@plan = Hadouken::Plan.new
|
9
|
+
@plan.environment = @args[:environment]
|
10
|
+
@plan.dry_run = @args[:dry_run]
|
11
|
+
@plan.interactive = @args[:interactive]
|
12
|
+
@plan.planfile = @args[:planfile]
|
13
|
+
@plan.artifact = @args[:artifact]
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.run!
|
17
|
+
runner = Hadouken::Runner.new
|
18
|
+
plan = runner.plan
|
19
|
+
|
20
|
+
yield(plan)
|
21
|
+
|
22
|
+
ts0 = Time.now
|
23
|
+
Hadouken::Executor.run!(plan)
|
24
|
+
te0 = Time.now
|
25
|
+
|
26
|
+
Hadouken.logger.info "plan executed in %0.2f" % (te0 - ts0)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.optparse
|
30
|
+
args = {}
|
31
|
+
parser = OptionParser.new do |opts|
|
32
|
+
opts.banner = "Usage: #{$0} [options]"
|
33
|
+
opts.separator ""
|
34
|
+
opts.separator "options:"
|
35
|
+
|
36
|
+
opts.on("--interactive", "output stdout/stderr to console") {|o| args[:interactive] = o}
|
37
|
+
opts.on("--dry-run", "take no action" ) {|o| args[:dry_run] = o}
|
38
|
+
opts.on("--env ENV", "stage|production|ding-dong|..." ) {|o| args[:environment] = o}
|
39
|
+
|
40
|
+
opts.on("--history PATH", "where to store history files") do |o|
|
41
|
+
args[:history] = o || 'history'
|
42
|
+
FileUtils.mkdir_p args[:history]
|
43
|
+
Hadouken::Hosts.history_filepath = args[:history]
|
44
|
+
end
|
45
|
+
|
46
|
+
opts.on("--artifact URL", "URL to the service artifact" ) do |o|
|
47
|
+
begin
|
48
|
+
args[:artifact] = URI.parse(o)
|
49
|
+
raise URI::InvalidURIError unless args[:artifact].is_a?(URI::HTTP)
|
50
|
+
rescue URI::InvalidURIError
|
51
|
+
puts "Sorry, invalid artifact url: #{o}"
|
52
|
+
exit 1
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
opts.on("--level LEVEL", "debug|info|warn|error|fatal" ) do |o|
|
57
|
+
if o !~ /^(debug|info|warn|error|fatal)$/i
|
58
|
+
puts "Sorry, I don't know what that log level is ..."
|
59
|
+
exit -1
|
60
|
+
else
|
61
|
+
args[:level] = case o.downcase
|
62
|
+
when /debug/ then Logger::DEBUG
|
63
|
+
when /info/ then Logger::INFO
|
64
|
+
when /warn/ then Logger::WARN
|
65
|
+
when /error/ then Logger::ERROR
|
66
|
+
when /fatal/ then Logger::FATAL
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
Hadouken.logger.level = args[:level] || Logger::INFO
|
71
|
+
end
|
72
|
+
end
|
73
|
+
parser.parse!
|
74
|
+
|
75
|
+
unless args.has_key?(:artifact) &&
|
76
|
+
args.has_key?(:environment)
|
77
|
+
puts parser
|
78
|
+
exit 1
|
79
|
+
end
|
80
|
+
|
81
|
+
return args
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Hadouken::Strategy::Base
|
2
|
+
attr_reader :plan
|
3
|
+
attr_reader :max_hosts
|
4
|
+
attr_reader :traversal
|
5
|
+
|
6
|
+
def initialize(plan, opts={})
|
7
|
+
@plan = plan
|
8
|
+
@max_hosts = opts[:max_hosts]
|
9
|
+
@traversal = opts[:traversal] || :breadth
|
10
|
+
end
|
11
|
+
|
12
|
+
def host_strategy
|
13
|
+
raise ArgumentError, "not implemneted here"
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Hadouken::Strategy::ByGroup < Hadouken::Strategy::Base
|
2
|
+
def host_strategy
|
3
|
+
host_sets = []
|
4
|
+
plan.groups.each do |group|
|
5
|
+
hosts = group.hosts
|
6
|
+
slice = max_hosts || hosts.size
|
7
|
+
|
8
|
+
hosts.each_slice(slice) do |host_slice|
|
9
|
+
host_sets << host_slice
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
balanced
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class Hadouken::Strategy::ByGroupParallel < Hadouken::Strategy::Base
|
2
|
+
def host_strategy
|
3
|
+
return @host_sets if @host_sets
|
4
|
+
|
5
|
+
# transform a array of groups, hosts into a new array that balances
|
6
|
+
# hosts from each group into a single array.
|
7
|
+
@host_sets = []
|
8
|
+
regroup = []
|
9
|
+
groups = []
|
10
|
+
max_size = 0
|
11
|
+
|
12
|
+
plan.groups.each do |group|
|
13
|
+
hosts = group.hosts
|
14
|
+
max_size = [max_size, hosts.size].max
|
15
|
+
groups << hosts
|
16
|
+
end
|
17
|
+
|
18
|
+
[max_size, groups.size].max.times do
|
19
|
+
groups.each do |hosts|
|
20
|
+
if hosts.size == 0
|
21
|
+
#TODO groups.delete(name)
|
22
|
+
else
|
23
|
+
regroup << hosts.shift
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
regroup.each_slice(max_size) do |host_slice|
|
29
|
+
@host_sets << host_slice
|
30
|
+
end
|
31
|
+
|
32
|
+
@balanced
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class Hadouken::Strategy::ByHost < Hadouken::Strategy::Base
|
2
|
+
def host_strategy
|
3
|
+
hosts = plan.groups.map{|g| g.hosts}.flatten.uniq
|
4
|
+
slice = max_hosts || hosts.size
|
5
|
+
host_sets = []
|
6
|
+
|
7
|
+
slice = max_hosts || hosts.size
|
8
|
+
hosts.each_slice(slice) do |host_slice|
|
9
|
+
host_sets << host_slice
|
10
|
+
end
|
11
|
+
|
12
|
+
host_sets
|
13
|
+
end
|
14
|
+
end
|