2 # Some basic utility helper methods useful to clients, agents, runner etc.
6 # Finds out if this MCollective has an agent by the name passed
8 # If the passed name starts with a / it's assumed to be regex
9 # and will use regex to match
10 def self.has_agent?(agent)
11 agent = Regexp.new(agent.gsub("\/", "")) if agent.match("^/")
13 if agent.is_a?(Regexp)
14 if Agents.agentlist.grep(agent).size > 0
20 return Agents.agentlist.include?(agent)
26 # On windows ^c can't interrupt the VM if its blocking on
27 # IO, so this sets up a dummy thread that sleeps and this
28 # will have the end result of being interruptable at least
29 # once a second. This is a common pattern found in Rails etc
30 def self.setup_windows_sleeper
31 Thread.new { loop { sleep 1 } } if Util.windows?
34 # Checks if this node has a configuration management class by parsing the
35 # a text file with just a list of classes, recipes, roles etc. This is
36 # ala the classes.txt from puppet.
38 # If the passed name starts with a / it's assumed to be regex
39 # and will use regex to match
40 def self.has_cf_class?(klass)
41 klass = Regexp.new(klass.gsub("\/", "")) if klass.match("^/")
42 cfile = Config.instance.classesfile
44 Log.debug("Looking for configuration management classes in #{cfile}")
47 File.readlines(cfile).each do |k|
48 if klass.is_a?(Regexp)
49 return true if k.chomp.match(klass)
51 return true if k.chomp == klass
55 Log.warn("Parsing classes file '#{cfile}' failed: #{e.class}: #{e}")
61 # Gets the value of a specific fact, mostly just a duplicate of MCollective::Facts.get_fact
62 # but it kind of goes with the other classes here
63 def self.get_fact(fact)
67 # Compares fact == value,
69 # If the passed value starts with a / it's assumed to be regex
70 # and will use regex to match
71 def self.has_fact?(fact, value, operator)
73 Log.debug("Comparing #{fact} #{operator} #{value}")
74 Log.debug("where :fact = '#{fact}', :operator = '#{operator}', :value = '#{value}'")
77 return false if fact.nil?
82 # to maintain backward compat we send the value
83 # as /.../ which is what 1.0.x needed. this strips
84 # off the /'s wich is what we need here
85 if value =~ /^\/(.+)\/$/
89 return true if fact.match(Regexp.new(value))
91 elsif operator == "=="
92 return true if fact == value
94 elsif ['<=', '>=', '<', '>', '!='].include?(operator)
95 # Yuk - need to type cast, but to_i and to_f are overzealous
96 if value =~ /^[0-9]+$/ && fact =~ /^[0-9]+$/
98 value = Integer(value)
99 elsif value =~ /^[0-9]+.[0-9]+$/ && fact =~ /^[0-9]+.[0-9]+$/
104 return true if eval("fact #{operator} value")
110 # Checks if the configured identity matches the one supplied
112 # If the passed name starts with a / it's assumed to be regex
113 # and will use regex to match
114 def self.has_identity?(identity)
115 identity = Regexp.new(identity.gsub("\/", "")) if identity.match("^/")
117 if identity.is_a?(Regexp)
118 return Config.instance.identity.match(identity)
120 return true if Config.instance.identity == identity
126 # Checks if the passed in filter is an empty one
127 def self.empty_filter?(filter)
128 filter == empty_filter || filter == {}
131 # Creates an empty filter
132 def self.empty_filter
140 # Returns the PuppetLabs mcollective path for windows
141 def self.windows_prefix
143 prefix = File.join(Dir::COMMON_APPDATA, "PuppetLabs", "mcollective")
146 # Picks a config file defaults to ~/.mcollective
147 # else /etc/mcollective/client.cfg
148 def self.config_file_for_user
149 # expand_path is pretty lame, it relies on HOME environment
150 # which isnt't always there so just handling all exceptions
151 # here as cant find reverting to default
153 config = File.expand_path("~/.mcollective")
155 unless File.readable?(config) && File.file?(config)
157 config = File.join(self.windows_prefix, "etc", "client.cfg")
159 config = "/etc/mcollective/client.cfg"
162 rescue Exception => e
164 config = File.join(self.windows_prefix, "etc", "client.cfg")
166 config = "/etc/mcollective/client.cfg"
173 # Creates a standard options hash
174 def self.default_options
178 :config => config_file_for_user,
180 :discovery_method => nil,
181 :discovery_options => Config.instance.default_discovery_options,
182 :filter => empty_filter}
185 def self.make_subscriptions(agent, type, collective=nil)
186 config = Config.instance
188 raise("Unknown target type #{type}") unless [:broadcast, :directed, :reply].include?(type)
191 config.collectives.map do |c|
192 {:agent => agent, :type => type, :collective => c}
195 raise("Unknown collective '#{collective}' known collectives are '#{config.collectives.join ', '}'") unless config.collectives.include?(collective)
197 [{:agent => agent, :type => type, :collective => collective}]
201 # Helper to subscribe to a topic on multiple collectives or just one
202 def self.subscribe(targets)
203 connection = PluginManager["connector_plugin"]
205 targets = [targets].flatten
207 targets.each do |target|
208 connection.subscribe(target[:agent], target[:type], target[:collective])
212 # Helper to unsubscribe to a topic on multiple collectives or just one
213 def self.unsubscribe(targets)
214 connection = PluginManager["connector_plugin"]
216 targets = [targets].flatten
218 targets.each do |target|
219 connection.unsubscribe(target[:agent], target[:type], target[:collective])
223 # Wrapper around PluginManager.loadclass
224 def self.loadclass(klass)
225 PluginManager.loadclass(klass)
228 # Parse a fact filter string like foo=bar into the tuple hash thats needed
229 def self.parse_fact_string(fact)
230 if fact =~ /^([^ ]+?)[ ]*=>[ ]*(.+)/
231 return {:fact => $1, :value => $2, :operator => '>=' }
232 elsif fact =~ /^([^ ]+?)[ ]*=<[ ]*(.+)/
233 return {:fact => $1, :value => $2, :operator => '<=' }
234 elsif fact =~ /^([^ ]+?)[ ]*(<=|>=|<|>|!=|==|=~)[ ]*(.+)/
235 return {:fact => $1, :value => $3, :operator => $2 }
236 elsif fact =~ /^(.+?)[ ]*=[ ]*\/(.+)\/$/
237 return {:fact => $1, :value => "/#{$2}/", :operator => '=~' }
238 elsif fact =~ /^([^= ]+?)[ ]*=[ ]*(.+)/
239 return {:fact => $1, :value => $2, :operator => '==' }
241 raise "Could not parse fact #{fact} it does not appear to be in a valid format"
245 # Escapes a string so it's safe to use in system() or backticks
247 # Taken from Shellwords#shellescape since it's only in a few ruby versions
248 def self.shellescape(str)
249 return "''" if str.empty?
253 # Process as a single byte sequence because not all shell
254 # implementations are multibyte aware.
255 str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/n, "\\\\\\1")
257 # A LF cannot be escaped with a backslash because a backslash + LF
258 # combo is regarded as line continuation and simply ignored.
259 str.gsub!(/\n/, "'\n'")
265 !!(RbConfig::CONFIG['host_os'] =~ /mswin|win32|dos|mingw|cygwin/i)
268 # Return color codes, if the config color= option is false
269 # just return a empty string
271 colorize = Config.instance.color
273 colors = {:red => "
\e[31m",
281 return colors[code] || ""
287 # Helper to return a string in specific color
288 def self.colorize(code, msg)
289 "%s%s%s" % [ color(code), msg, color(:reset) ]
292 # Returns the current ruby version as per RUBY_VERSION, mostly
293 # doing this here to aid testing
294 def self.ruby_version
298 def self.mcollective_version
302 # Returns an aligned_string of text relative to the size of the terminal
303 # window. If a line in the string exceeds the width of the terminal window
304 # the line will be chopped off at the whitespace chacter closest to the
305 # end of the line and prepended to the next line, keeping all indentation.
307 # The terminal size is detected by default, but custom line widths can
308 # passed. All strings will also be left aligned with 5 whitespace characters
310 def self.align_text(text, console_cols = nil, preamble = 5)
312 console_cols = terminal_dimensions[0]
314 # if unknown size we default to the typical unix default
315 console_cols = 80 if console_cols == 0
318 console_cols -= preamble
320 # Return unaligned text if console window is too small
321 return text if console_cols <= 0
323 # If console is 0 this implies unknown so we assume the common
324 # minimal unix configuration of 80 characters
325 console_cols = 80 if console_cols <= 0
327 text = text.split("\n")
331 text.each_with_index do |line, i|
334 while whitespace < line.length && line[whitespace].chr == ' '
338 # If the current line is empty, indent it so that a snippet
339 # from the previous line is aligned correctly.
341 line = (" " * whitespace)
344 # If text was snipped from the previous line, prepend it to the
345 # current line after any current indentation.
347 # Reset whitespaces to 0 if there are more whitespaces than there are
349 whitespace = 0 if whitespace >= console_cols
351 # If the current line is empty and being prepended to, create a new
352 # empty line in the text so that formatting is preserved.
353 if text[i + 1] && line == (" " * whitespace)
354 text.insert(i + 1, "")
357 # Add the snipped text to the current line
358 line.insert(whitespace, "#{piece} ")
363 # Compare the line length to the allowed line length.
364 # If it exceeds it, snip the offending text from the line
365 # and store it so that it can be prepended to the next line.
366 if line.length > (console_cols + preamble)
367 reverse = console_cols
369 while line[reverse].chr != ' '
373 piece = line.slice!(reverse, (line.length - 1)).lstrip
376 # If a snippet exists when all the columns in the text have been
377 # updated, create a new line and append the snippet to it, using
378 # the same left alignment as the last line in the text.
379 if piece != '' && text[i+1].nil?
380 text[i+1] = "#{' ' * (whitespace)}#{piece}"
384 # Add the preamble to the line and add it to the text
385 line = ((' ' * preamble) + line)
392 # Figures out the columns and lines of the current tty
394 # Returns [0, 0] if it can't figure it out or if you're
395 # not running on a tty
396 def self.terminal_dimensions(stdout = STDOUT, environment = ENV)
397 return [0, 0] unless stdout.tty?
399 return [80, 40] if Util.windows?
401 if environment["COLUMNS"] && environment["LINES"]
402 return [environment["COLUMNS"].to_i, environment["LINES"].to_i]
404 elsif environment["TERM"] && command_in_path?("tput")
405 return [`tput cols`.to_i, `tput lines`.to_i]
407 elsif command_in_path?('stty')
408 return `stty size`.scan(/\d+/).map {|s| s.to_i }
416 # Checks in PATH returns true if the command is found
417 def self.command_in_path?(command)
418 found = ENV["PATH"].split(File::PATH_SEPARATOR).map do |p|
419 File.exist?(File.join(p, command))
425 # compare two software versions as commonly found in
428 # returns 0 if a == b
429 # returns -1 if a < b
432 # Code originally from Puppet
433 def self.versioncmp(version_a, version_b)
434 vre = /[-.]|\d+|[^-.\d]+/
435 ax = version_a.scan(vre)
436 bx = version_b.scan(vre)
438 while (ax.length>0 && bx.length>0)
442 if( a == b ) then next
443 elsif (a == '-' && b == '-') then next
444 elsif (a == '-') then return -1
445 elsif (b == '-') then return 1
446 elsif (a == '.' && b == '.') then next
447 elsif (a == '.' ) then return -1
448 elsif (b == '.' ) then return 1
449 elsif (a =~ /^\d+$/ && b =~ /^\d+$/) then
450 if( a =~ /^0/ or b =~ /^0/ ) then
451 return a.to_s.upcase <=> b.to_s.upcase
453 return a.to_i <=> b.to_i
455 return a.upcase <=> b.upcase
459 version_a <=> version_b;
462 # we should really use Pathname#absolute? but it's not in all the
463 # ruby versions we support and it comes down to roughly this
464 def self.absolute_path?(path, separator=File::SEPARATOR, alt_separator=File::ALT_SEPARATOR)
466 path_matcher = /^([a-zA-Z]:){0,1}[#{Regexp.quote alt_separator}#{Regexp.quote separator}]/
468 path_matcher = /^#{Regexp.quote separator}/
471 !!path.match(path_matcher)
474 # Converts a string into a boolean value
475 # Strings matching 1,y,yes,true or t will return TrueClass
476 # Any other value will return FalseClass
477 def self.str_to_bool(val)
478 clean_val = val.to_s.strip
479 if clean_val =~ /^(1|yes|true|y|t)$/i
481 elsif clean_val =~ /^(0|no|false|n|f)$/i
484 raise_code(:PLMC42, "Cannot convert string value '%{value}' into a boolean.", :error, :value => clean_val)
488 # Looks up and interprolate the hash values into a i18n string
489 def self.t(msgid, args={})
490 if msgid.is_a?(Symbol)
491 I18n.t("%s.pattern" % msgid, args)
497 # Looks up the template directory and returns its full path
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)
503 template_path = File.join("/etc/mcollective", template_file)