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.
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "net-ssh"
4
+ gem "net-ssh-multi"
5
+ gem "yajl-ruby"
6
+
7
+ group :development do
8
+ gem "shoulda", ">= 0"
9
+ gem "mocha", ">= 0"
10
+ gem "bundler", "~> 1.0.0"
11
+ gem "jeweler", "~> 1.6.4"
12
+ gem "rcov", ">= 0"
13
+ end
@@ -0,0 +1,32 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ git (1.2.5)
5
+ jeweler (1.6.4)
6
+ bundler (~> 1.0)
7
+ git (>= 1.2.5)
8
+ rake
9
+ mocha (0.9.12)
10
+ net-ssh (2.2.1)
11
+ net-ssh-gateway (1.1.0)
12
+ net-ssh (>= 1.99.1)
13
+ net-ssh-multi (1.1)
14
+ net-ssh (>= 2.1.4)
15
+ net-ssh-gateway (>= 0.99.0)
16
+ rake (0.9.2.2)
17
+ rcov (0.9.10)
18
+ shoulda (2.11.3)
19
+ yajl-ruby (1.0.0)
20
+
21
+ PLATFORMS
22
+ ruby
23
+
24
+ DEPENDENCIES
25
+ bundler (~> 1.0.0)
26
+ jeweler (~> 1.6.4)
27
+ mocha
28
+ net-ssh
29
+ net-ssh-multi
30
+ rcov
31
+ shoulda
32
+ yajl-ruby
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Matt Knopp
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,55 @@
1
+ hadouken - soon
2
+
3
+ ### running
4
+
5
+ ./serviceie.rb --interactive \
6
+ --level debug \
7
+ --environment production \
8
+ --history /opt/deploys \
9
+ --artifact https://artifacts/latest.tgz
10
+
11
+
12
+ ### serviceie.rb
13
+
14
+ Hadouken::Runner.run!
15
+ plan = Hadouken::Plan.new
16
+ plan.name = "serviceie"
17
+ plan.user = "serviceie"
18
+ plan.base = "/opt/serviceie"
19
+
20
+ # define some groups 10x10
21
+ #
22
+ plan.add_group :web, :range => (1..10), :pattern => 'serviceie-web-%02d.example.com'
23
+ plan.add_group :api, :range => (1..10), :pattern => 'serviceie-api-%02d.example.com'
24
+
25
+
26
+ # download latest.tgz from our artifact repository
27
+ # runs in parallel on all hosts
28
+ #
29
+ plan.tasks.add Hadouken::Strategy::ByHost.new(plan)
30
+ plan.tasks.add "curl -sSfL -output /tmp/latest.tgz #{artifact}"
31
+ plan.tasks.add "mv /tmp/latest.tgz #{plan.base}/latest.tgz"
32
+
33
+
34
+ # runs commands depth first on the api hosts, two at a time
35
+ # - restart service
36
+ # - verify service
37
+ #
38
+ plan.tasks.add Hadouken::Strategy::ByHost.new(plan, :max_hosts => 2, :traversal => :depth)
39
+ plan.tasks.add "restart serviceie-api", :group => :api
40
+ plan.tasks.add Proc.new { |opts|
41
+ host = opts[:host]
42
+ 10.times do
43
+ response = Typheous::Request.get("http://#{host}:8081/healthcheck")
44
+ break if response.status_code == 200
45
+ end
46
+ }, :group => :api
47
+
48
+
49
+ # finally restart the webs as fast as possible
50
+ #
51
+ plan.tasks.add Hadouken::Strategy::ByHost.new(plan)
52
+ plan.tasks.add "restart windard-web, :group => :web
53
+ end
54
+
55
+
@@ -0,0 +1,53 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "hadouken"
18
+ gem.homepage = "http://github.com/mhat/hadouken"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{run commands over ssh in a way that makes sense for deploying artifacts}
21
+ gem.description = %Q{run commands over ssh in a way that makes sense for deploying artifacts}
22
+ gem.email = ["mknopp@yammer-inc.com, cgray@yammer-inc.com"]
23
+ gem.authors = ["Matt Knopp", "Chris Gray"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rake/testtask'
29
+ Rake::TestTask.new(:test) do |test|
30
+ test.libs << 'lib' << 'test'
31
+ test.pattern = 'test/**/test_*.rb'
32
+ test.verbose = true
33
+ end
34
+
35
+ require 'rcov/rcovtask'
36
+ Rcov::RcovTask.new do |test|
37
+ test.libs << 'test'
38
+ test.pattern = 'test/**/test_*.rb'
39
+ test.verbose = true
40
+ test.rcov_opts << '--exclude "gems/*"'
41
+ end
42
+
43
+ task :default => :test
44
+
45
+ #require 'rdoc/task'
46
+ #Rake::RDocTask.new do |rdoc|
47
+ # version = File.exist?('VERSION') ? File.read('VERSION') : ""
48
+ #
49
+ # rdoc.rdoc_dir = 'rdoc'
50
+ # rdoc.title = "hadouken #{version}"
51
+ # rdoc.rdoc_files.include('README*')
52
+ # rdoc.rdoc_files.include('lib/**/*.rb')
53
+ #end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.4.pre
@@ -0,0 +1,84 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "hadouken"
8
+ s.version = "0.1.4.pre"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new("> 1.3.1") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Matt Knopp", "Chris Gray"]
12
+ s.date = "2012-01-27"
13
+ s.description = "run commands over ssh in a way that makes sense for deploying artifacts"
14
+ s.email = ["mknopp@yammer-inc.com, cgray@yammer-inc.com"]
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.md"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ "Gemfile",
22
+ "Gemfile.lock",
23
+ "LICENSE.txt",
24
+ "README.md",
25
+ "Rakefile",
26
+ "VERSION",
27
+ "hadouken.gemspec",
28
+ "lib/hadouken.rb",
29
+ "lib/hadouken/executor.rb",
30
+ "lib/hadouken/ext/net_ssh_multi_session_actions.rb",
31
+ "lib/hadouken/group.rb",
32
+ "lib/hadouken/groups.rb",
33
+ "lib/hadouken/host.rb",
34
+ "lib/hadouken/plan.rb",
35
+ "lib/hadouken/runner.rb",
36
+ "lib/hadouken/strategy/base.rb",
37
+ "lib/hadouken/strategy/by_group.rb",
38
+ "lib/hadouken/strategy/by_group_parallel.rb",
39
+ "lib/hadouken/strategy/by_host.rb",
40
+ "lib/hadouken/task.rb",
41
+ "lib/hadouken/tasks.rb",
42
+ "test/helper.rb",
43
+ "test/test_hadouken.rb"
44
+ ]
45
+ s.homepage = "http://github.com/mhat/hadouken"
46
+ s.licenses = ["MIT"]
47
+ s.require_paths = ["lib"]
48
+ s.rubygems_version = "1.8.11"
49
+ s.summary = "run commands over ssh in a way that makes sense for deploying artifacts"
50
+
51
+ if s.respond_to? :specification_version then
52
+ s.specification_version = 3
53
+
54
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
55
+ s.add_runtime_dependency(%q<net-ssh>, [">= 0"])
56
+ s.add_runtime_dependency(%q<net-ssh-multi>, [">= 0"])
57
+ s.add_runtime_dependency(%q<yajl-ruby>, [">= 0"])
58
+ s.add_development_dependency(%q<shoulda>, [">= 0"])
59
+ s.add_development_dependency(%q<mocha>, [">= 0"])
60
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
61
+ s.add_development_dependency(%q<jeweler>, ["~> 1.6.4"])
62
+ s.add_development_dependency(%q<rcov>, [">= 0"])
63
+ else
64
+ s.add_dependency(%q<net-ssh>, [">= 0"])
65
+ s.add_dependency(%q<net-ssh-multi>, [">= 0"])
66
+ s.add_dependency(%q<yajl-ruby>, [">= 0"])
67
+ s.add_dependency(%q<shoulda>, [">= 0"])
68
+ s.add_dependency(%q<mocha>, [">= 0"])
69
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
70
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
71
+ s.add_dependency(%q<rcov>, [">= 0"])
72
+ end
73
+ else
74
+ s.add_dependency(%q<net-ssh>, [">= 0"])
75
+ s.add_dependency(%q<net-ssh-multi>, [">= 0"])
76
+ s.add_dependency(%q<yajl-ruby>, [">= 0"])
77
+ s.add_dependency(%q<shoulda>, [">= 0"])
78
+ s.add_dependency(%q<mocha>, [">= 0"])
79
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
80
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
81
+ s.add_dependency(%q<rcov>, [">= 0"])
82
+ end
83
+ end
84
+
@@ -0,0 +1,29 @@
1
+ module Hadouken; end;
2
+ module Hadouken::Strategy; end;
3
+
4
+ require 'rubygems'
5
+ require 'fileutils'
6
+ require 'yajl'
7
+ require 'uri'
8
+ require 'net/ssh/multi'
9
+
10
+ require 'hadouken/executor'
11
+ require 'hadouken/group'
12
+ require 'hadouken/groups'
13
+ require 'hadouken/host'
14
+ require 'hadouken/plan'
15
+ require 'hadouken/runner'
16
+ require 'hadouken/strategy/base'
17
+ require 'hadouken/strategy/by_host'
18
+ require 'hadouken/strategy/by_group'
19
+ require 'hadouken/strategy/by_group_parallel'
20
+ require 'hadouken/task'
21
+ require 'hadouken/tasks'
22
+
23
+ require 'hadouken/ext/net_ssh_multi_session_actions'
24
+
25
+ module Hadouken
26
+ def self.logger
27
+ @@logger ||= Logger.new(STDOUT)
28
+ end
29
+ end
@@ -0,0 +1,236 @@
1
+ class Hadouken::Executor
2
+
3
+ class Phase
4
+ attr_accessor :strategy
5
+ attr_accessor :tasks
6
+ def initialize
7
+ @tasks = []
8
+ end
9
+ end
10
+
11
+ attr_reader :plan
12
+
13
+ def self.run!(plan)
14
+ exec = Hadouken::Executor.new(plan)
15
+ exec.phases
16
+ exec.session!
17
+
18
+ if Hadouken::Hosts.any?
19
+ exec.execute!
20
+ else
21
+ Hadouken.logger.error "No hosts have been defined, this deploy is boring!"
22
+ end
23
+ end
24
+
25
+
26
+ def initialize(plan)
27
+ @plan = plan
28
+ @session = Net::SSH::Multi.start
29
+
30
+ ## TODO: find a better place for this
31
+ unless @plan.tasks.first.is_a?(Hadouken::Strategy::Base)
32
+ raise RuntimeError, "first task in plan is not a strategy"
33
+ end
34
+ end
35
+
36
+
37
+ def phases
38
+ return @phases if @phases
39
+ @phases = []
40
+ plan.tasks.each do |task|
41
+ if task.is_a?(Hadouken::Strategy::Base)
42
+ @phases << Phase.new
43
+ @phases.last.strategy = task
44
+ end
45
+
46
+ if task.is_a?(Hadouken::Task::Base)
47
+ @phases.last.tasks << task
48
+ end
49
+ end
50
+
51
+ @phases
52
+ end
53
+
54
+ def session
55
+ @session ||= session!
56
+ end
57
+
58
+ def session!
59
+ @session = Net::SSH::Multi.start(:on_error => Proc.new{ |server|
60
+ host = Hadouken::Hosts.get(server.host)
61
+ Hadouken.logger.debug "error with #{server.host}, disabling"
62
+ host.history.add "ssh.connection.new", :fail
63
+ host.disable!
64
+ })
65
+
66
+ plan.groups.each do |group|
67
+ group.hosts.each do |host|
68
+ unless host.server
69
+ Hadouken.logger.debug "session.use #{plan.user}@#{host}"
70
+ server = session.use "#{plan.user}@#{host}"
71
+ host.server = server
72
+ end
73
+ end
74
+ end
75
+
76
+ @session
77
+ end
78
+
79
+ def execute!
80
+ # the heavy lifting: pivot our structure
81
+ # from tasks : task : hosts[]
82
+ # to hosts : host : tasks[]
83
+ phases.each_with_index do |phase, phase_index|
84
+ strategy = phase.strategy
85
+ hosts_with_tasks = {}
86
+ host_sets = strategy.host_strategy
87
+ Hadouken.logger.info "idx:#{phase_index}, strategy=#{strategy}"
88
+
89
+ ## assign work
90
+ host_sets.each do |host_set|
91
+ Hadouken.logger.info "hosts=#{host_set.join(', ')}"
92
+
93
+ host_set.each do |host|
94
+ hosts_with_tasks[host] ||= []
95
+
96
+ phase.tasks.each do |task|
97
+ # if this is not a group task then assign it to the host OR if
98
+ # this is a group task and the host is part of the task-group,
99
+ # then assign the task to the host
100
+ if !task.group? || (task.group? && task.group.has_host?(host))
101
+ hosts_with_tasks[host] << task
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ case strategy.traversal
108
+ when :breadth then execute_breadth_traversal! host_sets, hosts_with_tasks
109
+ when :depth then execute_depth_traversal! host_sets, hosts_with_tasks
110
+ else raise RuntimeError, "unknown tranversal=#{strategy.traversal}"
111
+ end
112
+ end
113
+ end
114
+
115
+ private
116
+ def execute_depth_traversal!(host_sets, hosts_with_tasks)
117
+ # run all of the commands assigned to the hosts in a host_set then
118
+ # move on to the next host set. rinse. repeat.
119
+
120
+ host_sets.each do |host_set|
121
+ while hosts_with_tasks.values_at(*host_set.map).select{|t| t.any?}.any?
122
+ channels = []
123
+ host_set.each do |host|
124
+ # not all hosts will necessarily have the same number of tasks
125
+ next unless hosts_with_tasks[host].any?
126
+ next unless task = hosts_with_tasks[host].shift
127
+
128
+ case task
129
+ when Hadouken::Task::Callback then Hadouken.logger.debug "callback for #{host}"
130
+ when Hadouken::Task::Command then Hadouken.logger.debug "session.on(#{host}).exec(#{task.command})"
131
+ end
132
+
133
+ if ! plan.dry_run?
134
+ if ! host.enabled?
135
+ case task
136
+ when Hadouken::Task::Callback then host.history.add task.to_s, :noop
137
+ when Hadouken::Task::Command then host.history.add task.command, :noop
138
+ end
139
+ else
140
+ case task
141
+ when Hadouken::Task::Callback
142
+ ret = task.call({:host => host})
143
+ host.history.add task.to_s, ret
144
+ host.disable! unless ret == 0
145
+
146
+ when Hadouken::Task::Command
147
+ Hadouken.logger.info "running #{task.command} on #{host}"
148
+ channels << [task.command, session.on(host.server).hadouken_exec(task.command)]
149
+ end
150
+ end
151
+ end
152
+ end
153
+
154
+ # wait for the work assigned to complete before performing more work.
155
+ wait_on_channels(channels)
156
+
157
+ end # while hosts_with_tasks
158
+ end # host_sets.each
159
+ end
160
+
161
+
162
+ def execute_breadth_traversal!(host_sets, hosts_with_tasks)
163
+ # perform whatever tasks have been assigned; i try to do as much as
164
+ # possible in parallel within the terms of the current strategy.
165
+
166
+ while hosts_with_tasks.any?
167
+
168
+ host_sets.each do |host_set|
169
+ channels = []
170
+ host_set.each do |host|
171
+ if hosts_with_tasks.has_key?(host)
172
+
173
+ unless task = hosts_with_tasks[host].shift
174
+ # remove the host from hosts-with-tasks when there are no more tasks!
175
+ hosts_with_tasks.delete(host)
176
+ else
177
+ case task
178
+ when Hadouken::Task::Callback then Hadouken.logger.debug "callback for #{host}"
179
+ when Hadouken::Task::Command then Hadouken.logger.debug "session.on(#{host}).exec(#{task.command})"
180
+ end
181
+
182
+ if ! plan.dry_run?
183
+ if ! host.enabled?
184
+ case task
185
+ when Hadouken::Task::Callback then host.history.add task.to_s, :noop
186
+ when Hadouken::Task::Command then host.history.add task.command, :noop
187
+ end
188
+ else
189
+ case task
190
+ when Hadouken::Task::Callback
191
+ ret = task.call({:host => host})
192
+ host.history.add task.to_s, ret
193
+ host.disable! unless ret == 0
194
+ when Hadouken::Task::Command
195
+ Hadouken.logger.info "running #{task.command} on #{host}"
196
+ channels << [task.command, session.on(host.server).hadouken_exec(task.command)]
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end
202
+ end
203
+
204
+ # wait for the work assigned to complete before performing more work.
205
+ wait_on_channels(channels)
206
+
207
+ end # host_sets.each
208
+ end # while
209
+ end
210
+
211
+ def wait_on_channels(channels)
212
+ if channels.count > 0
213
+ Hadouken.logger.info "waiting for #{channels.count} commands to execute"
214
+ return if plan.dry_run?
215
+
216
+ session.loop
217
+ channels.each do |command, channel|
218
+ channel.each do |subchannel|
219
+ host = Hadouken::Hosts.get(subchannel[:host])
220
+ host.history.add(command, subchannel[:exit_status], subchannel[:stdout], subchannel[:stderr])
221
+ if plan.interactive?
222
+ Hadouken.logger.info "[STDOUT] - #{host.name}: %s" % [ subchannel[:stdout].join("\n") ] if subchannel[:stdout]
223
+ Hadouken.logger.warn "[STDERR] - #{host.name}: %s" % [ subchannel[:stderr].join("\n") ] if subchannel[:stderr]
224
+ end
225
+
226
+ unless subchannel[:exit_status] == 0
227
+ Hadouken.logger.debug "got status=#{subchannel[:exit_status]} on #{subchannel[:host]}"
228
+ host.disable!
229
+ end
230
+ end
231
+ end
232
+ end
233
+ end
234
+
235
+
236
+ end