switchtower 0.9.0

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,53 @@
1
+
2
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
3
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
4
+
5
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
6
+
7
+ <head>
8
+ <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
9
+ <title>System down for maintenance</title>
10
+
11
+ <style type="text/css">
12
+ div.outer {
13
+ position: absolute;
14
+ left: 50%;
15
+ top: 50%;
16
+ width: 500px;
17
+ height: 300px;
18
+ margin-left: -260px;
19
+ margin-top: -150px;
20
+ }
21
+
22
+ .DialogBody {
23
+ margin: 0;
24
+ padding: 10px;
25
+ text-align: left;
26
+ border: 1px solid #ccc;
27
+ border-right: 1px solid #999;
28
+ border-bottom: 1px solid #999;
29
+ background-color: #fff;
30
+ }
31
+
32
+ body { background-color: #fff; }
33
+ </style>
34
+ </head>
35
+
36
+ <body>
37
+
38
+ <div class="outer">
39
+ <div class="DialogBody" style="text-align: center;">
40
+ <div style="text-align: center; width: 200px; margin: 0 auto;">
41
+ <p style="color: red; font-size: 16px; line-height: 20px;">
42
+ The system is down for <%= reason ? reason : "maintenance" %>
43
+ as of <%= Time.now.strftime("%H:%M %Z") %>.
44
+ </p>
45
+ <p style="color: #666;">
46
+ It'll be back <%= deadline ? "by #{deadline}" : "shortly" %>.
47
+ </p>
48
+ </div>
49
+ </div>
50
+ </div>
51
+
52
+ </body>
53
+ </html>
@@ -0,0 +1,43 @@
1
+ module SwitchTower
2
+ module SCM
3
+
4
+ # The ancestor class of the various SCM module implementations.
5
+ class Base
6
+ attr_reader :configuration
7
+
8
+ def initialize(configuration) #:nodoc:
9
+ @configuration = configuration
10
+ end
11
+
12
+ def latest_revision
13
+ nil
14
+ end
15
+
16
+ def current_revision(actor)
17
+ raise "#{self.class} doesn't support querying the deployed revision"
18
+ end
19
+
20
+ def diff(actor, from=nil, to=nil)
21
+ raise "#{self.class} doesn't support diff(from, to)"
22
+ end
23
+
24
+ private
25
+
26
+ def run_checkout(actor, guts, &block)
27
+ log = "#{configuration.deploy_to}/revisions.log"
28
+ directory = File.basename(configuration.release_path)
29
+
30
+ command = <<-STR
31
+ if [[ ! -d #{configuration.release_path} ]]; then
32
+ #{guts}
33
+ echo `date +"%Y-%m-%d %H:%M:%S"` $USER #{configuration.revision} #{directory} >> #{log};
34
+ chmod 666 #{log};
35
+ fi
36
+ STR
37
+
38
+ actor.run(command, &block)
39
+ end
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,73 @@
1
+ require 'time'
2
+ require 'switchtower/scm/base'
3
+
4
+ module SwitchTower
5
+ module SCM
6
+
7
+ # An SCM module for using CVS as your source control tool. You can
8
+ # specify it by placing the following line in your configuration:
9
+ #
10
+ # set :scm, :cvs
11
+ #
12
+ # Also, this module accepts a <tt>:cvs</tt> configuration variable,
13
+ # which (if specified) will be used as the full path to the cvs
14
+ # executable on the remote machine:
15
+ #
16
+ # set :cvs, "/opt/local/bin/cvs"
17
+ #
18
+ # You can specify the location of your local copy (used to query
19
+ # the revisions, etc.) via the <tt>:local</tt> variable, which defaults to
20
+ # ".".
21
+ #
22
+ # Also, you can specify the CVS_RSH variable to use on the remote machine(s)
23
+ # via the <tt>:cvs_rsh</tt> variable. This defaults to the value of the
24
+ # CVS_RSH environment variable locally, or if it is not set, to "ssh".
25
+ class Cvs < Base
26
+ # Return a string representing the date of the last revision (CVS is
27
+ # seriously retarded, in that it does not give you a way to query when
28
+ # the last revision was made to the repository, so this is a fairly
29
+ # expensive operation...)
30
+ def latest_revision
31
+ return @latest_revision if @latest_revision
32
+ configuration.logger.debug "querying latest revision..."
33
+ @latest_revision = cvs_log(configuration.local).
34
+ split(/\r?\n/).
35
+ grep(/^date: (.*?);/) { Time.parse($1).strftime("%F %T") }.
36
+ sort.
37
+ last
38
+ end
39
+
40
+ # Check out (on all servers associated with the current task) the latest
41
+ # revision. Uses the given actor instance to execute the command.
42
+ def checkout(actor)
43
+ cvs = configuration[:cvs] || "cvs"
44
+ cvs_rsh = configuration[:cvs_rsh] || ENV['CVS_RSH'] || "ssh"
45
+
46
+ command = <<-CMD
47
+ cd #{configuration.releases_path};
48
+ CVS_RSH="#{cvs_rsh}" #{cvs} -d #{configuration.repository} -Q co -D "#{configuration.revision}" -d #{File.basename(actor.release_path)} #{actor.application};
49
+ CMD
50
+
51
+ run_checkout(actor, command) do |ch, stream, out|
52
+ prefix = "#{stream} :: #{ch[:host]}"
53
+ actor.logger.info out, prefix
54
+ if out =~ %r{password:}
55
+ actor.logger.info "CVS is asking for a password", prefix
56
+ ch.send_data "#{actor.password}\n"
57
+ elsif out =~ %r{^Enter passphrase}
58
+ message = "CVS needs your key's passphrase and cannot proceed"
59
+ actor.logger.info message, prefix
60
+ raise message
61
+ end
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def cvs_log(path)
68
+ `cd #{path || "."} && cvs -q log -N -rHEAD`
69
+ end
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1,27 @@
1
+ require 'switchtower/scm/base'
2
+
3
+ module SwitchTower
4
+ module SCM
5
+
6
+ # An SCM module for using darcs as your source control tool. Use it by
7
+ # specifying the following line in your configuration:
8
+ #
9
+ # set :scm, :darcs
10
+ #
11
+ # Also, this module accepts a <tt>:darcs</tt> configuration variable,
12
+ # which (if specified) will be used as the full path to the darcs
13
+ # executable on the remote machine:
14
+ #
15
+ # set :darcs, "/opt/local/bin/darcs"
16
+ class Darcs < Base
17
+ # Check out (on all servers associated with the current task) the latest
18
+ # revision. Uses the given actor instance to execute the command.
19
+ def checkout(actor)
20
+ darcs = configuration[:darcs] ? configuration[:darcs] : "darcs"
21
+ revision = configuration[:revision] ? %(--to-match "#{configuration.revision}") : ""
22
+ run_checkout(actor, "#{darcs} get -q --set-scripts-executable #{revision} #{configuration.repository} #{actor.release_path};")
23
+ end
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,104 @@
1
+ require 'switchtower/scm/base'
2
+
3
+ module SwitchTower
4
+ module SCM
5
+
6
+ # An SCM module for using subversion as your source control tool. This
7
+ # module is used by default, but you can explicitly specify it by
8
+ # placing the following line in your configuration:
9
+ #
10
+ # set :scm, :subversion
11
+ #
12
+ # Also, this module accepts a <tt>:svn</tt> configuration variable,
13
+ # which (if specified) will be used as the full path to the svn
14
+ # executable on the remote machine:
15
+ #
16
+ # set :svn, "/opt/local/bin/svn"
17
+ class Subversion < Base
18
+ # Return an integer identifying the last known revision in the svn
19
+ # repository. (This integer is currently the revision number.) If latest
20
+ # revision does not exist in the given repository, this routine will
21
+ # walk up the directory tree until it finds it.
22
+ def latest_revision
23
+ configuration.logger.debug "querying latest revision..." unless @latest_revision
24
+ repo = configuration.repository
25
+ until @latest_revision
26
+ match = svn_log(repo).scan(/r(\d+)/).first
27
+ @latest_revision = match ? match.first : nil
28
+ if @latest_revision.nil?
29
+ # if a revision number was not reported, move up a level in the path
30
+ # and try again.
31
+ repo = File.dirname(repo)
32
+ end
33
+ end
34
+ @latest_revision
35
+ end
36
+
37
+ # Return the number of the revision currently deployed.
38
+ def current_revision(actor)
39
+ latest = actor.releases.last
40
+ grep = %(grep " #{latest}$" #{configuration.deploy_to}/revisions.log)
41
+ result = ""
42
+ actor.run(grep, :once => true) do |ch, str, out|
43
+ result << out if str == :out
44
+ raise "could not determine current revision" if str == :err
45
+ end
46
+
47
+ date, time, user, rev, dir = result.split
48
+ raise "current revision not found in revisions.log" unless dir == latest
49
+
50
+ rev.to_i
51
+ end
52
+
53
+ # Return a string containing the diff between the two revisions. +from+
54
+ # and +to+ may be in any format that svn recognizes as a valid revision
55
+ # identifier. If +from+ is +nil+, it defaults to the last deployed
56
+ # revision. If +to+ is +nil+, it defaults to HEAD.
57
+ def diff(actor, from=nil, to=nil)
58
+ from ||= current_revision(actor)
59
+ to ||= "HEAD"
60
+
61
+ `svn diff #{configuration.repository}@#{from} #{configuration.repository}@#{to}`
62
+ end
63
+
64
+ # Check out (on all servers associated with the current task) the latest
65
+ # revision. Uses the given actor instance to execute the command. If
66
+ # svn asks for a password this will automatically provide it (assuming
67
+ # the requested password is the same as the password for logging into the
68
+ # remote server.)
69
+ def checkout(actor)
70
+ svn = configuration[:svn] ? configuration[:svn] : "svn"
71
+
72
+ command = "#{svn} co -q -r#{configuration.revision} #{configuration.repository} #{actor.release_path};"
73
+
74
+ run_checkout(actor, command) do |ch, stream, out|
75
+ prefix = "#{stream} :: #{ch[:host]}"
76
+ actor.logger.info out, prefix
77
+ if out =~ /^Password.*:/
78
+ actor.logger.info "subversion is asking for a password", prefix
79
+ ch.send_data "#{actor.password}\n"
80
+ elsif out =~ %r{\(yes/no\)}
81
+ actor.logger.info "subversion is asking whether to connect or not",
82
+ prefix
83
+ ch.send_data "yes\n"
84
+ elsif out =~ %r{passphrase}
85
+ message = "subversion needs your key's passphrase, sending empty string"
86
+ actor.logger.info message, prefix
87
+ ch.send_data "\n"
88
+ elsif out =~ %r{The entry \'(\w+)\' is no longer a directory}
89
+ message = "subversion can't update because directory '#{$1}' was replaced. Please add it to svn:ignore."
90
+ actor.logger.info message, prefix
91
+ raise message
92
+ end
93
+ end
94
+ end
95
+
96
+ private
97
+
98
+ def svn_log(path)
99
+ `svn log -q -rhead #{path}`
100
+ end
101
+ end
102
+
103
+ end
104
+ end
@@ -0,0 +1,30 @@
1
+ require 'net/ssh'
2
+
3
+ module SwitchTower
4
+ # A helper class for dealing with SSH connections.
5
+ class SSH
6
+ # An abstraction to make it possible to connect to the server via public key
7
+ # without prompting for the password. If the public key authentication fails
8
+ # this will fall back to password authentication.
9
+ #
10
+ # If a block is given, the new session is yielded to it, otherwise the new
11
+ # session is returned.
12
+ def self.connect(server, config, port=22, &block)
13
+ methods = [ %w(publickey hostbased), %w(password keyboard-interactive) ]
14
+ password_value = nil
15
+
16
+ begin
17
+ Net::SSH.start(server,
18
+ :username => config.user,
19
+ :password => password_value,
20
+ :port => port,
21
+ :auth_methods => methods.shift,
22
+ &block)
23
+ rescue Net::SSH::AuthenticationFailed
24
+ raise if methods.empty?
25
+ password_value = config.password
26
+ retry
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,9 @@
1
+ module SwitchTower
2
+ module Version #:nodoc:
3
+ MAJOR = 0
4
+ MINOR = 9
5
+ TINY = 0
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join(".")
8
+ end
9
+ end
@@ -0,0 +1,261 @@
1
+ $:.unshift File.dirname(__FILE__) + "/../lib"
2
+
3
+ require 'stringio'
4
+ require 'test/unit'
5
+ require 'switchtower/actor'
6
+ require 'switchtower/logger'
7
+
8
+ class ActorTest < Test::Unit::TestCase
9
+
10
+ class TestingConnectionFactory
11
+ def initialize(config)
12
+ end
13
+
14
+ def connect_to(server)
15
+ server
16
+ end
17
+ end
18
+
19
+ class GatewayConnectionFactory
20
+ def connect_to(server)
21
+ server
22
+ end
23
+ end
24
+
25
+ class TestingCommand
26
+ def self.invoked!
27
+ @invoked = true
28
+ end
29
+
30
+ def self.invoked?
31
+ @invoked
32
+ end
33
+
34
+ def self.reset!
35
+ @invoked = nil
36
+ end
37
+
38
+ def initialize(*args)
39
+ end
40
+
41
+ def process!
42
+ self.class.invoked!
43
+ end
44
+ end
45
+
46
+ class TestActor < SwitchTower::Actor
47
+ attr_reader :factory
48
+
49
+ self.connection_factory = TestingConnectionFactory
50
+ self.command_factory = TestingCommand
51
+
52
+ def establish_gateway
53
+ GatewayConnectionFactory.new
54
+ end
55
+ end
56
+
57
+ class MockConfiguration
58
+ Role = Struct.new(:host, :options)
59
+
60
+ attr_accessor :gateway, :pretend
61
+
62
+ def delegated_method
63
+ "result of method"
64
+ end
65
+
66
+ ROLES = { :db => [ Role.new("01.example.com", :primary => true),
67
+ Role.new("02.example.com", {}),
68
+ Role.new("all.example.com", {})],
69
+ :web => [ Role.new("03.example.com", {}),
70
+ Role.new("04.example.com", {}),
71
+ Role.new("all.example.com", {})],
72
+ :app => [ Role.new("05.example.com", {}),
73
+ Role.new("06.example.com", {}),
74
+ Role.new("07.example.com", {}),
75
+ Role.new("all.example.com", {})] }
76
+
77
+ def roles
78
+ ROLES
79
+ end
80
+
81
+ def logger
82
+ @logger ||= SwitchTower::Logger.new(:output => StringIO.new)
83
+ end
84
+ end
85
+
86
+ def setup
87
+ TestingCommand.reset!
88
+ @actor = TestActor.new(MockConfiguration.new)
89
+ end
90
+
91
+ def test_define_task_creates_method
92
+ @actor.define_task :hello do
93
+ "result"
94
+ end
95
+ assert @actor.respond_to?(:hello)
96
+ assert_equal "result", @actor.hello
97
+ end
98
+
99
+ def test_define_task_with_successful_transaction
100
+ class << @actor
101
+ attr_reader :rolled_back
102
+ attr_reader :history
103
+ end
104
+
105
+ @actor.define_task :hello do
106
+ (@history ||= []) << :hello
107
+ on_rollback { @rolled_back = true }
108
+ "hello"
109
+ end
110
+
111
+ @actor.define_task :goodbye do
112
+ (@history ||= []) << :goodbye
113
+ transaction do
114
+ hello
115
+ end
116
+ "goodbye"
117
+ end
118
+
119
+ assert_nothing_raised { @actor.goodbye }
120
+ assert !@actor.rolled_back
121
+ assert_equal [:goodbye, :hello], @actor.history
122
+ end
123
+
124
+ def test_define_task_with_failed_transaction
125
+ class << @actor
126
+ attr_reader :rolled_back
127
+ attr_reader :history
128
+ end
129
+
130
+ @actor.define_task :hello do
131
+ (@history ||= []) << :hello
132
+ on_rollback { @rolled_back = true }
133
+ "hello"
134
+ end
135
+
136
+ @actor.define_task :goodbye do
137
+ (@history ||= []) << :goodbye
138
+ transaction do
139
+ hello
140
+ raise "ouch"
141
+ end
142
+ "goodbye"
143
+ end
144
+
145
+ assert_raise(RuntimeError) do
146
+ @actor.goodbye
147
+ end
148
+
149
+ assert @actor.rolled_back
150
+ assert_equal [:goodbye, :hello], @actor.history
151
+ end
152
+
153
+ def test_delegates_to_configuration
154
+ @actor.define_task :hello do
155
+ delegated_method
156
+ end
157
+ assert_equal "result of method", @actor.hello
158
+ end
159
+
160
+ def test_task_servers_with_duplicates
161
+ @actor.define_task :foo do
162
+ run "do this"
163
+ end
164
+
165
+ assert_equal %w(01.example.com 02.example.com 03.example.com 04.example.com 05.example.com 06.example.com 07.example.com all.example.com), @actor.tasks[:foo].servers(@actor.configuration).sort
166
+ end
167
+
168
+ def test_run_in_task_without_explicit_roles_selects_all_roles
169
+ @actor.define_task :foo do
170
+ run "do this"
171
+ end
172
+
173
+ @actor.foo
174
+ assert_equal %w(01.example.com 02.example.com 03.example.com 04.example.com 05.example.com 06.example.com 07.example.com all.example.com), @actor.sessions.keys.sort
175
+ end
176
+
177
+ def test_run_in_task_with_single_role_selects_that_role
178
+ @actor.define_task :foo, :roles => :db do
179
+ run "do this"
180
+ end
181
+
182
+ @actor.foo
183
+ assert_equal %w(01.example.com 02.example.com all.example.com), @actor.sessions.keys.sort
184
+ end
185
+
186
+ def test_run_in_task_with_multiple_roles_selects_those_roles
187
+ @actor.define_task :foo, :roles => [:db, :web] do
188
+ run "do this"
189
+ end
190
+
191
+ @actor.foo
192
+ assert_equal %w(01.example.com 02.example.com 03.example.com 04.example.com all.example.com), @actor.sessions.keys.sort
193
+ end
194
+
195
+ def test_run_in_task_with_only_restricts_selected_roles
196
+ @actor.define_task :foo, :roles => :db, :only => { :primary => true } do
197
+ run "do this"
198
+ end
199
+
200
+ @actor.foo
201
+ assert_equal %w(01.example.com), @actor.sessions.keys.sort
202
+ end
203
+
204
+ def test_establish_connection_uses_gateway_if_specified
205
+ @actor.configuration.gateway = "10.example.com"
206
+ @actor.define_task :foo, :roles => :db do
207
+ run "do this"
208
+ end
209
+
210
+ @actor.foo
211
+ assert_instance_of GatewayConnectionFactory, @actor.factory
212
+ end
213
+
214
+ def test_run_when_not_pretend
215
+ @actor.define_task :foo do
216
+ run "do this"
217
+ end
218
+
219
+ @actor.configuration.pretend = false
220
+ @actor.foo
221
+ assert TestingCommand.invoked?
222
+ end
223
+
224
+ def test_run_when_pretend
225
+ @actor.define_task :foo do
226
+ run "do this"
227
+ end
228
+
229
+ @actor.configuration.pretend = true
230
+ @actor.foo
231
+ assert !TestingCommand.invoked?
232
+ end
233
+
234
+ def test_task_before_hook
235
+ history = []
236
+ @actor.define_task :foo do
237
+ history << "foo"
238
+ end
239
+
240
+ @actor.define_task :before_foo do
241
+ history << "before_foo"
242
+ end
243
+
244
+ @actor.foo
245
+ assert_equal %w(before_foo foo), history
246
+ end
247
+
248
+ def test_task_after_hook
249
+ history = []
250
+ @actor.define_task :foo do
251
+ history << "foo"
252
+ end
253
+
254
+ @actor.define_task :after_foo do
255
+ history << "after_foo"
256
+ end
257
+
258
+ @actor.foo
259
+ assert_equal %w(foo after_foo), history
260
+ end
261
+ end