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 181 181: def self.create_compound_callstack(call_string) 182: callstack = Matcher::Parser.new(call_string).execution_stack 183: callstack.each_with_index do |statement, i| 184: if statement.keys.first == "fstatement" 185: callstack[i]["fstatement"] = create_function_hash(statement.values.first) 186: end 187: end 188: callstack 189: 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 135 135: def self.eval_compound_fstatement(function_hash) 136: l_compare = execute_function(function_hash) 137: 138: # Break out early and return false if the function returns nil 139: return false unless l_compare 140: 141: # Prevent unwanted discovery by limiting comparison operators 142: # on Strings and Booleans 143: if((l_compare.is_a?(String) || l_compare.is_a?(TrueClass) || l_compare.is_a?(FalseClass)) && function_hash["operator"].match(/<|>/)) 144: Log.debug "Cannot do > and < comparison on Booleans and Strings '#{l_compare} #{function_hash["operator"]} #{function_hash["r_compare"]}'" 145: return false 146: end 147: 148: # Prevent backticks in function parameters 149: if function_hash["params"] =~ /`/ 150: Log.debug("Cannot use backticks in function parameters") 151: return false 152: end 153: 154: # Escape strings for evaluation 155: function_hash["r_compare"] = "\"#{function_hash["r_compare"]}\"" if(l_compare.is_a?(String) && !(function_hash["operator"] =~ /=~|!=~/)) 156: 157: # Do a regex comparison if right compare string is a regex 158: if function_hash["operator"] =~ /(=~|!=~)/ 159: # Fail if left compare value isn't a string 160: unless l_compare.is_a?(String) 161: Log.debug("Cannot do a regex check on a non string value.") 162: return false 163: else 164: compare_result = l_compare.match(function_hash["r_compare"]) 165: # Flip return value for != operator 166: if function_hash["operator"] == "!=~" 167: !((compare_result.nil?) ? false : true) 168: else 169: (compare_result.nil?) ? false : true 170: end 171: end 172: # Otherwise evaluate the logical comparison 173: else 174: l_compare = "\"#{l_compare}\"" if l_compare.is_a?(String) 175: result = eval("#{l_compare} #{function_hash["operator"]} #{function_hash["r_compare"]}") 176: (result.nil?) ? false : result 177: end 178: end
Evaluates a compound statement
# File lib/mcollective/matcher.rb, line 115 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
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: 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
Disabled; run with --debug to generate this.
Generated with the Darkfish Rdoc Generator 1.1.6.