Module MCollective::Matcher
In: lib/mcollective/matcher.rb
lib/mcollective/matcher/parser.rb
lib/mcollective/matcher/scanner.rb

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}

Methods

Classes and Modules

Class MCollective::Matcher::Parser
Class MCollective::Matcher::Scanner

Public Class methods

Creates a callstack to be evaluated from a compound evaluation string

[Source]

     # File lib/mcollective/matcher.rb, line 173
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

Helper creates a hash from a function call string

[Source]

    # File lib/mcollective/matcher.rb, line 17
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

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

[Source]

     # File lib/mcollective/matcher.rb, line 130
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

Evaluates a compound statement

[Source]

     # File lib/mcollective/matcher.rb, line 110
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

Returns the result of an executed function

[Source]

     # File lib/mcollective/matcher.rb, line 78
 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

[Validate]