hadouken 0.1.4.pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -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