module MCollective::Matcher

A parser and scanner that creates a stack machine for a simple fact and class matching language used on the CLI to facilitate a rich discovery language

Language EBNF

compound = [“(”] expression [“)”] {[“(”] expression [“)”]} expression = [!|not]statement [“and”|“or”] [!|not] statement char = A-Z | a-z | < | > | => | =< | _ | - |* | / { A-Z | a-z | < | > | => | =< | _ | - | * | / | } int = 0|1|2|3|4|5|6|7|8|9{|0|1|2|3|4|5|6|7|8|9|0}

Public Class Methods

create_compound_callstack(call_string) click to toggle source

Creates a callstack to be evaluated from a compound evaluation string

    # File lib/mcollective/matcher.rb
214 def self.create_compound_callstack(call_string)
215   callstack = Matcher::Parser.new(call_string).execution_stack
216   callstack.each_with_index do |statement, i|
217     if statement.keys.first == "fstatement"
218       callstack[i]["fstatement"] = create_function_hash(statement.values.first)
219     end
220   end
221   callstack
222 end
create_function_hash(function_call) click to toggle source

Helper creates a hash from a function call string

   # File lib/mcollective/matcher.rb
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
eval_compound_fstatement(function_hash) click to toggle source

Returns the result of an evaluated compound statement that includes a function

    # File lib/mcollective/matcher.rb
135 def self.eval_compound_fstatement(function_hash)
136   l_compare = execute_function(function_hash)
137   r_compare = function_hash["r_compare"]
138   operator = function_hash["operator"]
139 
140   # Break out early and return false if the function returns nil
141   if l_compare.nil?
142     return false
143   end
144 
145   # Prevent unwanted discovery by limiting comparison operators
146   # on Strings and Booleans
147   if((l_compare.is_a?(String) || l_compare.is_a?(TrueClass) ||
148       l_compare.is_a?(FalseClass)) && function_hash["operator"].match(/<|>/))
149     Log.debug("Cannot do > and < comparison on Booleans and Strings " +
150               "'#{l_compare} #{function_hash["operator"]} #{function_hash["r_compare"]}'")
151     return false
152   end
153 
154   # Prevent backticks in function parameters
155   if function_hash["params"] =~ /`/
156     Log.debug("Cannot use backticks in function parameters")
157     return false
158   end
159 
160   # Do a regex comparison if right compare string is a regex
161   if operator=~ /(=~|!=~)/
162     # Fail if left compare value isn't a string
163     unless l_compare.is_a?(String)
164       Log.debug("Cannot do a regex check on a non string value.")
165       return false
166     else
167       result = l_compare.match(r_compare)
168       # Flip return value for != operator
169       if function_hash["operator"] == "!=~"
170         return !result
171       else
172         return !!result
173       end
174     end
175     # Otherwise do a normal comparison while taking the type into account
176   else
177     if l_compare.is_a? String
178       r_compare = r_compare.to_s
179     elsif r_compare.is_a? String
180       if l_compare.is_a? Numeric
181         r_compare = r_compare.strip
182         begin
183           r_compare = Integer(r_compare)
184         rescue ArgumentError
185           begin
186             r_compare = Float(r_compare)
187           rescue ArgumentError
188             raise ArgumentError, "invalid numeric value: #{r_compare}"
189           end
190         end
191       elsif l_compare.is_a? TrueClass or l_compare.is_a? FalseClass
192         r_compare = r_compare.strip
193         if r_compare == true.to_s
194           r_compare = true
195         elsif r_compare == false.to_s
196           r_compare = false
197         else
198           raise ArgumentError, "invalid boolean value: #{r_compare}"
199         end
200       end
201     end
202     operator = operator.strip
203     if operator =~ /(?:(!=|<=|>=|<|>)|==?)/
204       operator = $1 ? $1.to_sym : :==
205     else
206       raise ArgumentError, "invalid operator: #{operator}"
207     end
208     result = l_compare.send(operator, r_compare)
209     return result
210   end
211 end
eval_compound_statement(expression) click to toggle source

Evaluates a compound statement

    # File lib/mcollective/matcher.rb
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]
123     else
124       optype = "=~"
125     end
126 
127     return Util.has_fact?(name,value, optype).to_s
128   else
129     return Util.has_cf_class?(expression.values.first)
130   end
131 end
execute_function(function_hash) click to toggle source

Returns the result of an executed function

    # File lib/mcollective/matcher.rb
 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     begin
100       eval_result = result.send(function_hash["value"])
101     rescue
102       # If data field has not been set we set the comparison result to nil
103       eval_result = nil
104     end
105     return eval_result
106   else
107     return result
108   end
109 rescue NoMethodError
110   Log.debug("cannot execute discovery function '#{function_hash["name"]}'. data plugin not found")
111   raise DDLValidationError
112 end