vlad 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,589 +0,0 @@
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
@@ -1,72 +0,0 @@
1
- require 'minitest/autorun'
2
- require 'stringio'
3
- require 'vlad'
4
-
5
- class StringIO
6
- def readpartial(size) read end # suck!
7
- end
8
-
9
- module Process
10
- def self.expected status
11
- @@expected ||= []
12
- @@expected << status
13
- end
14
-
15
- class << self
16
- alias :waitpid2_old :waitpid2
17
-
18
- def waitpid2(pid)
19
- [ @@expected.shift ]
20
- end
21
- end
22
- end
23
-
24
- class Rake::RemoteTask
25
- attr_accessor :commands, :action, :input, :output, :error
26
-
27
- Status = Struct.new :exitstatus
28
-
29
- class Status
30
- def success?() exitstatus == 0 end
31
- end
32
-
33
- def system *command
34
- @commands << command
35
- self.action ? self.action[command.join(' ')] : true
36
- end
37
-
38
- def popen4 *command
39
- @commands << command
40
-
41
- @input = StringIO.new
42
- out = StringIO.new @output.shift.to_s
43
- err = StringIO.new @error.shift.to_s
44
-
45
- raise if block_given?
46
-
47
- status = self.action ? self.action[command.join(' ')] : 0
48
- Process.expected Status.new(status)
49
-
50
- return 42, @input, out, err
51
- end
52
-
53
- def select reads, writes, errs, timeout
54
- [reads, writes, errs]
55
- end
56
-
57
- end
58
-
59
- class VladTestCase < MiniTest::Unit::TestCase
60
- def setup
61
- @vlad = Rake::RemoteTask
62
- @vlad.reset
63
- Rake.application.clear
64
- @task_count = Rake.application.tasks.size
65
- @vlad.set :domain, "example.com"
66
- end
67
-
68
- def util_set_hosts
69
- @vlad.host "app.example.com", :app
70
- @vlad.host "db.example.com", :db
71
- end
72
- end