2 # Some basic utility helper methods useful to clients, agents, runner etc.
4 # Finds out if this MCollective has an agent by the name passed
6 # If the passed name starts with a / it's assumed to be regex
7 # and will use regex to match
8 def self.has_agent?(agent)
9 agent = Regexp.new(agent.gsub("\/", "")) if agent.match("^/")
11 if agent.is_a?(Regexp)
12 if Agents.agentlist.grep(agent).size > 0
18 return Agents.agentlist.include?(agent)
24 # On windows ^c can't interrupt the VM if its blocking on
25 # IO, so this sets up a dummy thread that sleeps and this
26 # will have the end result of being interruptable at least
27 # once a second. This is a common pattern found in Rails etc
28 def self.setup_windows_sleeper
29 Thread.new { loop { sleep 1 } } if Util.windows?
32 # Checks if this node has a configuration management class by parsing the
33 # a text file with just a list of classes, recipes, roles etc. This is
34 # ala the classes.txt from puppet.
36 # If the passed name starts with a / it's assumed to be regex
37 # and will use regex to match
38 def self.has_cf_class?(klass)
39 klass = Regexp.new(klass.gsub("\/", "")) if klass.match("^/")
40 cfile = Config.instance.classesfile
42 Log.debug("Looking for configuration management classes in #{cfile}")
45 File.readlines(cfile).each do |k|
46 if klass.is_a?(Regexp)
47 return true if k.chomp.match(klass)
49 return true if k.chomp == klass
53 Log.warn("Parsing classes file '#{cfile}' failed: #{e.class}: #{e}")
59 # Gets the value of a specific fact, mostly just a duplicate of MCollective::Facts.get_fact
60 # but it kind of goes with the other classes here
61 def self.get_fact(fact)
65 # Compares fact == value,
67 # If the passed value starts with a / it's assumed to be regex
68 # and will use regex to match
69 def self.has_fact?(fact, value, operator)
71 Log.debug("Comparing #{fact} #{operator} #{value}")
72 Log.debug("where :fact = '#{fact}', :operator = '#{operator}', :value = '#{value}'")
75 return false if fact.nil?
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 =~ /^\/(.+)\/$/
87 return true if fact.match(Regexp.new(value))
89 elsif operator == "=="
90 return true if fact == value
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]+$/
96 value = Integer(value)
97 elsif value =~ /^[0-9]+.[0-9]+$/ && fact =~ /^[0-9]+.[0-9]+$/
102 return true if eval("fact #{operator} value")
108 # Checks if the configured identity matches the one supplied
110 # If the passed name starts with a / it's assumed to be regex
111 # and will use regex to match
112 def self.has_identity?(identity)
113 identity = Regexp.new(identity.gsub("\/", "")) if identity.match("^/")
115 if identity.is_a?(Regexp)
116 return Config.instance.identity.match(identity)
118 return true if Config.instance.identity == identity
124 # Checks if the passed in filter is an empty one
125 def self.empty_filter?(filter)
126 filter == empty_filter || filter == {}
129 # Creates an empty filter
130 def self.empty_filter
138 # Picks a config file defaults to ~/.mcollective
139 # else /etc/mcollective/client.cfg
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
145 config = File.expand_path("~/.mcollective")
147 unless File.readable?(config) && File.file?(config)
148 config = "/etc/mcollective/client.cfg"
150 rescue Exception => e
151 config = "/etc/mcollective/client.cfg"
157 # Creates a standard options hash
158 def self.default_options
162 :config => config_file_for_user,
164 :discovery_method => nil,
165 :discovery_options => Config.instance.default_discovery_options,
166 :filter => empty_filter}
169 def self.make_subscriptions(agent, type, collective=nil)
170 config = Config.instance
172 raise("Unknown target type #{type}") unless [:broadcast, :directed, :reply].include?(type)
175 config.collectives.map do |c|
176 {:agent => agent, :type => type, :collective => c}
179 raise("Unknown collective '#{collective}' known collectives are '#{config.collectives.join ', '}'") unless config.collectives.include?(collective)
181 [{:agent => agent, :type => type, :collective => collective}]
185 # Helper to subscribe to a topic on multiple collectives or just one
186 def self.subscribe(targets)
187 connection = PluginManager["connector_plugin"]
189 targets = [targets].flatten
191 targets.each do |target|
192 connection.subscribe(target[:agent], target[:type], target[:collective])
196 # Helper to unsubscribe to a topic on multiple collectives or just one
197 def self.unsubscribe(targets)
198 connection = PluginManager["connector_plugin"]
200 targets = [targets].flatten
202 targets.each do |target|
203 connection.unsubscribe(target[:agent], target[:type], target[:collective])
207 # Wrapper around PluginManager.loadclass
208 def self.loadclass(klass)
209 PluginManager.loadclass(klass)
212 # Parse a fact filter string like foo=bar into the tuple hash thats needed
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 => '==' }
225 raise "Could not parse fact #{fact} it does not appear to be in a valid format"
229 # Escapes a string so it's safe to use in system() or backticks
231 # Taken from Shellwords#shellescape since it's only in a few ruby versions
232 def self.shellescape(str)
233 return "''" if str.empty?
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")
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'")
249 !!(RbConfig::CONFIG['host_os'] =~ /mswin|win32|dos|mingw|cygwin/i)
252 # Return color codes, if the config color= option is false
253 # just return a empty string
255 colorize = Config.instance.color
257 colors = {:red => "
\e[31m",
265 return colors[code] || ""
271 # Helper to return a string in specific color
272 def self.colorize(code, msg)
273 "%s%s%s" % [ color(code), msg, color(:reset) ]
276 # Returns the current ruby version as per RUBY_VERSION, mostly
277 # doing this here to aid testing
278 def self.ruby_version
282 def self.mcollective_version
286 # Returns an aligned_string of text relative to the size of the terminal
287 # window. If a line in the string exceeds the width of the terminal window
288 # the line will be chopped off at the whitespace chacter closest to the
289 # end of the line and prepended to the next line, keeping all indentation.
291 # The terminal size is detected by default, but custom line widths can
292 # passed. All strings will also be left aligned with 5 whitespace characters
294 def self.align_text(text, console_cols = nil, preamble = 5)
296 console_cols = terminal_dimensions[0]
298 # if unknown size we default to the typical unix default
299 console_cols = 80 if console_cols == 0
302 console_cols -= preamble
304 # Return unaligned text if console window is too small
305 return text if console_cols <= 0
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
311 text = text.split("\n")
315 text.each_with_index do |line, i|
318 while whitespace < line.length && line[whitespace].chr == ' '
322 # If the current line is empty, indent it so that a snippet
323 # from the previous line is aligned correctly.
325 line = (" " * whitespace)
328 # If text was snipped from the previous line, prepend it to the
329 # current line after any current indentation.
331 # Reset whitespaces to 0 if there are more whitespaces than there are
333 whitespace = 0 if whitespace >= console_cols
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, "")
341 # Add the snipped text to the current line
342 line.insert(whitespace, "#{piece} ")
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
353 while line[reverse].chr != ' '
357 piece = line.slice!(reverse, (line.length - 1)).lstrip
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}"
368 # Add the preamble to the line and add it to the text
369 line = ((' ' * preamble) + line)
376 # Figures out the columns and lines of the current tty
378 # Returns [0, 0] if it can't figure it out or if you're
379 # not running on a tty
380 def self.terminal_dimensions(stdout = STDOUT, environment = ENV)
381 return [0, 0] unless stdout.tty?
383 return [80, 40] if Util.windows?
385 if environment["COLUMNS"] && environment["LINES"]
386 return [environment["COLUMNS"].to_i, environment["LINES"].to_i]
388 elsif environment["TERM"] && command_in_path?("tput")
389 return [`tput cols`.to_i, `tput lines`.to_i]
391 elsif command_in_path?('stty')
392 return `stty size`.scan(/\d+/).map {|s| s.to_i }
400 # Checks in PATH returns true if the command is found
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))
409 # compare two software versions as commonly found in
412 # returns 0 if a == b
413 # returns -1 if a < b
416 # Code originally from Puppet but refactored to a more
417 # ruby style that fits in better with this code base
418 def self.versioncmp(version_a, version_b)
419 vre = /[-.]|\d+|[^-.\d]+/
420 ax = version_a.scan(vre)
421 bx = version_b.scan(vre)
423 until ax.empty? || bx.empty?
428 next if a == '-' && b == '-'
429 return -1 if a == '-'
431 next if a == '.' && b == '.'
432 return -1 if a == '.'
435 if a =~ /^[^0]\d+$/ && b =~ /^[^0]\d+$/
436 return Integer(a) <=> Integer(b)
438 return a.upcase <=> b.upcase
442 version_a <=> version_b
445 # we should really use Pathname#absolute? but it's not in all the
446 # ruby versions we support and it comes down to roughly this
447 def self.absolute_path?(path, separator=File::SEPARATOR, alt_separator=File::ALT_SEPARATOR)
449 path_matcher = /^[#{Regexp.quote alt_separator}#{Regexp.quote separator}]/
451 path_matcher = /^#{Regexp.quote separator}/
454 !!path.match(path_matcher)
457 # Looks up and interprolate the hash values into a i18n string
458 def self.t(msgid, args={})
459 if msgid.is_a?(Symbol)
460 I18n.t("%s.pattern" % msgid, args)