fpauser-rake-remote_task 2.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []