class Rake::RemoteTask

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

Constants

Status
VERSION

Attributes

action[RW]
commands[RW]
error[RW]
input[RW]
options[RW]

Options for execution of this task.

output[RW]
remote_actions[R]

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

target_dir[R]

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

target_host[R]

The host this task is running on during execution.

Public Class Methods

all_hosts() click to toggle source

Returns an Array with every host configured.

# File lib/rake/remote_task.rb, line 250
def self.all_hosts
  hosts_for(roles.keys)
end
append(name, value = nil, &default_block) click to toggle source

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.

# 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_roles() click to toggle source
# File lib/rake/remote_task.rb, line 75
def self.current_roles
  @@current_roles
end
default_env() click to toggle source

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

# File lib/rake/remote_task.rb, line 258
def self.default_env
  @@default_env
end
env() click to toggle source

The vlad environment.

# File lib/rake/remote_task.rb, line 269
def self.env
  @@env
end
fetch(name, default = nil) click to toggle source

Fetches environment variable name from the environment using default default.

# 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) click to toggle source

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'
# 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) click to toggle source

Returns an Array of all hosts in roles.

# 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_array() click to toggle source
# File lib/rake/remote_task.rb, line 273
def self.is_array
  @@is_array
end
new(task_name, app) click to toggle source

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

Calls superclass method
# 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
per_thread() click to toggle source
# File lib/rake/remote_task.rb, line 262
def self.per_thread
  @@per_thread
end
remote_task(name, *args, &block) click to toggle source

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

# 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
reset() click to toggle source

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

# 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 = {}) { || ... } click to toggle source

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.

# 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
roles() click to toggle source

The configured roles.

# File lib/rake/remote_task.rb, line 429
def self.roles
  host domain, :app, :web, :db if @@roles.empty?

  @@roles
end
set(name, value = nil, &default_block) click to toggle source

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.

# 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_defaults() click to toggle source

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

# 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 = %x`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
task() click to toggle source

The Rake::RemoteTask executing in this Thread.

# File lib/rake/remote_task.rb, line 575
def self.task
  Thread.current[:task]
end
tasks() click to toggle source

The configured Rake::RemoteTasks.

# File lib/rake/remote_task.rb, line 582
def self.tasks
  @@tasks
end

Public Instance Methods

defined_target_hosts?() click to toggle source

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

# 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) click to toggle source

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

# 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
Also aliased as: original_enhance
execute(args = nil) click to toggle source

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

Calls superclass method
# 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) click to toggle source

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

# 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_enhance(deps=nil, &block)

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

Alias for: enhance
popen4(*command) click to toggle source
# 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)) { || ... } click to toggle source

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).

# 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) click to toggle source

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.

# 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) { |inn, stream == out ? :out : :err, data| ... } click to toggle source

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.

# 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) click to toggle source
# File lib/rake/test_case.rb, line 54
def select reads, writes, errs, timeout
  [reads, writes, errs]
end
sudo(command) click to toggle source

Execute command under sudo using run.

# File lib/rake/remote_task.rb, line 589
def sudo command
  run [sudo_cmd, sudo_flags, command].flatten.compact.join(" ")
end
system(*command) click to toggle source
# File lib/rake/test_case.rb, line 34
def system *command
  @commands << command
  self.action ? self.action[command.join(' ')] : true
end
target_host=(host) click to toggle source

Sets the target host. Allows you to set an optional directory using the format:

host.domain:/dir
# File lib/rake/remote_task.rb, line 599
def target_host= host
  if host =~ /^(.+):(.+?)$/
    @target_host = $1
    @target_dir  = $2
  else
    @target_host = host
    @target_dir  = nil
  end
end
target_hosts() click to toggle source

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
# 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