vlad 1.0.0

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