module MCollective::Matcher - mcollective version 2.12.5

# 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