rick-vlad 1.2.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,573 @@
1
+ require 'rubygems'
2
+ require 'open4'
3
+ require 'rake'
4
+ require 'vlad'
5
+
6
+ $TESTING ||= false
7
+ $TRACE = Rake.application.options.trace
8
+
9
+ module Rake
10
+ module TaskManager
11
+ ##
12
+ # This gives us access to the tasks already defined in rake.
13
+ def all_tasks
14
+ @tasks
15
+ end
16
+ end
17
+
18
+ ##
19
+ # Hooks into rake and allows us to clear out a task by name or
20
+ # regexp. Use this if you want to completely override a task instead
21
+ # of extend it.
22
+ def self.clear_tasks(*tasks)
23
+ tasks.flatten.each do |name|
24
+ case name
25
+ when Regexp then
26
+ Rake.application.all_tasks.delete_if { |k,_| k =~ name }
27
+ else
28
+ Rake.application.all_tasks.delete(name)
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ ##
35
+ # Declare a remote host and its roles. Equivalent to <tt>role</tt>,
36
+ # but shorter for multiple roles.
37
+ def host host_name, *roles
38
+ Rake::RemoteTask.host host_name, *roles
39
+ end
40
+
41
+ ##
42
+ # Copy a (usually generated) file to +remote_path+. Contents of block
43
+ # are copied to +remote_path+ and you may specify an optional
44
+ # base_name for the tempfile (aids in debugging).
45
+
46
+ def put remote_path, base_name = 'vlad.unknown'
47
+ Tempfile.open base_name do |fp|
48
+ fp.puts yield
49
+ fp.flush
50
+ rsync fp.path, remote_path
51
+ end
52
+ end
53
+
54
+ ##
55
+ # Declare a Vlad task that will execute on all hosts by default. To
56
+ # limit that task to specific roles, use:
57
+ #
58
+ # remote_task :example, :roles => [:app, :web] do
59
+ def remote_task name, options = {}, &b
60
+ Rake::RemoteTask.remote_task name, options, &b
61
+ end
62
+
63
+ ##
64
+ # Declare a role and assign a remote host to it. Equivalent to the
65
+ # <tt>host</tt> method; provided for capistrano compatibility.
66
+ def role role_name, host, args = {}
67
+ Rake::RemoteTask.role role_name, host, args
68
+ end
69
+
70
+ ##
71
+ # Execute the given command on the <tt>target_host</tt> for the
72
+ # current task.
73
+ def run *args, &b
74
+ Thread.current[:task].run(*args, &b)
75
+ end
76
+
77
+ # rsync the given files to <tt>target_host</tt>.
78
+ def rsync local, remote
79
+ Thread.current[:task].rsync local, remote
80
+ end
81
+
82
+ # Declare a variable called +name+ and assign it a value. A
83
+ # globally-visible method with the name of the variable is defined.
84
+ # If a block is given, it will be called when the variable is first
85
+ # accessed. Subsequent references to the variable will always return
86
+ # the same value. Raises <tt>ArgumentError</tt> if the +name+ would
87
+ # conflict with an existing method.
88
+ def set name, val = nil, &b
89
+ Rake::RemoteTask.set name, val, &b
90
+ end
91
+
92
+ # Returns the name of the host that the current task is executing on.
93
+ # <tt>target_host</tt> can uniquely identify a particular task/host
94
+ # combination.
95
+ def target_host
96
+ Thread.current[:task].target_host
97
+ end
98
+
99
+ ##
100
+ # Execute the given command with sudo on the <tt>target_host</tt>
101
+ # for the current task.
102
+ def sudo *args, &b
103
+ Thread.current[:task].sudo(*args, &b)
104
+ end
105
+
106
+ if Gem::Version.new(RAKEVERSION) < Gem::Version.new('0.8') then
107
+ class Rake::Task
108
+ alias vlad_original_execute execute
109
+
110
+ def execute(args = nil)
111
+ vlad_original_execute
112
+ end
113
+ end
114
+ end
115
+
116
+ ##
117
+ # Rake::RemoteTask is a subclass of Rake::Task that adds
118
+ # remote_actions that execute in parallel on multiple hosts via ssh.
119
+
120
+ class Rake::RemoteTask < Rake::Task
121
+
122
+ include Open4
123
+
124
+ ##
125
+ # Options for execution of this task.
126
+
127
+ attr_accessor :options
128
+
129
+ ##
130
+ # The host this task is running on during execution.
131
+
132
+ attr_accessor :target_host
133
+
134
+ ##
135
+ # An Array of Actions this host will perform during execution. Use
136
+ # enhance to add new actions to a task.
137
+
138
+ attr_reader :remote_actions
139
+
140
+ ##
141
+ # Create a new task named +task_name+ attached to Rake::Application +app+.
142
+
143
+ def initialize(task_name, app)
144
+ super
145
+ @remote_actions = []
146
+ end
147
+
148
+ ##
149
+ # Add a local action to this task. This calls Rake::Task#enhance.
150
+
151
+ alias_method :original_enhance, :enhance
152
+
153
+ ##
154
+ # Add remote action +block+ to this task with dependencies +deps+. See
155
+ # Rake::Task#enhance.
156
+
157
+ def enhance(deps=nil, &block)
158
+ original_enhance(deps) # can't use super because block passed regardless.
159
+ @remote_actions << Action.new(self, block) if block_given?
160
+ self
161
+ end
162
+
163
+ ##
164
+ # Execute this action. Local actions will be performed first, then remote
165
+ # actions will be performed in parallel on each host configured for this
166
+ # RemoteTask.
167
+
168
+ def execute(args = nil)
169
+ raise(Vlad::ConfigurationError,
170
+ "No target hosts specified for task: #{self.name}") if
171
+ target_hosts.empty?
172
+
173
+ super args
174
+
175
+ @remote_actions.each { |act| act.execute(target_hosts, args) }
176
+ end
177
+
178
+ ##
179
+ # Use rsync to send +local+ to +remote+ on target_host.
180
+
181
+ def rsync local, remote
182
+ cmd = [rsync_cmd, rsync_flags, local, "#{@target_host}:#{remote}"].flatten.compact
183
+
184
+ success = system(*cmd)
185
+
186
+ unless success then
187
+ raise Vlad::CommandFailedError, "execution failed: #{cmd.join ' '}"
188
+ end
189
+ end
190
+
191
+ ##
192
+ # Use ssh to execute +command+ on target_host. If +command+ uses sudo, the
193
+ # sudo password will be prompted for then saved for subsequent sudo commands.
194
+
195
+ def run command
196
+ cmd = [ssh_cmd, ssh_flags, target_host, command].compact
197
+ result = []
198
+
199
+ warn cmd.join(' ') if $TRACE
200
+
201
+ pid, inn, out, err = popen4(*cmd)
202
+
203
+ inn.sync = true
204
+ streams = [out, err]
205
+ out_stream = {
206
+ out => $stdout,
207
+ err => $stderr,
208
+ }
209
+
210
+ # Handle process termination ourselves
211
+ status = nil
212
+ Thread.start do
213
+ status = Process.waitpid2(pid).last
214
+ end
215
+
216
+ until streams.empty? do
217
+ # don't busy loop
218
+ selected, = select streams, nil, nil, 0.1
219
+
220
+ next if selected.nil? or selected.empty?
221
+
222
+ selected.each do |stream|
223
+ if stream.eof? then
224
+ streams.delete stream if status # we've quit, so no more writing
225
+ next
226
+ end
227
+
228
+ data = stream.readpartial(1024)
229
+ out_stream[stream].write data
230
+
231
+ if stream == err and data =~ /^Password:/ then
232
+ inn.puts sudo_password
233
+ data << "\n"
234
+ $stderr.write "\n"
235
+ end
236
+
237
+ result << data
238
+ end
239
+ end
240
+
241
+ unless status.success? then
242
+ raise(Vlad::CommandFailedError,
243
+ "execution failed with status #{status.exitstatus}: #{cmd.join ' '}")
244
+ end
245
+
246
+ result.join
247
+ end
248
+
249
+ ##
250
+ # Returns an Array with every host configured.
251
+
252
+ def self.all_hosts
253
+ hosts_for(roles.keys)
254
+ end
255
+
256
+ ##
257
+ # The default environment values. Used for resetting (mostly for
258
+ # tests).
259
+
260
+ def self.default_env
261
+ @@default_env
262
+ end
263
+
264
+ ##
265
+ # The vlad environment.
266
+
267
+ def self.env
268
+ @@env
269
+ end
270
+
271
+ ##
272
+ # Fetches environment variable +name+ from the environment using
273
+ # default +default+.
274
+
275
+ def self.fetch name, default = nil
276
+ name = name.to_s if Symbol === name
277
+ if @@env.has_key? name then
278
+ protect_env(name) do
279
+ v = @@env[name]
280
+ v = @@env[name] = v.call if Proc === v
281
+ v
282
+ end
283
+ elsif default
284
+ v = @@env[name] = default
285
+ else
286
+ raise Vlad::FetchError
287
+ end
288
+ end
289
+
290
+ ##
291
+ # Add host +host_name+ that belongs to +roles+. Extra arguments may
292
+ # be specified for the host as a hash as the last argument.
293
+ #
294
+ # host is the inversion of role:
295
+ #
296
+ # host 'db1.example.com', :db, :master_db
297
+ #
298
+ # Is equivalent to:
299
+ #
300
+ # role :db, 'db1.example.com'
301
+ # role :master_db, 'db1.example.com'
302
+
303
+ def self.host host_name, *roles
304
+ opts = Hash === roles.last ? roles.pop : {}
305
+
306
+ roles.each do |role_name|
307
+ role role_name, host_name, opts.dup
308
+ end
309
+ end
310
+
311
+ ##
312
+ # Returns an Array of all hosts in +roles+.
313
+
314
+ def self.hosts_for *roles
315
+ roles.flatten.map { |r|
316
+ self.roles[r].keys
317
+ }.flatten.uniq.sort
318
+ end
319
+
320
+ def self.mandatory name, desc # :nodoc:
321
+ self.set(name) do
322
+ raise(Vlad::ConfigurationError,
323
+ "Please specify the #{desc} via the #{name.inspect} variable")
324
+ end
325
+ end
326
+
327
+ ##
328
+ # Ensures exclusive access to +name+.
329
+
330
+ def self.protect_env name # :nodoc:
331
+ @@env_locks[name].synchronize do
332
+ yield
333
+ end
334
+ end
335
+
336
+ ##
337
+ # Adds a remote task named +name+ with options +options+ that will
338
+ # execute +block+.
339
+
340
+ def self.remote_task name, options = {}, &block
341
+ t = Rake::RemoteTask.define_task(name, &block)
342
+ t.options = options
343
+ t
344
+ end
345
+
346
+ ##
347
+ # Ensures +name+ does not conflict with an existing method.
348
+
349
+ def self.reserved_name? name # :nodoc:
350
+ !@@env.has_key?(name.to_s) && self.respond_to?(name)
351
+ end
352
+
353
+ ##
354
+ # Resets vlad, restoring all roles, tasks and environment variables
355
+ # to the defaults.
356
+
357
+ def self.reset
358
+ @@roles = Hash.new { |h,k| h[k] = {} }
359
+ @@env = {}
360
+ @@tasks = {}
361
+ @@env_locks = Hash.new { |h,k| h[k] = Mutex.new }
362
+
363
+ @@default_env.each do |k,v|
364
+ case v
365
+ when Symbol, Fixnum, nil, true, false, 42 then # ummmm... yeah. bite me.
366
+ @@env[k] = v
367
+ else
368
+ @@env[k] = v.dup
369
+ end
370
+ end
371
+ end
372
+
373
+ ##
374
+ # Adds role +role_name+ with +host+ and +args+ for that host.
375
+
376
+ def self.role role_name, host, args = {}
377
+ raise ArgumentError, "invalid host" if host.nil? or host.empty?
378
+ @@roles[role_name][host] = args
379
+ end
380
+
381
+ ##
382
+ # The configured roles.
383
+
384
+ def self.roles
385
+ host domain, :app, :web, :db if @@roles.empty?
386
+
387
+ @@roles
388
+ end
389
+
390
+ ##
391
+ # Set environment variable +name+ to +value+ or +default_block+.
392
+ #
393
+ # If +default_block+ is defined, the block will be executed the
394
+ # first time the variable is fetched, and the value will be used for
395
+ # every subsequent fetch.
396
+
397
+ def self.set name, value = nil, &default_block
398
+ raise ArgumentError, "cannot provide both a value and a block" if
399
+ value and default_block
400
+ raise ArgumentError, "cannot set reserved name: '#{name}'" if
401
+ Rake::RemoteTask.reserved_name?(name) unless $TESTING
402
+
403
+ Rake::RemoteTask.default_env[name.to_s] = Rake::RemoteTask.env[name.to_s] =
404
+ value || default_block
405
+
406
+ Object.send :define_method, name do
407
+ Rake::RemoteTask.fetch name
408
+ end
409
+ end
410
+
411
+ ##
412
+ # Sets all the default values. Should only be called once. Use reset
413
+ # if you need to restore values.
414
+
415
+ def self.set_defaults
416
+ @@default_env ||= {}
417
+ self.reset
418
+
419
+ mandatory :code_repo, "code repo path"
420
+ mandatory :deploy_to, "deploy path"
421
+ mandatory :domain, "server domain"
422
+
423
+ simple_set(:deploy_timestamped, true,
424
+ :deploy_via, :export,
425
+ :keep_releases, 5,
426
+ :migrate_args, "",
427
+ :migrate_target, :latest,
428
+ :rails_env, "production",
429
+ :rake_cmd, "rake",
430
+ :revision, "head",
431
+ :rsync_cmd, "rsync",
432
+ :rsync_flags, ['-azP', '--delete'],
433
+ :ssh_cmd, "ssh",
434
+ :ssh_flags, nil,
435
+ :sudo_cmd, "sudo",
436
+ :sudo_flags, nil)
437
+
438
+ set(:current_release) { File.join(releases_path, releases[-1]) }
439
+ set(:latest_release) { deploy_timestamped ?release_path: current_release }
440
+ set(:previous_release) { File.join(releases_path, releases[-2]) }
441
+ set(:release_name) { Time.now.utc.strftime("%Y%m%d%H%M%S") }
442
+ set(:release_path) { File.join(releases_path, release_name) }
443
+ set(:releases) { task.run("ls -x #{releases_path}").split.sort }
444
+
445
+ set_path :current_path, "current"
446
+ set_path :releases_path, "releases"
447
+ set_path :scm_path, "scm"
448
+ set_path :shared_path, "shared"
449
+
450
+ set(:sudo_password) do
451
+ state = `stty -g`
452
+
453
+ raise Vlad::Error, "stty(1) not found" unless $?.success?
454
+
455
+ begin
456
+ system "stty -echo"
457
+ $stdout.print "sudo password: "
458
+ $stdout.flush
459
+ sudo_password = $stdin.gets
460
+ $stdout.puts
461
+ ensure
462
+ system "stty #{state}"
463
+ end
464
+ sudo_password
465
+ end
466
+ end
467
+
468
+ def self.set_path(name, subdir) # :nodoc:
469
+ set(name) { File.join(deploy_to, subdir) }
470
+ end
471
+
472
+ def self.simple_set(*args) # :nodoc:
473
+ args = Hash[*args]
474
+ args.each do |k, v|
475
+ set k, v
476
+ end
477
+ end
478
+
479
+ ##
480
+ # The Rake::RemoteTask executing in this Thread.
481
+
482
+ def self.task
483
+ Thread.current[:task]
484
+ end
485
+
486
+ ##
487
+ # The configured Rake::RemoteTasks.
488
+
489
+ def self.tasks
490
+ @@tasks
491
+ end
492
+
493
+ ##
494
+ # Execute +command+ under sudo using run.
495
+
496
+ def sudo command
497
+ run [sudo_cmd, sudo_flags, command].compact.join(" ")
498
+ end
499
+
500
+ ##
501
+ # The hosts this task will execute on. The hosts are determined from
502
+ # the role this task belongs to.
503
+ #
504
+ # The target hosts may be overridden by providing a comma-separated
505
+ # list of commands to the HOSTS environment variable:
506
+ #
507
+ # rake my_task HOSTS=app1.example.com,app2.example.com
508
+
509
+ def target_hosts
510
+ if hosts = ENV["HOSTS"] then
511
+ hosts.strip.gsub(/\s+/, '').split(",")
512
+ else
513
+ roles = options[:roles]
514
+ roles ? Rake::RemoteTask.hosts_for(roles) : Rake::RemoteTask.all_hosts
515
+ end
516
+ end
517
+
518
+ ##
519
+ # Action is used to run a task's remote_actions in parallel on each
520
+ # of its hosts. Actions are created automatically in
521
+ # Rake::RemoteTask#enhance.
522
+
523
+ class Action
524
+
525
+ ##
526
+ # The task this action is attached to.
527
+
528
+ attr_reader :task
529
+
530
+ ##
531
+ # The block this action will execute.
532
+
533
+ attr_reader :block
534
+
535
+ ##
536
+ # An Array of threads, one for each host this action executes on.
537
+
538
+ attr_reader :workers
539
+
540
+ ##
541
+ # Creates a new Action that will run +block+ for +task+.
542
+
543
+ def initialize task, block
544
+ @task = task
545
+ @block = block
546
+ @workers = []
547
+ end
548
+
549
+ def == other # :nodoc:
550
+ return false unless Action === other
551
+ block == other.block && task == other.task
552
+ end
553
+
554
+ ##
555
+ # Execute this action on +hosts+ in parallel. Returns when block
556
+ # has completed for each host.
557
+
558
+ def execute hosts, args = nil
559
+ hosts.each do |host|
560
+ t = task.clone
561
+ t.target_host = host
562
+ thread = Thread.new(t) do |task|
563
+ Thread.current[:task] = task
564
+ block.call args
565
+ end
566
+ @workers << thread
567
+ end
568
+ @workers.each { |w| w.join }
569
+ end
570
+ end
571
+ end
572
+
573
+ Rake::RemoteTask.set_defaults
@@ -0,0 +1,37 @@
1
+ require 'vlad'
2
+
3
+ namespace :vlad do
4
+ ##
5
+ # Apache web server
6
+
7
+ set :web_command, "apachectl"
8
+
9
+ desc "(Re)Start the web servers"
10
+
11
+ remote_task :start_web, :roles => :web do
12
+ run "#{web_command} restart"
13
+ end
14
+
15
+ desc "Stop the web servers"
16
+
17
+ remote_task :stop_web, :roles => :web do
18
+ run "#{web_command} stop"
19
+ end
20
+
21
+ ##
22
+ # Everything HTTP.
23
+
24
+ desc "(Re)Start the web and app servers"
25
+
26
+ remote_task :start do
27
+ Rake::Task['vlad:start_app'].invoke
28
+ Rake::Task['vlad:start_web'].invoke
29
+ end
30
+
31
+ desc "Stop the web and app servers"
32
+
33
+ remote_task :stop do
34
+ Rake::Task['vlad:stop_app'].invoke
35
+ Rake::Task['vlad:stop_web'].invoke
36
+ end
37
+ end