module MCollective::Util

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

Public Class Methods

absolute_path?(path, separator=File::SEPARATOR, alt_separator=File::ALT_SEPARATOR) click to toggle source

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
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
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
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
279 def self.color(code)
280   colorize = Config.instance.color
281 
282   colors = {:red => "",
283             :green => "",
284             :yellow => "",
285             :cyan => "",
286             :bold => "",
287             :reset => ""}
288 
289   if colorize
290     return colors[code] || ""
291   else
292     return ""
293   end
294 end
colorize(code, msg) click to toggle source

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
command_in_path?(command) click to toggle source

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
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
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
default_options() click to toggle source

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
empty_filter() click to toggle source

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
empty_filter?(filter) click to toggle source

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
field_number(field_size, max_size=90) click to toggle source

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
field_size(elements, min_size=40) click to toggle source

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
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
61 def self.get_fact(fact)
62   Facts.get_fact(fact)
63 end
get_hidden_input(message='Please enter data: ') click to toggle source
    # File lib/mcollective/util.rb
560 def self.get_hidden_input(message='Please enter data: ')
561   unless message.nil?
562     print message
563   end
564   if versioncmp(ruby_version, '1.9.3') >= 0
565     require 'io/console'
566     input = $stdin.noecho(&:gets)
567   else
568     # Use hacks to get hidden input on Ruby <1.9.3
569     if self.windows?
570       input = self.get_hidden_input_on_windows()
571     else
572       input = self.get_hidden_input_on_unix()
573     end
574   end
575   input.chomp! if input
576   input
577 end
get_hidden_input_on_unix() click to toggle source
    # File lib/mcollective/util.rb
545 def self.get_hidden_input_on_unix()
546   unless $stdin.tty?
547     raise 'Could not hook to stdin to hide input. If using SSH, try using -t flag while connecting to server.'
548   end
549   unless system 'stty -echo -icanon'
550     raise 'Could not hide input using stty command.'
551   end
552   input = $stdin.gets
553   ensure
554     unless system 'stty echo icanon'
555       raise 'Could not enable echoing of input. Try executing `stty echo icanon` to debug.'
556     end
557   input
558 end
get_hidden_input_on_windows() click to toggle source
    # File lib/mcollective/util.rb
524 def self.get_hidden_input_on_windows()
525   require 'Win32API'
526   # Hook into getch from crtdll. Keep reading all keys till return
527   # or newline is hit.
528   # If key is backspace or delete, then delete the character and update
529   # the buffer.
530   input = ''
531   while char = Win32API.new("crtdll", "_getch", [ ], "I").Call do
532     break if char == 10 || char == 13 # return or newline
533     if char == 127 || char == 8 # backspace and delete
534       if input.length > 0
535         input.slice!(-1, 1)
536       end
537     else
538       input << char.chr
539     end
540   end
541   char = ''
542   input
543 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
 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
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
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
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
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
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
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
loadclass(klass) click to toggle source

Wrapper around PluginManager.loadclass

    # File lib/mcollective/util.rb
233 def self.loadclass(klass)
234   PluginManager.loadclass(klass)
235 end
make_subscriptions(agent, type, collective=nil) click to toggle source
    # 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
mcollective_version() click to toggle source
    # File lib/mcollective/util.rb
307 def self.mcollective_version
308   MCollective::VERSION
309 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
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
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
303 def self.ruby_version
304   RUBY_VERSION
305 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
28 def self.setup_windows_sleeper
29   Thread.new { loop { sleep 1 } } if Util.windows?
30 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
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
str_to_bool(val) click to toggle source

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
subscribe(targets) click to toggle source

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_direct_addressing_queue() click to toggle source

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
templatepath(template_file) click to toggle source

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
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
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
unsubscribe(targets) click to toggle source

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
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
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
windows?() click to toggle source
    # File lib/mcollective/util.rb
273 def self.windows?
274   !!(RbConfig::CONFIG['host_os'] =~ /mswin|win32|dos|mingw|cygwin/i)
275 end
windows_prefix() click to toggle source

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