nuex-vlad 1.3.2

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