class Blower::Context

Blower tasks are executed within a context.

The context can be used to share information between tasks by storing it in instance variables.

Attributes

failures[RW]

The failed hosts.

file[RW]

The file name of the currently running task. Context#cp interprets relative file names relative to this file name's directory component.

hosts[RW]

The target hosts.

path[RW]

Search path for tasks.

user[RW]

Username override. If not-nil, this user is used for all remote accesses.

Public Class Methods

new(path) click to toggle source

Create a new Context. @param [Array] path The search path for tasks.

# File lib/blower/context.rb, line 48
def initialize (path)
  @path = path
  @data = {}
  @hosts = []
  @failures = []
end

Public Instance Methods

as(user, quiet: false) { || ... } click to toggle source

Yield with a temporary username override @macro quietable

# File lib/blower/context.rb, line 105
def as (user, quiet: false)
  let :@user => user do
    log.info "as #{user}", quiet: quiet do
      yield
    end
  end
end
cp(from, to, as: user, on: hosts, quiet: false, once: nil, delete: false) click to toggle source

Copy a file or readable to the host filesystems. @overload cp(readable, to, as: user, on: hosts, quiet: false)

@param [#read] from An object from which to read the contents of the new file.
@param [String] to The file name to write the string to.
@macro onable
@macro asable
@macro quietable
@macro onceable

@overload cp(filename, to, as: user, on: hosts, quiet: false)

@param [String] from The name of the local file to copy.
@param [String] to The file name to write the string to.
@macro onable
@macro asable
@macro quietable
@macro onceable
# File lib/blower/context.rb, line 181
def cp (from, to, as: user, on: hosts, quiet: false, once: nil, delete: false)
  self.once once, quiet: quiet do
    log.info "cp: #{from} -> #{to}", quiet: quiet do
      Dir.chdir File.dirname(file) do
        hash_map(hosts) do |host|
          host.cp from, to, as: as, quiet: quiet, delete: delete
        end
      end
    end
  end
end
get(name, default = nil) click to toggle source

Return a context variable. @param name The name of the variable. @param default The value to return if the variable is not set.

# File lib/blower/context.rb, line 58
def get (name, default = nil)
  @data.fetch(name, default)
end
on(*hosts, quiet: false) { |*hosts| ... } click to toggle source

Yield with a temporary host list @macro quietable

# File lib/blower/context.rb, line 95
def on (*hosts, quiet: false)
  let :@hosts => hosts.flatten do
    log.info "on #{@hosts.map(&:name).join(", ")}", quiet: quiet do
      yield *hosts
    end
  end
end
once(key, store: "/var/cache/blower.json", quiet: false) { || ... } click to toggle source

Execute a block only once per host. It is usually preferable to make tasks idempotent, but when that isn't possible, once will only execute the block on hosts where a block with the same key hasn't previously been successfully executed. @param [String] key Uniquely identifies the block. @param [String] store File to store once's state in. @macro quietable

# File lib/blower/context.rb, line 278
def once (key, store: "/var/cache/blower.json", quiet: false)
  return yield unless key
  log.info "once: #{key}", quiet: quiet do
    hash_map(hosts) do |host|
      done = begin
        JSON.parse(host.read(store, quiet: true))
      rescue => e
        {}
      end
      unless done[key]
        on [host] do
          yield
        end
        done[key] = true
        host.write(done.to_json, store, quiet: true)
      end
    end
  end
end
ping(on: hosts, quiet: false) click to toggle source

Ping each host by trying to connect to port 22 @macro onable @macro quietable

# File lib/blower/context.rb, line 263
def ping (on: hosts, quiet: false)
  log.info "ping", quiet: quiet do
    hash_map(hosts) do |host|
      host.ping
    end
  end
end
read(filename, as: user, on: hosts, quiet: false) click to toggle source

Reads a remote file from each host. @param [String] filename The file to read. @return [Hash] A hash of Host objects to Strings of the file contents. @macro onable @macro asable @macro quietable

# File lib/blower/context.rb, line 199
def read (filename, as: user, on: hosts, quiet: false)
  log.info "read: #{filename}", quiet: quiet do
    hash_map(hosts) do |host|
      host.read filename, as: as
    end
  end
end
render(from, to, as: user, on: hosts, quiet: false, once: nil) click to toggle source

Renders and installs files from ERB templates. Files are under from in ERB format. from/foo/bar.conf.erb is rendered and written to to/foo/bar.conf. Non-ERB files are ignored. @param [String] from The directory to search for .erb files. @param [String] to The remote directory to put files in. @macro onable @macro asable @macro quietable @macro onceable

# File lib/blower/context.rb, line 232
def render (from, to, as: user, on: hosts, quiet: false, once: nil)
  self.once once, quiet: quiet do
    Dir.chdir File.dirname(file) do
      Find.find(from).each do |path|
        if File.directory?(path)
          to_path = to + path[from.length..-1]
          sh "mkdir -p #{to_path.shellescape}"
        elsif path =~ /\.erb$/
          template = ERB.new(File.read(path))
          to_path = to + path[from.length..-5]
          log.info "render: #{path} -> #{to_path}", quiet: quiet do
            hash_map(hosts) do |host|
              host.cp StringIO.new(template.result(binding)), to_path, as: as, quiet: quiet
            end
          end
        else
          to_path = to + path[from.length..-1]
          log.info "copy: #{path} -> #{to_path}", quiet: quiet do
            hash_map(hosts) do |host|
              host.cp File.open(path), to_path, as: as, quiet: quiet
            end
          end
        end
      end
    end
  end
end
run(task, optional: false, quiet: false, once: nil) click to toggle source

Find and execute a task. For each entry in the search path, it checks for path/task.rb, path/task/blow.rb, and finally for bare path/task. The search stops at the first match. If found, the task is executed within the context, with +@file+ bound to the task's file name. @param [String] task The name of the task. @macro quietable @macro onceable @raise [TaskNotFound] If no task is found. @raise Whatever the task itself raises. @return The result of evaluating the task file.

# File lib/blower/context.rb, line 122
def run (task, optional: false, quiet: false, once: nil)
  @run_cache ||= {}
  once once, quiet: quiet do
    log.info "run #{task}", quiet: quiet do
      file = find_task(task)
      if @run_cache.has_key? file
        log.info "*cached*"
        @run_cache[file]
      else
        @run_cache[file] = begin
          code = File.read(file)
          let :@file => file do
            instance_eval(code, file)
          end
        end
      end
    end
  end
rescue TaskNotFound => e
  return if optional
  raise e
end
set(hash) click to toggle source

Merge the hash into the context variables. @param [Hash] hash The values to merge into the context variables.

# File lib/blower/context.rb, line 72
def set (hash)
  @data.merge! hash
end
sh(command, as: user, on: hosts, quiet: false, once: nil) click to toggle source

Execute a shell command on each host @macro onable @macro asable @macro quietable @macro onceable

# File lib/blower/context.rb, line 156
def sh (command, as: user, on: hosts, quiet: false, once: nil)
  self.once once, quiet: quiet do
    log.info "sh: #{command}", quiet: quiet do
      hash_map(hosts) do |host|
        host.sh command, as: as, quiet: quiet
      end
    end
  end
end
sh?(command, as: user, on: hosts, quiet: false, once: nil) click to toggle source
# File lib/blower/context.rb, line 145
def sh? (command, as: user, on: hosts, quiet: false, once: nil)
  sh command, as: as, on: on, quiet: quiet, once: once
rescue
  nil
end
unset(*names) click to toggle source

Remove context variables @param names The names to remove from the variables.

# File lib/blower/context.rb, line 64
def unset (*names)
  names.each do |name|
    @data.delete name
  end
end
with(hash, quiet: false) { || ... } click to toggle source

Yield with the hash temporary merged into the context variables. Only variables specifically named in hash will be reset when the yield returns. @param [Hash] hash The values to merge into the context variables. @return Whatever the yielded-to block returns. @macro quietable

# File lib/blower/context.rb, line 81
def with (hash, quiet: false)
  old_values = data.values_at(hash.keys)
  log.debug "with #{hash}", quiet: quiet do
    set hash
    yield
  end
ensure
  hash.keys.each.with_index do |key, i|
    @data[key] = old_values[i]
  end
end
write(string, to, as: user, on: hosts, quiet: false, once: nil) click to toggle source

Writes a string to a file on the host filesystems. @param [String] string The string to write. @param [String] to The file name to write the string to. @macro onable @macro asable @macro quietable @macro onceable

# File lib/blower/context.rb, line 214
def write (string, to, as: user, on: hosts, quiet: false, once: nil)
  self.once once, quiet: quiet do
    log.info "write: #{string.bytesize} bytes -> #{to}", quiet: quiet do
      hash_map(hosts) do |host|
        host.write string, to, as: as, quiet: quiet
      end
    end
  end
end

Private Instance Methods

each(hosts = self.hosts, serial: false) { |host, i += 1| ... } click to toggle source
# File lib/blower/context.rb, line 371
def each (hosts = self.hosts, serial: false)
  fail "No hosts" if hosts.empty?
  q = (@threads || serial) && Queue.new
  if q && serial
    q.push nil
  elsif q
    @threads.times { q.push nil }
  end
  indent = Thread.current[:indent]
  i = -1
  threads = [hosts].flatten.map.with_index do |host|
    Thread.new do
      Thread.current[:indent] = indent
      begin
        q.pop if q
        yield host, i += 1
      rescue => e
        host.log.error e.message
        hosts.delete host
        @failures |= [host]
      ensure
        q.push nil if q
        sleep @delay if @delay
      end
    end
  end
  threads.each(&:join)
  fail "No hosts remaining" if hosts.empty?
end
find_task(name) click to toggle source
# File lib/blower/context.rb, line 401
def find_task (name)
  log.debug "Searching for task #{name}" do
    path.each do |path|
      log.trace "checking #{File.join(path, name + ".rb")}"
      file = File.join(path, name + ".rb")
      return file if File.exists?(file)
      log.trace "checking #{File.join(path, name, "/blow.rb")}"
      file = File.join(path, name, "/blow.rb")
      return file if File.exists?(file)
      log.trace "checking #{File.join(path, name)}"
      file = File.join(path, name)
      return file if File.exists?(file)
    end
  end
  fail TaskNotFound, "Task not found: #{name}"
end
hash_map(hosts = self.hosts) { |host, i| ... } click to toggle source
# File lib/blower/context.rb, line 323
def hash_map (hosts = self.hosts)
  hh = HostHash.new.tap do |result|
    each(hosts) do |host, i|
      result[host] = yield(host, i)
    end
  end
  if @singular
    hh.values.first
  else
    hh
  end
end
locally(&block) click to toggle source
# File lib/blower/context.rb, line 365
def locally (&block)
  on Local.new("<local>") do
    singularly &block
  end
end
on_each(hosts = self.hosts, serial: true) { |host, i| ... } click to toggle source
# File lib/blower/context.rb, line 355
def on_each (hosts = self.hosts, serial: true)
  each(hosts.dup, serial: serial) do |host, i|
    on host do
      singularly do
        yield host, i
      end
    end
  end
end
on_one(host = self.hosts.sample, serial: true) { |host, i| ... } click to toggle source
# File lib/blower/context.rb, line 343
def on_one (host = self.hosts.sample, serial: true)
  ret = nil
  each([host], serial: serial) do |host, i|
    on host do
      singularly do
        ret = yield host, i
      end
    end
  end
  ret
end
singularly(flag = true) { || ... } click to toggle source
# File lib/blower/context.rb, line 336
def singularly (flag = true)
  was, @singular = @singular, flag
  yield
ensure
  @singular = was
end