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