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
The failed hosts.
The file name of the currently running task. Context#cp
interprets relative file names relative to this file name's directory component.
The target hosts.
Search path for tasks.
Username override. If not-nil, this user is used for all remote accesses.
Public Class Methods
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
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
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
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
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
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 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
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
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
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
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
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
# 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
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
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
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
# 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
# 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
# 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
# File lib/blower/context.rb, line 365 def locally (&block) on Local.new("<local>") do singularly &block end end
# 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
# 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
# File lib/blower/context.rb, line 336 def singularly (flag = true) was, @singular = @singular, flag yield ensure @singular = was end