module MCollective::Util - mcollective version 2.12.5

# module MCollective::Util

Some basic utility helper methods useful to clients, agents, runner etc.

### Public Class Methods

absolute_path?(path, separator=File::SEPARATOR, alt_separator=File::ALT_SEPARATOR) click to toggle source

we should really use Pathname#absolute? but it's not in all the ruby versions we support and it comes down to roughly this

        # File lib/mcollective/util.rb
    473 def self.absolute_path?(path, separator=File::SEPARATOR, alt_separator=File::ALT_SEPARATOR)
    474   if alt_separator
    475     path_matcher = /^([a-zA-Z]:){0,1}[#{Regexp.quote alt_separator}#{Regexp.quote separator}]/
    476   else
    477     path_matcher = /^#{Regexp.quote separator}/
    478   end
    479
    480   !!path.match(path_matcher)
    481 end

align_text(text, console_cols = nil, preamble = 5) click to toggle source

Returns an aligned_string of text relative to the size of the terminal window. If a line in the string exceeds the width of the terminal window the line will be chopped off at the whitespace chacter closest to the end of the line and prepended to the next line, keeping all indentation.

The terminal size is detected by default, but custom line widths can passed. All strings will also be left aligned with 5 whitespace characters by default.

        # File lib/mcollective/util.rb
    319 def self.align_text(text, console_cols = nil, preamble = 5)
    320   unless console_cols
    321     console_cols = terminal_dimensions[0]
    322
    323     # if unknown size we default to the typical unix default
    324     console_cols = 80 if console_cols == 0
    325   end
    326
    327   console_cols -= preamble
    328
    329   # Return unaligned text if console window is too small
    330   return text if console_cols <= 0
    331
    332   # If console is 0 this implies unknown so we assume the common
    333   # minimal unix configuration of 80 characters
    334   console_cols = 80 if console_cols <= 0
    335
    336   text = text.split("\n")
    337   piece = ''
    338   whitespace = 0
    339
    340   text.each_with_index do |line, i|
    341     whitespace = 0
    342
    343     while whitespace < line.length && line[whitespace].chr == ' '
    344       whitespace += 1
    345     end
    346
    347     # If the current line is empty, indent it so that a snippet
    348     # from the previous line is aligned correctly.
    349     if line == ""
    350       line = (" " * whitespace)
    351     end
    352
    353     # If text was snipped from the previous line, prepend it to the
    354     # current line after any current indentation.
    355     if piece != ''
    356       # Reset whitespaces to 0 if there are more whitespaces than there are
    357       # console columns
    358       whitespace = 0 if whitespace >= console_cols
    359
    360       # If the current line is empty and being prepended to, create a new
    361       # empty line in the text so that formatting is preserved.
    362       if text[i + 1] && line == (" " * whitespace)
    363         text.insert(i + 1, "")
    364       end
    365
    366       # Add the snipped text to the current line
    367       line.insert(whitespace, "#{piece} ")
    368     end
    369
    370     piece = ''
    371
    372     # Compare the line length to the allowed line length.
    373     # If it exceeds it, snip the offending text from the line
    374     # and store it so that it can be prepended to the next line.
    375     if line.length > (console_cols + preamble)
    376       reverse = console_cols
    377
    378       while line[reverse].chr != ' '
    379         reverse -= 1
    380       end
    381
    382       piece = line.slice!(reverse, (line.length - 1)).lstrip
    383     end
    384
    385     # If a snippet exists when all the columns in the text have been
    386     # updated, create a new line and append the snippet to it, using
    387     # the same left alignment as the last line in the text.
    388     if piece != '' && text[i+1].nil?
    389       text[i+1] = "#{' ' * (whitespace)}#{piece}"
    390       piece = ''
    391     end
    392
    393     # Add the preamble to the line and add it to the text
    394     line = ((' ' * preamble) + line)
    395     text[i] = line
    396   end
    397
    398   text.join("\n")
    399 end

color(code) click to toggle source

Return color codes, if the config color= option is false just return a empty string

        # File lib/mcollective/util.rb
    279 def self.color(code)
    280   colorize = Config.instance.color
    281
    282   colors = {:red => "[31m",
    283             :green => "[32m",
    284             :yellow => "[33m",
    285             :cyan => "[36m",
    286             :bold => "[1m",
    287             :reset => "[0m"}
    288
    289   if colorize
    290     return colors[code] || ""
    291   else
    292     return ""
    293   end
    294 end

colorize(code, msg) click to toggle source

Helper to return a string in specific color

        # File lib/mcollective/util.rb
    297 def self.colorize(code, msg)
    298   "%s%s%s" % [ color(code), msg, color(:reset) ]
    299 end

command_in_path?(command) click to toggle source

Checks in PATH returns true if the command is found

        # File lib/mcollective/util.rb
    426 def self.command_in_path?(command)
    427   found = ENV["PATH"].split(File::PATH_SEPARATOR).map do |p|
    428     File.exist?(File.join(p, command))
    429   end
    430
    431   found.include?(true)
    432 end

config_file_for_user() click to toggle source

Picks a config file defaults to ~/.mcollective else /etc/mcollective/client.cfg

        # File lib/mcollective/util.rb
    157 def self.config_file_for_user
    158   # the set of acceptable config files
    159   config_paths = []
    160
    161   # user dotfile
    162   begin
    163     # File.expand_path will raise if HOME isn't set, catch it
    164     user_path = File.expand_path("~/.mcollective")
    165     config_paths << user_path
    166   rescue Exception
    167   end
    168
    169   # standard locations
    170   if self.windows?
    171     config_paths << File.join(self.windows_prefix, 'etc', 'client.cfg')
    172   else
    173     config_paths << '/etc/puppetlabs/mcollective/client.cfg'
    174     config_paths << '/etc/mcollective/client.cfg'
    175   end
    176
    177   # use the first readable config file, or if none are the first listed
    178   found = config_paths.find_index { |file| File.readable?(file) } || 0
    179   return config_paths[found]
    180 end

default_options() click to toggle source

Creates a standard options hash

        # File lib/mcollective/util.rb
    183 def self.default_options
    184   {:verbose           => false,
    185    :disctimeout       => nil,
    186    :timeout           => 5,
    187    :config            => config_file_for_user,
    188    :collective        => nil,
    189    :discovery_method  => nil,
    190    :discovery_options => Config.instance.default_discovery_options,
    191    :filter            => empty_filter}
    192 end

empty_filter() click to toggle source

Creates an empty filter

        # File lib/mcollective/util.rb
    141 def self.empty_filter
    142   {"fact"     => [],
    143    "cf_class" => [],
    144    "agent"    => [],
    145    "identity" => [],
    146    "compound" => []}
    147 end

empty_filter?(filter) click to toggle source

Checks if the passed in filter is an empty one

        # File lib/mcollective/util.rb
    136 def self.empty_filter?(filter)
    137   filter == empty_filter || filter == {}
    138 end

field_number(field_size, max_size=90) click to toggle source

Calculate number of fields for printing

        # File lib/mcollective/util.rb
    519 def self.field_number(field_size, max_size=90)
    520   number = (max_size/field_size).to_i
    521   (number == 0) ? 1 : number
    522 end

field_size(elements, min_size=40) click to toggle source

Get field size for printing

        # File lib/mcollective/util.rb
    513 def self.field_size(elements, min_size=40)
    514   max_length = elements.max_by { |e| e.length }.length
    515   max_length > min_size ? max_length : min_size
    516 end

get_fact(fact) click to toggle source

Gets the value of a specific fact, mostly just a duplicate of `MCollective::Facts.get_fact` but it kind of goes with the other classes here

       # File lib/mcollective/util.rb
    61 def self.get_fact(fact)
    62   Facts.get_fact(fact)
    63 end

get_hidden_input(message='Please enter data: ') click to toggle source

        # File lib/mcollective/util.rb
    560 def self.get_hidden_input(message='Please enter data: ')
    561   unless message.nil?
    562     print message
    563   end
    564   if versioncmp(ruby_version, '1.9.3') >= 0
    565     require 'io/console'
    566     input = $stdin.noecho(&:gets)
    567   else
    568     # Use hacks to get hidden input on Ruby <1.9.3
    569     if self.windows?
    570       input = self.get_hidden_input_on_windows()
    571     else
    572       input = self.get_hidden_input_on_unix()
    573     end
    574   end
    575   input.chomp! if input
    576   input
    577 end

get_hidden_input_on_unix() click to toggle source

        # File lib/mcollective/util.rb
    545 def self.get_hidden_input_on_unix()
    546   unless $stdin.tty?
    547     raise 'Could not hook to stdin to hide input. If using SSH, try using -t flag while connecting to server.'
    548   end
    549   unless system 'stty -echo -icanon'
    550     raise 'Could not hide input using stty command.'
    551   end
    552   input = $stdin.gets
    553   ensure
    554     unless system 'stty echo icanon'
    555       raise 'Could not enable echoing of input. Try executing `stty echo icanon` to debug.'
    556     end
    557   input
    558 end

get_hidden_input_on_windows() click to toggle source

        # File lib/mcollective/util.rb
    524 def self.get_hidden_input_on_windows()
    525   require 'Win32API'
    526   # Hook into getch from crtdll. Keep reading all keys till return
    527   # or newline is hit.
    528   # If key is backspace or delete, then delete the character and update
    529   # the buffer.
    530   input = ''
    531   while char = Win32API.new("crtdll", "_getch", [ ], "I").Call do
    532     break if char == 10 || char == 13 # return or newline
    533     if char == 127 || char == 8 # backspace and delete
    534       if input.length > 0
    535         input.slice!(-1, 1)
    536       end
    537     else
    538       input << char.chr
    539     end
    540   end
    541   char = ''
    542   input
    543 end

has_agent?(agent) click to toggle source

Finds out if this `MCollective` has an agent by the name passed

If the passed name starts with a / it's assumed to be regex and will use regex to match

       # File lib/mcollective/util.rb
     8 def self.has_agent?(agent)
     9   agent = Regexp.new(agent.gsub("\/", "")) if agent.match("^/")
    10
    11   if agent.is_a?(Regexp)
    12     if Agents.agentlist.grep(agent).size > 0
    13       return true
    14     else
    15       return false
    16     end
    17   else
    18     return Agents.agentlist.include?(agent)
    19   end
    20
    21   false
    22 end

has_cf_class?(klass) click to toggle source

Checks if this node has a configuration management class by parsing the a text file with just a list of classes, recipes, roles etc. This is ala the classes.txt from puppet.

If the passed name starts with a / it's assumed to be regex and will use regex to match

       # File lib/mcollective/util.rb
    38 def self.has_cf_class?(klass)
    39   klass = Regexp.new(klass.gsub("\/", "")) if klass.match("^/")
    40   cfile = Config.instance.classesfile
    41
    42   Log.debug("Looking for configuration management classes in #{cfile}")
    43
    44   begin
    45     File.readlines(cfile).each do |k|
    46       if klass.is_a?(Regexp)
    47         return true if k.chomp.match(klass)
    48       else
    49         return true if k.chomp == klass
    50       end
    51     end
    52   rescue Exception => e
    53     Log.warn("Parsing classes file '#{cfile}' failed: #{e.class}: #{e}")
    54   end
    55
    56   false
    57 end

has_fact?(fact, value, operator) click to toggle source

Compares fact == value,

If the passed value starts with a / it's assumed to be regex and will use regex to match

       # File lib/mcollective/util.rb
    69 def self.has_fact?(fact, value, operator)
    70
    71   Log.debug("Comparing #{fact} #{operator} #{value}")
    72   Log.debug("where :fact = '#{fact}', :operator = '#{operator}', :value = '#{value}'")
    73
    74   fact = Facts[fact]
    75   return false if fact.nil?
    76
    77   fact = fact.clone
    78   case fact
    79   when Array
    80     return fact.any? { |element| test_fact_value(element, value, operator)}
    81   when Hash
    82     return fact.keys.any? { |element| test_fact_value(element, value, operator)}
    83   else
    84     return test_fact_value(fact, value, operator)
    85   end
    86 end

has_identity?(identity) click to toggle source

Checks if the configured identity matches the one supplied

If the passed name starts with a / it's assumed to be regex and will use regex to match

        # File lib/mcollective/util.rb
    123 def self.has_identity?(identity)
    124   identity = Regexp.new(identity.gsub("\/", "")) if identity.match("^/")
    125
    126   if identity.is_a?(Regexp)
    127     return Config.instance.identity.match(identity)
    128   else
    129     return true if Config.instance.identity == identity
    130   end
    131
    132   false
    133 end

loadclass(klass) click to toggle source

Wrapper around `PluginManager.loadclass`

        # File lib/mcollective/util.rb
    233 def self.loadclass(klass)
    234   PluginManager.loadclass(klass)
    235 end

make_subscriptions(agent, type, collective=nil) click to toggle source

        # File lib/mcollective/util.rb
    194 def self.make_subscriptions(agent, type, collective=nil)
    195   config = Config.instance
    196
    197   raise("Unknown target type #{type}") unless [:broadcast, :directed, :reply].include?(type)
    198
    199   if collective.nil?
    200     config.collectives.map do |c|
    201       {:agent => agent, :type => type, :collective => c}
    202     end
    203   else
    204     raise("Unknown collective '#{collective}' known collectives are '#{config.collectives.join ', '}'") unless config.collectives.include?(collective)
    205
    206     [{:agent => agent, :type => type, :collective => collective}]
    207   end
    208 end

mcollective_version() click to toggle source

        # File lib/mcollective/util.rb
    307 def self.mcollective_version
    308   MCollective::VERSION
    309 end

parse_fact_string(fact) click to toggle source

Parse a fact filter string like foo=bar into the tuple hash thats needed

        # File lib/mcollective/util.rb
    238 def self.parse_fact_string(fact)
    239   if fact =~ /^([^ ]+?)[ ]*=>[ ]*(.+)/
    240     return {:fact => $1, :value => $2, :operator => '>=' }
    241   elsif fact =~ /^([^ ]+?)[ ]*=<[ ]*(.+)/
    242     return {:fact => $1, :value => $2, :operator => '<=' }
    243   elsif fact =~ /^([^ ]+?)[ ]*(<=|>=|<|>|!=|==|=~)[ ]*(.+)/
    244     return {:fact => $1, :value => $3, :operator => $2 }
    245   elsif fact =~ /^(.+?)[ ]*=[ ]*\/(.+)\/$/
    246     return {:fact => $1, :value => "/#{$2}/", :operator => '=~' }
    247   elsif fact =~ /^([^= ]+?)[ ]*=[ ]*(.+)/
    248     return {:fact => $1, :value => $2, :operator => '==' }
    249   else
    250     raise "Could not parse fact #{fact} it does not appear to be in a valid format"
    251   end
    252 end

ruby_version() click to toggle source

Returns the current ruby version as per RUBY_VERSION, mostly doing this here to aid testing

        # File lib/mcollective/util.rb
    303 def self.ruby_version
    304   RUBY_VERSION
    305 end

setup_windows_sleeper() click to toggle source

On windows ^c can't interrupt the VM if its blocking on IO, so this sets up a dummy thread that sleeps and this will have the end result of being interruptable at least once a second. This is a common pattern found in Rails etc

       # File lib/mcollective/util.rb
    28 def self.setup_windows_sleeper
    29   Thread.new { loop { sleep 1 } } if Util.windows?
    30 end

shellescape(str) click to toggle source

Escapes a string so it's safe to use in system() or backticks

Taken from Shellwords#shellescape since it's only in a few ruby versions

        # File lib/mcollective/util.rb
    257 def self.shellescape(str)
    258   return "''" if str.empty?
    259
    260   str = str.dup
    261
    262   # Process as a single byte sequence because not all shell
    263   # implementations are multibyte aware.
    264   str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/n, "\\\\\\1")
    265
    266   # A LF cannot be escaped with a backslash because a backslash + LF
    267   # combo is regarded as line continuation and simply ignored.
    268   str.gsub!(/\n/, "'\n'")
    269
    270   return str
    271 end

str_to_bool(val) click to toggle source

Converts a string into a boolean value Strings matching 1,y,yes,true or t will return TrueClass Any other value will return FalseClass

        # File lib/mcollective/util.rb
    486 def self.str_to_bool(val)
    487   clean_val = val.to_s.strip
    488   if clean_val =~ /^(1|yes|true|y|t)$/i
    489     return  true
    490   elsif clean_val =~ /^(0|no|false|n|f)$/i
    491     return false
    492   else
    493     raise("Cannot convert string value '#{clean_val}' into a boolean.")
    494   end
    495 end

subscribe(targets) click to toggle source

Helper to subscribe to a topic on multiple collectives or just one

        # File lib/mcollective/util.rb
    211 def self.subscribe(targets)
    212   connection = PluginManager["connector_plugin"]
    213
    214   targets = [targets].flatten
    215
    216   targets.each do |target|
    217     connection.subscribe(target[:agent], target[:type], target[:collective])
    218   end
    219 end

subscribe_to_direct_addressing_queue() click to toggle source

subscribe to the direct addressing queue

        # File lib/mcollective/util.rb
    508 def self.subscribe_to_direct_addressing_queue
    509   subscribe(make_subscriptions("mcollective", :directed))
    510 end

templatepath(template_file) click to toggle source

Looks up the template directory and returns its full path

        # File lib/mcollective/util.rb
    498 def self.templatepath(template_file)
    499   config_dir = File.dirname(Config.instance.configfile)
    500   template_path = File.join(config_dir, template_file)
    501   return template_path if File.exists?(template_path)
    502
    503   template_path = File.join("/etc/mcollective", template_file)
    504   return template_path
    505 end

terminal_dimensions(stdout = STDOUT, environment = ENV) click to toggle source

Figures out the columns and lines of the current tty

Returns [0, 0] if it can't figure it out or if you're not running on a tty

        # File lib/mcollective/util.rb
    405 def self.terminal_dimensions(stdout = STDOUT, environment = ENV)
    406   return [0, 0] unless stdout.tty?
    407
    408   return [80, 40] if Util.windows?
    409
    410   if environment["COLUMNS"] && environment["LINES"]
    411     return [environment["COLUMNS"].to_i, environment["LINES"].to_i]
    412
    413   elsif environment["TERM"] && command_in_path?("tput")
    414     return [`tput cols`.to_i, `tput lines`.to_i]
    415
    416   elsif command_in_path?('stty')
    417     return `stty size`.scan(/\d+/).map {|s| s.to_i }
    418   else
    419     return [0, 0]
    420   end
    421 rescue
    422   [0, 0]
    423 end

unsubscribe(targets) click to toggle source

Helper to unsubscribe to a topic on multiple collectives or just one

        # File lib/mcollective/util.rb
    222 def self.unsubscribe(targets)
    223   connection = PluginManager["connector_plugin"]
    224
    225   targets = [targets].flatten
    226
    227   targets.each do |target|
    228     connection.unsubscribe(target[:agent], target[:type], target[:collective])
    229   end
    230 end

versioncmp(version_a, version_b) click to toggle source

compare two software versions as commonly found in package versions.

returns 0 if a == b returns -1 if a < b returns 1 if a > b

Code originally from `Puppet`

        # File lib/mcollective/util.rb
    442 def self.versioncmp(version_a, version_b)
    443   vre = /[-.]|\d+|[^-.\d]+/
    444   ax = version_a.scan(vre)
    445   bx = version_b.scan(vre)
    446
    447   while (ax.length>0 && bx.length>0)
    448     a = ax.shift
    449     b = bx.shift
    450
    451     if( a == b )                 then next
    452     elsif (a == '-' && b == '-') then next
    453     elsif (a == '-')             then return -1
    454     elsif (b == '-')             then return 1
    455     elsif (a == '.' && b == '.') then next
    456     elsif (a == '.' )            then return -1
    457     elsif (b == '.' )            then return 1
    458     elsif (a =~ /^\d+$/ && b =~ /^\d+$/) then
    459       if( a =~ /^0/ or b =~ /^0/ ) then
    460         return a.to_s.upcase <=> b.to_s.upcase
    461       end
    462       return a.to_i <=> b.to_i
    463     else
    464       return a.upcase <=> b.upcase
    465     end
    466   end
    467
    468   version_a <=> version_b;
    469 end

windows?() click to toggle source

        # File lib/mcollective/util.rb
    273 def self.windows?
    274   !!(RbConfig::CONFIG['host_os'] =~ /mswin|win32|dos|mingw|cygwin/i)
    275 end

windows_prefix() click to toggle source

Returns the PuppetLabs mcollective path for windows

        # File lib/mcollective/util.rb
    150 def self.windows_prefix
    151   require 'win32/dir'
    152   prefix = File.join(Dir::COMMON_APPDATA, "PuppetLabs", "mcollective")
    153 end