blob: 0d36ff20a0a2745b041a014f64f027130389971b [file] [log] [blame]
{-# LANGUAGE RankNTypes, GADTs, StandaloneDeriving #-}
{-| Implementation of the Ganeti Query2 filterning.
The filtering of results should be done in two phases.
In the first phase, before contacting any remote nodes for runtime
data, the filtering should be executed with 'Nothing' for the runtime
context. This will make all non-runtime filters filter correctly,
whereas all runtime filters will respond successfully. As described in
the Python version too, this makes for example /Or/ filters very
inefficient if they contain runtime fields.
Once this first filtering phase has been done, we hopefully eliminated
some remote nodes out of the list of candidates, we run the remote
data gathering, and we evaluate the filter again, this time with a
'Just' runtime context. This will make all filters work correctly.
Note that the second run will re-evaluate the config/simple fields,
without caching; this is not perfect, but we consider config accesses
very cheap (and the configuration snapshot we have won't change
between the two runs, hence we will not get inconsistent results).
-}
{-
Copyright (C) 2012, 2013 Google Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-}
module Ganeti.Query.Filter
( compileFilter
, evaluateQueryFilter
, evaluateFilterM
, evaluateFilterJSON
, requestedNames
, makeSimpleFilter
, Comparator
, Comparison(..)
, toCompFun
, FilterOp(..)
) where
import Prelude ()
import Ganeti.Prelude
import Control.Monad (liftM, mzero)
import Control.Monad.Trans.Maybe (MaybeT, runMaybeT)
import Control.Monad.Trans.Class (lift)
import qualified Data.Map as Map
import Data.Maybe
import Text.JSON (JSValue(..), fromJSString)
import Text.JSON.Pretty (pp_value)
import qualified Text.Regex.PCRE as PCRE
import Ganeti.BasicTypes
import Ganeti.Errors
import Ganeti.Objects
import Ganeti.Query.Language
import Ganeti.Query.Types
import Ganeti.Utils.Monad (anyM, allM)
import Ganeti.JSON (fromJVal)
-- | Compiles a filter based on field names to one based on getters.
compileFilter :: FieldMap a b
-> Filter FilterField
-> ErrorResult (Filter (FieldGetter a b, QffMode))
compileFilter fm =
traverse (\field -> maybe
(Bad . ParameterError $ "Can't find field named '" ++
field ++ "'")
(\(_, g, q) -> Ok (g, q)) (field `Map.lookup` fm))
-- | Processes a field value given a QffMode.
qffField :: QffMode -> JSValue -> ErrorResult JSValue
qffField QffNormal v = Ok v
qffField QffHostname v = Ok v
qffField QffTimestamp v =
case v of
JSArray [secs@(JSRational _ _), JSRational _ _] -> return secs
_ -> Bad $ ProgrammerError
"Internal error: Getter returned non-timestamp for QffTimestamp"
-- | Wraps a getter, filter pair. If the getter is 'FieldRuntime' but
-- we don't have a runtime context, we skip the filtering, returning
-- `Nothing` in the MaybeT. Otherwise, we pass the actual value to the filter.
wrapGetter :: ConfigData
-> Maybe b
-> a
-> (FieldGetter a b, QffMode)
-> (JSValue -> ErrorResult Bool)
-> MaybeT ErrorResult Bool
wrapGetter cfg b a (getter, qff) faction =
case tryGetter cfg b a getter of
Nothing -> mzero -- runtime missing, signalling that with MaybeT Nothing
Just v -> lift $
case v of
ResultEntry RSNormal (Just fval) -> qffField qff fval >>= faction
ResultEntry RSNormal Nothing ->
Bad $ ProgrammerError
"Internal error: Getter returned RSNormal/Nothing"
_ -> Ok True -- filter has no data to work, accepting it
-- | Helper to evaluate a filter getter (and the value it generates) in
-- a boolean context.
trueFilter :: JSValue -> ErrorResult Bool
trueFilter (JSBool x) = Ok $! x
trueFilter v = Bad . ParameterError $
"Unexpected value '" ++ show (pp_value v) ++
"' in boolean context"
-- | A type synonim for a rank-2 comparator function. This is used so
-- that we can pass the usual '<=', '>', '==' functions to 'binOpFilter'
-- and for them to be used in multiple contexts.
type Comparator = forall a . (Eq a, Ord a) => a -> a -> Bool
-- | Equality checker.
--
-- This will handle hostnames correctly, if the mode is set to
-- 'QffHostname'.
eqFilter :: QffMode -> FilterValue -> JSValue -> ErrorResult Bool
-- send 'QffNormal' queries to 'binOpFilter'
eqFilter QffNormal flv jsv = binOpFilter (==) flv jsv
-- and 'QffTimestamp' as well
eqFilter QffTimestamp flv jsv = binOpFilter (==) flv jsv
-- error out if we set 'QffHostname' on a non-string field
eqFilter QffHostname _ (JSRational _ _) =
Bad . ProgrammerError $ "QffHostname field returned a numeric value"
-- test strings via 'compareNameComponent'
eqFilter QffHostname (QuotedString y) (JSString x) =
Ok $ goodLookupResult (fromJSString x `compareNameComponent` y)
-- send all other combinations (all errors) to 'binOpFilter', which
-- has good error messages
eqFilter _ flv jsv = binOpFilter (==) flv jsv
-- | Helper to evaluate a filder getter (and the value it generates)
-- in a boolean context. Note the order of arguments is reversed from
-- the filter definitions (due to the call chain), make sure to
-- compare in the reverse order too!.
binOpFilter :: Comparator -> FilterValue -> JSValue -> ErrorResult Bool
binOpFilter comp (QuotedString y) (JSString x) =
Ok $! fromJSString x `comp` y
binOpFilter comp (NumericValue y) (JSRational _ x) =
Ok $! x `comp` fromIntegral y
binOpFilter _ expr actual =
Bad . ParameterError $ "Invalid types in comparison, trying to compare " ++
show (pp_value actual) ++ " with '" ++ show expr ++ "'"
-- | Implements the 'RegexpFilter' matching.
regexpFilter :: FilterRegex -> JSValue -> ErrorResult Bool
regexpFilter re (JSString val) =
Ok $! PCRE.match (compiledRegex re) (fromJSString val)
regexpFilter _ x =
Bad . ParameterError $ "Invalid field value used in regexp matching,\
\ expecting string but got '" ++ show (pp_value x) ++ "'"
-- | Implements the 'ContainsFilter' matching.
containsFilter :: FilterValue -> JSValue -> ErrorResult Bool
-- note: the next two implementations are the same, but we have to
-- repeat them due to the encapsulation done by FilterValue
containsFilter (QuotedString val) lst = do
lst' <- fromJVal lst :: ErrorResult [String]
return $! val `elem` lst'
containsFilter (NumericValue val) lst = do
lst' <- fromJVal lst :: ErrorResult [Integer]
return $! val `elem` lst'
-- | Ways we can compare things in the filter language.
data Comparison = Eq | Lt | Le | Gt | Ge
deriving (Eq, Ord, Show)
-- | Turns a comparison into the corresponding Haskell function.
toCompFun :: Comparison -> Comparator
toCompFun cmp = case cmp of
Eq -> (==)
Lt -> (<)
Le -> (<=)
Gt -> (>)
Ge -> (>=)
-- | Operations in the leaves of the Ganeti filter language.
data FilterOp field val where
Truth :: FilterOp field ()
Comp :: Comparison -> FilterOp field FilterValue
Regex :: FilterOp field FilterRegex
Contains :: FilterOp field FilterValue
deriving instance Eq (FilterOp field val)
deriving instance Show (FilterOp field val)
-- | Checks if a filter matches.
--
-- The leaves of the filter are evaluated against an object using the passed
-- `opFun`; that is why the object need not be passed in.
--
-- The `field` type describes the "accessors" that are used to query
-- values from the object; those values are to be matched against the
-- `val` type in the filter leaves.
--
-- Useful monads @m@ for this are `ErrorResult` and `Maybe`.
evaluateFilterM :: (Monad m, Applicative m)
=> (forall val .
FilterOp field val -> field -> val -> m Bool)
-> Filter field
-> m Bool
evaluateFilterM opFun fil = case fil of
EmptyFilter -> return True
AndFilter flts -> allM recurse flts
OrFilter flts -> anyM recurse flts
NotFilter flt -> not <$> recurse flt
TrueFilter field -> opFun Truth field ()
EQFilter field val -> opFun (Comp Eq) field val
LTFilter field val -> opFun (Comp Lt) field val
LEFilter field val -> opFun (Comp Le) field val
GTFilter field val -> opFun (Comp Gt) field val
GEFilter field val -> opFun (Comp Ge) field val
RegexpFilter field re -> opFun Regex field re
ContainsFilter field val -> opFun Contains field val
where
recurse = evaluateFilterM opFun
-- | Verifies if a given item passes a filter. The runtime context
-- might be missing, in which case most of the filters will consider
-- this as passing the filter.
evaluateQueryFilter :: ConfigData -> Maybe b -> a
-> Filter (FieldGetter a b, QffMode)
-> ErrorResult Bool
evaluateQueryFilter c mb a =
-- `Nothing` in the MaybeT means "missing but needed runtime context".
-- Turn those cases into True (let the filter pass).
fmap (fromMaybe True) . runMaybeT . evaluateFilterM (\op -> case op of
Truth -> \gQff () -> wrap gQff trueFilter
-- We're special casing comparison for host names.
-- All other comparisons behave as usual.
Comp Eq -> \gQff val -> wrap gQff $ eqFilter (snd gQff) val
Comp cmp -> \gQff val -> wrap gQff $ binOpFilter (toCompFun cmp) val
Regex -> \gQff re -> wrap gQff $ regexpFilter re
Contains -> \gQff val -> wrap gQff $ containsFilter val
)
where
wrap = wrapGetter c mb a
-- | Evaluates a `Filter` on a JSON object.
evaluateFilterJSON :: Filter JSValue -> ErrorResult Bool
evaluateFilterJSON =
evaluateFilterM $ \op -> case op of
Comp cmp -> let compFun = toCompFun cmp
in \json fv -> pure $ json `compFun` showFilterValue fv
Truth -> \field () -> trueFilter field
Regex -> flip regexpFilter
Contains -> flip containsFilter
-- | Runs a getter with potentially missing runtime context.
tryGetter :: ConfigData -> Maybe b -> a -> FieldGetter a b -> Maybe ResultEntry
tryGetter _ _ item (FieldSimple getter) = Just $ getter item
tryGetter cfg _ item (FieldConfig getter) = Just $ getter cfg item
tryGetter _ rt item (FieldRuntime getter) =
maybe Nothing (\rt' -> Just $ getter rt' item) rt
tryGetter cfg rt item (FieldConfigRuntime getter) =
maybe Nothing (\rt' -> Just $ getter cfg rt' item) rt
tryGetter _ _ _ FieldUnknown = Just $ ResultEntry RSUnknown Nothing
-- | Computes the requested names, if only names were requested (and
-- with equality). Otherwise returns 'Nothing'.
requestedNames :: FilterField -> Filter FilterField -> Maybe [FilterValue]
requestedNames _ EmptyFilter = Just []
requestedNames namefield (OrFilter flts) =
liftM concat $ mapM (requestedNames namefield) flts
requestedNames namefield (EQFilter fld val) =
if namefield == fld
then Just [val]
else Nothing
requestedNames _ _ = Nothing
-- | Builds a simple filter from a list of names.
makeSimpleFilter :: String -> [Either String Integer] -> Filter FilterField
makeSimpleFilter _ [] = EmptyFilter
makeSimpleFilter namefield vals =
OrFilter $ map (EQFilter namefield . either QuotedString NumericValue) vals