vlad 1.0.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.
data/History.txt ADDED
@@ -0,0 +1,5 @@
1
+ == 1.0.0 / 2007-08-04
2
+
3
+ * 1 major enhancement
4
+ * Birthday!
5
+
data/Manifest.txt ADDED
@@ -0,0 +1,19 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ considerations.txt
6
+ doco/getting_started.txt
7
+ doco/migration.txt
8
+ doco/perforce.txt
9
+ doco/variables.txt
10
+ lib/rake_remote_task.rb
11
+ lib/vlad.rb
12
+ lib/vlad/perforce.rb
13
+ lib/vlad/subversion.rb
14
+ lib/vlad_tasks.rb
15
+ test/test_rake_remote_task.rb
16
+ test/test_vlad.rb
17
+ test/test_vlad_perforce.rb
18
+ test/test_vlad_subversion.rb
19
+ test/vlad_test_case.rb
data/README.txt ADDED
@@ -0,0 +1,81 @@
1
+ Vlad the Deployer
2
+ by the Ruby Hit Squad
3
+ http://rubyhitsquad.com/
4
+ http://rubyforge.org/projects/hitsquad/
5
+
6
+ == DESCRIPTION:
7
+
8
+ Vlad the Deployer is pragmatic application deployment automation,
9
+ without mercy. Much like Capistrano, but with 1/10th the
10
+ complexity. Vlad integrates seamlessly with Rake, and uses familiar
11
+ and standard tools like ssh and rsync.
12
+
13
+ Impale your application on the heartless spike of the Deployer.
14
+
15
+ == FEATURES/PROBLEMS:
16
+
17
+ * Full deployment automation stack.
18
+ * Supports single server deployment with just 4 variables defined.
19
+ * Very few dependencies. All simple.
20
+ * Uses ssh with your ssh settings already in place.
21
+ * Uses rsync for efficient transfers.
22
+ * Run remote commands on one or more servers.
23
+ * Syncs files to one or more servers.
24
+ * Mix and match local and remote tasks.
25
+ * Built on rake. easy.
26
+ * Compatible with all of your tab completion shell script rake-tastic goodness.
27
+ * Ships with tests that actually pass.
28
+ * Engine is under 500 lines of code.
29
+ * Super uper simple.
30
+ * Does NOT support Windows right now. Coming soon in 1.1.
31
+ * This is 1.0.0... expect rough edges.
32
+
33
+ == SYNOPSIS:
34
+
35
+ rake vlad:setup # first time only
36
+ rake vlad:update
37
+ rake vlad:migrate # optional
38
+ rake vlad:start
39
+
40
+ == REQUIREMENTS:
41
+
42
+ * Rake
43
+ * Hoe
44
+ * Rubyforge
45
+ * open4
46
+
47
+ == INSTALL:
48
+
49
+ * sudo gem install -y vlad
50
+
51
+ == SPECIAL THANKS:
52
+
53
+ * First, of course, to Capistrano. For coming up with the idea and
54
+ providing a lot of meat for the recipes.
55
+ * Scott Baron for coming up with one of the best project names evar.
56
+ * Bradley Taylor for giving us permission to use RailsMachine recipes sans-LGPL.
57
+
58
+ == LICENSE:
59
+
60
+ (The MIT License)
61
+
62
+ Copyright (c) 2007 Ryan Davis and the rest of the Ruby Hit Squad
63
+
64
+ Permission is hereby granted, free of charge, to any person obtaining
65
+ a copy of this software and associated documentation files (the
66
+ 'Software'), to deal in the Software without restriction, including
67
+ without limitation the rights to use, copy, modify, merge, publish,
68
+ distribute, sublicense, and/or sell copies of the Software, and to
69
+ permit persons to whom the Software is furnished to do so, subject to
70
+ the following conditions:
71
+
72
+ The above copyright notice and this permission notice shall be
73
+ included in all copies or substantial portions of the Software.
74
+
75
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
76
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
77
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
78
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
79
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
80
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
81
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ $: << 'lib'
6
+ require 'vlad'
7
+
8
+ Hoe.new('vlad', Vlad::VERSION) do |p|
9
+ p.rubyforge_name = 'hitsquad'
10
+ p.author = ["Ryan Davis", "Eric Hodel", "Wilson Bilkovich"]
11
+ p.email = "ryand-ruby@zenspider.com"
12
+ p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/).map { |s| s.strip }[2..-1]
13
+ p.description = p.paragraphs_of('README.txt', 2..5).join("\n\n")
14
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
15
+ p.extra_deps << 'rake'
16
+ p.extra_deps << 'open4'
17
+ end
18
+
19
+ task :flog do
20
+ sh 'find lib -name \*.rb | grep -v vlad_tasks | xargs flog | head -1'
21
+ end
22
+
23
+ task :flog_full do
24
+ sh 'find lib -name \*.rb | xargs flog -a'
25
+ end
26
+
27
+ # vim: syntax=Ruby
@@ -0,0 +1,91 @@
1
+ * might we want per connection values?
2
+
3
+ * :except => {:no_release => true}
4
+
5
+ It is common to configure tasks to 'announce' deployments in IRC, Campfire,
6
+ etc. If you have 6 app servers, you don't want to see 6 announcements. In
7
+ Capistrano, this is handled via the :no_release => true flag. Various tasks
8
+ only execute on the 'release' servers.
9
+
10
+ An easier way to meet this would be to introduce a :release role in the
11
+ default setup
12
+
13
+ role :release, "app1.example.com"
14
+
15
+ remote_task :announce_in_irc, :roles => :release ...
16
+
17
+ Drawback: Yet another thing to change when you migrate a project from cap to
18
+ vlad
19
+
20
+ * 'dynamic deployments'
21
+
22
+ role :app, "app1.example.com"
23
+ role :app, "app2.example.com"
24
+
25
+ Let's say that app1 and app2 need slightly different monit configurations.
26
+
27
+ In Capistrano, you might approach this by making two additional roles, and
28
+ splitting your 'push a monit config' task into two. This sucks.
29
+
30
+ Vlad makes the 'execution context' of a task available. In Vlad, you would:
31
+
32
+ remote_task :update_monit, :roles => :app
33
+ rsync "templates/#{target_host}.monitrc", "/etc/monitrc"
34
+ end
35
+
36
+ * fine-grained tasks
37
+
38
+ remote_task :update
39
+ remote_task :symlink
40
+ remote_task :migrate
41
+ remote_task :deploy => [:update, :symlink, :migrate, :restart]
42
+
43
+ Let's assume that this is a multi-server config with shared deploy path.
44
+ The user wants to do only a single checkout. If we make "update" be one big
45
+ task body that includes the update, symlink, and migrate steps,
46
+ it is difficult for the user to override the roles for the particular steps
47
+ they need to change.
48
+
49
+ If we break these into separate tasks, they can say:
50
+
51
+ Rake::Task["migrate"].options[:roles] = :master_db
52
+
53
+ and the migrations will only run on the master db
54
+
55
+ * sudo / via how? and if we call it via I will stab ppl. "user" is sufficient.
56
+
57
+ * handling 'use_sudo'
58
+
59
+ 1. Check for this inside the 'run' command, and preface the command
60
+ with 'sudo' if necessary
61
+
62
+ 2. Default this to 'false' in the reset method, and check for it
63
+ in the default tasks that we provide:
64
+ if use_sudo then
65
+ sudo "blah"
66
+ else
67
+ run "blah"
68
+ end
69
+
70
+ Option 2 has fewer moving parts, but clutters up the tasks that care about
71
+ this.
72
+
73
+ * Dependencies
74
+
75
+ Task dependencies aren't settable when creating a Rake::RemoteTask.
76
+
77
+ * Apache configuration
78
+
79
+ Pull in railsmachine/rails/recipes/apache.rb's apache configuration. Needs
80
+ erb to work.
81
+
82
+ * I really like tasks with naming <cmd>_<role> (eg setup_app,
83
+ start_web). We could easily make the front end remote_task command
84
+ look for such a convention and apply the :role => x automatically.
85
+
86
+ * from bousquet: get a couple of server environment recipes that prepare your
87
+ machine that would be the golden ticket:
88
+
89
+ rake vlad:prepare TYPE=accelerator | ubuntu | osx | osxserver | site5 | ...
90
+
91
+ and have people maintaining those setups who depend on them
@@ -0,0 +1,27 @@
1
+
2
+ == Quick Start for a 1-Server Solution:
3
+
4
+ === Setup
5
+
6
+ * Create a deploy file, usually in "config/deploy.rb":
7
+
8
+ set :application, "project"
9
+ set :domain, "example.com"
10
+ set :deploy_to, "/path/to/install"
11
+ set :repository, 'http://svn.example.com/project/branches/stable/'
12
+
13
+ This defaults to using 'svn export' from +repository+, and a single
14
+ server for +app+, +db+, and +www+. If you need to tweak these things,
15
+ refer to the variable documentation.
16
+
17
+ * Add the following to your Rakefile:
18
+
19
+ require 'vlad'
20
+ Vlad.load 'config/deploy.rb'
21
+
22
+ * <tt>rake vlad:setup</tt>
23
+
24
+ === Launch
25
+
26
+ * <tt>rake vlad:update vlad:migrate vlad:start</tt>
27
+
@@ -0,0 +1,21 @@
1
+ == Converting from Capistrano
2
+
3
+ * 'task' blocks are renamed to 'remote_task'.
4
+ * Most variables are the same. See variables.txt for details.
5
+ * No +with_command+ / +sudo+ / +via+ wonkiness
6
+ * Uses real ssh so env vars and the like are not a problem
7
+ - no +with_env+ as a result.
8
+ * Vlad doesn't use ':no_release' or ':primary'.
9
+ - If you have a task that needs to run on only one host from a role,
10
+ you should declare a new role for that host:
11
+
12
+ role :master_db, "master.example.com"
13
+
14
+ ..and then override the role for the task you want to limit:
15
+
16
+ Rake::Task["mytask"].options[:roles] = :master_db
17
+
18
+ * The 'host' method can be used to consolidate multiple 'role' calls.
19
+ - host "www.example.com", :app, :web, :db
20
+ specifies a host with three roles.
21
+ * migrate_env is now migrate_args.
data/doco/perforce.txt ADDED
@@ -0,0 +1,5 @@
1
+ # Using Perforce
2
+
3
+ TODO: write real doco.
4
+
5
+ Set up a .p4config file and put it in the client's scm directories.
@@ -0,0 +1,44 @@
1
+
2
+ == Variables
3
+
4
+ application:: REQUIRED: Name of your application. e.g. hitsquad
5
+ repository:: REQUIRED: Repository path: e.g. http://repo.example.com/svn
6
+ deploy_to:: REQUIRED: Deploy path on target machines. e.g. /var/www/app
7
+ domain:: REQUIRED: Used for the common case of a single target
8
+ server. e.g. example.com
9
+ current_path:: The full path on the remote host that will be symlinked
10
+ as 'current'. Defaults to "#{deploy_to}/current".
11
+ current_release:: The full path to the current release's actual location.
12
+ Defaults to "#{releases_path}/#{releases.last}".
13
+ deploy_timestamped:: Create timestamped release directories instead of using
14
+ revision numbers. Defaults to true.
15
+ deploy_via:: Which SCM command should be used when deploying the app.
16
+ Defaults to "export".
17
+ latest_release:: The most recent release, which may not yet have been
18
+ symlinked. Defaults to release_path.
19
+ migrate_args:: Set this to change the RAILS_ENV that 'rake db:migrate'
20
+ will run under. Defaults to "".
21
+ migrate_target:: Set this if you need to specify a particular migration
22
+ 'VERSION' number. Defaults to "latest".
23
+ rails_env:: Specifies the RAILS_ENV environment variable that will
24
+ be used. Defaults to "production".
25
+ rake:: Set this if you need to specify an alternate path to
26
+ 'rake'. Defaults to "rake".
27
+ release_name:: Name of the release directory, if deploy_timestamped is
28
+ true. Defaults to timestamp: "YYYYMMDDHHMMSS".
29
+ release_path:: Path to this release, which may not have been created
30
+ yet. Defaults to "#{releases_path}/#{release_name}".
31
+ releases:: An array of all existing releases, oldest first.
32
+ Defaults to latest release directory name.
33
+ releases_path:: Full path to the 'releases' directory on the remote host.
34
+ Defaults to "#{deploy_to}/releases".
35
+ scm:: Which SCM module to use. Valid options are :subversion
36
+ and :perforce as of 1.0. Defaults to "subversion"
37
+ scm_path:: Path on the remote host that will be used as 'working
38
+ space' for SCM tasks. Defaults to "#{deploy_to}/scm".
39
+ sudo_password:: Asks for password when referenced.
40
+ source:: Read-Only: An SCM worker instance defined by scm.
41
+ shared_path:: Full path to remote 'shared' directory, symlinked into
42
+ your app by default. Defaults to "#{deploy_to}/shared".
43
+ web_command:: Command to execute when controlling the web server.
44
+ Defaults to "apachectl".
@@ -0,0 +1,399 @@
1
+ require 'rubygems'
2
+ require 'open4'
3
+ require 'vlad'
4
+
5
+ ##
6
+ # Rake::RemoteTask is a subclass of Rake::Task that adds remote_actions that
7
+ # execute in parallel on multiple hosts via ssh.
8
+
9
+ class Rake::RemoteTask < Rake::Task
10
+
11
+ include Open4
12
+
13
+ ##
14
+ # Options for execution of this task.
15
+
16
+ attr_accessor :options
17
+
18
+ ##
19
+ # The host this task is running on during execution.
20
+
21
+ attr_accessor :target_host
22
+
23
+ ##
24
+ # An Array of Actions this host will perform during execution. Use enhance
25
+ # to add new actions to a task.
26
+
27
+ attr_reader :remote_actions
28
+
29
+ ##
30
+ # Create a new task named +task_name+ attached to Rake::Application +app+.
31
+
32
+ def initialize(task_name, app)
33
+ super
34
+ @remote_actions = []
35
+ end
36
+
37
+ ##
38
+ # Add a local action to this task. This calls Rake::Task#enhance.
39
+
40
+ alias_method :original_enhance, :enhance
41
+
42
+ ##
43
+ # Add remote action +block+ to this task with dependencies +deps+. See
44
+ # Rake::Task#enhance.
45
+
46
+ def enhance(deps=nil, &block)
47
+ original_enhance(deps) # can't use super because block passed regardless.
48
+ @remote_actions << Action.new(self, block) if block_given?
49
+ self
50
+ end
51
+
52
+ ##
53
+ # Execute this action. Local actions will be performed first, then remote
54
+ # actions will be performed in parallel on each host configured for this
55
+ # RemoteTask.
56
+
57
+ def execute
58
+ raise Vlad::ConfigurationError, "No target hosts specified for task: #{self.name}" if target_hosts.empty?
59
+ super
60
+ @remote_actions.each { |act| act.execute(target_hosts) }
61
+ end
62
+
63
+ ##
64
+ # Use rsync to send +local+ to +remote+ on target_host.
65
+
66
+ def rsync local, remote
67
+ cmd = ['rsync', '-azP', '--delete', local, "#{@target_host}:#{remote}"]
68
+
69
+ success = system(*cmd)
70
+
71
+ unless success then
72
+ raise Vlad::CommandFailedError, "execution failed: #{cmd.join ' '}"
73
+ end
74
+ end
75
+
76
+ ##
77
+ # Use ssh to execute +command+ on target_host. If +command+ uses sudo, the
78
+ # sudo password will be prompted for then saved for subsequent sudo commands.
79
+
80
+ def run command
81
+ cmd = ["ssh", target_host, command]
82
+ result = []
83
+
84
+ puts cmd.join(' ') if Rake.application.options.trace
85
+
86
+ status = popen4(*cmd) do |pid, inn, out, err|
87
+ inn.sync = true
88
+
89
+ until out.eof? and err.eof? do
90
+ unless err.eof? then
91
+ data = err.readpartial(1024)
92
+ result << data
93
+ $stderr.write data
94
+
95
+ if data =~ /^Password:/ then
96
+ inn.puts sudo_password
97
+ result << "\n"
98
+ $stderr.write "\n"
99
+ end
100
+ end
101
+
102
+ unless out.eof? then
103
+ data = out.readpartial(1024)
104
+ result << data
105
+ $stdout.write data
106
+ end
107
+ end
108
+ end
109
+
110
+ unless status.success? then
111
+ raise Vlad::CommandFailedError, "execution failed with status #{status.exitstatus}: #{cmd.join ' '}"
112
+ end
113
+
114
+ result.join
115
+ end
116
+
117
+ ##
118
+ # Execute +command+ under sudo using run.
119
+
120
+ def sudo command
121
+ run "sudo #{command}"
122
+ end
123
+
124
+ ##
125
+ # The hosts this task will execute on. The hosts are determined from the
126
+ # role this task belongs to.
127
+ #
128
+ # The target hosts may be overridden by providing a comma-separated list of
129
+ # commands to the HOSTS environment variable:
130
+ #
131
+ # rake my_task HOSTS=app1.example.com,app2.example.com
132
+
133
+ def target_hosts
134
+ if hosts = ENV["HOSTS"] then
135
+ hosts.strip.gsub(/\s+/, '').split(",")
136
+ else
137
+ roles = options[:roles]
138
+ roles ? Rake::RemoteTask.hosts_for(roles) : Rake::RemoteTask.all_hosts
139
+ end
140
+ end
141
+
142
+ ##
143
+ # Returns an Array with every host configured.
144
+
145
+ def self.all_hosts
146
+ hosts_for(roles.keys)
147
+ end
148
+
149
+ ##
150
+ # Fetches environment variable +name+ from the environment using default
151
+ # +default+.
152
+
153
+ def self.fetch name, default = nil
154
+ name = name.to_s if Symbol === name
155
+ if @@env.has_key? name then
156
+ protect_env(name) do
157
+ v = @@env[name]
158
+ v = @@env[name] = v.call if Proc === v
159
+ v
160
+ end
161
+ elsif default
162
+ v = @@env[name] = default
163
+ else
164
+ raise Vlad::FetchError
165
+ end
166
+ end
167
+
168
+ ##
169
+ # Add host +host_name+ that belongs to +roles+. Extra arguments may be
170
+ # specified for the host as a hash as the last argument.
171
+ #
172
+ # host is the inversion of role:
173
+ #
174
+ # host 'db1.example.com', :db, :master_db
175
+ #
176
+ # Is equivalent to:
177
+ #
178
+ # role :db, 'db1.example.com'
179
+ # role :master_db, 'db1.example.com'
180
+
181
+ def self.host host_name, *roles
182
+ opts = Hash === roles.last ? roles.pop : {}
183
+
184
+ roles.each do |role_name|
185
+ role role_name, host_name, opts.dup
186
+ end
187
+ end
188
+
189
+ ##
190
+ # Returns an Array of all hosts in +roles+.
191
+
192
+ def self.hosts_for *roles
193
+ roles.flatten.map { |r|
194
+ self.roles[r].keys
195
+ }.flatten.uniq.sort
196
+ end
197
+
198
+ ##
199
+ # Ensures exclusive access to +name+.
200
+
201
+ def self.protect_env name # :nodoc:
202
+ @@env_locks[name.to_s].synchronize do
203
+ yield
204
+ end
205
+ end
206
+
207
+ ##
208
+ # Ensures +name+ does not conflict with an existing method.
209
+
210
+ def self.reserved_name? name # :nodoc:
211
+ !@@env.has_key?(name.to_s) && self.respond_to?(name)
212
+ end
213
+
214
+ ##
215
+ # The Rake::RemoteTask executing in this Thread.
216
+
217
+ def self.task
218
+ Thread.current[:task]
219
+ end
220
+
221
+ ##
222
+ # The configured roles.
223
+
224
+ def self.roles
225
+ host domain, :app, :web, :db if @@roles.empty?
226
+
227
+ @@roles
228
+ end
229
+
230
+ ##
231
+ # The configured Rake::RemoteTasks.
232
+
233
+ def self.tasks
234
+ @@tasks
235
+ end
236
+
237
+ ##
238
+ # The vlad environment.
239
+
240
+ def self.env
241
+ @@env
242
+ end
243
+
244
+ ##
245
+ # Resets vlad, restoring all roles, tasks and environment variables to the
246
+ # defaults.
247
+
248
+ def self.reset
249
+ @@roles = Hash.new { |h,k| h[k] = {} }
250
+ @@env = {}
251
+ @@tasks = {}
252
+ @@env_locks = Hash.new { |h,k| h[k] = Mutex.new }
253
+
254
+ # mandatory
255
+ set(:application) { raise(Vlad::ConfigurationError,
256
+ "Please specify the name of the application") }
257
+ set(:repository) { raise(Vlad::ConfigurationError,
258
+ "Please specify the repository path") }
259
+ set(:deploy_to) { raise(Vlad::ConfigurationError,
260
+ "Please specify the deploy path") }
261
+ set(:domain) { raise(Vlad::ConfigurationError,
262
+ "Please specify the server domain") }
263
+
264
+ # optional
265
+ set(:current_path) { File.join(deploy_to, "current") }
266
+ set(:current_release) { File.join(releases_path, releases[-1]) }
267
+ set :keep_releases, 5
268
+ set :deploy_timestamped, true
269
+ set :deploy_via, :export
270
+ set(:latest_release) { deploy_timestamped ? release_path : current_release }
271
+ set :migrate_args, ""
272
+ set :migrate_target, :latest
273
+ set(:previous_release){ File.join(releases_path, releases[-2]) }
274
+ set :rails_env, "production"
275
+ set :rake, "rake"
276
+ set(:release_name) { Time.now.utc.strftime("%Y%m%d%H%M%S") }
277
+ set(:release_path) { File.join(releases_path, release_name) }
278
+ set(:releases) { task.run("ls -x #{releases_path}").split.sort }
279
+ set(:releases_path) { File.join(deploy_to, "releases") }
280
+ set :scm, :subversion
281
+ set(:scm_path) { File.join(deploy_to, "scm") }
282
+ set(:shared_path) { File.join(deploy_to, "shared") }
283
+
284
+ set(:sudo_password) do
285
+ state = `stty -g`
286
+
287
+ raise Vlad::Error, "stty(1) not found" unless $?.success?
288
+
289
+ begin
290
+ system "stty -echo"
291
+ $stdout.print "sudo password: "
292
+ $stdout.flush
293
+ sudo_password = $stdin.gets
294
+ $stdout.puts
295
+ ensure
296
+ system "stty #{state}"
297
+ end
298
+ sudo_password
299
+ end
300
+
301
+ set(:source) do
302
+ require "vlad/#{scm}"
303
+ Vlad.const_get(scm.to_s.capitalize).new
304
+ end
305
+ end
306
+
307
+ ##
308
+ # Adds role +role_name+ with +host+ and +args+ for that host.
309
+
310
+ def self.role role_name, host, args = {}
311
+ raise ArgumentError, "invalid host" if host.nil? or host.empty?
312
+ @@roles[role_name][host] = args
313
+ end
314
+
315
+ ##
316
+ # Adds a remote task named +name+ with options +options+ that will execute
317
+ # +block+.
318
+
319
+ def self.remote_task name, options = {}, &block
320
+ t = Rake::RemoteTask.define_task(name, &block)
321
+ t.options = options
322
+ roles = options[:roles]
323
+ t
324
+ end
325
+
326
+ ##
327
+ # Set environment variable +name+ to +value+ or +default_block+.
328
+ #
329
+ # If +default_block+ is defined, the block will be executed the first time
330
+ # the variable is fetched, and the value will be used for every subsequent
331
+ # fetch.
332
+
333
+ def self.set name, value = nil, &default_block
334
+ raise ArgumentError, "cannot provide both a value and a block" if
335
+ value and default_block
336
+ raise ArgumentError, "cannot set reserved name: '#{name}'" if
337
+ Rake::RemoteTask.reserved_name?(name)
338
+
339
+ Rake::RemoteTask.env[name.to_s] = value || default_block
340
+
341
+ Object.send :define_method, name do
342
+ Rake::RemoteTask.fetch name
343
+ end
344
+ end
345
+
346
+ ##
347
+ # Action is used to run a task's remote_actions in parallel on each of its
348
+ # hosts. Actions are created automatically in Rake::RemoteTask#enhance.
349
+
350
+ class Action
351
+
352
+ ##
353
+ # The task this action is attached to.
354
+
355
+ attr_reader :task
356
+
357
+ ##
358
+ # The block this action will execute.
359
+
360
+ attr_reader :block
361
+
362
+ ##
363
+ # An Array of threads, one for each host this action executes on.
364
+
365
+ attr_reader :workers
366
+
367
+ ##
368
+ # Creates a new Action that will run +block+ for +task+.
369
+
370
+ def initialize task, block
371
+ @task = task
372
+ @block = block
373
+ @workers = []
374
+ end
375
+
376
+ def == other # :nodoc:
377
+ return false unless Action === other
378
+ block == other.block && task == other.task
379
+ end
380
+
381
+ ##
382
+ # Execute this action on +hosts+ in parallel. Returns when block has
383
+ # completed for each host.
384
+
385
+ def execute hosts
386
+ hosts.each do |host|
387
+ t = task.clone
388
+ t.target_host = host
389
+ thread = Thread.new(t) do |task|
390
+ Thread.current[:task] = task
391
+ block.call
392
+ end
393
+ @workers << thread
394
+ end
395
+ @workers.each { |w| w.join }
396
+ end
397
+ end
398
+ end
399
+