hadouken 0.1.4.pre
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.
- 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
|