fpauser-rake-remote_task 2.0.3

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,24 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'autotest/restart'
4
+
5
+ Autotest.add_hook :initialize do |at|
6
+ at.testlib = "minitest/autorun"
7
+ # at.extra_files << "../some/external/dependency.rb"
8
+ #
9
+ # at.libs << ":../some/external"
10
+ #
11
+ # at.add_exception 'vendor'
12
+ #
13
+ # at.add_mapping(/dependency.rb/) do |f, _|
14
+ # at.files_matching(/test_.*rb$/)
15
+ # end
16
+ #
17
+ # %w(TestA TestB).each do |klass|
18
+ # at.extra_class_map[klass] = "test/test_misc.rb"
19
+ # end
20
+ end
21
+
22
+ # Autotest.add_hook :run_command do |at|
23
+ # system "rake build"
24
+ # end
data/Gemfile ADDED
@@ -0,0 +1 @@
1
+ gemspec
@@ -0,0 +1,17 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ fpauser-rake-remote_task (2.0.3)
5
+ open4 (>= 0.9.0)
6
+ rake (>= 0.8.0)
7
+
8
+ GEM
9
+ specs:
10
+ open4 (0.9.6)
11
+ rake (0.9.2)
12
+
13
+ PLATFORMS
14
+ ruby
15
+
16
+ DEPENDENCIES
17
+ fpauser-rake-remote_task!
@@ -0,0 +1,26 @@
1
+ === 2.0.3 / 2011-02-18
2
+
3
+ * 2 minor enhancements:
4
+
5
+ * Make failure code available via CommandFailedError. (ameuret)
6
+ * Support owner/group in vlad. (RichGuk)
7
+
8
+ === 2.0.2 / 2010-09-02
9
+
10
+ * 1 bug fix:
11
+
12
+ * 1.9 fixes to quell test warnings and the like
13
+
14
+ === 2.0.1 / 2010-07-23
15
+
16
+ * 1 bug fix:
17
+
18
+ * Fixed syntax error caused by being anal about 80 col boundary (bleything).
19
+
20
+ === 2.0.0 / 2010-07-15
21
+
22
+ * 1 major enhancement
23
+
24
+ * Split out from Vlad.
25
+
26
+
@@ -0,0 +1,8 @@
1
+ .autotest
2
+ History.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ lib/rake/remote_task.rb
7
+ lib/rake/test_case.rb
8
+ test/test_rake_remote_task.rb
@@ -0,0 +1,56 @@
1
+ = rake-remote_task
2
+
3
+ * http://rubyhitsquad.com/
4
+ * http://rubyforge.org/projects/hitsquad/
5
+
6
+ == DESCRIPTION:
7
+
8
+ Vlad the Deployer's sexy brainchild is rake-remote_task, extending
9
+ Rake with remote task goodness.
10
+
11
+ == FEATURES/PROBLEMS:
12
+
13
+ * Run remote commands on one or more servers.
14
+ * Mix and match local and remote tasks.
15
+ * Uses ssh with your ssh settings already in place.
16
+ * Uses rsync for efficient transfers.
17
+
18
+ == SYNOPSIS:
19
+
20
+ remote_task :setup_app, :roles => :app do
21
+ # ...
22
+ run "umask #{umask} && mkdir -p #{dirs.join(' ')}"
23
+ end
24
+
25
+ == REQUIREMENTS:
26
+
27
+ * rake, >= 0.8
28
+
29
+ == INSTALL:
30
+
31
+ * sudo gem install rake-remote_task
32
+
33
+ == LICENSE:
34
+
35
+ (The MIT License)
36
+
37
+ Copyright (c) Ryan Davis, RubyHitSquad
38
+
39
+ Permission is hereby granted, free of charge, to any person obtaining
40
+ a copy of this software and associated documentation files (the
41
+ 'Software'), to deal in the Software without restriction, including
42
+ without limitation the rights to use, copy, modify, merge, publish,
43
+ distribute, sublicense, and/or sell copies of the Software, and to
44
+ permit persons to whom the Software is furnished to do so, subject to
45
+ the following conditions:
46
+
47
+ The above copyright notice and this permission notice shall be
48
+ included in all copies or substantial portions of the Software.
49
+
50
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
51
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
52
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
53
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
54
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
55
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
56
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,24 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+
6
+ # HACK Hoe.plugin :isolate # fudging releases - work it out w/ release script
7
+ Hoe.plugin :seattlerb
8
+
9
+ Hoe.spec 'rake-remote_task' do
10
+ developer 'Ryan Davis', 'ryand-ruby@zenspider.com'
11
+ developer 'Eric Hodel', 'drbrain@segment7.net'
12
+ developer 'Wilson Bilkovich', 'wilson@supremetyrant.com'
13
+
14
+ self.rubyforge_name = 'hitsquad'
15
+
16
+ extra_deps << ['rake', '~> 0.8.0']
17
+ extra_deps << ['open4', '~> 0.9.0']
18
+
19
+ extra_dev_deps << ['minitest', '~> 1.7.0']
20
+
21
+ multiruby_skip << "rubinius"
22
+ end
23
+
24
+ # vim: syntax=ruby
@@ -0,0 +1,643 @@
1
+ require 'rubygems'
2
+ require 'open4'
3
+ require 'rake'
4
+
5
+ $TESTING ||= false
6
+ $TRACE = Rake.application.options.trace
7
+ $-w = true if $TRACE # asshat, don't mess with my warn.
8
+
9
+ [
10
+ ["Thread.current[:task]", :get, :put, :rsync, :run, :sudo, :target_host],
11
+ ["Rake::RemoteTask", :host, :remote_task, :role, :set]
12
+ ].each do |methods|
13
+ receiver = methods.shift
14
+ methods.each do |method|
15
+ eval "def #{method} *args, &block; #{receiver}.#{method}(*args, &block);end"
16
+ end
17
+ end
18
+
19
+ module Rake
20
+ ##
21
+ # Base error class for all Vlad errors.
22
+ class Error < RuntimeError; end
23
+
24
+ ##
25
+ # Raised when you have incorrectly configured Vlad.
26
+ class ConfigurationError < Error; end
27
+
28
+ ##
29
+ # Raised when a remote command fails.
30
+ class CommandFailedError < Error
31
+ attr_reader :status
32
+ def initialize( status )
33
+ @status = status
34
+ end
35
+ end
36
+
37
+ ##
38
+ # Raised when an environment variable hasn't been set.
39
+ class FetchError < Error; end
40
+ end
41
+
42
+ ##
43
+ # Rake::RemoteTask is a subclass of Rake::Task that adds
44
+ # remote_actions that execute in parallel on multiple hosts via ssh.
45
+
46
+ class Rake::RemoteTask < Rake::Task
47
+
48
+ VERSION = "2.0.3"
49
+
50
+ @@current_roles = []
51
+
52
+ include Open4
53
+
54
+ ##
55
+ # Options for execution of this task.
56
+
57
+ attr_accessor :options
58
+
59
+ ##
60
+ # The host this task is running on during execution.
61
+
62
+ attr_reader :target_host
63
+
64
+ ##
65
+ # The directory on the host this task is running in during execution.
66
+
67
+ attr_reader :target_dir
68
+
69
+ ##
70
+ # An Array of Actions this host will perform during execution. Use
71
+ # enhance to add new actions to a task.
72
+
73
+ attr_reader :remote_actions
74
+
75
+ def self.current_roles
76
+ @@current_roles
77
+ end
78
+
79
+ ##
80
+ # Create a new task named +task_name+ attached to Rake::Application +app+.
81
+
82
+ def initialize(task_name, app)
83
+ super
84
+
85
+ @remote_actions = []
86
+ @happy = false # used for deprecation warnings on get/put/rsync
87
+ end
88
+
89
+ ##
90
+ # Add a local action to this task. This calls Rake::Task#enhance.
91
+
92
+ alias_method :original_enhance, :enhance
93
+
94
+ ##
95
+ # Add remote action +block+ to this task with dependencies +deps+. See
96
+ # Rake::Task#enhance.
97
+
98
+ def enhance(deps=nil, &block)
99
+ original_enhance(deps) # can't use super because block passed regardless.
100
+ @remote_actions << Action.new(self, block) if block_given?
101
+ self
102
+ end
103
+
104
+ ##
105
+ # Execute this action. Local actions will be performed first, then remote
106
+ # actions will be performed in parallel on each host configured for this
107
+ # RemoteTask.
108
+
109
+ def execute(args = nil)
110
+ raise(Rake::ConfigurationError,
111
+ "No target hosts specified on task #{self.name} for roles #{options[:roles].inspect}") unless
112
+ defined_target_hosts?
113
+
114
+ super args
115
+
116
+ @remote_actions.each { |act| act.execute(target_hosts, self, args) }
117
+ end
118
+
119
+ ##
120
+ # Pull +files+ from the remote +host+ using rsync to +local_dir+.
121
+ # TODO: what if role has multiple hosts & the files overlap? subdirs?
122
+
123
+ def get local_dir, *files
124
+ @happy = true
125
+ host = target_host
126
+ rsync files.map { |f| "#{host}:#{f}" }, local_dir
127
+ @happy = false
128
+ end
129
+
130
+ ##
131
+ # Copy a (usually generated) file to +remote_path+. Contents of block
132
+ # are copied to +remote_path+ and you may specify an optional
133
+ # base_name for the tempfile (aids in debugging).
134
+
135
+ def put remote_path, base_name = File.basename(remote_path)
136
+ require 'tempfile'
137
+ Tempfile.open base_name do |fp|
138
+ fp.puts yield
139
+ fp.flush
140
+ @happy = true
141
+ rsync fp.path, "#{target_host}:#{remote_path}"
142
+ @happy = false
143
+ end
144
+ end
145
+
146
+ ##
147
+ # Execute rsync with +args+. Tacks on pre-specified +rsync_cmd+ and
148
+ # +rsync_flags+.
149
+ #
150
+ # Favor #get and #put for most tasks. Old-style direct use where the
151
+ # target_host was implicit is now deprecated.
152
+
153
+ def rsync *args
154
+ unless @happy || args[-1] =~ /:/ then
155
+ warn "rsync deprecation: pass target_host:remote_path explicitly"
156
+ args[-1] = "#{target_host}:#{args[-1]}"
157
+ end
158
+
159
+ cmd = [rsync_cmd, rsync_flags, args].flatten.compact
160
+ cmdstr = cmd.join ' '
161
+
162
+ warn cmdstr if $TRACE
163
+
164
+ success = system(*cmd)
165
+
166
+ raise Rake::CommandFailedError.new($?), "execution failed: #{cmdstr}" unless success
167
+ end
168
+
169
+ ##
170
+ # Use ssh to execute +command+ on target_host. If +command+ uses sudo, the
171
+ # sudo password will be prompted for then saved for subsequent sudo commands.
172
+
173
+ def run command
174
+ command = "cd #{target_dir} && #{command}" if target_dir
175
+ cmd = [ssh_cmd, ssh_flags, target_host, command].flatten
176
+ result = []
177
+
178
+ trace = [ssh_cmd, ssh_flags, target_host, "'#{command}'"].flatten.join(' ')
179
+ warn trace if $TRACE
180
+
181
+ pid, inn, out, err = popen4(*cmd)
182
+
183
+ inn.sync = true
184
+ streams = [out, err]
185
+ out_stream = {
186
+ out => $stdout,
187
+ err => $stderr,
188
+ }
189
+
190
+ # Handle process termination ourselves
191
+ status = nil
192
+ Thread.start do
193
+ status = Process.waitpid2(pid).last
194
+ end
195
+
196
+ until streams.empty? do
197
+ # don't busy loop
198
+ selected, = select streams, nil, nil, 0.1
199
+
200
+ next if selected.nil? or selected.empty?
201
+
202
+ selected.each do |stream|
203
+ if stream.eof? then
204
+ streams.delete stream if status # we've quit, so no more writing
205
+ next
206
+ end
207
+
208
+ data = stream.readpartial(1024)
209
+ out_stream[stream].write data
210
+
211
+ if stream == err and data =~ sudo_prompt then
212
+ inn.puts sudo_password
213
+ data << "\n"
214
+ $stderr.write "\n"
215
+ end
216
+
217
+ result << data
218
+ end
219
+ end
220
+
221
+ unless status.success? then
222
+ raise(Rake::CommandFailedError.new(status),
223
+ "execution failed with status #{status.exitstatus}: #{cmd.join ' '}")
224
+ end
225
+
226
+ result.join
227
+ ensure
228
+ inn.close rescue nil
229
+ out.close rescue nil
230
+ err.close rescue nil
231
+ end
232
+
233
+ ##
234
+ # Returns an Array with every host configured.
235
+
236
+ def self.all_hosts
237
+ hosts_for(roles.keys)
238
+ end
239
+
240
+ ##
241
+ # The default environment values. Used for resetting (mostly for
242
+ # tests).
243
+
244
+ def self.default_env
245
+ @@default_env
246
+ end
247
+
248
+ def self.per_thread
249
+ @@per_thread
250
+ end
251
+
252
+ ##
253
+ # The vlad environment.
254
+
255
+ def self.env
256
+ @@env
257
+ end
258
+
259
+ ##
260
+ # Fetches environment variable +name+ from the environment using
261
+ # default +default+.
262
+
263
+ def self.fetch name, default = nil
264
+ name = name.to_s if Symbol === name
265
+ if @@env.has_key? name then
266
+ protect_env(name) do
267
+ v = @@env[name]
268
+ v = @@env[name] = v.call if Proc === v unless per_thread[name]
269
+ v = v.call if Proc === v
270
+ v
271
+ end
272
+ elsif default || default == false
273
+ v = @@env[name] = default
274
+ else
275
+ raise Rake::FetchError
276
+ end
277
+ end
278
+
279
+ ##
280
+ # Add host +host_name+ that belongs to +roles+. Extra arguments may
281
+ # be specified for the host as a hash as the last argument.
282
+ #
283
+ # host is the inversion of role:
284
+ #
285
+ # host 'db1.example.com', :db, :master_db
286
+ #
287
+ # Is equivalent to:
288
+ #
289
+ # role :db, 'db1.example.com'
290
+ # role :master_db, 'db1.example.com'
291
+
292
+ def self.host host_name, *roles
293
+ opts = Hash === roles.last ? roles.pop : {}
294
+
295
+ roles.each do |role_name|
296
+ role role_name, host_name, opts.dup
297
+ end
298
+ end
299
+
300
+ ##
301
+ # Returns an Array of all hosts in +roles+.
302
+
303
+ def self.hosts_for *roles
304
+ roles.flatten.map { |r|
305
+ self.roles[r].keys
306
+ }.flatten.uniq.sort
307
+ end
308
+
309
+ def self.mandatory name, desc # :nodoc:
310
+ self.set(name) do
311
+ raise(Rake::ConfigurationError,
312
+ "Please specify the #{desc} via the #{name.inspect} variable")
313
+ end
314
+ end
315
+
316
+ ##
317
+ # Ensures exclusive access to +name+.
318
+
319
+ def self.protect_env name # :nodoc:
320
+ @@env_locks[name].synchronize do
321
+ yield
322
+ end
323
+ end
324
+
325
+ ##
326
+ # Adds a remote task named +name+ with options +options+ that will
327
+ # execute +block+.
328
+
329
+ def self.remote_task name, *args, &block
330
+ options = (Hash === args.last) ? args.pop : {}
331
+ t = Rake::RemoteTask.define_task(name, *args, &block)
332
+ options[:roles] = Array options[:roles]
333
+ options[:roles] |= @@current_roles
334
+ t.options = options
335
+ t
336
+ end
337
+
338
+ ##
339
+ # Ensures +name+ does not conflict with an existing method.
340
+
341
+ def self.reserved_name? name # :nodoc:
342
+ !@@env.has_key?(name.to_s) && self.respond_to?(name)
343
+ end
344
+
345
+ ##
346
+ # Resets vlad, restoring all roles, tasks and environment variables
347
+ # to the defaults.
348
+
349
+ def self.reset
350
+ @@def_role_hash = {} # official default role value
351
+ @@env = {}
352
+ @@tasks = {}
353
+ @@roles = Hash.new { |h,k| h[k] = @@def_role_hash }
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
+ # TODO: merge:
369
+ # Declare a role and assign a remote host to it. Equivalent to the
370
+ # <tt>host</tt> method; provided for capistrano compatibility.
371
+
372
+ def self.role role_name, host = nil, args = {}
373
+ if block_given? then
374
+ raise ArgumentError, 'host not allowed with block' unless host.nil?
375
+
376
+ begin
377
+ current_roles << role_name
378
+ yield
379
+ ensure
380
+ current_roles.delete role_name
381
+ end
382
+ else
383
+ raise ArgumentError, 'host required' if host.nil?
384
+
385
+ [*host].each do |hst|
386
+ raise ArgumentError, "invalid host: #{hst}" if hst.nil? or hst.empty?
387
+ end
388
+ @@roles[role_name] = {} if @@def_role_hash.eql? @@roles[role_name]
389
+ @@roles[role_name][host] = args
390
+ end
391
+ end
392
+
393
+ ##
394
+ # The configured roles.
395
+
396
+ def self.roles
397
+ host domain, :app, :web, :db if @@roles.empty?
398
+
399
+ @@roles
400
+ end
401
+
402
+ ##
403
+ # Set environment variable +name+ to +value+ or +default_block+.
404
+ #
405
+ # If +default_block+ is defined, the block will be executed the
406
+ # first time the variable is fetched, and the value will be used for
407
+ # every subsequent fetch.
408
+
409
+ def self.set name, value = nil, &default_block
410
+ raise ArgumentError, "cannot provide both a value and a block" if
411
+ value and default_block unless
412
+ value == :per_thread
413
+ raise ArgumentError, "cannot set reserved name: '#{name}'" if
414
+ Rake::RemoteTask.reserved_name?(name) unless $TESTING
415
+
416
+ name = name.to_s
417
+
418
+ Rake::RemoteTask.per_thread[name] = true if
419
+ default_block && value == :per_thread
420
+
421
+ Rake::RemoteTask.default_env[name] = Rake::RemoteTask.env[name] =
422
+ default_block || value
423
+
424
+ if Object.public_instance_methods.include? name.to_sym then
425
+ Object.send :alias_method, :"old_#{name}", name
426
+ end
427
+
428
+ Object.send :define_method, name do
429
+ Rake::RemoteTask.fetch name
430
+ end
431
+ end
432
+
433
+ ##
434
+ # Sets all the default values. Should only be called once. Use reset
435
+ # if you need to restore values.
436
+
437
+ def self.set_defaults
438
+ @@default_env ||= {}
439
+ @@per_thread ||= {}
440
+ self.reset
441
+
442
+ mandatory :repository, "repository path"
443
+ mandatory :deploy_to, "deploy path"
444
+ mandatory :domain, "server domain"
445
+
446
+ simple_set(:deploy_timestamped, true,
447
+ :deploy_via, :export,
448
+ :keep_releases, 5,
449
+ :rake_cmd, "rake",
450
+ :revision, "head",
451
+ :rsync_cmd, "rsync",
452
+ :rsync_flags, ['-azP', '--delete'],
453
+ :ssh_cmd, "ssh",
454
+ :ssh_flags, [],
455
+ :sudo_cmd, "sudo",
456
+ :sudo_flags, ['-p Password:'],
457
+ :sudo_prompt, /^Password:/,
458
+ :umask, '02',
459
+ :mkdirs, [],
460
+ :shared_paths, {},
461
+ :perm_owner, nil,
462
+ :perm_group, nil)
463
+
464
+ set(:current_release) { File.join(releases_path, releases[-1]) }
465
+ set(:latest_release) {
466
+ deploy_timestamped ? release_path : current_release
467
+ }
468
+ set(:previous_release) { File.join(releases_path, releases[-2]) }
469
+ set(:release_name) { Time.now.utc.strftime("%Y%m%d%H%M%S") }
470
+ set(:release_path) { File.join(releases_path, release_name) }
471
+ set(:releases) { task.run("ls -x #{releases_path}").split.sort }
472
+
473
+ set_path :current_path, "current"
474
+ set_path :releases_path, "releases"
475
+ set_path :scm_path, "scm"
476
+ set_path :shared_path, "shared"
477
+
478
+ set(:sudo_password) do
479
+ state = `stty -g`
480
+
481
+ raise Rake::Error, "stty(1) not found" unless $?.success?
482
+
483
+ begin
484
+ system "stty -echo"
485
+ $stdout.print "sudo password: "
486
+ $stdout.flush
487
+ sudo_password = $stdin.gets
488
+ $stdout.puts
489
+ ensure
490
+ system "stty #{state}"
491
+ end
492
+ sudo_password
493
+ end
494
+ end
495
+
496
+ def self.set_path(name, subdir) # :nodoc:
497
+ set(name) { File.join(deploy_to, subdir) }
498
+ end
499
+
500
+ def self.simple_set(*args) # :nodoc:
501
+ args = Hash[*args]
502
+ args.each do |k, v|
503
+ set k, v
504
+ end
505
+ end
506
+
507
+ ##
508
+ # The Rake::RemoteTask executing in this Thread.
509
+
510
+ def self.task
511
+ Thread.current[:task]
512
+ end
513
+
514
+ ##
515
+ # The configured Rake::RemoteTasks.
516
+
517
+ def self.tasks
518
+ @@tasks
519
+ end
520
+
521
+ ##
522
+ # Execute +command+ under sudo using run.
523
+
524
+ def sudo command
525
+ run [sudo_cmd, sudo_flags, command].flatten.compact.join(" ")
526
+ end
527
+
528
+ ##
529
+ # Sets the target host. Allows you to set an optional directory
530
+ # using the format:
531
+ #
532
+ # host.domain:/dir
533
+
534
+ def target_host= host
535
+ if host =~ /^(.+):(.+?)$/
536
+ @target_host = $1
537
+ @target_dir = $2
538
+ else
539
+ @target_host = host
540
+ @target_dir = nil
541
+ end
542
+ end
543
+
544
+ ##
545
+ # The hosts this task will execute on. The hosts are determined from
546
+ # the role this task belongs to.
547
+ #
548
+ # The target hosts may be overridden by providing a comma-separated
549
+ # list of commands to the HOSTS environment variable:
550
+ #
551
+ # rake my_task HOSTS=app1.example.com,app2.example.com
552
+
553
+ def target_hosts
554
+ if hosts = ENV["HOSTS"] then
555
+ hosts.strip.gsub(/\s+/, '').split(",")
556
+ else
557
+ roles = Array options[:roles]
558
+
559
+ if roles.empty? then
560
+ Rake::RemoteTask.all_hosts
561
+ else
562
+ Rake::RemoteTask.hosts_for roles
563
+ end
564
+ end
565
+ end
566
+
567
+ ##
568
+ # Similar to target_hosts, but returns true if user defined any hosts, even
569
+ # an empty list.
570
+
571
+ def defined_target_hosts?
572
+ return true if ENV["HOSTS"]
573
+ roles = Array options[:roles]
574
+ return true if roles.empty?
575
+ # borrowed from hosts_for:
576
+ roles.flatten.each { |r|
577
+ return true unless @@def_role_hash.eql? Rake::RemoteTask.roles[r]
578
+ }
579
+ return false
580
+ end
581
+
582
+ ##
583
+ # Action is used to run a task's remote_actions in parallel on each
584
+ # of its hosts. Actions are created automatically in
585
+ # Rake::RemoteTask#enhance.
586
+
587
+ class Action
588
+
589
+ ##
590
+ # The task this action is attached to.
591
+
592
+ attr_reader :task
593
+
594
+ ##
595
+ # The block this action will execute.
596
+
597
+ attr_reader :block
598
+
599
+ ##
600
+ # An Array of threads, one for each host this action executes on.
601
+
602
+ attr_reader :workers
603
+
604
+ ##
605
+ # Creates a new Action that will run +block+ for +task+.
606
+
607
+ def initialize task, block
608
+ @task = task
609
+ @block = block
610
+ @workers = ThreadGroup.new
611
+ end
612
+
613
+ def == other # :nodoc:
614
+ return false unless Action === other
615
+ block == other.block && task == other.task
616
+ end
617
+
618
+ ##
619
+ # Execute this action on +hosts+ in parallel. Returns when block
620
+ # has completed for each host.
621
+
622
+ def execute hosts, task, args
623
+ hosts.each do |host|
624
+ t = task.clone
625
+ t.target_host = host
626
+ thread = Thread.new(t) do |task2|
627
+ Thread.current[:task] = task2
628
+ case block.arity
629
+ when 1
630
+ block.call task2
631
+ else
632
+ block.call task2, args
633
+ end
634
+ Thread.current[:task] = nil
635
+ end
636
+ @workers.add thread
637
+ end
638
+ @workers.list.each { |thr| thr.join }
639
+ end
640
+ end
641
+ end
642
+
643
+ Rake::RemoteTask.set_defaults
@@ -0,0 +1,72 @@
1
+ require 'minitest/autorun'
2
+ require 'rake'
3
+ require 'rake/remote_task'
4
+ require 'stringio'
5
+
6
+ class StringIO
7
+ def readpartial(size) read end # suck!
8
+ end
9
+
10
+ module Process
11
+ def self.expected status
12
+ @@expected ||= []
13
+ @@expected << status
14
+ end
15
+
16
+ class << self
17
+ alias :waitpid2_old :waitpid2
18
+
19
+ def waitpid2(pid)
20
+ [ @@expected.shift ]
21
+ end
22
+ end
23
+ end
24
+
25
+ class Rake::RemoteTask
26
+ attr_accessor :commands, :action, :input, :output, :error
27
+
28
+ Status = Struct.new :exitstatus
29
+
30
+ class Status
31
+ def success?() exitstatus == 0 end
32
+ end
33
+
34
+ def system *command
35
+ @commands << command
36
+ self.action ? self.action[command.join(' ')] : true
37
+ end
38
+
39
+ def popen4 *command
40
+ @commands << command
41
+
42
+ @input = StringIO.new
43
+ out = StringIO.new @output.shift.to_s
44
+ err = StringIO.new @error.shift.to_s
45
+
46
+ raise if block_given?
47
+
48
+ status = self.action ? self.action[command.join(' ')] : 0
49
+ Process.expected Status.new(status)
50
+
51
+ return 42, @input, out, err
52
+ end
53
+
54
+ def select reads, writes, errs, timeout
55
+ [reads, writes, errs]
56
+ end
57
+ end
58
+
59
+ class Rake::TestCase < MiniTest::Unit::TestCase
60
+ def setup
61
+ @rake = Rake::RemoteTask
62
+ @rake.reset
63
+ Rake.application.clear
64
+ @task_count = Rake.application.tasks.size
65
+ @rake.set :domain, "example.com"
66
+ end
67
+
68
+ def util_set_hosts
69
+ @rake.host "app.example.com", :app
70
+ @rake.host "db.example.com", :db
71
+ end
72
+ end
@@ -0,0 +1,272 @@
1
+ require 'rake/test_case'
2
+
3
+ class TestRakeRemoteTask < Rake::TestCase
4
+ # TODO: move to minitest
5
+ def assert_silent
6
+ out, err = capture_io do
7
+ yield
8
+ end
9
+
10
+ assert_empty err
11
+ assert_empty out
12
+ end
13
+
14
+ def test_enhance
15
+ util_set_hosts
16
+ body = Proc.new { 5 }
17
+ task = @rake.remote_task(:some_task => :foo, &body)
18
+ action = Rake::RemoteTask::Action.new(task, body)
19
+ assert_equal [action], task.remote_actions
20
+ assert_equal task, action.task
21
+ assert_equal ["foo"], task.prerequisites
22
+ end
23
+
24
+ def test_enhance_with_no_task_body
25
+ util_set_hosts
26
+ util_setup_task
27
+ assert_equal [], @task.remote_actions
28
+ assert_equal [], @task.prerequisites
29
+ end
30
+
31
+ def test_execute
32
+ util_set_hosts
33
+ set :some_variable, 1
34
+ set :can_set_nil, nil
35
+ set :lies_are, false
36
+ x = 5
37
+ task = @rake.remote_task(:some_task) { x += some_variable }
38
+ task.execute nil
39
+ assert_equal 1, task.some_variable
40
+ assert_equal 7, x
41
+ assert task.can_set_nil.nil?
42
+ assert_equal false, task.lies_are
43
+ end
44
+
45
+ def test_set_false
46
+ set :can_set_nil, nil
47
+ set :lies_are, false
48
+
49
+ assert_equal nil, task.can_set_nil
50
+
51
+ assert_equal false, task.lies_are
52
+ assert_equal false, Rake::RemoteTask.fetch(:lies_are)
53
+ end
54
+
55
+
56
+ def test_fetch_false
57
+ assert_equal false, Rake::RemoteTask.fetch(:unknown, false)
58
+ end
59
+
60
+ def test_execute_exposes_target_host
61
+ host "app.example.com", :app
62
+ task = remote_task(:target_task) { set(:test_target_host, target_host) }
63
+ task.execute nil
64
+ assert_equal "app.example.com", Rake::RemoteTask.fetch(:test_target_host)
65
+ end
66
+
67
+ def test_execute_with_no_hosts
68
+ @rake.host "app.example.com", :app
69
+ t = @rake.remote_task(:flunk, :roles => :db) { flunk "should not have run" }
70
+ e = assert_raises(Rake::ConfigurationError) { t.execute nil }
71
+ assert_equal "No target hosts specified on task flunk for roles [:db]",
72
+ e.message
73
+ end
74
+
75
+ def test_execute_with_no_roles
76
+ t = @rake.remote_task(:flunk, :roles => :junk) { flunk "should not have run" }
77
+ e = assert_raises(Rake::ConfigurationError) { t.execute nil }
78
+ assert_equal "No target hosts specified on task flunk for roles [:junk]",
79
+ e.message
80
+ end
81
+
82
+ def test_execute_with_roles
83
+ util_set_hosts
84
+ set :some_variable, 1
85
+ x = 5
86
+ task = @rake.remote_task(:some_task, :roles => :db) { x += some_variable }
87
+ task.execute nil
88
+ assert_equal 1, task.some_variable
89
+ assert_equal 6, x
90
+ end
91
+
92
+ def test_rsync
93
+ util_setup_task
94
+ @task.target_host = "app.example.com"
95
+
96
+ assert_silent do
97
+ @task.rsync 'localfile', 'host:remotefile'
98
+ end
99
+
100
+ commands = @task.commands
101
+
102
+ assert_equal 1, commands.size, 'not enough commands'
103
+ assert_equal(%w[rsync -azP --delete localfile host:remotefile],
104
+ commands.first)
105
+ end
106
+
107
+ def test_rsync_fail
108
+ util_setup_task
109
+ @task.target_host = "app.example.com"
110
+ @task.action = proc { false }
111
+
112
+ e = assert_raises Rake::CommandFailedError do
113
+ assert_silent do
114
+ @task.rsync 'local', 'host:remote'
115
+ end
116
+ end
117
+ exp = "execution failed: rsync -azP --delete local host:remote"
118
+ assert_equal exp, e.message
119
+ end
120
+
121
+ def test_rsync_deprecation
122
+ util_setup_task
123
+ @task.target_host = "app.example.com"
124
+
125
+ out, err = capture_io do
126
+ @task.rsync 'localfile', 'remotefile'
127
+ end
128
+
129
+ commands = @task.commands
130
+
131
+ assert_equal 1, commands.size, 'not enough commands'
132
+ assert_equal(%w[rsync -azP --delete localfile app.example.com:remotefile],
133
+ commands.first)
134
+
135
+ assert_equal("rsync deprecation: pass target_host:remote_path explicitly\n",
136
+ err)
137
+ assert_empty out
138
+ # flunk "not yet"
139
+ end
140
+
141
+ def test_get
142
+ util_setup_task
143
+ @task.target_host = "app.example.com"
144
+
145
+ assert_silent do
146
+ @task.get 'tmp', "remote1", "remote2"
147
+ end
148
+
149
+ commands = @task.commands
150
+
151
+ expected = %w[rsync -azP --delete app.example.com:remote1 app.example.com:remote2 tmp]
152
+
153
+ assert_equal 1, commands.size
154
+ assert_equal expected, commands.first
155
+ end
156
+
157
+ def test_put
158
+ util_setup_task
159
+ @task.target_host = "app.example.com"
160
+
161
+ assert_silent do
162
+ @task.put 'dest' do
163
+ "whatever"
164
+ end
165
+ end
166
+
167
+ commands = @task.commands
168
+
169
+ expected = %w[rsync -azP --delete HAPPY app.example.com:dest]
170
+ commands.first[3] = 'HAPPY'
171
+
172
+ assert_equal 1, commands.size
173
+ assert_equal expected, commands.first
174
+ end
175
+
176
+ def test_run
177
+ util_setup_task
178
+ @task.output << "file1\nfile2\n"
179
+ @task.target_host = "app.example.com"
180
+ result = nil
181
+
182
+ out, err = capture_io do
183
+ result = @task.run("ls")
184
+ end
185
+
186
+ commands = @task.commands
187
+
188
+ assert_equal 1, commands.size, 'not enough commands'
189
+ assert_equal ["ssh", "app.example.com", "ls"],
190
+ commands.first, 'app'
191
+ assert_equal "file1\nfile2\n", result
192
+
193
+ assert_equal "file1\nfile2\n", out
194
+ assert_equal '', err
195
+ end
196
+
197
+ def test_run_dir
198
+ util_setup_task
199
+ @task.target_host = "app.example.com:/www/dir1"
200
+
201
+ @task.run("ls")
202
+
203
+ commands = @task.commands
204
+
205
+ assert_equal 1, commands.size, 'not enough commands'
206
+ assert_equal [["ssh", "app.example.com", "cd /www/dir1 && ls"]], commands
207
+ end
208
+
209
+ def test_run_failing_command
210
+ util_set_hosts
211
+ util_setup_task
212
+ @task.input = StringIO.new "file1\nfile2\n"
213
+ @task.target_host = 'app.example.com'
214
+ @task.action = proc { 1 }
215
+
216
+ e = assert_raises(Rake::CommandFailedError) { @task.run("ls") }
217
+ assert_equal "execution failed with status 1: ssh app.example.com ls", e.message
218
+
219
+ assert_equal 1, @task.commands.size
220
+ end
221
+
222
+ def test_run_sudo
223
+ util_setup_task
224
+ @task.output << "file1\nfile2\n"
225
+ @task.error << 'Password:'
226
+ @task.target_host = "app.example.com"
227
+ def @task.sudo_password() "my password" end # gets defined by set
228
+ result = nil
229
+
230
+ out, err = capture_io do
231
+ result = @task.run("sudo ls")
232
+ end
233
+
234
+ commands = @task.commands
235
+
236
+ assert_equal 1, commands.size, 'not enough commands'
237
+ assert_equal ['ssh', 'app.example.com', 'sudo ls'],
238
+ commands.first
239
+
240
+ assert_equal "my password\n", @task.input.string
241
+
242
+ # WARN: Technically incorrect, the password line should be
243
+ # first... this is an artifact of changes to the IO code in run
244
+ # and the fact that we have a very simplistic (non-blocking)
245
+ # testing model.
246
+ assert_equal "file1\nfile2\nPassword:\n", result
247
+
248
+ assert_equal "file1\nfile2\n", out
249
+ assert_equal "Password:\n", err
250
+ end
251
+
252
+ def test_sudo
253
+ util_setup_task
254
+ @task.target_host = "app.example.com"
255
+ @task.sudo "ls"
256
+
257
+ commands = @task.commands
258
+
259
+ assert_equal 1, commands.size, 'wrong number of commands'
260
+ assert_equal ["ssh", "app.example.com", "sudo -p Password: ls"],
261
+ commands.first, 'app'
262
+ end
263
+
264
+ def util_setup_task(options = {})
265
+ @task = @rake.remote_task :test_task, options
266
+ @task.commands = []
267
+ @task.output = []
268
+ @task.error = []
269
+ @task.action = nil
270
+ @task
271
+ end
272
+ end
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fpauser-rake-remote_task
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.0.3
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ryan Davis
9
+ - Eric Hodel
10
+ - Wilson Bilkovich
11
+ - Falk Pauser
12
+ autorequire:
13
+ bindir: bin
14
+ cert_chain: []
15
+ date: 2011-06-22 00:00:00.000000000 +02:00
16
+ default_executable:
17
+ dependencies:
18
+ - !ruby/object:Gem::Dependency
19
+ name: rake
20
+ requirement: &9800500 !ruby/object:Gem::Requirement
21
+ none: false
22
+ requirements:
23
+ - - ! '>='
24
+ - !ruby/object:Gem::Version
25
+ version: 0.8.0
26
+ type: :runtime
27
+ prerelease: false
28
+ version_requirements: *9800500
29
+ - !ruby/object:Gem::Dependency
30
+ name: open4
31
+ requirement: &9799780 !ruby/object:Gem::Requirement
32
+ none: false
33
+ requirements:
34
+ - - ! '>='
35
+ - !ruby/object:Gem::Version
36
+ version: 0.9.0
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: *9799780
40
+ description:
41
+ email:
42
+ - ryand-ruby@zenspider.com
43
+ - drbrain@segment7.net
44
+ - wilson@supremetyrant.com
45
+ - falk.pauser@gmail.com
46
+ executables: []
47
+ extensions: []
48
+ extra_rdoc_files:
49
+ - README.txt
50
+ files:
51
+ - .autotest
52
+ - Gemfile
53
+ - Gemfile.lock
54
+ - History.txt
55
+ - Manifest.txt
56
+ - README.txt
57
+ - Rakefile
58
+ - lib/rake/remote_task.rb
59
+ - lib/rake/test_case.rb
60
+ - test/test_rake_remote_task.rb
61
+ has_rdoc: true
62
+ homepage: http://github.com/fpauser/rake-remote_task
63
+ licenses: []
64
+ post_install_message:
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ! '>='
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ! '>='
78
+ - !ruby/object:Gem::Version
79
+ version: 1.3.6
80
+ requirements: []
81
+ rubyforge_project:
82
+ rubygems_version: 1.6.2
83
+ signing_key:
84
+ specification_version: 3
85
+ summary: Vlad the Deployer's sexy brainchild is rake-remote_task, extending Rake with
86
+ remote task goodness.
87
+ test_files: []