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 464 464: def self.absolute_path?(path, separator=File::SEPARATOR, alt_separator=File::ALT_SEPARATOR) 465: if alt_separator 466: path_matcher = /^([a-zA-Z]:){0,1}[#{Regexp.quote alt_separator}#{Regexp.quote separator}]/ 467: else 468: path_matcher = /^#{Regexp.quote separator}/ 469: end 470: 471: !!path.match(path_matcher) 472: 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 310 310: def self.align_text(text, console_cols = nil, preamble = 5) 311: unless console_cols 312: console_cols = terminal_dimensions[0] 313: 314: # if unknown size we default to the typical unix default 315: console_cols = 80 if console_cols == 0 316: end 317: 318: console_cols -= preamble 319: 320: # Return unaligned text if console window is too small 321: return text if console_cols <= 0 322: 323: # If console is 0 this implies unknown so we assume the common 324: # minimal unix configuration of 80 characters 325: console_cols = 80 if console_cols <= 0 326: 327: text = text.split("\n") 328: piece = '' 329: whitespace = 0 330: 331: text.each_with_index do |line, i| 332: whitespace = 0 333: 334: while whitespace < line.length && line[whitespace].chr == ' ' 335: whitespace += 1 336: end 337: 338: # If the current line is empty, indent it so that a snippet 339: # from the previous line is aligned correctly. 340: if line == "" 341: line = (" " * whitespace) 342: end 343: 344: # If text was snipped from the previous line, prepend it to the 345: # current line after any current indentation. 346: if piece != '' 347: # Reset whitespaces to 0 if there are more whitespaces than there are 348: # console columns 349: whitespace = 0 if whitespace >= console_cols 350: 351: # If the current line is empty and being prepended to, create a new 352: # empty line in the text so that formatting is preserved. 353: if text[i + 1] && line == (" " * whitespace) 354: text.insert(i + 1, "") 355: end 356: 357: # Add the snipped text to the current line 358: line.insert(whitespace, "#{piece} ") 359: end 360: 361: piece = '' 362: 363: # Compare the line length to the allowed line length. 364: # If it exceeds it, snip the offending text from the line 365: # and store it so that it can be prepended to the next line. 366: if line.length > (console_cols + preamble) 367: reverse = console_cols 368: 369: while line[reverse].chr != ' ' 370: reverse -= 1 371: end 372: 373: piece = line.slice!(reverse, (line.length - 1)).lstrip 374: end 375: 376: # If a snippet exists when all the columns in the text have been 377: # updated, create a new line and append the snippet to it, using 378: # the same left alignment as the last line in the text. 379: if piece != '' && text[i+1].nil? 380: text[i+1] = "#{' ' * (whitespace)}#{piece}" 381: piece = '' 382: end 383: 384: # Add the preamble to the line and add it to the text 385: line = ((' ' * preamble) + line) 386: text[i] = line 387: end 388: 389: text.join("\n") 390: end
Return color codes, if the config color= option is false just return a empty string
# File lib/mcollective/util.rb, line 270 270: def self.color(code) 271: colorize = Config.instance.color 272: 273: colors = {:red => "[31m", 274: :green => "[32m", 275: :yellow => "[33m", 276: :cyan => "[36m", 277: :bold => "[1m", 278: :reset => "[0m"} 279: 280: if colorize 281: return colors[code] || "" 282: else 283: return "" 284: end 285: end
Helper to return a string in specific color
# File lib/mcollective/util.rb, line 288 288: def self.colorize(code, msg) 289: "%s%s%s" % [ color(code), msg, color(:reset) ] 290: end
Checks in PATH returns true if the command is found
# File lib/mcollective/util.rb, line 417 417: def self.command_in_path?(command) 418: found = ENV["PATH"].split(File::PATH_SEPARATOR).map do |p| 419: File.exist?(File.join(p, command)) 420: end 421: 422: found.include?(true) 423: end
Picks a config file defaults to ~/.mcollective else /etc/mcollective/client.cfg
# File lib/mcollective/util.rb, line 148 148: def self.config_file_for_user 149: # expand_path is pretty lame, it relies on HOME environment 150: # which isnt't always there so just handling all exceptions 151: # here as cant find reverting to default 152: begin 153: config = File.expand_path("~/.mcollective") 154: 155: unless File.readable?(config) && File.file?(config) 156: if self.windows? 157: config = File.join(self.windows_prefix, "etc", "client.cfg") 158: else 159: config = "/etc/mcollective/client.cfg" 160: end 161: end 162: rescue Exception => e 163: if self.windows? 164: config = File.join(self.windows_prefix, "etc", "client.cfg") 165: else 166: config = "/etc/mcollective/client.cfg" 167: end 168: end 169: 170: return config 171: end
Creates a standard options hash
# File lib/mcollective/util.rb, line 174 174: def self.default_options 175: {:verbose => false, 176: :disctimeout => nil, 177: :timeout => 5, 178: :config => config_file_for_user, 179: :collective => nil, 180: :discovery_method => nil, 181: :discovery_options => Config.instance.default_discovery_options, 182: :filter => empty_filter} 183: end
Creates an empty filter
# File lib/mcollective/util.rb, line 132 132: def self.empty_filter 133: {"fact" => [], 134: "cf_class" => [], 135: "agent" => [], 136: "identity" => [], 137: "compound" => []} 138: end
Checks if the passed in filter is an empty one
# File lib/mcollective/util.rb, line 127 127: def self.empty_filter?(filter) 128: filter == empty_filter || filter == {} 129: 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 63 63: def self.get_fact(fact) 64: Facts.get_fact(fact) 65: 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 10 10: def self.has_agent?(agent) 11: agent = Regexp.new(agent.gsub("\/", "")) if agent.match("^/") 12: 13: if agent.is_a?(Regexp) 14: if Agents.agentlist.grep(agent).size > 0 15: return true 16: else 17: return false 18: end 19: else 20: return Agents.agentlist.include?(agent) 21: end 22: 23: false 24: 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 40 40: def self.has_cf_class?(klass) 41: klass = Regexp.new(klass.gsub("\/", "")) if klass.match("^/") 42: cfile = Config.instance.classesfile 43: 44: Log.debug("Looking for configuration management classes in #{cfile}") 45: 46: begin 47: File.readlines(cfile).each do |k| 48: if klass.is_a?(Regexp) 49: return true if k.chomp.match(klass) 50: else 51: return true if k.chomp == klass 52: end 53: end 54: rescue Exception => e 55: Log.warn("Parsing classes file '#{cfile}' failed: #{e.class}: #{e}") 56: end 57: 58: false 59: 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 71 71: def self.has_fact?(fact, value, operator) 72: 73: Log.debug("Comparing #{fact} #{operator} #{value}") 74: Log.debug("where :fact = '#{fact}', :operator = '#{operator}', :value = '#{value}'") 75: 76: fact = Facts[fact] 77: return false if fact.nil? 78: 79: fact = fact.clone 80: 81: if operator == '=~' 82: # to maintain backward compat we send the value 83: # as /.../ which is what 1.0.x needed. this strips 84: # off the /'s wich is what we need here 85: if value =~ /^\/(.+)\/$/ 86: value = $1 87: end 88: 89: return true if fact.match(Regexp.new(value)) 90: 91: elsif operator == "==" 92: return true if fact == value 93: 94: elsif ['<=', '>=', '<', '>', '!='].include?(operator) 95: # Yuk - need to type cast, but to_i and to_f are overzealous 96: if value =~ /^[0-9]+$/ && fact =~ /^[0-9]+$/ 97: fact = Integer(fact) 98: value = Integer(value) 99: elsif value =~ /^[0-9]+.[0-9]+$/ && fact =~ /^[0-9]+.[0-9]+$/ 100: fact = Float(fact) 101: value = Float(value) 102: end 103: 104: return true if eval("fact #{operator} value") 105: end 106: 107: false 108: 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 114 114: def self.has_identity?(identity) 115: identity = Regexp.new(identity.gsub("\/", "")) if identity.match("^/") 116: 117: if identity.is_a?(Regexp) 118: return Config.instance.identity.match(identity) 119: else 120: return true if Config.instance.identity == identity 121: end 122: 123: false 124: end
Wrapper around PluginManager.loadclass
# File lib/mcollective/util.rb, line 224 224: def self.loadclass(klass) 225: PluginManager.loadclass(klass) 226: end
(Not documented)
# File lib/mcollective/util.rb, line 185 185: def self.make_subscriptions(agent, type, collective=nil) 186: config = Config.instance 187: 188: raise("Unknown target type #{type}") unless [:broadcast, :directed, :reply].include?(type) 189: 190: if collective.nil? 191: config.collectives.map do |c| 192: {:agent => agent, :type => type, :collective => c} 193: end 194: else 195: raise("Unknown collective '#{collective}' known collectives are '#{config.collectives.join ', '}'") unless config.collectives.include?(collective) 196: 197: [{:agent => agent, :type => type, :collective => collective}] 198: end 199: end
(Not documented)
# File lib/mcollective/util.rb, line 298 298: def self.mcollective_version 299: MCollective::VERSION 300: end
Parse a fact filter string like foo=bar into the tuple hash thats needed
# File lib/mcollective/util.rb, line 229 229: def self.parse_fact_string(fact) 230: if fact =~ /^([^ ]+?)[ ]*=>[ ]*(.+)/ 231: return {:fact => $1, :value => $2, :operator => '>=' } 232: elsif fact =~ /^([^ ]+?)[ ]*=<[ ]*(.+)/ 233: return {:fact => $1, :value => $2, :operator => '<=' } 234: elsif fact =~ /^([^ ]+?)[ ]*(<=|>=|<|>|!=|==|=~)[ ]*(.+)/ 235: return {:fact => $1, :value => $3, :operator => $2 } 236: elsif fact =~ /^(.+?)[ ]*=[ ]*\/(.+)\/$/ 237: return {:fact => $1, :value => "/#{$2}/", :operator => '=~' } 238: elsif fact =~ /^([^= ]+?)[ ]*=[ ]*(.+)/ 239: return {:fact => $1, :value => $2, :operator => '==' } 240: else 241: raise "Could not parse fact #{fact} it does not appear to be in a valid format" 242: end 243: end
Returns the current ruby version as per RUBY_VERSION, mostly doing this here to aid testing
# File lib/mcollective/util.rb, line 294 294: def self.ruby_version 295: RUBY_VERSION 296: 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 30 30: def self.setup_windows_sleeper 31: Thread.new { loop { sleep 1 } } if Util.windows? 32: 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 248 248: def self.shellescape(str) 249: return "''" if str.empty? 250: 251: str = str.dup 252: 253: # Process as a single byte sequence because not all shell 254: # implementations are multibyte aware. 255: str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/n, "\\\\\\1") 256: 257: # A LF cannot be escaped with a backslash because a backslash + LF 258: # combo is regarded as line continuation and simply ignored. 259: str.gsub!(/\n/, "'\n'") 260: 261: return str 262: 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 477 477: def self.str_to_bool(val) 478: clean_val = val.to_s.strip 479: if clean_val =~ /^(1|yes|true|y|t)$/i 480: return true 481: elsif clean_val =~ /^(0|no|false|n|f)$/i 482: return false 483: else 484: raise_code(:PLMC42, "Cannot convert string value '%{value}' into a boolean.", :error, :value => clean_val) 485: end 486: end
Helper to subscribe to a topic on multiple collectives or just one
# File lib/mcollective/util.rb, line 202 202: def self.subscribe(targets) 203: connection = PluginManager["connector_plugin"] 204: 205: targets = [targets].flatten 206: 207: targets.each do |target| 208: connection.subscribe(target[:agent], target[:type], target[:collective]) 209: end 210: end
Looks up and interprolate the hash values into a i18n string
# File lib/mcollective/util.rb, line 489 489: def self.t(msgid, args={}) 490: if msgid.is_a?(Symbol) 491: I18n.t("%s.pattern" % msgid, args) 492: else 493: I18n.t(msgid, args) 494: end 495: end
Looks up the template directory and returns its full path
# File lib/mcollective/util.rb, line 498 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
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 396 396: def self.terminal_dimensions(stdout = STDOUT, environment = ENV) 397: return [0, 0] unless stdout.tty? 398: 399: return [80, 40] if Util.windows? 400: 401: if environment["COLUMNS"] && environment["LINES"] 402: return [environment["COLUMNS"].to_i, environment["LINES"].to_i] 403: 404: elsif environment["TERM"] && command_in_path?("tput") 405: return [`tput cols`.to_i, `tput lines`.to_i] 406: 407: elsif command_in_path?('stty') 408: return `stty size`.scan(/\d+/).map {|s| s.to_i } 409: else 410: return [0, 0] 411: end 412: rescue 413: [0, 0] 414: end
Helper to unsubscribe to a topic on multiple collectives or just one
# File lib/mcollective/util.rb, line 213 213: def self.unsubscribe(targets) 214: connection = PluginManager["connector_plugin"] 215: 216: targets = [targets].flatten 217: 218: targets.each do |target| 219: connection.unsubscribe(target[:agent], target[:type], target[:collective]) 220: end 221: 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 433 433: def self.versioncmp(version_a, version_b) 434: vre = /[-.]|\d+|[^-.\d]+/ 435: ax = version_a.scan(vre) 436: bx = version_b.scan(vre) 437: 438: while (ax.length>0 && bx.length>0) 439: a = ax.shift 440: b = bx.shift 441: 442: if( a == b ) then next 443: elsif (a == '-' && b == '-') then next 444: elsif (a == '-') then return -1 445: elsif (b == '-') then return 1 446: elsif (a == '.' && b == '.') then next 447: elsif (a == '.' ) then return -1 448: elsif (b == '.' ) then return 1 449: elsif (a =~ /^\d+$/ && b =~ /^\d+$/) then 450: if( a =~ /^0/ or b =~ /^0/ ) then 451: return a.to_s.upcase <=> b.to_s.upcase 452: end 453: return a.to_i <=> b.to_i 454: else 455: return a.upcase <=> b.upcase 456: end 457: end 458: 459: version_a <=> version_b; 460: end
Disabled; run with --debug to generate this.
Generated with the Darkfish Rdoc Generator 1.1.6.