Class: Rake::RemoteTask

Inherits:
Task
  • Object
show all
Includes:
Open4
Defined in:
lib/rake/remote_task.rb,
lib/rake/test_case.rb

Overview

Rake::RemoteTask is a subclass of Rake::Task that adds remote_actions that execute in parallel on multiple hosts via ssh.

Defined Under Namespace

Classes: Action, Status

Constant Summary collapse

VERSION =
"2.4.4"
@@current_roles =
[]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(task_name, app) ⇒ RemoteTask

Create a new task named task_name attached to Rake::Application app.



82
83
84
85
86
87
# File 'lib/rake/remote_task.rb', line 82

def initialize(task_name, app)
  super

  @remote_actions = []
  @happy = false # used for deprecation warnings on get/put/rsync
end

Instance Attribute Details

#actionObject

Returns the value of attribute action.



26
27
28
# File 'lib/rake/test_case.rb', line 26

def action
  @action
end

#commandsObject

Returns the value of attribute commands.



26
27
28
# File 'lib/rake/test_case.rb', line 26

def commands
  @commands
end

#errorObject

Returns the value of attribute error.



26
27
28
# File 'lib/rake/test_case.rb', line 26

def error
  @error
end

#inputObject

Returns the value of attribute input.



26
27
28
# File 'lib/rake/test_case.rb', line 26

def input
  @input
end

#optionsObject

Options for execution of this task.



57
58
59
# File 'lib/rake/remote_task.rb', line 57

def options
  @options
end

#outputObject

Returns the value of attribute output.



26
27
28
# File 'lib/rake/test_case.rb', line 26

def output
  @output
end

#remote_actionsObject (readonly)

An Array of Actions this host will perform during execution. Use enhance to add new actions to a task.



73
74
75
# File 'lib/rake/remote_task.rb', line 73

def remote_actions
  @remote_actions
end

#target_dirObject (readonly)

The directory on the host this task is running in during execution.



67
68
69
# File 'lib/rake/remote_task.rb', line 67

def target_dir
  @target_dir
end

#target_hostObject

The host this task is running on during execution.



62
63
64
# File 'lib/rake/remote_task.rb', line 62

def target_host
  @target_host
end

Class Method Details

.all_hostsObject

Returns an Array with every host configured.



250
251
252
# File 'lib/rake/remote_task.rb', line 250

def self.all_hosts
  hosts_for(roles.keys)
end

.append(name, value = nil, &default_block) ⇒ Object

Append value or default_block to environment variable name

To initialize an empty array, just do append name

If default_block is defined, the block will be executed the first time the variable is fetched, and the value will be used for every subsequent fetch.

Raises:

  • (ArgumentError)


477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
# File 'lib/rake/remote_task.rb', line 477

def self.append name, value = nil, &default_block
  raise ArgumentError, "cannot provide both a value and a block" if
    value and default_block unless
    value == :per_thread
  raise ArgumentError, "cannot set reserved name: '#{name}'" if
    Rake::RemoteTask.reserved_name?(name) unless $TESTING

  name = name.to_s

  set(name, []) unless @@is_array[name]
  Rake::RemoteTask.is_array[name] = true
  Rake::RemoteTask.per_thread[name] ||= default_block && value == :per_thread

  v = default_block || value
  if v then
    Rake::RemoteTask.default_env[name] << v
    Rake::RemoteTask.env[name] << v
  end
end

.current_rolesObject



75
76
77
# File 'lib/rake/remote_task.rb', line 75

def self.current_roles
  @@current_roles
end

.default_envObject

The default environment values. Used for resetting (mostly for tests).



258
259
260
# File 'lib/rake/remote_task.rb', line 258

def self.default_env
  @@default_env
end

.envObject

The vlad environment.



269
270
271
# File 'lib/rake/remote_task.rb', line 269

def self.env
  @@env
end

.fetch(name, default = nil) ⇒ Object

Fetches environment variable name from the environment using default default.



281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/rake/remote_task.rb', line 281

def self.fetch name, default = nil
  name = name.to_s if Symbol === name
  if @@env.has_key? name then
    protect_env(name) do
      v = @@env[name]
      if @@is_array[name] then
        v = v.map do |item|
          Proc === item ? item.call : item
        end
        unless per_thread[name] then
          @@env[name] = v
          @@is_array[name] = false
        end
      elsif Proc === v then
        v = v.call
        @@env[name] = v unless per_thread[name]
      end
      v
    end
  elsif default || default == false then
    @@env[name] = default
  else
    raise Rake::FetchError
  end
end

.host(host_name, *roles) ⇒ Object

Add host host_name that belongs to roles. Extra arguments may be specified for the host as a hash as the last argument.

host is the inversion of role:

host 'db1.example.com', :db, :master_db

Is equivalent to:

role :db, 'db1.example.com'
role :master_db, 'db1.example.com'


320
321
322
323
324
325
326
# File 'lib/rake/remote_task.rb', line 320

def self.host host_name, *roles
  opts = Hash === roles.last ? roles.pop : {}

  roles.each do |role_name|
    role role_name, host_name, opts.dup
  end
end

.hosts_for(*roles) ⇒ Object

Returns an Array of all hosts in roles.



331
332
333
334
335
# File 'lib/rake/remote_task.rb', line 331

def self.hosts_for *roles
  roles.flatten.map { |r|
    self.roles[r].keys
  }.flatten.uniq.sort
end

.is_arrayObject



273
274
275
# File 'lib/rake/remote_task.rb', line 273

def self.is_array
  @@is_array
end

.mandatory(name, desc) ⇒ Object

:nodoc:



337
338
339
340
341
342
# File 'lib/rake/remote_task.rb', line 337

def self.mandatory name, desc # :nodoc:
  self.set(name) do
    raise(Rake::ConfigurationError,
          "Please specify the #{desc} via the #{name.inspect} variable")
  end
end

.per_threadObject



262
263
264
# File 'lib/rake/remote_task.rb', line 262

def self.per_thread
  @@per_thread
end

.protect_env(name) ⇒ Object

Ensures exclusive access to name.



347
348
349
350
351
# File 'lib/rake/remote_task.rb', line 347

def self.protect_env name # :nodoc:
  @@env_locks[name].synchronize do
    yield
  end
end

.remote_task(name, *args, &block) ⇒ Object

Adds a remote task named name with options options that will execute block.



357
358
359
360
361
362
363
364
# File 'lib/rake/remote_task.rb', line 357

def self.remote_task name, *args, &block
  options = (Hash === args.last) ? args.pop : {}
  t = Rake::RemoteTask.define_task(name, *args, &block)
  options[:roles] = Array options[:roles]
  options[:roles] |= @@current_roles
  t.options = options
  t
end

.reserved_name?(name) ⇒ Boolean

Ensures name does not conflict with an existing method.

Returns:

  • (Boolean)


369
370
371
# File 'lib/rake/remote_task.rb', line 369

def self.reserved_name? name # :nodoc:
  !@@env.has_key?(name.to_s) && self.respond_to?(name)
end

.resetObject

Resets vlad, restoring all roles, tasks and environment variables to the defaults.



377
378
379
380
381
382
383
384
385
386
387
388
# File 'lib/rake/remote_task.rb', line 377

def self.reset
  @@def_role_hash = {}                # official default role value
  @@env           = {}
  @@is_array      = {}
  @@tasks         = {}
  @@roles         = Hash.new { |h,k| h[k] = @@def_role_hash }
  @@env_locks     = Hash.new { |h,k| h[k] = Mutex.new }

  @@default_env.each do |k,v|
    @@env[k] = safe_dup(v)
  end
end

.role(role_name, host = nil, args = {}) ⇒ Object

Adds role role_name with host and args for that host. TODO: merge: Declare a role and assign a remote host to it. Equivalent to the host method; provided for capistrano compatibility.



405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
# File 'lib/rake/remote_task.rb', line 405

def self.role role_name, host = nil, args = {}
  if block_given? then
    raise ArgumentError, 'host not allowed with block' unless host.nil?

    begin
      current_roles << role_name
      yield
    ensure
      current_roles.delete role_name
    end
  else
    raise ArgumentError, 'host required' if host.nil?

    [*host].each do |hst|
      raise ArgumentError, "invalid host: #{hst}" if hst.nil? or hst.empty?
    end
    @@roles[role_name] = {} if @@def_role_hash.eql? @@roles[role_name]
    @@roles[role_name][host] = args
  end
end

.rolesObject

The configured roles.



429
430
431
432
433
# File 'lib/rake/remote_task.rb', line 429

def self.roles
  host domain, :app, :web, :db if @@roles.empty?

  @@roles
end

.safe_dup(v) ⇒ Object

:nodoc:



390
391
392
393
394
395
396
397
# File 'lib/rake/remote_task.rb', line 390

def self.safe_dup v # :nodoc:
  case v
  when Symbol, Integer, nil, true, false, 42 then # ummmm... yeah. bite me.
    v
  else
    v.dup
  end
end

.set(name, value = nil, &default_block) ⇒ Object

Set environment variable name to value or default_block.

If default_block is defined, the block will be executed the first time the variable is fetched, and the value will be used for every subsequent fetch.

Raises:

  • (ArgumentError)


442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
# File 'lib/rake/remote_task.rb', line 442

def self.set name, value = nil, &default_block
  raise ArgumentError, "cannot provide both a value and a block" if
    value and default_block unless
    value == :per_thread
  raise ArgumentError, "cannot set reserved name: '#{name}'" if
    Rake::RemoteTask.reserved_name?(name) unless $TESTING

  name = name.to_s

  Rake::RemoteTask.per_thread[name] = true if
    default_block && value == :per_thread

  Rake::RemoteTask.default_env[name] = default_block || safe_dup(value)
  Rake::RemoteTask.env[name]         = default_block || safe_dup(value)

  if Object.private_instance_methods.include? name.to_sym then
    Object.send :alias_method, :"old_#{name}", name
  end

  Object.send :define_method, name do
    Rake::RemoteTask.fetch name
  end

  Object.send :private, name
end

.set_defaultsObject

Sets all the default values. Should only be called once. Use reset if you need to restore values.



501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
# File 'lib/rake/remote_task.rb', line 501

def self.set_defaults
  @@default_env ||= {}
  @@per_thread  ||= {}
  self.reset

  mandatory :repository, "repository path"
  mandatory :deploy_to,  "deploy path"
  mandatory :domain,     "server domain"

  simple_set(:deploy_timestamped, true,
             :deploy_via,         :export,
             :keep_releases,      5,
             :rake_cmd,           "rake",
             :rsync_cmd,          "rsync",
             :rsync_flags,        ['-azP', '--delete'],
             :ssh_cmd,            "ssh",
             :ssh_flags,          [],
             :sudo_cmd,           "sudo",
             :sudo_flags,         ['-p Password:'],
             :sudo_prompt,        /^Password:/,
             :umask,              nil,
             :mkdirs,             [],
             :shared_paths,       {},
             :perm_owner,         nil,
             :perm_group,         nil)

  append :command_prefix, []

  set(:current_release)    { (releases.any?) ? File.join(releases_path, releases[-1]) : nil }
  set(:latest_release)     {
    deploy_timestamped ? release_path : current_release
  }
  set(:previous_release)   { File.join(releases_path, releases[-2]) }
  set(:release_name)       { Time.now.utc.strftime("%Y%m%d%H%M%S") }
  set(:release_path)       { File.join(releases_path, release_name) }
  set(:releases)           { task.run("ls -1 #{releases_path}").split("\n").select { |l| l =~ /^\d+$/ }.sort }

  set_path :current_path,  "current"
  set_path :releases_path, "releases"
  set_path :scm_path,      "scm"
  set_path :shared_path,   "shared"

  set(:sudo_password) do
    state = `stty -g`

    raise Rake::Error, "stty(1) not found" unless $?.success?

    begin
      system "stty -echo"
      $stdout.print "sudo password: "
      $stdout.flush
      sudo_password = $stdin.gets
      $stdout.puts
    ensure
      system "stty #{state}"
    end
    sudo_password
  end
end

.set_path(name, subdir) ⇒ Object

:nodoc:



561
562
563
# File 'lib/rake/remote_task.rb', line 561

def self.set_path(name, subdir) # :nodoc:
  set(name) { File.join(deploy_to, subdir) }
end

.simple_set(*args) ⇒ Object

:nodoc:



565
566
567
568
569
570
# File 'lib/rake/remote_task.rb', line 565

def self.simple_set(*args) # :nodoc:
  args = Hash[*args]
  args.each do |k, v|
    set k, v
  end
end

.taskObject

The Rake::RemoteTask executing in this Thread.



575
576
577
# File 'lib/rake/remote_task.rb', line 575

def self.task
  Thread.current[:task]
end

.tasksObject

The configured Rake::RemoteTasks.



582
583
584
# File 'lib/rake/remote_task.rb', line 582

def self.tasks
  @@tasks
end

Instance Method Details

#defined_target_hosts?Boolean

Similar to target_hosts, but returns true if user defined any hosts, even an empty list.

Returns:

  • (Boolean)


636
637
638
639
640
641
642
643
644
645
# File 'lib/rake/remote_task.rb', line 636

def defined_target_hosts?
  return true if ENV["HOSTS"]
  roles = Array options[:roles]
  return true if roles.empty?
  # borrowed from hosts_for:
  roles.flatten.each { |r|
    return true unless @@def_role_hash.eql? Rake::RemoteTask.roles[r]
  }
  return false
end

#enhance(deps = nil, &block) ⇒ Object

Add remote action block to this task with dependencies deps. See Rake::Task#enhance.



98
99
100
101
102
# File 'lib/rake/remote_task.rb', line 98

def enhance(deps=nil, &block)
  original_enhance(deps) # can't use super because block passed regardless.
  @remote_actions << Action.new(self, block) if block_given?
  self
end

#execute(args = nil) ⇒ Object

Execute this action. Local actions will be performed first, then remote actions will be performed in parallel on each host configured for this RemoteTask.



109
110
111
112
113
114
115
116
117
# File 'lib/rake/remote_task.rb', line 109

def execute(args = nil)
  raise(Rake::ConfigurationError,
        "No target hosts specified on task #{self.name} for roles #{options[:roles].inspect}") unless
    defined_target_hosts?

  super args

  @remote_actions.each { |act| act.execute(target_hosts, self, args) }
end

#get(local_dir, *files) ⇒ Object

Pull files from the remote host using rsync to local_dir. TODO: what if role has multiple hosts & the files overlap? subdirs?



123
124
125
126
127
128
# File 'lib/rake/remote_task.rb', line 123

def get local_dir, *files
  @happy = true
  host = target_host
  rsync files.map { |f| "#{host}:#{f}" }, local_dir
  @happy = false
end

#original_enhanceObject

Add a local action to this task. This calls Rake::Task#enhance.



92
# File 'lib/rake/remote_task.rb', line 92

alias_method :original_enhance, :enhance

#popen4(*command) ⇒ Object



39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/rake/test_case.rb', line 39

def popen4 *command
  @commands << command

  @input = StringIO.new
  out = StringIO.new @output.shift.to_s
  err = StringIO.new @error.shift.to_s

  raise if block_given?

  status = self.action ? self.action[command.join(' ')] : 0
  Process.expected Status.new(status)

  return 42, @input, out, err
end

#put(remote_path, base_name = File.basename(remote_path)) ⇒ Object

Copy a (usually generated) file to remote_path. Contents of block are copied to remote_path and you may specify an optional base_name for the tempfile (aids in debugging).



135
136
137
138
139
140
141
142
143
144
# File 'lib/rake/remote_task.rb', line 135

def put remote_path, base_name = File.basename(remote_path)
  require 'tempfile'
  Tempfile.open base_name do |fp|
    fp.puts yield
    fp.flush
    @happy = true
    rsync fp.path, "#{target_host}:#{remote_path}"
    @happy = false
  end
end

#rsync(*args) ⇒ Object

Execute rsync with args. Tacks on pre-specified rsync_cmd and rsync_flags.

Favor #get and #put for most tasks. Old-style direct use where the target_host was implicit is now deprecated.

Raises:



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/rake/remote_task.rb', line 153

def rsync *args
  unless @happy || args[-1] =~ /:/ then
    warn "rsync deprecation: pass target_host:remote_path explicitly"
    args[-1] = "#{target_host}:#{args[-1]}"
  end

  cmd    = [rsync_cmd, rsync_flags, args].flatten.compact
  cmdstr = cmd.join ' '

  warn cmdstr if $TRACE

  success = system(*cmd)

  raise Rake::CommandFailedError.new($?), "execution failed: #{cmdstr}" unless success
end

#run(command) ⇒ Object

Use ssh to execute command on target_host. If command uses sudo, the sudo password will be prompted for then saved for subsequent sudo commands.

If command_prefix has been filled up with one or several commands, they will be run on target_host before command.

Yields input channel, :out or :err, and data.



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/rake/remote_task.rb', line 178

def run command
  commands = []

  commands << "cd #{target_dir}" if target_dir
  commands += command_prefix.flatten if command_prefix
  commands << "#{command}"

  command = commands.join(" && ")

  cmd     = [ssh_cmd, ssh_flags, target_host, command].flatten
  result  = []

  trace = [ssh_cmd, ssh_flags, target_host, "'#{command}'"].flatten.join(' ')
  warn trace if $TRACE

  pid, inn, out, err = popen4(*cmd)

  inn.sync   = true
  streams    = [out, err]
  out_stream = {
    out => $stdout,
    err => $stderr,
  }

  # Handle process termination ourselves
  status = nil
  Thread.start do
    status = Process.waitpid2(pid).last
  end

  until streams.empty? do
    # don't busy loop
    selected, = select streams, nil, nil, 0.1

    next if selected.nil? or selected.empty?

    selected.each do |stream|
      if stream.eof? then
        streams.delete stream if status # we've quit, so no more writing
        next
      end

      data = stream.readpartial(1024)
      out_stream[stream].write data

      if stream == err and data =~ sudo_prompt then
        inn.puts sudo_password
        data << "\n"
        $stderr.write "\n"
      end

      yield inn, stream == out ? :out : :err, data if block_given?

      result << data
    end
  end

  unless status.success? then
    raise(Rake::CommandFailedError.new(status),
          "execution failed with status #{status.exitstatus}: #{cmd.join ' '}")
  end

  result.join
ensure
  inn.close rescue nil
  out.close rescue nil
  err.close rescue nil
end

#select(reads, writes, errs, timeout) ⇒ Object



54
55
56
# File 'lib/rake/test_case.rb', line 54

def select reads, writes, errs, timeout
  [reads, writes, errs]
end

#sudo(command) ⇒ Object

Execute command under sudo using run.



589
590
591
# File 'lib/rake/remote_task.rb', line 589

def sudo command
  run [sudo_cmd, sudo_flags, command].flatten.compact.join(" ")
end

#system(*command) ⇒ Object



34
35
36
37
# File 'lib/rake/test_case.rb', line 34

def system *command
  @commands << command
  self.action ? self.action[command.join(' ')] : true
end

#target_hostsObject

The hosts this task will execute on. The hosts are determined from the role this task belongs to.

The target hosts may be overridden by providing a comma-separated list of commands to the HOSTS environment variable:

rake my_task HOSTS=app1.example.com,app2.example.com


618
619
620
621
622
623
624
625
626
627
628
629
630
# File 'lib/rake/remote_task.rb', line 618

def target_hosts
  if hosts = ENV["HOSTS"] then
    hosts.strip.gsub(/\s+/, '').split(",")
  else
    roles = Array options[:roles]

    if roles.empty? then
      Rake::RemoteTask.all_hosts
    else
      Rake::RemoteTask.hosts_for roles
    end
  end
end