module MCollective::Util
Some basic utility helper methods useful to clients, agents, runner etc.
Public Class Methods
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
Wrapper around PluginManager.loadclass
# File lib/mcollective/util.rb 233 def self.loadclass(klass) 234 PluginManager.loadclass(klass) 235 end
# 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
# File lib/mcollective/util.rb 307 def self.mcollective_version 308 MCollective::VERSION 309 end
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
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
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
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
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
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 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
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
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
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
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
# File lib/mcollective/util.rb 273 def self.windows? 274 !!(RbConfig::CONFIG['host_os'] =~ /mswin|win32|dos|mingw|cygwin/i) 275 end
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