d374776eb968ffc3454c2c4ed183e5d4b6093dbe
[packages/precise/mcollective.git] / lib / mcollective / matcher.rb
1 module MCollective
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
5   #
6   # Language EBNF
7   #
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}
12   module Matcher
13     autoload :Parser, "mcollective/matcher/parser"
14     autoload :Scanner, "mcollective/matcher/scanner"
15
16     # Helper creates a hash from a function call string
17     def self.create_function_hash(function_call)
18       func_hash = {}
19       f = ""
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
24
25       # Deal with dots in function parameters and functions without dot values
26       if func.match(/^.+\(.*\)$/)
27         f = func
28       else
29         func_parts = func.split(".")
30         func_hash["value"] = func_parts.pop
31         f = func_parts.join(".")
32       end
33
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"] = "=="
42       end
43
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
48       else
49
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(/('|")/)
56
57         func_quotes.each_with_index do |item, i|
58           if item.match(/'|"/)
59             func_quotes.delete_at(i)
60             break
61           end
62         end
63
64         func_quotes.reverse.each_with_index do |item,i|
65           if item.match(/'|"/)
66             func_quotes.delete_at(func_quotes.size - i - 1)
67             break
68           end
69         end
70
71         func_hash["params"] = func_quotes.join
72       end
73
74       func_hash
75     end
76
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.
82       #
83       # In the case where we return the result as false it opens us op to unexpected
84       # negation behavior.
85       #
86       #   !foo('bar').name = bar
87       #
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.
92       #
93       # Instead we raise a DDLValidationError to prevent this unexpected behavior from
94       # happening.
95
96       result = Data.send(function_hash["name"], function_hash["params"])
97
98       if function_hash["value"]
99         eval_result = result.send(function_hash["value"])
100         return eval_result
101       else
102         return result
103       end
104     rescue NoMethodError
105       Log.debug("cannot execute discovery function '#{function_hash["name"]}'. data plugin not found")
106       raise DDLValidationError
107     end
108
109     # Evaluates a compound statement
110     def self.eval_compound_statement(expression)
111       if expression.values.first =~ /^\//
112         return Util.has_cf_class?(expression.values.first)
113       elsif expression.values.first =~ />=|<=|=|<|>/
114         optype = expression.values.first.match(/>=|<=|=|<|>/)
115         name, value = expression.values.first.split(optype[0])
116         unless value.split("")[0] == "/"
117           optype[0] == "=" ? optype = "==" : optype = optype[0]
118         else
119           optype = "=~"
120         end
121
122         return Util.has_fact?(name,value, optype).to_s
123       else
124         return Util.has_cf_class?(expression.values.first)
125       end
126     end
127
128     # Returns the result of an evaluated compound statement that
129     # includes a function
130     def self.eval_compound_fstatement(function_hash)
131       l_compare = execute_function(function_hash)
132
133       # Prevent unwanted discovery by limiting comparison operators
134       # on Strings and Booleans
135       if((l_compare.is_a?(String) || l_compare.is_a?(TrueClass) || l_compare.is_a?(FalseClass)) && function_hash["operator"].match(/<|>/))
136         Log.debug "Cannot do > and < comparison on Booleans and Strings '#{l_compare} #{function_hash["operator"]} #{function_hash["r_compare"]}'"
137         return false
138       end
139
140       # Prevent backticks in function parameters
141       if function_hash["params"] =~ /`/
142         Log.debug("Cannot use backticks in function parameters")
143         return false
144       end
145
146       # Escape strings for evaluation
147       function_hash["r_compare"] = "\"#{function_hash["r_compare"]}\"" if(l_compare.is_a?(String)  && !(function_hash["operator"] =~ /=~|!=~/))
148
149       # Do a regex comparison if right compare string is a regex
150       if function_hash["operator"] =~ /(=~|!=~)/
151         # Fail if left compare value isn't a string
152         unless l_compare.is_a?(String)
153           Log.debug("Cannot do a regex check on a non string value.")
154           return false
155         else
156           compare_result = l_compare.match(function_hash["r_compare"])
157           # Flip return value for != operator
158           if function_hash["operator"] == "!=~"
159             !((compare_result.nil?) ? false : true)
160           else
161             (compare_result.nil?) ? false : true
162           end
163         end
164         # Otherwise evaluate the logical comparison
165       else
166         l_compare = "\"#{l_compare}\"" if l_compare.is_a?(String)
167         result = eval("#{l_compare} #{function_hash["operator"]} #{function_hash["r_compare"]}")
168         (result.nil?) ? false : result
169       end
170     end
171
172     # Creates a callstack to be evaluated from a compound evaluation string
173     def self.create_compound_callstack(call_string)
174       callstack = Matcher::Parser.new(call_string).execution_stack
175       callstack.each_with_index do |statement, i|
176         if statement.keys.first == "fstatement"
177           callstack[i]["fstatement"] = create_function_hash(statement.values.first)
178         end
179       end
180       callstack
181     end
182   end
183 end