remote_task 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 brianthecoder
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,17 @@
1
+ = remote_task
2
+
3
+ Description goes here.
4
+
5
+ == Note on Patches/Pull Requests
6
+
7
+ * Fork the project.
8
+ * Make your feature addition or bug fix.
9
+ * Add tests for it. This is important so I don't break it in a
10
+ future version unintentionally.
11
+ * Commit, do not mess with rakefile, version, or history.
12
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
13
+ * Send me a pull request. Bonus points for topic branches.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2009 brianthecoder. See LICENSE for details.
@@ -0,0 +1,52 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "remote_task"
8
+ gem.summary = %Q{just remote_task from vlad}
9
+ gem.description = %Q{just remote_task from vlad, got tired of messing with vlad and cap}
10
+ gem.email = "wbsmith83@gmail.com"
11
+ gem.homepage = "http://github.com/BrianTheCoder/remote_task"
12
+ gem.authors = ["brianthecoder"]
13
+ gem.add_development_dependency "minitest"
14
+ end
15
+ Jeweler::GemcutterTasks.new
16
+ rescue LoadError
17
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
18
+ end
19
+
20
+ require 'rake/testtask'
21
+ Rake::TestTask.new(:test) do |test|
22
+ test.libs << 'lib' << 'test'
23
+ test.pattern = 'test/**/test_*.rb'
24
+ test.verbose = true
25
+ end
26
+
27
+ begin
28
+ require 'rcov/rcovtask'
29
+ Rcov::RcovTask.new do |test|
30
+ test.libs << 'test'
31
+ test.pattern = 'test/**/test_*.rb'
32
+ test.verbose = true
33
+ end
34
+ rescue LoadError
35
+ task :rcov do
36
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
37
+ end
38
+ end
39
+
40
+ task :test => :check_dependencies
41
+
42
+ task :default => :test
43
+
44
+ require 'rake/rdoctask'
45
+ Rake::RDocTask.new do |rdoc|
46
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
47
+
48
+ rdoc.rdoc_dir = 'rdoc'
49
+ rdoc.title = "remote_task #{version}"
50
+ rdoc.rdoc_files.include('README*')
51
+ rdoc.rdoc_files.include('lib/**/*.rb')
52
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,584 @@
1
+ require 'open4'
2
+ require 'rake'
3
+
4
+ $TESTING ||= false
5
+ $TRACE = Rake.application.options.trace
6
+ $-w = true if $TRACE # asshat, don't mess with my warn.
7
+
8
+ def export receiver, *methods
9
+ methods.each do |method|
10
+ eval "def #{method} *args, &block; #{receiver}.#{method}(*args, &block);end"
11
+ end
12
+ end
13
+
14
+ export "Thread.current[:task]", :get, :put, :rsync, :run, :sudo, :target_host
15
+ export "Rake::RemoteTask", :host, :remote_task, :role, :set
16
+
17
+ ##
18
+ # Rake::RemoteTask is a subclass of Rake::Task that adds
19
+ # remote_actions that execute in parallel on multiple hosts via ssh.
20
+
21
+ class Rake::RemoteTask < Rake::Task
22
+ ##
23
+ # Raised when you have incorrectly configured Vlad.
24
+ class ConfigurationError < StandardError; end
25
+
26
+ ##
27
+ # Raised when a remote command fails.
28
+ class CommandFailedError < StandardError; end
29
+
30
+ ##
31
+ # Raised when an environment variable hasn't been set.
32
+ class FetchError < StandardError; end
33
+
34
+ @@current_roles = []
35
+
36
+ include Open4
37
+
38
+ ##
39
+ # Options for execution of this task.
40
+ attr_accessor :options
41
+
42
+ ##
43
+ # The host this task is running on during execution.
44
+ attr_accessor :target_host
45
+
46
+ ##
47
+ # An Array of Actions this host will perform during execution. Use
48
+ # enhance to add new actions to a task.
49
+ attr_reader :remote_actions
50
+
51
+ def self.current_roles
52
+ @@current_roles
53
+ end
54
+
55
+ ##
56
+ # Create a new task named +task_name+ attached to Rake::Application +app+.
57
+
58
+ def initialize(task_name, app)
59
+ super
60
+
61
+ @remote_actions = []
62
+ @happy = false # used for deprecation warnings on get/put/rsync
63
+ end
64
+
65
+ ##
66
+ # Add a local action to this task. This calls Rake::Task#enhance.
67
+
68
+ alias_method :original_enhance, :enhance
69
+
70
+ ##
71
+ # Add remote action +block+ to this task with dependencies +deps+. See
72
+ # Rake::Task#enhance.
73
+
74
+ def enhance(deps=nil, &block)
75
+ original_enhance(deps) # can't use super because block passed regardless.
76
+ @remote_actions << Action.new(self, block) if block_given?
77
+ self
78
+ end
79
+
80
+ ##
81
+ # Execute this action. Local actions will be performed first, then remote
82
+ # actions will be performed in parallel on each host configured for this
83
+ # RemoteTask.
84
+
85
+ def execute(args = nil)
86
+ raise(ConfigurationError,
87
+ "No target hosts specified on task #{self.name} for roles #{options[:roles].inspect}") if
88
+ ! defined_target_hosts?
89
+
90
+ super args
91
+
92
+ @remote_actions.each { |act| act.execute(target_hosts, self, args) }
93
+ end
94
+
95
+ ##
96
+ # Pull +files+ from the remote +host+ using rsync to +local_dir+.
97
+ # TODO: what if role has multiple hosts & the files overlap? subdirs?
98
+
99
+ def get local_dir, *files
100
+ @happy = true
101
+ host = target_host
102
+ rsync files.map { |f| "#{host}:#{f}" }, local_dir
103
+ @happy = false
104
+ end
105
+
106
+ ##
107
+ # Copy a (usually generated) file to +remote_path+. Contents of block
108
+ # are copied to +remote_path+ and you may specify an optional
109
+ # base_name for the tempfile (aids in debugging).
110
+
111
+ def put remote_path, base_name = File.basename(remote_path)
112
+ require 'tempfile'
113
+ Tempfile.open base_name do |fp|
114
+ fp.puts yield
115
+ fp.flush
116
+ @happy = true
117
+ rsync fp.path, "#{target_host}:#{remote_path}"
118
+ @happy = false
119
+ end
120
+ end
121
+
122
+ ##
123
+ # Execute rsync with +args+. Tacks on pre-specified +rsync_cmd+ and
124
+ # +rsync_flags+.
125
+ #
126
+ # Favor #get and #put for most tasks. Old-style direct use where the
127
+ # target_host was implicit is now deprecated.
128
+
129
+ def rsync *args
130
+ unless @happy || args[-1] =~ /:/ then
131
+ warn "rsync deprecation: pass target_host:remote_path explicitly"
132
+ args[-1] = "#{target_host}:#{args[-1]}"
133
+ end
134
+
135
+ cmd = [rsync_cmd, rsync_flags, args].flatten.compact
136
+ cmdstr = cmd.join ' '
137
+
138
+ warn cmdstr if $TRACE
139
+
140
+ success = system(*cmd)
141
+
142
+ raise CommandFailedError, "execution failed: #{cmdstr}" unless success
143
+ end
144
+
145
+ ##
146
+ # Use ssh to execute +command+ on target_host. If +command+ uses sudo, the
147
+ # sudo password will be prompted for then saved for subsequent sudo commands.
148
+
149
+ def run command
150
+ cmd = [ssh_cmd, ssh_flags, target_host, command].flatten
151
+ result = []
152
+
153
+ trace = [ssh_cmd, ssh_flags, target_host, "'#{command}'"].flatten.join(' ')
154
+ warn trace if $TRACE
155
+
156
+ pid, inn, out, err = popen4(*cmd)
157
+
158
+ inn.sync = true
159
+ streams = [out, err]
160
+ out_stream = {
161
+ out => $stdout,
162
+ err => $stderr,
163
+ }
164
+
165
+ # Handle process termination ourselves
166
+ status = nil
167
+ Thread.start do
168
+ status = Process.waitpid2(pid).last
169
+ end
170
+
171
+ until streams.empty? do
172
+ # don't busy loop
173
+ selected, = select streams, nil, nil, 0.1
174
+
175
+ next if selected.nil? or selected.empty?
176
+
177
+ selected.each do |stream|
178
+ if stream.eof? then
179
+ streams.delete stream if status # we've quit, so no more writing
180
+ next
181
+ end
182
+
183
+ data = stream.readpartial(1024)
184
+ out_stream[stream].write data
185
+
186
+ if stream == err and data =~ sudo_prompt then
187
+ inn.puts sudo_password
188
+ data << "\n"
189
+ $stderr.write "\n"
190
+ end
191
+
192
+ result << data
193
+ end
194
+ end
195
+
196
+ unless status.success? then
197
+ raise(CommandFailedError,
198
+ "execution failed with status #{status.exitstatus}: #{cmd.join ' '}")
199
+ end
200
+
201
+ result.join
202
+ ensure
203
+ inn.close rescue nil
204
+ out.close rescue nil
205
+ err.close rescue nil
206
+ end
207
+
208
+ ##
209
+ # Returns an Array with every host configured.
210
+
211
+ def self.all_hosts
212
+ hosts_for(roles.keys)
213
+ end
214
+
215
+ ##
216
+ # The default environment values. Used for resetting (mostly for
217
+ # tests).
218
+
219
+ def self.default_env
220
+ @@default_env
221
+ end
222
+
223
+ def self.per_thread
224
+ @@per_thread
225
+ end
226
+
227
+ ##
228
+ # The environment.
229
+ def self.env
230
+ @@env
231
+ end
232
+
233
+ ##
234
+ # Fetches environment variable +name+ from the environment using
235
+ # default +default+.
236
+
237
+ def self.fetch name, default = nil
238
+ name = name.to_s if Symbol === name
239
+ if @@env.has_key? name then
240
+ protect_env(name) do
241
+ v = @@env[name]
242
+ v = @@env[name] = v.call if Proc === v unless per_thread[name]
243
+ v = v.call if Proc === v
244
+ v
245
+ end
246
+ elsif default || default == false
247
+ v = @@env[name] = default
248
+ else
249
+ raise Vlad::FetchError
250
+ end
251
+ end
252
+
253
+ ##
254
+ # Add host +host_name+ that belongs to +roles+. Extra arguments may
255
+ # be specified for the host as a hash as the last argument.
256
+ #
257
+ # host is the inversion of role:
258
+ #
259
+ # host 'db1.example.com', :db, :master_db
260
+ #
261
+ # Is equivalent to:
262
+ #
263
+ # role :db, 'db1.example.com'
264
+ # role :master_db, 'db1.example.com'
265
+
266
+ def self.host host_name, *roles
267
+ opts = Hash === roles.last ? roles.pop : {}
268
+
269
+ roles.each do |role_name|
270
+ role role_name, host_name, opts.dup
271
+ end
272
+ end
273
+
274
+ ##
275
+ # Returns an Array of all hosts in +roles+.
276
+
277
+ def self.hosts_for *roles
278
+ roles.flatten.map { |r|
279
+ self.roles[r].keys
280
+ }.flatten.uniq.sort
281
+ end
282
+
283
+ def self.mandatory name, desc # :nodoc:
284
+ self.set(name) do
285
+ raise(ConfigurationError,
286
+ "Please specify the #{desc} via the #{name.inspect} variable")
287
+ end
288
+ end
289
+
290
+ ##
291
+ # Ensures exclusive access to +name+.
292
+
293
+ def self.protect_env name # :nodoc:
294
+ @@env_locks[name].synchronize do
295
+ yield
296
+ end
297
+ end
298
+
299
+ ##
300
+ # Adds a remote task named +name+ with options +options+ that will
301
+ # execute +block+.
302
+
303
+ def self.remote_task name, *args, &block
304
+ options = (Hash === args.last) ? args.pop : {}
305
+ t = Rake::RemoteTask.define_task(name, *args, &block)
306
+ options[:roles] = Array options[:roles]
307
+ options[:roles] |= @@current_roles
308
+ t.options = options
309
+ t
310
+ end
311
+
312
+ ##
313
+ # Ensures +name+ does not conflict with an existing method.
314
+
315
+ def self.reserved_name? name # :nodoc:
316
+ !@@env.has_key?(name.to_s) && self.respond_to?(name)
317
+ end
318
+
319
+ ##
320
+ # Resets vlad, restoring all roles, tasks and environment variables
321
+ # to the defaults.
322
+
323
+ def self.reset
324
+ @@def_role_hash = {} # official default role value
325
+ @@env = {}
326
+ @@tasks = {}
327
+ @@roles = Hash.new { |h,k| h[k] = @@def_role_hash }
328
+ @@env_locks = Hash.new { |h,k| h[k] = Mutex.new }
329
+
330
+ @@default_env.each do |k,v|
331
+ case v
332
+ when Symbol, Fixnum, nil, true, false, 42 then # ummmm... yeah. bite me.
333
+ @@env[k] = v
334
+ else
335
+ @@env[k] = v.dup
336
+ end
337
+ end
338
+ end
339
+
340
+ ##
341
+ # Adds role +role_name+ with +host+ and +args+ for that host.
342
+ # TODO: merge:
343
+ # Declare a role and assign a remote host to it. Equivalent to the
344
+ # <tt>host</tt> method; provided for capistrano compatibility.
345
+
346
+ def self.role role_name, host = nil, args = {}
347
+ if block_given? then
348
+ raise ArgumentError, 'host not allowed with block' unless host.nil?
349
+
350
+ begin
351
+ current_roles << role_name
352
+ yield
353
+ ensure
354
+ current_roles.delete role_name
355
+ end
356
+ else
357
+ raise ArgumentError, 'host required' if host.nil?
358
+
359
+ [*host].each do |hst|
360
+ raise ArgumentError, "invalid host: #{hst}" if hst.nil? or hst.empty?
361
+ end
362
+ @@roles[role_name] = {} if @@def_role_hash.eql? @@roles[role_name]
363
+ @@roles[role_name][host] = args
364
+ end
365
+ end
366
+
367
+ ##
368
+ # The configured roles.
369
+
370
+ def self.roles
371
+ host domain, :app, :web, :db if @@roles.empty?
372
+
373
+ @@roles
374
+ end
375
+
376
+ ##
377
+ # Set environment variable +name+ to +value+ or +default_block+.
378
+ #
379
+ # If +default_block+ is defined, the block will be executed the
380
+ # first time the variable is fetched, and the value will be used for
381
+ # every subsequent fetch.
382
+
383
+ def self.set name, value = nil, &default_block
384
+ raise ArgumentError, "cannot provide both a value and a block" if
385
+ value and default_block unless
386
+ value == :per_thread
387
+ raise ArgumentError, "cannot set reserved name: '#{name}'" if
388
+ Rake::RemoteTask.reserved_name?(name) unless $TESTING
389
+
390
+ name = name.to_s
391
+
392
+ Rake::RemoteTask.per_thread[name] = true if
393
+ default_block && value == :per_thread
394
+
395
+ Rake::RemoteTask.default_env[name] = Rake::RemoteTask.env[name] =
396
+ default_block || value
397
+
398
+ Object.send :define_method, name do
399
+ Rake::RemoteTask.fetch name
400
+ end
401
+ end
402
+
403
+ ##
404
+ # Sets all the default values. Should only be called once. Use reset
405
+ # if you need to restore values.
406
+
407
+ def self.set_defaults
408
+ @@default_env ||= {}
409
+ @@per_thread ||= {}
410
+ self.reset
411
+
412
+ mandatory :repository, "repository path"
413
+ mandatory :deploy_to, "deploy path"
414
+ mandatory :domain, "server domain"
415
+
416
+ simple_set(:deploy_timestamped, true,
417
+ :deploy_via, :export,
418
+ :keep_releases, 5,
419
+ :migrate_args, "",
420
+ :migrate_target, :latest,
421
+ :rails_env, "production",
422
+ :rake_cmd, "rake",
423
+ :revision, "head",
424
+ :rsync_cmd, "rsync",
425
+ :rsync_flags, ['-azP', '--delete'],
426
+ :ssh_cmd, "ssh",
427
+ :ssh_flags, [],
428
+ :sudo_cmd, "sudo",
429
+ :sudo_flags, ['-p Password:'],
430
+ :sudo_prompt, /^Password:/,
431
+ :umask, '02')
432
+
433
+ set(:current_release) { File.join(releases_path, releases[-1]) }
434
+ set(:latest_release) { deploy_timestamped ?release_path: current_release }
435
+ set(:previous_release) { File.join(releases_path, releases[-2]) }
436
+ set(:release_name) { Time.now.utc.strftime("%Y%m%d%H%M%S") }
437
+ set(:release_path) { File.join(releases_path, release_name) }
438
+ set(:releases) { task.run("ls -x #{releases_path}").split.sort }
439
+
440
+ set_path :current_path, "current"
441
+ set_path :releases_path, "releases"
442
+ set_path :scm_path, "scm"
443
+ set_path :shared_path, "shared"
444
+
445
+ set(:sudo_password) do
446
+ state = `stty -g`
447
+
448
+ raise Vlad::Error, "stty(1) not found" unless $?.success?
449
+
450
+ begin
451
+ system "stty -echo"
452
+ $stdout.print "sudo password: "
453
+ $stdout.flush
454
+ sudo_password = $stdin.gets
455
+ $stdout.puts
456
+ ensure
457
+ system "stty #{state}"
458
+ end
459
+ sudo_password
460
+ end
461
+ end
462
+
463
+ def self.set_path(name, subdir) # :nodoc:
464
+ set(name){ File.join(deploy_to, subdir) }
465
+ end
466
+
467
+ def self.simple_set(*args) # :nodoc:
468
+ args = Hash[*args]
469
+ args.each do |k, v|
470
+ set k, v
471
+ end
472
+ end
473
+
474
+ ##
475
+ # The Rake::RemoteTask executing in this Thread.
476
+ def self.task
477
+ Thread.current[:task]
478
+ end
479
+
480
+ ##
481
+ # The configured Rake::RemoteTasks.
482
+ def self.tasks
483
+ @@tasks
484
+ end
485
+
486
+ ##
487
+ # Execute +command+ under sudo using run.
488
+ def sudo command
489
+ run [sudo_cmd, sudo_flags, command].flatten.compact.join(" ")
490
+ end
491
+
492
+ ##
493
+ # The hosts this task will execute on. The hosts are determined from
494
+ # the role this task belongs to.
495
+ #
496
+ # The target hosts may be overridden by providing a comma-separated
497
+ # list of commands to the HOSTS environment variable:
498
+ #
499
+ # rake my_task HOSTS=app1.example.com,app2.example.com
500
+ def target_hosts
501
+ if hosts = ENV["HOSTS"] then
502
+ hosts.strip.gsub(/\s+/, '').split(",")
503
+ else
504
+ roles = Array options[:roles]
505
+
506
+ if roles.empty? then
507
+ Rake::RemoteTask.all_hosts
508
+ else
509
+ Rake::RemoteTask.hosts_for roles
510
+ end
511
+ end
512
+ end
513
+
514
+ ##
515
+ # Similar to target_hosts, but returns true if user defined any hosts, even
516
+ # an empty list.
517
+ def defined_target_hosts?
518
+ return true if ENV["HOSTS"]
519
+ roles = Array options[:roles]
520
+ return true if roles.empty?
521
+ # borrowed from hosts_for:
522
+ roles.flatten.each { |r|
523
+ return true unless @@def_role_hash.eql? Rake::RemoteTask.roles[r]
524
+ }
525
+ return false
526
+ end
527
+
528
+ ##
529
+ # Action is used to run a task's remote_actions in parallel on each
530
+ # of its hosts. Actions are created automatically in
531
+ # Rake::RemoteTask#enhance.
532
+
533
+ class Action
534
+ ##
535
+ # The task this action is attached to.
536
+ attr_reader :task
537
+
538
+ ##
539
+ # The block this action will execute.
540
+ attr_reader :block
541
+
542
+ ##
543
+ # An Array of threads, one for each host this action executes on.
544
+ attr_reader :workers
545
+
546
+ ##
547
+ # Creates a new Action that will run +block+ for +task+.
548
+ def initialize task, block
549
+ @task = task
550
+ @block = block
551
+ @workers = ThreadGroup.new
552
+ end
553
+
554
+ def == other # :nodoc:
555
+ return false unless Action === other
556
+ block == other.block && task == other.task
557
+ end
558
+
559
+ ##
560
+ # Execute this action on +hosts+ in parallel. Returns when block
561
+ # has completed for each host.
562
+
563
+ def execute hosts, task, args
564
+ hosts.each do |host|
565
+ t = task.clone
566
+ t.target_host = host
567
+ thread = Thread.new(t) do |task|
568
+ Thread.current[:task] = task
569
+ case block.arity
570
+ when 1
571
+ block.call task
572
+ else
573
+ block.call task, args
574
+ end
575
+ Thread.current[:task] = nil
576
+ end
577
+ @workers.add thread
578
+ end
579
+ @workers.list.each { |thr| thr.join }
580
+ end
581
+ end
582
+ end
583
+
584
+ Rake::RemoteTask.set_defaults
@@ -0,0 +1,61 @@
1
+ require 'rubygems'
2
+ require 'minitest/autorun'
3
+
4
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
5
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
6
+ require 'remote_task'
7
+ require 'stringio'
8
+
9
+ class StringIO
10
+ def readpartial(size) read end # suck!
11
+ end
12
+
13
+ module Process
14
+ def self.expected status
15
+ @@expected ||= []
16
+ @@expected << status
17
+ end
18
+
19
+ class << self
20
+ alias :waitpid2_old :waitpid2
21
+
22
+ def waitpid2(pid)
23
+ [ @@expected.shift ]
24
+ end
25
+ end
26
+ end
27
+
28
+ class Rake::RemoteTask
29
+ attr_accessor :commands, :action, :input, :output, :error
30
+
31
+ Status = Struct.new :exitstatus
32
+
33
+ class Status
34
+ def success?() exitstatus == 0 end
35
+ end
36
+
37
+ def system *command
38
+ @commands << command
39
+ self.action ? self.action[command.join(' ')] : true
40
+ end
41
+
42
+ def popen4 *command
43
+ @commands << command
44
+
45
+ @input = StringIO.new
46
+ out = StringIO.new @output.shift.to_s
47
+ err = StringIO.new @error.shift.to_s
48
+
49
+ raise if block_given?
50
+
51
+ status = self.action ? self.action[command.join(' ')] : 0
52
+ Process.expected Status.new(status)
53
+
54
+ return 42, @input, out, err
55
+ end
56
+
57
+ def select reads, writes, errs, timeout
58
+ [reads, writes, errs]
59
+ end
60
+
61
+ end
@@ -0,0 +1,271 @@
1
+ require 'helper'
2
+
3
+ class TestRemoteTask < MiniTest::Unit::TestCase
4
+ def setup
5
+ @remote = Rake::RemoteTask
6
+ @remote.reset
7
+ Rake.application.clear
8
+ @task_count = Rake.application.tasks.size
9
+ @remote.set :domain, "example.com"
10
+ end
11
+
12
+ def util_set_hosts
13
+ @remote.host "app.example.com", :app
14
+ @remote.host "db.example.com", :db
15
+ end
16
+
17
+ # TODO: move to minitest
18
+ def assert_silent
19
+ out, err = capture_io{ yield }
20
+
21
+ assert_empty err
22
+ assert_empty out
23
+ end
24
+
25
+ def test_enhance
26
+ util_set_hosts
27
+ body = Proc.new{ 5 }
28
+ task = @remote.remote_task(:some_task => :foo, &body)
29
+ action = Rake::RemoteTask::Action.new(task, body)
30
+ assert_equal [action], task.remote_actions
31
+ assert_equal task, action.task
32
+ assert_equal ["foo"], task.prerequisites
33
+ end
34
+
35
+ def test_enhance_with_no_task_body
36
+ util_set_hosts
37
+ util_setup_task
38
+ assert_equal [], @task.remote_actions
39
+ assert_equal [], @task.prerequisites
40
+ end
41
+
42
+ def test_execute
43
+ util_set_hosts
44
+ set :some_variable, 1
45
+ set :can_set_nil, nil
46
+ set :lies_are, false
47
+ x = 5
48
+ task = @remote.remote_task(:some_task) { x += some_variable }
49
+ task.execute nil
50
+ assert_equal 1, task.some_variable
51
+ assert_equal 7, x
52
+ assert task.can_set_nil.nil?
53
+ assert_equal false, task.lies_are
54
+ end
55
+
56
+ def test_set_false
57
+ set :can_set_nil, nil
58
+ set :lies_are, false
59
+
60
+ assert_equal nil, task.can_set_nil
61
+
62
+ assert_equal false, task.lies_are
63
+ assert_equal false, Rake::RemoteTask.fetch(:lies_are)
64
+ end
65
+
66
+
67
+ def test_fetch_false
68
+ assert_equal false, Rake::RemoteTask.fetch(:unknown, false)
69
+ end
70
+
71
+ def test_execute_exposes_target_host
72
+ host "app.example.com", :app
73
+ task = remote_task(:target_task) { set(:test_target_host, target_host) }
74
+ task.execute nil
75
+ assert_equal "app.example.com", Rake::RemoteTask.fetch(:test_target_host)
76
+ end
77
+
78
+ def test_execute_with_no_hosts
79
+ @remote.host "app.example.com", :app
80
+ t = @remote.remote_task(:flunk, :roles => :db) { flunk "should not have run" }
81
+ e = assert_raises(Rake::RemoteTask::ConfigurationError) { t.execute nil }
82
+ assert_equal "No target hosts specified on task flunk for roles [:db]",
83
+ e.message
84
+ end
85
+
86
+ def test_execute_with_no_roles
87
+ t = @remote.remote_task(:flunk, :roles => :junk) { flunk "should not have run" }
88
+ e = assert_raises(Rake::RemoteTask::ConfigurationError) { t.execute nil }
89
+ assert_equal "No target hosts specified on task flunk for roles [:junk]",
90
+ e.message
91
+ end
92
+
93
+ def test_execute_with_roles
94
+ util_set_hosts
95
+ set :some_variable, 1
96
+ x = 5
97
+ task = @remote.remote_task(:some_task, :roles => :db) { x += some_variable }
98
+ task.execute nil
99
+ assert_equal 1, task.some_variable
100
+ assert_equal 6, x
101
+ end
102
+
103
+ def test_rsync
104
+ util_setup_task
105
+ @task.target_host = "app.example.com"
106
+
107
+ assert_silent do
108
+ @task.rsync 'localfile', 'host:remotefile'
109
+ end
110
+
111
+ commands = @task.commands
112
+
113
+ assert_equal 1, commands.size, 'not enough commands'
114
+ assert_equal(%w[rsync -azP --delete localfile host:remotefile],
115
+ commands.first)
116
+ end
117
+
118
+ def test_rsync_fail
119
+ util_setup_task
120
+ @task.target_host = "app.example.com"
121
+ @task.action = lambda { false }
122
+
123
+ e = assert_raises Rake::RemoteTask::CommandFailedError do
124
+ assert_silent do
125
+ @task.rsync 'local', 'host:remote'
126
+ end
127
+ end
128
+ exp = "execution failed: rsync -azP --delete local host:remote"
129
+ assert_equal exp, e.message
130
+ end
131
+
132
+ def test_rsync_deprecation
133
+ util_setup_task
134
+ @task.target_host = "app.example.com"
135
+
136
+ out, err = capture_io do
137
+ @task.rsync 'localfile', 'remotefile'
138
+ end
139
+
140
+ commands = @task.commands
141
+
142
+ assert_equal 1, commands.size, 'not enough commands'
143
+ assert_equal(%w[rsync -azP --delete localfile app.example.com:remotefile],
144
+ commands.first)
145
+
146
+ assert_equal("rsync deprecation: pass target_host:remote_path explicitly\n",
147
+ err)
148
+ assert_empty out
149
+ # flunk "not yet"
150
+ end
151
+
152
+ def test_get
153
+ util_setup_task
154
+ @task.target_host = "app.example.com"
155
+
156
+ assert_silent do
157
+ @task.get 'tmp', "remote1", "remote2"
158
+ end
159
+
160
+ commands = @task.commands
161
+
162
+ expected = %w[rsync -azP --delete app.example.com:remote1 app.example.com:remote2 tmp]
163
+
164
+ assert_equal 1, commands.size
165
+ assert_equal expected, commands.first
166
+ end
167
+
168
+ def test_put
169
+ util_setup_task
170
+ @task.target_host = "app.example.com"
171
+
172
+ assert_silent do
173
+ @task.put 'dest' do
174
+ "whatever"
175
+ end
176
+ end
177
+
178
+ commands = @task.commands
179
+
180
+ expected = %w[rsync -azP --delete HAPPY app.example.com:dest]
181
+ commands.first[3] = 'HAPPY'
182
+
183
+ assert_equal 1, commands.size
184
+ assert_equal expected, commands.first
185
+ end
186
+
187
+ def test_run
188
+ util_setup_task
189
+ @task.output << "file1\nfile2\n"
190
+ @task.target_host = "app.example.com"
191
+ result = nil
192
+
193
+ out, err = capture_io do
194
+ result = @task.run("ls")
195
+ end
196
+
197
+ commands = @task.commands
198
+
199
+ assert_equal 1, commands.size, 'not enough commands'
200
+ assert_equal ["ssh", "app.example.com", "ls"],
201
+ commands.first, 'app'
202
+ assert_equal "file1\nfile2\n", result
203
+
204
+ assert_equal "file1\nfile2\n", out
205
+ assert_equal '', err
206
+ end
207
+
208
+ def test_run_failing_command
209
+ util_set_hosts
210
+ util_setup_task
211
+ @task.input = StringIO.new "file1\nfile2\n"
212
+ @task.target_host = 'app.example.com'
213
+ @task.action = lambda { 1 }
214
+
215
+ e = assert_raises(Rake::RemoteTask::CommandFailedError) { @task.run("ls") }
216
+ assert_equal "execution failed with status 1: ssh app.example.com ls", e.message
217
+
218
+ assert_equal 1, @task.commands.size
219
+ end
220
+
221
+ def test_run_sudo
222
+ util_setup_task
223
+ @task.output << "file1\nfile2\n"
224
+ @task.error << 'Password:'
225
+ @task.target_host = "app.example.com"
226
+ def @task.sudo_password() "my password" end # gets defined by set
227
+ result = nil
228
+
229
+ out, err = capture_io do
230
+ result = @task.run("sudo ls")
231
+ end
232
+
233
+ commands = @task.commands
234
+
235
+ assert_equal 1, commands.size, 'not enough commands'
236
+ assert_equal ['ssh', 'app.example.com', 'sudo ls'],
237
+ commands.first
238
+
239
+ assert_equal "my password\n", @task.input.string
240
+
241
+ # WARN: Technically incorrect, the password line should be
242
+ # first... this is an artifact of changes to the IO code in run
243
+ # and the fact that we have a very simplistic (non-blocking)
244
+ # testing model.
245
+ assert_equal "file1\nfile2\nPassword:\n", result
246
+
247
+ assert_equal "file1\nfile2\n", out
248
+ assert_equal "Password:\n", err
249
+ end
250
+
251
+ def test_sudo
252
+ util_setup_task
253
+ @task.target_host = "app.example.com"
254
+ @task.sudo "ls"
255
+
256
+ commands = @task.commands
257
+
258
+ assert_equal 1, commands.size, 'wrong number of commands'
259
+ assert_equal ["ssh", "app.example.com", "sudo -p Password: ls"],
260
+ commands.first, 'app'
261
+ end
262
+
263
+ def util_setup_task(options = {})
264
+ @task = @remote.remote_task :test_task, options
265
+ @task.commands = []
266
+ @task.output = []
267
+ @task.error = []
268
+ @task.action = nil
269
+ @task
270
+ end
271
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: remote_task
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - brianthecoder
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-12-29 00:00:00 -06:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: minitest
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ description: just remote_task from vlad, got tired of messing with vlad and cap
26
+ email: wbsmith83@gmail.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - LICENSE
33
+ - README.rdoc
34
+ files:
35
+ - .document
36
+ - .gitignore
37
+ - LICENSE
38
+ - README.rdoc
39
+ - Rakefile
40
+ - VERSION
41
+ - lib/remote_task.rb
42
+ - test/helper.rb
43
+ - test/test_remote_task.rb
44
+ has_rdoc: true
45
+ homepage: http://github.com/BrianTheCoder/remote_task
46
+ licenses: []
47
+
48
+ post_install_message:
49
+ rdoc_options:
50
+ - --charset=UTF-8
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: "0"
58
+ version:
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: "0"
64
+ version:
65
+ requirements: []
66
+
67
+ rubyforge_project:
68
+ rubygems_version: 1.3.5
69
+ signing_key:
70
+ specification_version: 3
71
+ summary: just remote_task from vlad
72
+ test_files:
73
+ - test/helper.rb
74
+ - test/test_remote_task.rb