rick-vlad 1.2.0.4

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,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