# module MCollective::Util

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

### Public Class Methods

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, line 308
    def self.align_text(text, console_cols = nil, preamble = 5)
      unless console_cols
        console_cols = terminal_dimensions[0]

        # if unknown size we default to the typical unix default
        console_cols = 80 if console_cols == 0
      end

      console_cols -= preamble

      # Return unaligned text if console window is too small
      return text if console_cols <= 0

      # If console is 0 this implies unknown so we assume the common
      # minimal unix configuration of 80 characters
      console_cols = 80 if console_cols <= 0

      text = text.split("\n")
      piece = ''
      whitespace = 0

      text.each_with_index do |line, i|
        whitespace = 0

        while whitespace < line.length && line[whitespace].chr == ' '
          whitespace += 1
        end

        # If the current line is empty, indent it so that a snippet
        # from the previous line is aligned correctly.
        if line == ""
          line = (" " * whitespace)
        end

        # If text was snipped from the previous line, prepend it to the
        # current line after any current indentation.
        if piece != ''
          # Reset whitespaces to 0 if there are more whitespaces than there are
          # console columns
          whitespace = 0 if whitespace >= console_cols

          # If the current line is empty and being prepended to, create a new
          # empty line in the text so that formatting is preserved.
          if text[i + 1] && line == (" " * whitespace)
            text.insert(i + 1, "")
          end

          # Add the snipped text to the current line
          line.insert(whitespace, "#{piece} ")
        end

        piece = ''

        # Compare the line length to the allowed line length.
        # If it exceeds it, snip the offending text from the line
        # and store it so that it can be prepended to the next line.
        if line.length > (console_cols + preamble)
          reverse = console_cols

          while line[reverse].chr != ' '
            reverse -= 1
          end

          piece = line.slice!(reverse, (line.length - 1)).lstrip
        end

        # If a snippet exists when all the columns in the text have been
        # updated, create a new line and append the snippet to it, using
        # the same left alignment as the last line in the text.
        if piece != '' && text[i+1].nil?
          text[i+1] = "#{' ' * (whitespace)}#{piece}"
          piece = ''
        end

        # Add the preamble to the line and add it to the text
        line = ((' ' * preamble) + line)
        text[i] = line
      end

      text.join("\n")
    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, line 268
    def self.color(code)
      colorize = Config.instance.color

      colors = {:red => "[31m",
                :green => "[32m",
                :yellow => "[33m",
                :cyan => "[36m",
                :bold => "[1m",
                :reset => "[0m"}

      if colorize
        return colors[code] || ""
      else
        return ""
      end
    end

colorize(code, msg) click to toggle source

Helper to return a string in specific color

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

command_in_path?(command) click to toggle source

Checks in PATH returns true if the command is found

    # File lib/mcollective/util.rb, line 415
    def self.command_in_path?(command)
      found = ENV["PATH"].split(File::PATH_SEPARATOR).map do |p|
        File.exist?(File.join(p, command))
      end

      found.include?(true)
    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, line 146
    def self.config_file_for_user
      # expand_path is pretty lame, it relies on HOME environment
      # which isnt't always there so just handling all exceptions
      # here as cant find reverting to default
      begin
        config = File.expand_path("~/.mcollective")

        unless File.readable?(config) && File.file?(config)
          if self.windows?
            config = File.join(self.windows_prefix, "etc", "client.cfg")
          else
            config = "/etc/mcollective/client.cfg"
          end
        end
      rescue Exception => e
        if self.windows?
          config = File.join(self.windows_prefix, "etc", "client.cfg")
        else
          config = "/etc/mcollective/client.cfg"
        end
      end

      return config
    end

default_options() click to toggle source

Creates a standard options hash

    # File lib/mcollective/util.rb, line 172
    def self.default_options
      {:verbose           => false,
       :disctimeout       => nil,
       :timeout           => 5,
       :config            => config_file_for_user,
       :collective        => nil,
       :discovery_method  => nil,
       :discovery_options => Config.instance.default_discovery_options,
       :filter            => empty_filter}
    end

empty_filter() click to toggle source

Creates an empty filter

    # File lib/mcollective/util.rb, line 130
    def self.empty_filter
      {"fact"     => [],
       "cf_class" => [],
       "agent"    => [],
       "identity" => [],
       "compound" => []}
    end

empty_filter?(filter) click to toggle source

Checks if the passed in filter is an empty one

    # File lib/mcollective/util.rb, line 125
    def self.empty_filter?(filter)
      filter == empty_filter || filter == {}
    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, line 61
    def self.get_fact(fact)
      Facts.get_fact(fact)
    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, line 8
    def self.has_agent?(agent)
      agent = Regexp.new(agent.gsub("\/", "")) if agent.match("^/")

      if agent.is_a?(Regexp)
        if Agents.agentlist.grep(agent).size > 0
          return true
        else
          return false
        end
      else
        return Agents.agentlist.include?(agent)
      end

      false
    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, line 38
    def self.has_cf_class?(klass)
      klass = Regexp.new(klass.gsub("\/", "")) if klass.match("^/")
      cfile = Config.instance.classesfile

      Log.debug("Looking for configuration management classes in #{cfile}")

      begin
        File.readlines(cfile).each do |k|
          if klass.is_a?(Regexp)
            return true if k.chomp.match(klass)
          else
            return true if k.chomp == klass
          end
        end
      rescue Exception => e
        Log.warn("Parsing classes file '#{cfile}' failed: #{e.class}: #{e}")
      end

      false
    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, line 69
    def self.has_fact?(fact, value, operator)

      Log.debug("Comparing #{fact} #{operator} #{value}")
      Log.debug("where :fact = '#{fact}', :operator = '#{operator}', :value = '#{value}'")

      fact = Facts[fact]
      return false if fact.nil?

      fact = fact.clone

      if operator == '=~'
        # to maintain backward compat we send the value
        # as /.../ which is what 1.0.x needed.  this strips
        # off the /'s wich is what we need here
        if value =~ /^\/(.+)\/$/
          value = $1
        end

        return true if fact.match(Regexp.new(value))

      elsif operator == "=="
        return true if fact == value

      elsif ['<=', '>=', '<', '>', '!='].include?(operator)
        # Yuk - need to type cast, but to_i and to_f are overzealous
        if value =~ /^[0-9]+$/ && fact =~ /^[0-9]+$/
          fact = Integer(fact)
          value = Integer(value)
        elsif value =~ /^[0-9]+.[0-9]+$/ && fact =~ /^[0-9]+.[0-9]+$/
          fact = Float(fact)
          value = Float(value)
        end

        return true if eval("fact #{operator} value")
      end

      false
    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, line 112
    def self.has_identity?(identity)
      identity = Regexp.new(identity.gsub("\/", "")) if identity.match("^/")

      if identity.is_a?(Regexp)
        return Config.instance.identity.match(identity)
      else
        return true if Config.instance.identity == identity
      end

      false
    end

loadclass(klass) click to toggle source

Wrapper around MCollective::PluginManager.loadclass

    # File lib/mcollective/util.rb, line 222
    def self.loadclass(klass)
      PluginManager.loadclass(klass)
    end

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

    # File lib/mcollective/util.rb, line 183
    def self.make_subscriptions(agent, type, collective=nil)
      config = Config.instance

      raise("Unknown target type #{type}") unless [:broadcast, :directed, :reply].include?(type)

      if collective.nil?
        config.collectives.map do |c|
          {:agent => agent, :type => type, :collective => c}
        end
      else
        raise("Unknown collective '#{collective}' known collectives are '#{config.collectives.join ', '}'") unless config.collectives.include?(collective)

        [{:agent => agent, :type => type, :collective => collective}]
      end
    end

mcollective_version() click to toggle source

    # File lib/mcollective/util.rb, line 296
    def self.mcollective_version
      MCollective::VERSION
    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, line 227
    def self.parse_fact_string(fact)
      if fact =~ /^([^ ]+?)[ ]*=>[ ]*(.+)/
        return {:fact => $1, :value => $2, :operator => '>=' }
      elsif fact =~ /^([^ ]+?)[ ]*=<[ ]*(.+)/
        return {:fact => $1, :value => $2, :operator => '<=' }
      elsif fact =~ /^([^ ]+?)[ ]*(<=|>=|<|>|!=|==|=~)[ ]*(.+)/
        return {:fact => $1, :value => $3, :operator => $2 }
      elsif fact =~ /^(.+?)[ ]*=[ ]*\/(.+)\/$/
        return {:fact => $1, :value => "/#{$2}/", :operator => '=~' }
      elsif fact =~ /^([^= ]+?)[ ]*=[ ]*(.+)/
        return {:fact => $1, :value => $2, :operator => '==' }
      else
        raise "Could not parse fact #{fact} it does not appear to be in a valid format"
      end
    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, line 292
    def self.ruby_version
      RUBY_VERSION
    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, line 28
    def self.setup_windows_sleeper
      Thread.new { loop { sleep 1 } } if Util.windows?
    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, line 246
    def self.shellescape(str)
      return "''" if str.empty?

      str = str.dup

      # Process as a single byte sequence because not all shell
      # implementations are multibyte aware.
      str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/, "\\\\\\1")

      # A LF cannot be escaped with a backslash because a backslash + LF
      # combo is regarded as line continuation and simply ignored.
      str.gsub!(/\n/, "'\n'")

      return str
    end

subscribe(targets) click to toggle source

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

    # File lib/mcollective/util.rb, line 200
    def self.subscribe(targets)
      connection = PluginManager["connector_plugin"]

      targets = [targets].flatten

      targets.each do |target|
        connection.subscribe(target[:agent], target[:type], target[:collective])
      end
    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, line 394
    def self.terminal_dimensions(stdout = STDOUT, environment = ENV)
      return [0, 0] unless stdout.tty?

      return [80, 40] if Util.windows?

      if environment["COLUMNS"] && environment["LINES"]
        return [environment["COLUMNS"].to_i, environment["LINES"].to_i]

      elsif environment["TERM"] && command_in_path?("tput")
        return [%xtput cols`.to_i, %xtput lines`.to_i]

      elsif command_in_path?('stty')
        return %xstty size`.scan(/\d+/).map {|s| s.to_i }
      else
        return [0, 0]
      end
    rescue
      [0, 0]
    end

unsubscribe(targets) click to toggle source

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

    # File lib/mcollective/util.rb, line 211
    def self.unsubscribe(targets)
      connection = PluginManager["connector_plugin"]

      targets = [targets].flatten

      targets.each do |target|
        connection.unsubscribe(target[:agent], target[:type], target[:collective])
      end
    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, line 431
    def self.versioncmp(version_a, version_b)
      vre = /[-.]|\d+|[^-.\d]+/
        ax = version_a.scan(vre)
      bx = version_b.scan(vre)

      while (ax.length>0 && bx.length>0)
        a = ax.shift
        b = bx.shift

        if( a == b )                 then next
        elsif (a == '-' && b == '-') then next
        elsif (a == '-')             then return -1
        elsif (b == '-')             then return 1
        elsif (a == '.' && b == '.') then next
        elsif (a == '.' )            then return -1
        elsif (b == '.' )            then return 1
        elsif (a =~ /^\d+$/ && b =~ /^\d+$/) then
          if( a =~ /^0/ or b =~ /^0/ ) then
            return a.to_s.upcase <=> b.to_s.upcase
          end
          return a.to_i <=> b.to_i
        else
          return a.upcase <=> b.upcase
        end
      end

      version_a <=> version_b;
    end

windows?() click to toggle source

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

windows_prefix() click to toggle source

Returns the PuppetLabs mcollective path for windows

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