Module | MCollective::Util |
In: |
lib/mcollective/util.rb
|
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 447 447: def self.absolute_path?(path, separator=File::SEPARATOR, alt_separator=File::ALT_SEPARATOR) 448: if alt_separator 449: path_matcher = /^[#{Regexp.quote alt_separator}#{Regexp.quote separator}]/ 450: else 451: path_matcher = /^#{Regexp.quote separator}/ 452: end 453: 454: !!path.match(path_matcher) 455: 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 294 294: def self.align_text(text, console_cols = nil, preamble = 5) 295: unless console_cols 296: console_cols = terminal_dimensions[0] 297: 298: # if unknown size we default to the typical unix default 299: console_cols = 80 if console_cols == 0 300: end 301: 302: console_cols -= preamble 303: 304: # Return unaligned text if console window is too small 305: return text if console_cols <= 0 306: 307: # If console is 0 this implies unknown so we assume the common 308: # minimal unix configuration of 80 characters 309: console_cols = 80 if console_cols <= 0 310: 311: text = text.split("\n") 312: piece = '' 313: whitespace = 0 314: 315: text.each_with_index do |line, i| 316: whitespace = 0 317: 318: while whitespace < line.length && line[whitespace].chr == ' ' 319: whitespace += 1 320: end 321: 322: # If the current line is empty, indent it so that a snippet 323: # from the previous line is aligned correctly. 324: if line == "" 325: line = (" " * whitespace) 326: end 327: 328: # If text was snipped from the previous line, prepend it to the 329: # current line after any current indentation. 330: if piece != '' 331: # Reset whitespaces to 0 if there are more whitespaces than there are 332: # console columns 333: whitespace = 0 if whitespace >= console_cols 334: 335: # If the current line is empty and being prepended to, create a new 336: # empty line in the text so that formatting is preserved. 337: if text[i + 1] && line == (" " * whitespace) 338: text.insert(i + 1, "") 339: end 340: 341: # Add the snipped text to the current line 342: line.insert(whitespace, "#{piece} ") 343: end 344: 345: piece = '' 346: 347: # Compare the line length to the allowed line length. 348: # If it exceeds it, snip the offending text from the line 349: # and store it so that it can be prepended to the next line. 350: if line.length > (console_cols + preamble) 351: reverse = console_cols 352: 353: while line[reverse].chr != ' ' 354: reverse -= 1 355: end 356: 357: piece = line.slice!(reverse, (line.length - 1)).lstrip 358: end 359: 360: # If a snippet exists when all the columns in the text have been 361: # updated, create a new line and append the snippet to it, using 362: # the same left alignment as the last line in the text. 363: if piece != '' && text[i+1].nil? 364: text[i+1] = "#{' ' * (whitespace)}#{piece}" 365: piece = '' 366: end 367: 368: # Add the preamble to the line and add it to the text 369: line = ((' ' * preamble) + line) 370: text[i] = line 371: end 372: 373: text.join("\n") 374: end
Return color codes, if the config color= option is false just return a empty string
# File lib/mcollective/util.rb, line 254 254: def self.color(code) 255: colorize = Config.instance.color 256: 257: colors = {:red => "[31m", 258: :green => "[32m", 259: :yellow => "[33m", 260: :cyan => "[36m", 261: :bold => "[1m", 262: :reset => "[0m"} 263: 264: if colorize 265: return colors[code] || "" 266: else 267: return "" 268: end 269: end
Checks in PATH returns true if the command is found
# File lib/mcollective/util.rb, line 401 401: def self.command_in_path?(command) 402: found = ENV["PATH"].split(File::PATH_SEPARATOR).map do |p| 403: File.exist?(File.join(p, command)) 404: end 405: 406: found.include?(true) 407: end
Picks a config file defaults to ~/.mcollective else /etc/mcollective/client.cfg
# File lib/mcollective/util.rb, line 140 140: def self.config_file_for_user 141: # expand_path is pretty lame, it relies on HOME environment 142: # which isnt't always there so just handling all exceptions 143: # here as cant find reverting to default 144: begin 145: config = File.expand_path("~/.mcollective") 146: 147: unless File.readable?(config) && File.file?(config) 148: config = "/etc/mcollective/client.cfg" 149: end 150: rescue Exception => e 151: config = "/etc/mcollective/client.cfg" 152: end 153: 154: return config 155: end
Creates a standard options hash
# File lib/mcollective/util.rb, line 158 158: def self.default_options 159: {:verbose => false, 160: :disctimeout => nil, 161: :timeout => 5, 162: :config => config_file_for_user, 163: :collective => nil, 164: :discovery_method => nil, 165: :discovery_options => Config.instance.default_discovery_options, 166: :filter => empty_filter} 167: end
Creates an empty filter
# File lib/mcollective/util.rb, line 130 130: def self.empty_filter 131: {"fact" => [], 132: "cf_class" => [], 133: "agent" => [], 134: "identity" => [], 135: "compound" => []} 136: end
Checks if the passed in filter is an empty one
# File lib/mcollective/util.rb, line 125 125: def self.empty_filter?(filter) 126: filter == empty_filter || filter == {} 127: 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 61: def self.get_fact(fact) 62: Facts.get_fact(fact) 63: 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 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
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 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
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 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: 79: if operator == '=~' 80: # to maintain backward compat we send the value 81: # as /.../ which is what 1.0.x needed. this strips 82: # off the /'s wich is what we need here 83: if value =~ /^\/(.+)\/$/ 84: value = $1 85: end 86: 87: return true if fact.match(Regexp.new(value)) 88: 89: elsif operator == "==" 90: return true if fact == value 91: 92: elsif ['<=', '>=', '<', '>', '!='].include?(operator) 93: # Yuk - need to type cast, but to_i and to_f are overzealous 94: if value =~ /^[0-9]+$/ && fact =~ /^[0-9]+$/ 95: fact = Integer(fact) 96: value = Integer(value) 97: elsif value =~ /^[0-9]+.[0-9]+$/ && fact =~ /^[0-9]+.[0-9]+$/ 98: fact = Float(fact) 99: value = Float(value) 100: end 101: 102: return true if eval("fact #{operator} value") 103: end 104: 105: false 106: 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 112 112: def self.has_identity?(identity) 113: identity = Regexp.new(identity.gsub("\/", "")) if identity.match("^/") 114: 115: if identity.is_a?(Regexp) 116: return Config.instance.identity.match(identity) 117: else 118: return true if Config.instance.identity == identity 119: end 120: 121: false 122: end
Wrapper around PluginManager.loadclass
# File lib/mcollective/util.rb, line 208 208: def self.loadclass(klass) 209: PluginManager.loadclass(klass) 210: end
# File lib/mcollective/util.rb, line 169 169: def self.make_subscriptions(agent, type, collective=nil) 170: config = Config.instance 171: 172: raise("Unknown target type #{type}") unless [:broadcast, :directed, :reply].include?(type) 173: 174: if collective.nil? 175: config.collectives.map do |c| 176: {:agent => agent, :type => type, :collective => c} 177: end 178: else 179: raise("Unknown collective '#{collective}' known collectives are '#{config.collectives.join ', '}'") unless config.collectives.include?(collective) 180: 181: [{:agent => agent, :type => type, :collective => collective}] 182: end 183: end
# File lib/mcollective/util.rb, line 282 282: def self.mcollective_version 283: MCollective::VERSION 284: end
Parse a fact filter string like foo=bar into the tuple hash thats needed
# File lib/mcollective/util.rb, line 213 213: def self.parse_fact_string(fact) 214: if fact =~ /^([^ ]+?)[ ]*=>[ ]*(.+)/ 215: return {:fact => $1, :value => $2, :operator => '>=' } 216: elsif fact =~ /^([^ ]+?)[ ]*=<[ ]*(.+)/ 217: return {:fact => $1, :value => $2, :operator => '<=' } 218: elsif fact =~ /^([^ ]+?)[ ]*(<=|>=|<|>|!=|==|=~)[ ]*(.+)/ 219: return {:fact => $1, :value => $3, :operator => $2 } 220: elsif fact =~ /^(.+?)[ ]*=[ ]*\/(.+)\/$/ 221: return {:fact => $1, :value => "/#{$2}/", :operator => '=~' } 222: elsif fact =~ /^([^= ]+?)[ ]*=[ ]*(.+)/ 223: return {:fact => $1, :value => $2, :operator => '==' } 224: else 225: raise "Could not parse fact #{fact} it does not appear to be in a valid format" 226: end 227: end
Returns the current ruby version as per RUBY_VERSION, mostly doing this here to aid testing
# File lib/mcollective/util.rb, line 278 278: def self.ruby_version 279: RUBY_VERSION 280: 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 28: def self.setup_windows_sleeper 29: Thread.new { loop { sleep 1 } } if Util.windows? 30: 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 232 232: def self.shellescape(str) 233: return "''" if str.empty? 234: 235: str = str.dup 236: 237: # Process as a single byte sequence because not all shell 238: # implementations are multibyte aware. 239: str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/n, "\\\\\\1") 240: 241: # A LF cannot be escaped with a backslash because a backslash + LF 242: # combo is regarded as line continuation and simply ignored. 243: str.gsub!(/\n/, "'\n'") 244: 245: return str 246: end
Helper to subscribe to a topic on multiple collectives or just one
# File lib/mcollective/util.rb, line 186 186: def self.subscribe(targets) 187: connection = PluginManager["connector_plugin"] 188: 189: targets = [targets].flatten 190: 191: targets.each do |target| 192: connection.subscribe(target[:agent], target[:type], target[:collective]) 193: end 194: 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 380 380: def self.terminal_dimensions(stdout = STDOUT, environment = ENV) 381: return [0, 0] unless stdout.tty? 382: 383: return [80, 40] if Util.windows? 384: 385: if environment["COLUMNS"] && environment["LINES"] 386: return [environment["COLUMNS"].to_i, environment["LINES"].to_i] 387: 388: elsif environment["TERM"] && command_in_path?("tput") 389: return [`tput cols`.to_i, `tput lines`.to_i] 390: 391: elsif command_in_path?('stty') 392: return `stty size`.scan(/\d+/).map {|s| s.to_i } 393: else 394: return [0, 0] 395: end 396: rescue 397: [0, 0] 398: end
Helper to unsubscribe to a topic on multiple collectives or just one
# File lib/mcollective/util.rb, line 197 197: def self.unsubscribe(targets) 198: connection = PluginManager["connector_plugin"] 199: 200: targets = [targets].flatten 201: 202: targets.each do |target| 203: connection.unsubscribe(target[:agent], target[:type], target[:collective]) 204: end 205: 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 but refactored to a more ruby style that fits in better with this code base
# File lib/mcollective/util.rb, line 418 418: def self.versioncmp(version_a, version_b) 419: vre = /[-.]|\d+|[^-.\d]+/ 420: ax = version_a.scan(vre) 421: bx = version_b.scan(vre) 422: 423: until ax.empty? || bx.empty? 424: a = ax.shift 425: b = bx.shift 426: 427: next if a == b 428: next if a == '-' && b == '-' 429: return -1 if a == '-' 430: return 1 if b == '-' 431: next if a == '.' && b == '.' 432: return -1 if a == '.' 433: return 1 if b == '.' 434: 435: if a =~ /^[^0]\d+$/ && b =~ /^[^0]\d+$/ 436: return Integer(a) <=> Integer(b) 437: else 438: return a.upcase <=> b.upcase 439: end 440: end 441: 442: version_a <=> version_b 443: end