Some basic utility helper methods useful to clients, agents, runner etc.
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, line 473 def self.absolute_path?(path, separator=File::SEPARATOR, alt_separator=File::ALT_SEPARATOR) if alt_separator path_matcher = /^([a-zA-Z]:){0,1}[#{Regexp.quote alt_separator}#{Regexp.quote separator}]/ else path_matcher = /^#{Regexp.quote separator}/ end !!path.match(path_matcher) end
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 319 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
Return color codes, if the config color= option is false just return a empty string
# File lib/mcollective/util.rb, line 279 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
Helper to return a string in specific color
# File lib/mcollective/util.rb, line 297 def self.colorize(code, msg) "%s%s%s" % [ color(code), msg, color(:reset) ] end
Checks in PATH returns true if the command is found
# File lib/mcollective/util.rb, line 426 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
Picks a config file defaults to ~/.mcollective else /etc/mcollective/client.cfg
# File lib/mcollective/util.rb, line 157 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
Creates a standard options hash
# File lib/mcollective/util.rb, line 183 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
Creates an empty filter
# File lib/mcollective/util.rb, line 141 def self.empty_filter {"fact" => [], "cf_class" => [], "agent" => [], "identity" => [], "compound" => []} end
Checks if the passed in filter is an empty one
# File lib/mcollective/util.rb, line 136 def self.empty_filter?(filter) filter == empty_filter || filter == {} end
Calculate number of fields for printing
# File lib/mcollective/util.rb, line 519 def self.field_number(field_size, max_size=90) number = (max_size/field_size).to_i (number == 0) ? 1 : number end
Get field size for printing
# File lib/mcollective/util.rb, line 513 def self.field_size(elements, min_size=40) max_length = elements.max_by { |e| e.length }.length max_length > min_size ? max_length : min_size end
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
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
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
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 case fact when Array return fact.any? { |element| test_fact_value(element, value, operator)} when Hash return fact.keys.any? { |element| test_fact_value(element, value, operator)} else return test_fact_value(fact, value, operator) end end
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 123 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
Wrapper around MCollective::PluginManager.loadclass
# File lib/mcollective/util.rb, line 233 def self.loadclass(klass) PluginManager.loadclass(klass) end
# File lib/mcollective/util.rb, line 194 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
# File lib/mcollective/util.rb, line 307 def self.mcollective_version MCollective::VERSION end
Parse a fact filter string like foo=bar into the tuple hash thats needed
# File lib/mcollective/util.rb, line 238 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
Returns the current ruby version as per RUBY_VERSION, mostly doing this here to aid testing
# File lib/mcollective/util.rb, line 303 def self.ruby_version RUBY_VERSION end
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
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 257 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
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, line 486 def self.str_to_bool(val) clean_val = val.to_s.strip if clean_val =~ /^(1|yes|true|y|t)$/ return true elsif clean_val =~ /^(0|no|false|n|f)$/ return false else raise("Cannot convert string value '#{clean_val}' into a boolean.") end end
Helper to subscribe to a topic on multiple collectives or just one
# File lib/mcollective/util.rb, line 211 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
subscribe to the direct addressing queue
# File lib/mcollective/util.rb, line 508 def self.subscribe_to_direct_addressing_queue subscribe(make_subscriptions("mcollective", :directed)) end
Looks up the template directory and returns its full path
# File lib/mcollective/util.rb, line 498 def self.templatepath(template_file) config_dir = File.dirname(Config.instance.configfile) template_path = File.join(config_dir, template_file) return template_path if File.exists?(template_path) template_path = File.join("/etc/mcollective", template_file) return template_path end
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 405 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
Helper to unsubscribe to a topic on multiple collectives or just one
# File lib/mcollective/util.rb, line 222 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
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 442 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
# File lib/mcollective/util.rb, line 273 def self.windows? !!(RbConfig::CONFIG['host_os'] =~ /mswin|win32|dos|mingw|cygwin/) end
Returns the PuppetLabs mcollective path for windows
# File lib/mcollective/util.rb, line 150 def self.windows_prefix require 'win32/dir' prefix = File.join(Dir::COMMON_APPDATA, "PuppetLabs", "mcollective") end