Module MCollective::Util
In: lib/mcollective/util.rb

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

Methods

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

[Source]

     # 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.

[Source]

     # 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

[Source]

     # File lib/mcollective/util.rb, line 254
254:     def self.color(code)
255:       colorize = Config.instance.color
256: 
257:       colors = {:red => "",
258:                 :green => "",
259:                 :yellow => "",
260:                 :cyan => "",
261:                 :bold => "",
262:                 :reset => ""}
263: 
264:       if colorize
265:         return colors[code] || ""
266:       else
267:         return ""
268:       end
269:     end

Helper to return a string in specific color

[Source]

     # File lib/mcollective/util.rb, line 272
272:     def self.colorize(code, msg)
273:       "%s%s%s" % [ color(code), msg, color(:reset) ]
274:     end

Checks in PATH returns true if the command is found

[Source]

     # 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

[Source]

     # 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

[Source]

     # 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

[Source]

     # 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

[Source]

     # 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

[Source]

    # 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

[Source]

    # 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

[Source]

    # 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

[Source]

     # 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

[Source]

     # 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

[Source]

     # File lib/mcollective/util.rb, line 208
208:     def self.loadclass(klass)
209:       PluginManager.loadclass(klass)
210:     end

[Source]

     # 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

[Source]

     # 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

[Source]

     # 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

[Source]

     # 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

[Source]

    # 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

[Source]

     # 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

[Source]

     # 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

[Source]

     # 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

[Source]

     # 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

[Source]

     # 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

[Source]

     # File lib/mcollective/util.rb, line 248
248:     def self.windows?
249:       !!(RbConfig::CONFIG['host_os'] =~ /mswin|win32|dos|mingw|cygwin/i)
250:     end

[Validate]