Class: Rake::RemoteTask

Inherits:
Task
  • Object
show all
Includes:
Open4
Defined in:
lib/ext/rake/remote_task.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, CommandFailedError, ConfigurationError, Error, FetchError

Constant Summary collapse

@@current_roles =
[]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Task

invoke_if_defined, #task_original_execute

Constructor Details

#initialize(task_name, app) ⇒ RemoteTask

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



48
49
50
51
# File 'lib/ext/rake/remote_task.rb', line 48

def initialize(task_name, app)
  super
  @remote_actions = []
end

Instance Attribute Details

#optionsObject

Options for execution of this task.



34
35
36
# File 'lib/ext/rake/remote_task.rb', line 34

def options
  @options
end

#remote_actionsObject (readonly)

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



41
42
43
# File 'lib/ext/rake/remote_task.rb', line 41

def remote_actions
  @remote_actions
end

#target_hostObject

The host this task is running on during execution.



37
38
39
# File 'lib/ext/rake/remote_task.rb', line 37

def target_host
  @target_host
end

Class Method Details

.all_hostsObject

Returns an Array with every host configured.



170
171
172
# File 'lib/ext/rake/remote_task.rb', line 170

def self.all_hosts
  hosts_for(roles.keys)
end

.current_rolesObject



43
44
45
# File 'lib/ext/rake/remote_task.rb', line 43

def self.current_roles
  @@current_roles
end

.default_envObject

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



176
177
178
# File 'lib/ext/rake/remote_task.rb', line 176

def self.default_env
  @@default_env
end

.envObject

The remote task environment.



185
186
187
# File 'lib/ext/rake/remote_task.rb', line 185

def self.env
  @@env
end

.fetch(name, default = nil) ⇒ Object

Fetches environment variable name from the environment using default default.



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/ext/rake/remote_task.rb', line 191

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]
      v = @@env[name] = v.call if Proc === v unless per_thread[name]
      v = v.call if Proc === v
      v
    end
  elsif default || default == false
    v = @@env[name] = default
  else
    raise Rake::RemoteTask::FetchError
  end
end

.get_options_hash(*options) ⇒ Object

Fetches a set of options and turns it into a hash to be passed to the application specific delegate tasks.



209
210
211
212
213
214
215
216
# File 'lib/ext/rake/remote_task.rb', line 209

def self.get_options_hash(*options)
  options_hash = {}
  options.each do |option|
    option_value = Rake::RemoteTask.fetch(option, false)
    options_hash[option] = option_value if option_value
  end
  options_hash
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'


229
230
231
232
233
234
235
# File 'lib/ext/rake/remote_task.rb', line 229

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.



238
239
240
241
242
# File 'lib/ext/rake/remote_task.rb', line 238

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

.mandatory(name, desc) ⇒ Object

:nodoc:



244
245
246
247
248
249
# File 'lib/ext/rake/remote_task.rb', line 244

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

.per_threadObject



180
181
182
# File 'lib/ext/rake/remote_task.rb', line 180

def self.per_thread
  @@per_thread
end

.protect_env(name) ⇒ Object

Ensures exclusive access to name.



252
253
254
255
256
# File 'lib/ext/rake/remote_task.rb', line 252

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.



260
261
262
263
264
265
266
267
# File 'lib/ext/rake/remote_task.rb', line 260

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)


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

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.



276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/ext/rake/remote_task.rb', line 276

def self.reset
  @@def_role_hash = {}                # official default role value
  @@env           = {}
  @@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|
    case v
    when Symbol, Fixnum, nil, true, false, 42 then # ummmm... yeah.
      @@env[k] = v
    else
      @@env[k] = v.dup
    end
  end
end

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

Adds role role_name with host and args for that host.



294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
# File 'lib/ext/rake/remote_task.rb', line 294

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.



316
317
318
319
320
# File 'lib/ext/rake/remote_task.rb', line 316

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

  @@roles
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)


327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
# File 'lib/ext/rake/remote_task.rb', line 327

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] = Rake::RemoteTask.env[name] =
    default_block || value

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

.set_defaultsObject

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



349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
# File 'lib/ext/rake/remote_task.rb', line 349

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(:app_env,            "production",
             :deploy_timestamped, true,
             :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:/)   
  
  set(:latest_release)     { deploy_timestamped ? File.join(releases_path, releases[-1]) : releases_path }
  set(:previous_release) do
    if deploy_timestamped
      if releases[-2]
        File.join(releases_path, releases[-2])
      else
        nil
      end
    else
      releases_path
    end
  end
  set(:release_name)       { deploy_timestamped ? Time.now.utc.strftime("%Y%m%d%H%M%S") : nil }
  set(:release_path)       { release_name ? File.join(releases_path, release_name) : releases_path }
  set(:releases)           { task.run("ls -x #{releases_path}").split.sort }

  set_path :current_path,        "current"
  set_path :releases_path,       "releases"
  set_path :scm_path,            "scm"
  set_path :shared_path,         "shared"
  set_path :shared_config_path,  File.join("shared", "config")
  
  set(:sudo_password) do
    state = `stty -g`

    raise Rake::RemoteTask::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:



410
411
412
# File 'lib/ext/rake/remote_task.rb', line 410

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

.simple_set(*args) ⇒ Object

:nodoc:



414
415
416
417
418
419
# File 'lib/ext/rake/remote_task.rb', line 414

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.



422
423
424
# File 'lib/ext/rake/remote_task.rb', line 422

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

.tasksObject

The configured Rake::RemoteTasks.



427
428
429
# File 'lib/ext/rake/remote_task.rb', line 427

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)


459
460
461
462
463
464
465
466
467
468
# File 'lib/ext/rake/remote_task.rb', line 459

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.



58
59
60
61
62
# File 'lib/ext/rake/remote_task.rb', line 58

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.



67
68
69
70
71
72
73
74
75
# File 'lib/ext/rake/remote_task.rb', line 67

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

  super args

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

#get(local_directory, *files) ⇒ Object

Pulls files from the target_host using rsync to local_directory.



78
79
80
# File 'lib/ext/rake/remote_task.rb', line 78

def get(local_directory, *files)
  rsync(files.map { |f| "#{target_host}:#{f}" }, local_directory)
end

#original_enhanceObject

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



54
# File 'lib/ext/rake/remote_task.rb', line 54

alias_method :original_enhance, :enhance

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

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



85
86
87
88
89
90
91
92
# File 'lib/ext/rake/remote_task.rb', line 85

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

#rsync(local, remote) ⇒ Object

Use rsync to send local to remote.



95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/ext/rake/remote_task.rb', line 95

def rsync(local, remote)
  cmd = [rsync_cmd, rsync_flags, local, remote].flatten.compact
  cmdstr = cmd.join(' ')

  warn cmdstr if $TRACE

  success = system(*cmd)

  unless success then
    raise Rake::RemoteTask::CommandFailedError, "execution failed: #{cmdstr}"
  end
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.



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/ext/rake/remote_task.rb', line 110

def run(command)
  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

      result << data
    end
  end

  unless status.success? then
    raise(Rake::RemoteTask::CommandFailedError,
          "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

#sudo(command) ⇒ Object

Execute command under sudo using run.



432
433
434
# File 'lib/ext/rake/remote_task.rb', line 432

def sudo(command)
  run [sudo_cmd, sudo_flags, command].flatten.compact.join(" ")
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


443
444
445
446
447
448
449
450
451
452
453
454
455
# File 'lib/ext/rake/remote_task.rb', line 443

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