Class: Rake::RemoteTask

Inherits:
Task
  • Object
show all
Includes:
Open4
Defined in:
lib/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

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Task

#vlad_original_execute

Constructor Details

#initialize(task_name, app) ⇒ RemoteTask

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



136
137
138
139
# File 'lib/rake_remote_task.rb', line 136

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

Instance Attribute Details

#optionsObject

Options for execution of this task.



120
121
122
# File 'lib/rake_remote_task.rb', line 120

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.



131
132
133
# File 'lib/rake_remote_task.rb', line 131

def remote_actions
  @remote_actions
end

#target_hostObject

The host this task is running on during execution.



125
126
127
# File 'lib/rake_remote_task.rb', line 125

def target_host
  @target_host
end

Class Method Details

.all_hostsObject

Returns an Array with every host configured.



245
246
247
# File 'lib/rake_remote_task.rb', line 245

def self.all_hosts
  hosts_for(roles.keys)
end

.default_envObject

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



253
254
255
# File 'lib/rake_remote_task.rb', line 253

def self.default_env
  @@default_env
end

.envObject

The vlad environment.



260
261
262
# File 'lib/rake_remote_task.rb', line 260

def self.env
  @@env
end

.fetch(name, default = nil) ⇒ Object

Fetches environment variable name from the environment using default default.



268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/rake_remote_task.rb', line 268

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
      v
    end
  elsif default
    v = @@env[name] = default
  else
    raise Vlad::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'


296
297
298
299
300
301
302
# File 'lib/rake_remote_task.rb', line 296

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.



307
308
309
310
311
# File 'lib/rake_remote_task.rb', line 307

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

.mandatory(name, desc) ⇒ Object

:nodoc:



313
314
315
316
317
318
# File 'lib/rake_remote_task.rb', line 313

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

.protect_env(name) ⇒ Object

Ensures exclusive access to name.



323
324
325
326
327
# File 'lib/rake_remote_task.rb', line 323

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

.remote_task(name, options = {}, &block) ⇒ Object

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



333
334
335
336
337
# File 'lib/rake_remote_task.rb', line 333

def self.remote_task name, options = {}, &block
  t = Rake::RemoteTask.define_task(name, &block)
  t.options = options
  t
end

.reserved_name?(name) ⇒ Boolean

Ensures name does not conflict with an existing method.

Returns:

  • (Boolean)


342
343
344
# File 'lib/rake_remote_task.rb', line 342

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.



350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
# File 'lib/rake_remote_task.rb', line 350

def self.reset
  @@roles = Hash.new { |h,k| h[k] = {} }
  @@env = {}
  @@tasks = {}
  @@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. bite me.
      @@env[k] = v
    else
      @@env[k] = v.dup
    end
  end
end

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

Adds role role_name with host and args for that host.

Raises:

  • (ArgumentError)


369
370
371
372
# File 'lib/rake_remote_task.rb', line 369

def self.role role_name, host, args = {}
  raise ArgumentError, "invalid host" if host.nil? or host.empty?
  @@roles[role_name][host] = args
end

.rolesObject

The configured roles.



377
378
379
380
381
# File 'lib/rake_remote_task.rb', line 377

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)


390
391
392
393
394
395
396
397
398
399
400
401
402
# File 'lib/rake_remote_task.rb', line 390

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

  Rake::RemoteTask.default_env[name.to_s] = Rake::RemoteTask.env[name.to_s] =
    value || default_block

  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.



408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
# File 'lib/rake_remote_task.rb', line 408

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

  mandatory :code_repo,  "code repo path"
  mandatory :deploy_to,  "deploy path"
  mandatory :domain,     "server domain"

  simple_set(:deploy_timestamped, true,
             :deploy_via,         :export,
             :keep_releases,      5,
             :migrate_args,       "",
             :migrate_target,     :latest,
             :rails_env,          "production",
             :rake_cmd,           "rake",
             :revision,           "head",
             :rsync_cmd,          "rsync",
             :rsync_flags,        ['-azP', '--delete'],
             :ssh_cmd,            "ssh",
             :ssh_flags,          nil,
             :sudo_cmd,           "sudo",
             :sudo_flags,         nil)

  set(:current_release)    { File.join(releases_path, vlad_releases[-1]) }
  set(:latest_release)     { deploy_timestamped ?release_path: current_release }
  set(:previous_release)   { File.join(releases_path, vlad_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(:vlad_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(:sudo_password) do
    state = `stty -g`

    raise Vlad::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:



461
462
463
# File 'lib/rake_remote_task.rb', line 461

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

.simple_set(*args) ⇒ Object

:nodoc:



465
466
467
468
469
470
# File 'lib/rake_remote_task.rb', line 465

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.



475
476
477
# File 'lib/rake_remote_task.rb', line 475

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

.tasksObject

The configured Rake::RemoteTasks.



482
483
484
# File 'lib/rake_remote_task.rb', line 482

def self.tasks
  @@tasks
end

Instance Method Details

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

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



150
151
152
153
154
# File 'lib/rake_remote_task.rb', line 150

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.



161
162
163
164
165
166
167
168
169
# File 'lib/rake_remote_task.rb', line 161

def execute(args = nil)
  raise(Vlad::ConfigurationError,
        "No target hosts specified for task: #{self.name}") if
    target_hosts.empty?

  super args

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

#original_enhanceObject

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



144
# File 'lib/rake_remote_task.rb', line 144

alias_method :original_enhance, :enhance

#rsync(local, remote) ⇒ Object

Use rsync to send local to remote on target_host.



174
175
176
177
178
179
180
181
182
# File 'lib/rake_remote_task.rb', line 174

def rsync local, remote
  cmd = [rsync_cmd, rsync_flags, local, "#{@target_host}:#{remote}"].flatten.compact

  success = system(*cmd)

  unless success then
    raise Vlad::CommandFailedError, "execution failed: #{cmd.join ' '}"
  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.



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
# File 'lib/rake_remote_task.rb', line 188

def run command
  cmd = [ssh_cmd, ssh_flags, target_host, command].compact
  result = []

  warn cmd.join(' ') 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 =~ /^Password:/ then
        inn.puts sudo_password
        data << "\n"
        $stderr.write "\n"
      end

      result << data
    end
  end

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

  result.join
end

#sudo(command) ⇒ Object

Execute command under sudo using run.



489
490
491
# File 'lib/rake_remote_task.rb', line 489

def sudo command
  run [sudo_cmd, sudo_flags, command].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


502
503
504
505
506
507
508
509
# File 'lib/rake_remote_task.rb', line 502

def target_hosts
  if hosts = ENV["HOSTS"] then
    hosts.strip.gsub(/\s+/, '').split(",")
  else
    roles = options[:roles]
    roles ? Rake::RemoteTask.hosts_for(roles) : Rake::RemoteTask.all_hosts
  end
end