vlad 2.0.0 → 2.1.0

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.
@@ -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