Statistics
| Branch: | Tag: | Revision:

root / htools / Ganeti / Query / Query.hs @ a2ae14e9

History | View | Annotate | Download (8.3 kB)

1
{-| Implementation of the Ganeti Query2 functionality.
2

    
3
 -}
4

    
5
{-
6

    
7
Copyright (C) 2012 Google Inc.
8

    
9
This program is free software; you can redistribute it and/or modify
10
it under the terms of the GNU General Public License as published by
11
the Free Software Foundation; either version 2 of the License, or
12
(at your option) any later version.
13

    
14
This program is distributed in the hope that it will be useful, but
15
WITHOUT ANY WARRANTY; without even the implied warranty of
16
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17
General Public License for more details.
18

    
19
You should have received a copy of the GNU General Public License
20
along with this program; if not, write to the Free Software
21
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
22
02110-1301, USA.
23

    
24
-}
25

    
26
{-
27

    
28
TODO: problems with the current model:
29

    
30
1. There's nothing preventing a result such as ResultEntry RSNormal
31
Nothing, or ResultEntry RSNoData (Just ...); ideally, we would
32
separate the the RSNormal and other types; we would need a new data
33
type for this, though, with JSON encoding/decoding
34

    
35
2. We don't have a way to 'bind' a FieldDefinition's field type
36
(e.q. QFTBool) with the actual value that is returned from a
37
FieldGetter. This means that the various getter functions can return
38
divergent types for the same field when evaluated against multiple
39
items. This is bad; it only works today because we 'hide' everything
40
behind JSValue, but is not nice at all. We should probably remove the
41
separation between FieldDefinition and the FieldGetter, and introduce
42
a new abstract data type, similar to QFT*, that contains the values
43
too.
44

    
45
-}
46

    
47
module Ganeti.Query.Query
48
    ( query
49
    , queryFields
50
    , queryCompat
51
    , getRequestedNames
52
    , nameField
53
    ) where
54

    
55
import Control.Monad (filterM)
56
import Control.Monad.Trans (lift)
57
import Data.List (intercalate)
58
import Data.Maybe (fromMaybe)
59
import qualified Data.Map as Map
60
import qualified Text.JSON as J
61

    
62
import Ganeti.BasicTypes
63
import Ganeti.Config
64
import Ganeti.JSON
65
import Ganeti.Rpc
66
import Ganeti.Query.Language
67
import Ganeti.Query.Common
68
import Ganeti.Query.Filter
69
import Ganeti.Query.Types
70
import Ganeti.Query.Node
71
import Ganeti.Query.Group
72
import Ganeti.Objects
73
import Ganeti.Utils
74

    
75
-- * Helper functions
76

    
77
-- | Builds an unknown field definition.
78
mkUnknownFDef :: String -> FieldData a b
79
mkUnknownFDef name =
80
  ( FieldDefinition name name QFTUnknown ("Unknown field '" ++ name ++ "'")
81
  , FieldUnknown )
82

    
83
-- | Runs a field getter on the existing contexts.
84
execGetter :: ConfigData -> b -> a -> FieldGetter a b -> ResultEntry
85
execGetter _   _ item (FieldSimple getter)  = getter item
86
execGetter cfg _ item (FieldConfig getter)  = getter cfg item
87
execGetter _  rt item (FieldRuntime getter) = getter rt item
88
execGetter _   _ _    FieldUnknown          = rsUnknown
89

    
90
-- * Main query execution
91

    
92
-- | Helper to build the list of requested fields. This transforms the
93
-- list of string fields to a list of field defs and getters, with
94
-- some of them possibly being unknown fields.
95
getSelectedFields :: FieldMap a b  -- ^ Defined fields
96
                  -> [String]      -- ^ Requested fields
97
                  -> FieldList a b -- ^ Selected fields
98
getSelectedFields defined =
99
  map (\name -> fromMaybe (mkUnknownFDef name) $ name `Map.lookup` defined)
100

    
101
-- | Collect live data from RPC query if enabled.
102
-- FIXME: Check which fields we actually need and possibly send empty
103
-- hvs/vgs if no info from hypervisor/volume group respectively
104
-- is required
105
maybeCollectLiveData:: Bool -> ConfigData -> [Node] -> IO [(Node, NodeRuntime)]
106

    
107
maybeCollectLiveData False _ nodes =
108
  return $ zip nodes (repeat $ Left (RpcResultError "Live data disabled"))
109

    
110
maybeCollectLiveData True cfg nodes = do
111
  let vgs = [clusterVolumeGroupName $ configCluster cfg]
112
      hvs = [getDefaultHypervisor cfg]
113
  executeRpcCall nodes (RpcCallNodeInfo vgs hvs)
114

    
115
-- | Check whether list of queried fields contains live fields.
116
needsLiveData :: [FieldGetter a b] -> Bool
117
needsLiveData = any isRuntimeField
118

    
119
-- | Checks whether we have requested exactly some names. This is a
120
-- simple wrapper over 'requestedNames' and 'nameField'.
121
needsNames :: Query -> Maybe [FilterValue]
122
needsNames (Query kind _ qfilter) = requestedNames (nameField kind) qfilter
123

    
124
-- | Computes the name field for different query types.
125
nameField :: ItemType -> FilterField
126
nameField QRJob = "id"
127
nameField _     = "name"
128

    
129
-- | Extracts all quoted strings from a list, ignoring the
130
-- 'NumericValue' entries.
131
getAllQuotedStrings :: [FilterValue] -> [String]
132
getAllQuotedStrings =
133
  concatMap extractor
134
    where extractor (NumericValue _)   = []
135
          extractor (QuotedString val) = [val]
136

    
137
-- | Checks that we have either requested a valid set of names, or we
138
-- have a more complex filter.
139
getRequestedNames :: Query -> [String]
140
getRequestedNames qry =
141
  case needsNames qry of
142
    Just names -> getAllQuotedStrings names
143
    Nothing    -> []
144

    
145
-- | Main query execution function.
146
query :: ConfigData   -- ^ The current configuration
147
      -> Bool         -- ^ Whether to collect live data
148
      -> Query        -- ^ The query (item, fields, filter)
149
      -> IO (Result QueryResult) -- ^ Result
150
query cfg live qry = queryInner cfg live qry $ getRequestedNames qry
151

    
152
-- | Inner query execution function.
153
queryInner :: ConfigData   -- ^ The current configuration
154
           -> Bool         -- ^ Whether to collect live data
155
           -> Query        -- ^ The query (item, fields, filter)
156
           -> [String]     -- ^ Requested names
157
           -> IO (Result QueryResult) -- ^ Result
158

    
159
queryInner cfg live (Query QRNode fields qfilter) wanted = runResultT $ do
160
  cfilter <- resultT $ compileFilter nodeFieldsMap qfilter
161
  let selected = getSelectedFields nodeFieldsMap fields
162
      (fdefs, fgetters) = unzip selected
163
      live' = live && needsLiveData fgetters
164
  nodes <- resultT $ case wanted of
165
             [] -> Ok . niceSortKey nodeName .
166
                   Map.elems . fromContainer $ configNodes cfg
167
             _  -> mapM (getNode cfg) wanted
168
  -- runs first pass of the filter, without a runtime context; this
169
  -- will limit the nodes that we'll contact for runtime data
170
  fnodes <- resultT $ filterM (\n -> evaluateFilter cfg Nothing n cfilter) nodes
171
  -- here we would run the runtime data gathering, then filter again
172
  -- the nodes, based on existing runtime data
173
  nruntimes <- lift $ maybeCollectLiveData live' cfg fnodes
174
  let fdata = map (\(node, nrt) -> map (execGetter cfg nrt node) fgetters)
175
              nruntimes
176
  return QueryResult { qresFields = fdefs, qresData = fdata }
177

    
178
queryInner cfg _ (Query QRGroup fields qfilter) wanted = return $ do
179
  cfilter <- compileFilter groupFieldsMap qfilter
180
  let selected = getSelectedFields groupFieldsMap fields
181
      (fdefs, fgetters) = unzip selected
182
  groups <- case wanted of
183
              [] -> Ok . niceSortKey groupName .
184
                    Map.elems . fromContainer $ configNodegroups cfg
185
              _  -> mapM (getGroup cfg) wanted
186
  -- there is no live data for groups, so filtering is much simpler
187
  fgroups <- filterM (\n -> evaluateFilter cfg Nothing n cfilter) groups
188
  let fdata = map (\node ->
189
                       map (execGetter cfg GroupRuntime node) fgetters) fgroups
190
  return QueryResult {qresFields = fdefs, qresData = fdata }
191

    
192
queryInner _ _ (Query qkind _ _) _ =
193
  return . Bad $ "Query '" ++ show qkind ++ "' not supported"
194

    
195
-- | Helper for 'queryFields'.
196
fieldsExtractor :: FieldMap a b -> [FilterField] -> QueryFieldsResult
197
fieldsExtractor fieldsMap fields =
198
  let selected = if null fields
199
                   then map snd $ Map.toAscList fieldsMap
200
                   else getSelectedFields fieldsMap fields
201
  in QueryFieldsResult (map fst selected)
202

    
203
-- | Query fields call.
204
queryFields :: QueryFields -> Result QueryFieldsResult
205
queryFields (QueryFields QRNode fields) =
206
  Ok $ fieldsExtractor nodeFieldsMap fields
207

    
208
queryFields (QueryFields QRGroup fields) =
209
  Ok $ fieldsExtractor groupFieldsMap fields
210

    
211
queryFields (QueryFields qkind _) =
212
  Bad $ "QueryFields '" ++ show qkind ++ "' not supported"
213

    
214
-- | Classic query converter. It gets a standard query result on input
215
-- and computes the classic style results.
216
queryCompat :: QueryResult -> Result [[J.JSValue]]
217
queryCompat (QueryResult fields qrdata) =
218
  case map fdefName $ filter ((== QFTUnknown) . fdefKind) fields of
219
    [] -> Ok $ map (map (maybe J.JSNull J.showJSON . rentryValue)) qrdata
220
    unknown -> Bad $ "Unknown output fields selected: " ++
221
                     intercalate ", " unknown