| 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}
Creates a callstack to be evaluated from a compound evaluation string
# 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
# 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
# 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
# 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
# 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