switchtower 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
data/bin/switchtower ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'rubygems'
5
+ rescue LoadError
6
+ # no rubygems to load, so we fail silently
7
+ end
8
+
9
+ require 'switchtower/cli'
10
+
11
+ SwitchTower::CLI.execute!
@@ -0,0 +1,113 @@
1
+ # You must always specify the application and repository for every recipe. The
2
+ # repository must be the URL of the repository you want this recipe to
3
+ # correspond to. The deploy_to path must be the path on each machine that will
4
+ # form the root of the application path.
5
+
6
+ set :application, "sample"
7
+ set :repository, "http://svn.example.com/#{application}/trunk"
8
+
9
+ # The deploy_to path is optional, defaulting to "/u/apps/#{application}".
10
+
11
+ set :deploy_to, "/path/to/app/root"
12
+
13
+ # The user value is optional, defaulting to user-name of the current user. This
14
+ # is the user name that will be used when logging into the deployment boxes.
15
+
16
+ set :user, "flippy"
17
+
18
+ # By default, the source control module (scm) is set to "subversion". You can
19
+ # set it to any supported scm:
20
+
21
+ set :scm, :subversion
22
+
23
+ # gateway is optional, but allows you to specify the address of a computer that
24
+ # will be used to tunnel other requests through, such as when your machines are
25
+ # all behind a VPN or something
26
+
27
+ set :gateway, "gateway.example.com"
28
+
29
+ # You can define any number of roles, each of which contains any number of
30
+ # machines. Roles might include such things as :web, or :app, or :db, defining
31
+ # what the purpose of each machine is. You can also specify options that can
32
+ # be used to single out a specific subset of boxes in a particular role, like
33
+ # :primary => true.
34
+
35
+ role :web, "www01.example.com", "www02.example.com"
36
+ role :app, "app01.example.com", "app02.example.com", "app03.example.com"
37
+ role :db, "db01.example.com", :primary => true
38
+ role :db, "db02.example.com", "db03.example.com"
39
+
40
+ # Define tasks that run on all (or only some) of the machines. You can specify
41
+ # a role (or set of roles) that each task should be executed on. You can also
42
+ # narrow the set of servers to a subset of a role by specifying options, which
43
+ # must match the options given for the servers to select (like :primary => true)
44
+
45
+ desc <<DESC
46
+ An imaginary backup task. (Execute the 'show_tasks' task to display all
47
+ available tasks.)
48
+ DESC
49
+
50
+ task :backup, :roles => :db, :only => { :primary => true } do
51
+ # the on_rollback handler is only executed if this task is executed within
52
+ # a transaction (see below), AND it or a subsequent task fails.
53
+ on_rollback { delete "/tmp/dump.sql" }
54
+
55
+ run "mysqldump -u theuser -p thedatabase > /tmp/dump.sql" do |ch, stream, out|
56
+ ch.send_data "thepassword\n" if out =~ /^Enter password:/
57
+ end
58
+ end
59
+
60
+ # Tasks may take advantage of several different helper methods to interact
61
+ # with the remote server(s). These are:
62
+ #
63
+ # * run(command, options={}, &block): execute the given command on all servers
64
+ # associated with the current task, in parallel. The block, if given, should
65
+ # accept three parameters: the communication channel, a symbol identifying the
66
+ # type of stream (:err or :out), and the data. The block is invoked for all
67
+ # output from the command, allowing you to inspect output and act
68
+ # accordingly.
69
+ # * sudo(command, options={}, &block): same as run, but it executes the command
70
+ # via sudo.
71
+ # * delete(path, options={}): deletes the given file or directory from all
72
+ # associated servers. If :recursive => true is given in the options, the
73
+ # delete uses "rm -rf" instead of "rm -f".
74
+ # * put(buffer, path, options={}): creates or overwrites a file at "path" on
75
+ # all associated servers, populating it with the contents of "buffer". You
76
+ # can specify :mode as an integer value, which will be used to set the mode
77
+ # on the file.
78
+ # * render(template, options={}) or render(options={}): renders the given
79
+ # template and returns a string. Alternatively, if the :template key is given,
80
+ # it will be treated as the contents of the template to render. Any other keys
81
+ # are treated as local variables, which are made available to the (ERb)
82
+ # template.
83
+
84
+ desc "Demonstrates the various helper methods available to recipes."
85
+ task :helper_demo do
86
+ # "setup" is a standard task which sets up the directory structure on the
87
+ # remote servers. It is a good idea to run the "setup" task at least once
88
+ # at the beginning of your app's lifetime (it is non-destructive).
89
+ setup
90
+
91
+ buffer = render("maintenance.rhtml", :deadline => ENV['UNTIL'])
92
+ put buffer, "#{shared_path}/system/maintenance.html", :mode => 0644
93
+ sudo "killall -USR1 dispatch.fcgi"
94
+ run "#{release_path}/script/spin"
95
+ delete "#{shared_path}/system/maintenance.html"
96
+ end
97
+
98
+ # You can use "transaction" to indicate that if any of the tasks within it fail,
99
+ # all should be rolled back (for each task that specifies an on_rollback
100
+ # handler).
101
+
102
+ desc "A task demonstrating the use of transactions."
103
+ task :long_deploy do
104
+ transaction do
105
+ update_code
106
+ disable_web
107
+ symlink
108
+ migrate
109
+ end
110
+
111
+ restart
112
+ enable_web
113
+ end
@@ -0,0 +1 @@
1
+ require 'switchtower/configuration'
@@ -0,0 +1,350 @@
1
+ require 'erb'
2
+ require 'switchtower/command'
3
+ require 'switchtower/gateway'
4
+ require 'switchtower/ssh'
5
+
6
+ module SwitchTower
7
+
8
+ # An Actor is the entity that actually does the work of determining which
9
+ # servers should be the target of a particular task, and of executing the
10
+ # task on each of them in parallel. An Actor is never instantiated
11
+ # directly--rather, you create a new Configuration instance, and access the
12
+ # new actor via Configuration#actor.
13
+ class Actor
14
+
15
+ # An adaptor for making the SSH interface look and act like that of the
16
+ # Gateway class.
17
+ class DefaultConnectionFactory #:nodoc:
18
+ def initialize(config)
19
+ @config= config
20
+ end
21
+
22
+ def connect_to(server)
23
+ SSH.connect(server, @config)
24
+ end
25
+ end
26
+
27
+ class <<self
28
+ attr_accessor :connection_factory
29
+ attr_accessor :command_factory
30
+ end
31
+
32
+ self.connection_factory = DefaultConnectionFactory
33
+ self.command_factory = Command
34
+
35
+ # The configuration instance associated with this actor.
36
+ attr_reader :configuration
37
+
38
+ # A hash of the tasks known to this actor, keyed by name. The values are
39
+ # instances of Actor::Task.
40
+ attr_reader :tasks
41
+
42
+ # A hash of the SSH sessions that are currently open and available.
43
+ # Because sessions are constructed lazily, this will only contain
44
+ # connections to those servers that have been the targets of one or more
45
+ # executed tasks.
46
+ attr_reader :sessions
47
+
48
+ # The call stack of the tasks. The currently executing task may inspect
49
+ # this to see who its caller was. The current task is always the last
50
+ # element of this stack.
51
+ attr_reader :task_call_frames
52
+
53
+ # The history of executed tasks. This will be an array of all tasks that
54
+ # have been executed, in the order in which they were called.
55
+ attr_reader :task_call_history
56
+
57
+ # A struct for representing a single instance of an invoked task.
58
+ TaskCallFrame = Struct.new(:name, :rollback)
59
+
60
+ # Represents the definition of a single task.
61
+ class Task #:nodoc:
62
+ attr_reader :name, :options
63
+
64
+ def initialize(name, options)
65
+ @name, @options = name, options
66
+ end
67
+
68
+ # Returns the list of servers (_not_ connections to servers) that are
69
+ # the target of this task.
70
+ def servers(configuration)
71
+ unless @servers
72
+ roles = [*(@options[:roles] || configuration.roles.keys)].map { |name| configuration.roles[name] or raise ArgumentError, "task #{self.name.inspect} references non-existant role #{name.inspect}" }.flatten
73
+ only = @options[:only] || {}
74
+
75
+ unless only.empty?
76
+ roles = roles.delete_if do |role|
77
+ catch(:done) do
78
+ only.keys.each do |key|
79
+ throw(:done, true) if role.options[key] != only[key]
80
+ end
81
+ false
82
+ end
83
+ end
84
+ end
85
+
86
+ @servers = roles.map { |role| role.host }.uniq
87
+ end
88
+
89
+ @servers
90
+ end
91
+ end
92
+
93
+ def initialize(config) #:nodoc:
94
+ @configuration = config
95
+ @tasks = {}
96
+ @task_call_frames = []
97
+ @sessions = {}
98
+ @factory = self.class.connection_factory.new(configuration)
99
+ end
100
+
101
+ # Define a new task for this actor. The block will be invoked when this
102
+ # task is called.
103
+ def define_task(name, options={}, &block)
104
+ @tasks[name] = Task.new(name, options)
105
+ define_method(name) do
106
+ send "before_#{name}" if respond_to? "before_#{name}"
107
+ logger.trace "executing task #{name}"
108
+ begin
109
+ push_task_call_frame name
110
+ result = instance_eval &block
111
+ ensure
112
+ pop_task_call_frame
113
+ end
114
+ send "after_#{name}" if respond_to? "after_#{name}"
115
+ result
116
+ end
117
+ end
118
+
119
+ # Execute the given command on all servers that are the target of the
120
+ # current task. If a block is given, it is invoked for all output
121
+ # generated by the command, and should accept three parameters: the SSH
122
+ # channel (which may be used to send data back to the remote process),
123
+ # the stream identifier (<tt>:err</tt> for stderr, and <tt>:out</tt> for
124
+ # stdout), and the data that was received.
125
+ #
126
+ # If +pretend+ mode is active, this does nothing.
127
+ def run(cmd, options={}, &block)
128
+ block ||= Proc.new do |ch, stream, out|
129
+ logger.debug(out, "#{stream} :: #{ch[:host]}")
130
+ end
131
+
132
+ logger.debug "executing #{cmd.strip.inspect}"
133
+
134
+ # get the currently executing task and determine which servers it uses
135
+ servers = tasks[task_call_frames.last.name].servers(configuration)
136
+ servers = servers.first if options[:once]
137
+ logger.trace "servers: #{servers.inspect}"
138
+
139
+ if !pretend
140
+ # establish connections to those servers, as necessary
141
+ establish_connections(servers)
142
+
143
+ # execute the command on each server in parallel
144
+ command = self.class.command_factory.new(servers, cmd, block, options, self)
145
+ command.process! # raises an exception if command fails on any server
146
+ end
147
+ end
148
+
149
+ # Deletes the given file from all servers targetted by the current task.
150
+ # If <tt>:recursive => true</tt> is specified, it may be used to remove
151
+ # directories.
152
+ def delete(path, options={})
153
+ cmd = "rm -%sf #{path}" % (options[:recursive] ? "r" : "")
154
+ run(cmd, options)
155
+ end
156
+
157
+ # Store the given data at the given location on all servers targetted by
158
+ # the current task. If <tt>:mode</tt> is specified it is used to set the
159
+ # mode on the file.
160
+ def put(data, path, options={})
161
+ # Poor-man's SFTP... just run a cat on the remote end, and send data
162
+ # to it.
163
+
164
+ cmd = "cat > #{path}"
165
+ cmd << " && chmod #{options[:mode].to_s(8)} #{path}" if options[:mode]
166
+ run(cmd, options.merge(:data => data + "\n\4")) do |ch, stream, out|
167
+ logger.important out, "#{stream} :: #{ch[:host]}" if out == :err
168
+ end
169
+ end
170
+
171
+ # Like #run, but executes the command via <tt>sudo</tt>. This assumes that
172
+ # the sudo password (if required) is the same as the password for logging
173
+ # in to the server.
174
+ def sudo(command, options={}, &block)
175
+ block ||= Proc.new do |ch, stream, out|
176
+ logger.debug(out, "#{stream} :: #{ch[:host]}")
177
+ end
178
+
179
+ run "sudo #{command}", options do |ch, stream, out|
180
+ if out =~ /^Password:/
181
+ ch.send_data "#{password}\n"
182
+ else
183
+ block.call(ch, stream, out)
184
+ end
185
+ end
186
+ end
187
+
188
+ # Renders an ERb template and returns the result. This is useful for
189
+ # dynamically building documents to store on the remote servers.
190
+ #
191
+ # Usage:
192
+ #
193
+ # render("something", :foo => "hello")
194
+ # look for "something.rhtml" in the current directory, or in the
195
+ # switchtower/recipes/templates directory, and render it with
196
+ # foo defined as a local variable with the value "hello".
197
+ #
198
+ # render(:file => "something", :foo => "hello")
199
+ # same as above
200
+ #
201
+ # render(:template => "<%= foo %> world", :foo => "hello")
202
+ # treat the given string as an ERb template and render it with
203
+ # the given hash of local variables active.
204
+ def render(*args)
205
+ options = args.last.is_a?(Hash) ? args.pop : {}
206
+ options[:file] = args.shift if args.first.is_a?(String)
207
+ raise ArgumentError, "too many parameters" unless args.empty?
208
+
209
+ case
210
+ when options[:file]
211
+ file = options.delete :file
212
+ unless file[0] == ?/
213
+ dirs = [".",
214
+ File.join(File.dirname(__FILE__), "recipes", "templates")]
215
+ dirs.each do |dir|
216
+ if File.file?(File.join(dir, file))
217
+ file = File.join(dir, file)
218
+ break
219
+ elsif File.file?(File.join(dir, file + ".rhtml"))
220
+ file = File.join(dir, file + ".rhtml")
221
+ break
222
+ end
223
+ end
224
+ end
225
+
226
+ render options.merge(:template => File.read(file))
227
+
228
+ when options[:template]
229
+ erb = ERB.new(options[:template])
230
+ b = Proc.new { binding }.call
231
+ options.each do |key, value|
232
+ next if key == :template
233
+ eval "#{key} = options[:#{key}]", b
234
+ end
235
+ erb.result(b)
236
+
237
+ else
238
+ raise ArgumentError, "no file or template given for rendering"
239
+ end
240
+ end
241
+
242
+ # Inspects the remote servers to determine the list of all released versions
243
+ # of the software. Releases are sorted with the most recent release last.
244
+ def releases
245
+ unless @releases
246
+ buffer = ""
247
+ run "ls -x1 #{releases_path}", :once => true do |ch, str, out|
248
+ buffer << out if str == :out
249
+ raise "could not determine releases #{out.inspect}" if str == :err
250
+ end
251
+ @releases = buffer.split.sort
252
+ end
253
+
254
+ @releases
255
+ end
256
+
257
+ # Returns the most recent deployed release
258
+ def current_release
259
+ release_path(releases.last)
260
+ end
261
+
262
+ # Returns the release immediately before the currently deployed one
263
+ def previous_release
264
+ release_path(releases[-2])
265
+ end
266
+
267
+ # Invoke a set of tasks in a transaction. If any task fails (raises an
268
+ # exception), all tasks executed within the transaction are inspected to
269
+ # see if they have an associated on_rollback hook, and if so, that hook
270
+ # is called.
271
+ def transaction
272
+ if task_call_history
273
+ yield
274
+ else
275
+ logger.info "transaction: start"
276
+ begin
277
+ @task_call_history = []
278
+ yield
279
+ logger.info "transaction: commit"
280
+ rescue Object => e
281
+ current = task_call_history.last
282
+ logger.important "transaction: rollback", current ? current.name : "transaction start"
283
+ task_call_history.reverse.each do |task|
284
+ begin
285
+ logger.debug "rolling back", task.name
286
+ task.rollback.call if task.rollback
287
+ rescue Object => e
288
+ logger.info "exception while rolling back: #{e.class}, #{e.message}", task.name
289
+ end
290
+ end
291
+ raise
292
+ ensure
293
+ @task_call_history = nil
294
+ end
295
+ end
296
+ end
297
+
298
+ # Specifies an on_rollback hook for the currently executing task. If this
299
+ # or any subsequent task then fails, and a transaction is active, this
300
+ # hook will be executed.
301
+ def on_rollback(&block)
302
+ task_call_frames.last.rollback = block
303
+ end
304
+
305
+ private
306
+
307
+ def metaclass
308
+ class << self; self; end
309
+ end
310
+
311
+ def define_method(name, &block)
312
+ metaclass.send(:define_method, name, &block)
313
+ end
314
+
315
+ def push_task_call_frame(name)
316
+ frame = TaskCallFrame.new(name)
317
+ task_call_frames.push frame
318
+ task_call_history.push frame if task_call_history
319
+ end
320
+
321
+ def pop_task_call_frame
322
+ task_call_frames.pop
323
+ end
324
+
325
+ def establish_connections(servers)
326
+ @factory = establish_gateway if needs_gateway?
327
+ servers.each do |server|
328
+ @sessions[server] ||= @factory.connect_to(server)
329
+ end
330
+ end
331
+
332
+ def establish_gateway
333
+ logger.debug "establishing connection to gateway #{gateway}"
334
+ @established_gateway = true
335
+ Gateway.new(gateway, configuration)
336
+ end
337
+
338
+ def needs_gateway?
339
+ gateway && !@established_gateway
340
+ end
341
+
342
+ def method_missing(sym, *args, &block)
343
+ if @configuration.respond_to?(sym)
344
+ @configuration.send(sym, *args, &block)
345
+ else
346
+ super
347
+ end
348
+ end
349
+ end
350
+ end