2 # A parser and scanner that creates a stack machine for a simple
3 # fact and class matching language used on the CLI to facilitate
4 # a rich discovery language
8 # compound = ["("] expression [")"] {["("] expression [")"]}
9 # expression = [!|not]statement ["and"|"or"] [!|not] statement
10 # char = A-Z | a-z | < | > | => | =< | _ | - |* | / { A-Z | a-z | < | > | => | =< | _ | - | * | / | }
11 # int = 0|1|2|3|4|5|6|7|8|9{|0|1|2|3|4|5|6|7|8|9|0}
13 autoload :Parser, "mcollective/matcher/parser"
14 autoload :Scanner, "mcollective/matcher/scanner"
16 # Helper creates a hash from a function call string
17 def self.create_function_hash(function_call)
20 func_parts = function_call.split(/(!=|>=|<=|<|>|=)/)
21 func_hash["r_compare"] = func_parts.pop
22 func_hash["operator"] = func_parts.pop
23 func = func_parts.join
25 # Deal with dots in function parameters and functions without dot values
26 if func.match(/^.+\(.*\)$/)
29 func_parts = func.split(".")
30 func_hash["value"] = func_parts.pop
31 f = func_parts.join(".")
34 # Deal with regular expression matches
35 if func_hash["r_compare"] =~ /^\/.*\/$/
36 func_hash["operator"] = "=~" if func_hash["operator"] == "="
37 func_hash["operator"] = "!=~" if func_hash["operator"] == "!="
38 func_hash["r_compare"] = Regexp.new(func_hash["r_compare"].gsub(/^\/|\/$/, ""))
39 # Convert = operators to == so they can be propperly evaluated
40 elsif func_hash["operator"] == "="
41 func_hash["operator"] = "=="
44 # Grab function name and parameters from left compare string
45 func_hash["name"], func_hash["params"] = f.split("(")
46 if func_hash["params"] == ")"
47 func_hash["params"] = nil
50 # Walk the function parameters from the front and from the
51 # back removing the first and last instances of single of
52 # double qoutes. We do this to handle the case where params
53 # contain escaped qoutes.
54 func_hash["params"] = func_hash["params"].gsub(")", "")
55 func_quotes = func_hash["params"].split(/('|")/)
57 func_quotes.each_with_index do |item, i|
59 func_quotes.delete_at(i)
64 func_quotes.reverse.each_with_index do |item,i|
66 func_quotes.delete_at(func_quotes.size - i - 1)
71 func_hash["params"] = func_quotes.join
77 # Returns the result of an executed function
78 def self.execute_function(function_hash)
79 # In the case where a data plugin isn't present there are two ways we can handle
80 # the raised exception. The function result can either be false or the entire
81 # expression can fail.
83 # In the case where we return the result as false it opens us op to unexpected
86 # !foo('bar').name = bar
88 # In this case the user would expect discovery to match on all machines where
89 # the name value of the foo function does not equal bar. If a non existent function
90 # returns false then it is posible to match machines where the name value of the
91 # foo function is bar.
93 # Instead we raise a DDLValidationError to prevent this unexpected behavior from
96 result = Data.send(function_hash["name"], function_hash["params"])
98 if function_hash["value"]
100 eval_result = result.send(function_hash["value"])
102 # If data field has not been set we set the comparison result to nil
110 Log.debug("cannot execute discovery function '#{function_hash["name"]}'. data plugin not found")
111 raise DDLValidationError
114 # Evaluates a compound statement
115 def self.eval_compound_statement(expression)
116 if expression.values.first =~ /^\//
117 return Util.has_cf_class?(expression.values.first)
118 elsif expression.values.first =~ />=|<=|=|<|>/
119 optype = expression.values.first.match(/>=|<=|=|<|>/)
120 name, value = expression.values.first.split(optype[0])
121 unless value.split("")[0] == "/"
122 optype[0] == "=" ? optype = "==" : optype = optype[0]
127 return Util.has_fact?(name,value, optype).to_s
129 return Util.has_cf_class?(expression.values.first)
133 # Returns the result of an evaluated compound statement that
134 # includes a function
135 def self.eval_compound_fstatement(function_hash)
136 l_compare = execute_function(function_hash)
138 # Break out early and return false if the function returns nil
139 return false unless l_compare
141 # Prevent unwanted discovery by limiting comparison operators
142 # on Strings and Booleans
143 if((l_compare.is_a?(String) || l_compare.is_a?(TrueClass) || l_compare.is_a?(FalseClass)) && function_hash["operator"].match(/<|>/))
144 Log.debug "Cannot do > and < comparison on Booleans and Strings '#{l_compare} #{function_hash["operator"]} #{function_hash["r_compare"]}'"
148 # Prevent backticks in function parameters
149 if function_hash["params"] =~ /`/
150 Log.debug("Cannot use backticks in function parameters")
154 # Escape strings for evaluation
155 function_hash["r_compare"] = "\"#{function_hash["r_compare"]}\"" if(l_compare.is_a?(String) && !(function_hash["operator"] =~ /=~|!=~/))
157 # Do a regex comparison if right compare string is a regex
158 if function_hash["operator"] =~ /(=~|!=~)/
159 # Fail if left compare value isn't a string
160 unless l_compare.is_a?(String)
161 Log.debug("Cannot do a regex check on a non string value.")
164 compare_result = l_compare.match(function_hash["r_compare"])
165 # Flip return value for != operator
166 if function_hash["operator"] == "!=~"
167 !((compare_result.nil?) ? false : true)
169 (compare_result.nil?) ? false : true
172 # Otherwise evaluate the logical comparison
174 l_compare = "\"#{l_compare}\"" if l_compare.is_a?(String)
175 result = eval("#{l_compare} #{function_hash["operator"]} #{function_hash["r_compare"]}")
176 (result.nil?) ? false : result
180 # Creates a callstack to be evaluated from a compound evaluation string
181 def self.create_compound_callstack(call_string)
182 callstack = Matcher::Parser.new(call_string).execution_stack
183 callstack.each_with_index do |statement, i|
184 if statement.keys.first == "fstatement"
185 callstack[i]["fstatement"] = create_function_hash(statement.values.first)