remote_task 0.1.0

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