andyverprauskus-vlad 1.2.0.2

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