Revision a7e484c4

b/Makefile.am
496 496
	htools/Ganeti/Query/Common.hs \
497 497
	htools/Ganeti/Query/Filter.hs \
498 498
	htools/Ganeti/Query/Group.hs \
499
	htools/Ganeti/Query/Job.hs \
499 500
	htools/Ganeti/Query/Language.hs \
500 501
	htools/Ganeti/Query/Node.hs \
501 502
	htools/Ganeti/Query/Query.hs \
b/htest/Test/Ganeti/Query/Query.hs
44 44

  
45 45
import Ganeti.BasicTypes
46 46
import Ganeti.Errors
47
import Ganeti.Query.Filter
47 48
import Ganeti.Query.Group
48 49
import Ganeti.Query.Language
49 50
import Ganeti.Query.Node
50 51
import Ganeti.Query.Query
52
import qualified Ganeti.Query.Job as Job
51 53

  
52 54
{-# ANN module "HLint: ignore Use camelCase" #-}
53 55

  
......
59 61

  
60 62
-- * Test cases
61 63

  
64
-- ** Node queries
65

  
62 66
-- | Tests that querying any existing fields, via either query or
63 67
-- queryFields, will not return unknown fields.
64 68
prop_queryNode_noUnknown :: Property
......
159 163
     (sortBy field_sort . map (\(f, _, _) -> f) $ Map.elems nodeFieldsMap)
160 164
     (sortBy field_sort fdefs)
161 165

  
162
-- * Same as above, but for group
166
-- ** Group queries
163 167

  
164 168
prop_queryGroup_noUnknown :: Property
165 169
prop_queryGroup_noUnknown =
......
231 235
     (sortBy field_sort . map (\(f, _, _) -> f) $ Map.elems groupFieldsMap)
232 236
     (sortBy field_sort fdefs)
233 237

  
238
-- ** Job queries
239

  
240
-- | Tests that querying any existing fields, via either query or
241
-- queryFields, will not return unknown fields. This uses 'undefined'
242
-- for config, as job queries shouldn't use the configuration, and an
243
-- explicit filter as otherwise non-live queries wouldn't return any
244
-- result rows.
245
prop_queryJob_noUnknown :: Property
246
prop_queryJob_noUnknown =
247
  forAll (listOf (arbitrary::Gen (Positive Integer))) $ \ids ->
248
  forAll (elements (Map.keys Job.fieldsMap)) $ \field -> monadicIO $ do
249
  let qtype = ItemTypeLuxi QRJob
250
      flt = makeSimpleFilter (nameField qtype) $
251
            map (\(Positive i) -> Right i) ids
252
  QueryResult fdefs fdata <-
253
    run (query undefined False (Query qtype [field] flt)) >>= resultProp
254
  QueryFieldsResult fdefs' <-
255
    resultProp $ queryFields (QueryFields qtype [field])
256
  stop $ conjoin
257
         [ printTestCase ("Got unknown fields via query (" ++
258
                          show fdefs ++ ")") (hasUnknownFields fdefs)
259
         , printTestCase ("Got unknown result status via query (" ++
260
                          show fdata ++ ")")
261
           (all (all ((/= RSUnknown) . rentryStatus)) fdata)
262
         , printTestCase ("Got unknown fields via query fields (" ++
263
                          show fdefs'++ ")") (hasUnknownFields fdefs')
264
         ]
265

  
266
-- | Tests that an unknown field is returned as such.
267
prop_queryJob_Unknown :: Property
268
prop_queryJob_Unknown =
269
  forAll (listOf (arbitrary::Gen (Positive Integer))) $ \ids ->
270
  forAll (arbitrary `suchThat` (`notElem` Map.keys Job.fieldsMap))
271
    $ \field -> monadicIO $ do
272
  let qtype = ItemTypeLuxi QRJob
273
      flt = makeSimpleFilter (nameField qtype) $
274
            map (\(Positive i) -> Right i) ids
275
  QueryResult fdefs fdata <-
276
    run (query undefined False (Query qtype [field] flt)) >>= resultProp
277
  QueryFieldsResult fdefs' <-
278
    resultProp $ queryFields (QueryFields qtype [field])
279
  stop $ conjoin
280
         [ printTestCase ("Got known fields via query (" ++ show fdefs ++ ")")
281
           (not $ hasUnknownFields fdefs)
282
         , printTestCase ("Got /= ResultUnknown result status via query (" ++
283
                          show fdata ++ ")")
284
           (all (all ((== RSUnknown) . rentryStatus)) fdata)
285
         , printTestCase ("Got a Just in a result value (" ++
286
                          show fdata ++ ")")
287
           (all (all (isNothing . rentryValue)) fdata)
288
         , printTestCase ("Got known fields via query fields (" ++ show fdefs'
289
                          ++ ")") (not $ hasUnknownFields fdefs')
290
         ]
291

  
292
-- ** Misc other tests
234 293

  
235 294
-- | Tests that requested names checking behaves as expected.
236 295
prop_getRequestedNames :: Property
......
258 317
  , 'prop_queryGroup_Unknown
259 318
  , 'prop_queryGroup_types
260 319
  , 'case_queryGroup_allfields
320
  , 'prop_queryJob_noUnknown
321
  , 'prop_queryJob_Unknown
261 322
  , 'prop_getRequestedNames
262 323
  ]
b/htools/Ganeti/Query/Job.hs
1
{-| Implementation of the Ganeti Query2 job queries.
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
module Ganeti.Query.Job
27
  ( RuntimeData
28
  , fieldsMap
29
  , loadRuntimeData
30
  , wantArchived
31
  ) where
32

  
33
import qualified Data.Map as Map
34
import qualified Text.JSON as J
35

  
36
import Ganeti.BasicTypes
37
import qualified Ganeti.Constants as C
38
import Ganeti.JQueue
39
import Ganeti.OpCodes (opSummary, metaOpCode)
40
import Ganeti.Path
41
import Ganeti.Query.Common
42
import Ganeti.Query.Language
43
import Ganeti.Query.Types
44
import Ganeti.Types
45

  
46
-- | The runtime data for a job.
47
type RuntimeData = Result (QueuedJob, Bool)
48

  
49
-- | Job priority explanation.
50
jobPrioDoc :: String
51
jobPrioDoc = "Current job priority (" ++ show C.opPrioLowest ++ " to " ++
52
             show C.opPrioHighest ++ ")"
53

  
54
-- | Timestamp doc.
55
tsDoc :: String -> String
56
tsDoc = (++ " (tuple containing seconds and microseconds)")
57

  
58
-- | Wrapper for unavailable job.
59
maybeJob :: (J.JSON a) =>
60
            (QueuedJob -> a) -> RuntimeData -> JobId -> ResultEntry
61
maybeJob _ (Bad _) _      = rsUnavail
62
maybeJob f (Ok (v, _))  _ = rsNormal $ f v
63

  
64
-- | Simple helper for a job getter.
65
jobGetter :: (J.JSON a) => (QueuedJob -> a) -> FieldGetter JobId RuntimeData
66
jobGetter = FieldRuntime . maybeJob
67

  
68
-- | Simple helper for a per-opcode getter.
69
opsGetter :: (J.JSON a) => (QueuedOpCode -> a) -> FieldGetter JobId RuntimeData
70
opsGetter f = FieldRuntime $ maybeJob (map f . qjOps)
71

  
72
-- | Archived field name.
73
archivedField :: String
74
archivedField = "archived"
75

  
76
-- | Check whether we should look at archived jobs as well.
77
wantArchived :: [FilterField] -> Bool
78
wantArchived = (archivedField `elem`)
79

  
80
-- | List of all node fields. FIXME: QFF_JOB_ID on the id field.
81
jobFields :: FieldList JobId RuntimeData
82
jobFields =
83
  [ (FieldDefinition "id" "ID" QFTNumber "Job ID", FieldSimple rsNormal,
84
     QffNormal)
85
  , (FieldDefinition "status" "Status" QFTText "Job status",
86
     jobGetter calcJobStatus, QffNormal)
87
  , (FieldDefinition "priority" "Priority" QFTNumber jobPrioDoc,
88
     jobGetter calcJobPriority, QffNormal)
89
  , (FieldDefinition archivedField "Archived" QFTBool
90
       "Whether job is archived",
91
     FieldRuntime (\jinfo _ -> case jinfo of
92
                                 Ok (_, archive) -> rsNormal archive
93
                                 _ -> rsUnavail), QffNormal)
94
  , (FieldDefinition "ops" "OpCodes" QFTOther "List of all opcodes",
95
     opsGetter qoInput, QffNormal)
96
  , (FieldDefinition "opresult" "OpCode_result" QFTOther
97
       "List of opcodes results", opsGetter qoResult, QffNormal)
98
  , (FieldDefinition "opstatus" "OpCode_status" QFTOther
99
       "List of opcodes status", opsGetter qoStatus, QffNormal)
100
  , (FieldDefinition "oplog" "OpCode_log" QFTOther
101
       "List of opcode output logs", opsGetter qoLog, QffNormal)
102
  , (FieldDefinition "opstart" "OpCode_start" QFTOther
103
       "List of opcode start timestamps (before acquiring locks)",
104
     opsGetter qoStartTimestamp, QffNormal)
105
  , (FieldDefinition "opexec" "OpCode_exec" QFTOther
106
       "List of opcode execution start timestamps (after acquiring locks)",
107
     opsGetter qoExecTimestamp, QffNormal)
108
  , (FieldDefinition "opend" "OpCode_end" QFTOther
109
       "List of opcode execution end timestamps",
110
     opsGetter qoEndTimestamp, QffNormal)
111
  , (FieldDefinition "oppriority" "OpCode_prio" QFTOther
112
       "List of opcode priorities", opsGetter qoPriority, QffNormal)
113
  , (FieldDefinition "summary" "Summary" QFTOther
114
       "List of per-opcode summaries",
115
     opsGetter (opSummary . metaOpCode . qoInput), QffNormal)
116
  , (FieldDefinition "received_ts" "Received" QFTOther
117
       (tsDoc "Timestamp of when job was received"),
118
     jobGetter qjReceivedTimestamp, QffTimestamp)
119
  , (FieldDefinition "start_ts" "Start" QFTOther
120
       (tsDoc "Timestamp of job start"),
121
     jobGetter qjStartTimestamp, QffTimestamp)
122
  , (FieldDefinition "end_ts" "End" QFTOther
123
       (tsDoc "Timestamp of job end"),
124
     jobGetter qjEndTimestamp, QffTimestamp)
125
  ]
126

  
127
-- | The node fields map.
128
fieldsMap :: FieldMap JobId RuntimeData
129
fieldsMap =
130
  Map.fromList $ map (\v@(f, _, _) -> (fdefName f, v)) jobFields
131

  
132
-- | Load the given jobs from disk.
133
loadRuntimeData :: [JobId] -> Bool -> IO [RuntimeData]
134
loadRuntimeData ids archived = do
135
  qdir <- queueDir
136
  mapM (loadJobFromDisk qdir archived) ids
b/htools/Ganeti/Query/Query.hs
52 52
    , nameField
53 53
    ) where
54 54

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

  
62 63
import Ganeti.BasicTypes
63
import Ganeti.Errors
64 64
import Ganeti.Config
65
import Ganeti.Errors
66
import Ganeti.JQueue
65 67
import Ganeti.JSON
66 68
import Ganeti.Rpc
67
import Ganeti.Query.Language
69
import Ganeti.Objects
68 70
import Ganeti.Query.Common
69 71
import Ganeti.Query.Filter
70
import Ganeti.Query.Types
71
import Ganeti.Query.Node
72
import qualified Ganeti.Query.Job as Query.Job
72 73
import Ganeti.Query.Group
73
import Ganeti.Objects
74
import Ganeti.Query.Language
75
import Ganeti.Query.Node
76
import Ganeti.Query.Types
77
import Ganeti.Path
78
import Ganeti.Types
74 79
import Ganeti.Utils
75 80

  
76 81
-- * Helper functions
......
144 149
    Just names -> getAllQuotedStrings names
145 150
    Nothing    -> []
146 151

  
152
-- | Compute the requested job IDs. This is custom since we need to
153
-- handle both strings and integers.
154
getRequestedJobIDs :: Filter FilterField -> Result [JobId]
155
getRequestedJobIDs qfilter =
156
  case requestedNames (nameField (ItemTypeLuxi QRJob)) qfilter of
157
    Nothing -> Ok []
158
    Just [] -> Ok []
159
    Just vals ->
160
      mapM (\e -> case e of
161
                    QuotedString s -> makeJobIdS s
162
                    NumericValue i -> makeJobId $ fromIntegral i
163
           ) vals
164

  
147 165
-- | Main query execution function.
148 166
query :: ConfigData   -- ^ The current configuration
149 167
      -> Bool         -- ^ Whether to collect live data
150 168
      -> Query        -- ^ The query (item, fields, filter)
151 169
      -> IO (ErrorResult QueryResult) -- ^ Result
170
query cfg live (Query (ItemTypeLuxi QRJob) fields qfilter) =
171
  queryJobs cfg live fields qfilter
152 172
query cfg live qry = queryInner cfg live qry $ getRequestedNames qry
153 173

  
154 174
-- | Inner query execution function.
......
197 217
queryInner _ _ (Query qkind _ _) _ =
198 218
  return . Bad . GenericError $ "Query '" ++ show qkind ++ "' not supported"
199 219

  
220
-- | Query jobs specific query function, needed as we need to accept
221
-- both 'QuotedString' and 'NumericValue' as wanted names.
222
queryJobs :: ConfigData                   -- ^ The current configuration
223
          -> Bool                         -- ^ Whether to collect live data
224
          -> [FilterField]                -- ^ Item
225
          -> Filter FilterField           -- ^ Filter
226
          -> IO (ErrorResult QueryResult) -- ^ Result
227
queryJobs cfg live fields qfilter =
228
  runResultT $ do
229
  rootdir <- lift queueDir
230
  let wanted_names = getRequestedJobIDs qfilter
231
      want_arch = Query.Job.wantArchived fields
232
  rjids <- case wanted_names of
233
             Bad msg -> resultT . Bad $ GenericError msg
234
             Ok [] -> if live
235
                        -- we can check the filesystem for actual jobs
236
                        then lift $ liftM sortJobIDs
237
                             (determineJobDirectories rootdir want_arch >>=
238
                              getJobIDs)
239
                        -- else we shouldn't look at the filesystem...
240
                        else return []
241
             Ok v -> resultT $ Ok v
242
  cfilter <- resultT $ compileFilter Query.Job.fieldsMap qfilter
243
  let selected = getSelectedFields Query.Job.fieldsMap fields
244
      (fdefs, fgetters, _) = unzip3 selected
245
      live' = live && needsLiveData fgetters
246
      disabled_data = Bad "live data disabled"
247
  -- runs first pass of the filter, without a runtime context; this
248
  -- will limit the jobs that we'll load from disk
249
  jids <- resultT $
250
          filterM (\jid -> evaluateFilter cfg Nothing jid cfilter) rjids
251
  -- here we run the runtime data gathering, filtering and evaluation,
252
  -- all in the same step, so that we don't keep jobs in memory longer
253
  -- than we need; we can't be fully lazy due to the multiple monad
254
  -- wrapping across different steps
255
  qdir <- lift queueDir
256
  fdata <- foldM
257
           -- big lambda, but we use many variables from outside it...
258
           (\lst jid -> do
259
              job <- lift $ if live'
260
                              then loadJobFromDisk qdir want_arch jid
261
                              else return disabled_data
262
              pass <- resultT $ evaluateFilter cfg (Just job) jid cfilter
263
              let nlst = if pass
264
                           then let row = map (execGetter cfg job jid) fgetters
265
                                in rnf row `seq` row:lst
266
                           else lst
267
              -- evaluate nlst (to WHNF), otherwise we're too lazy
268
              return $! nlst
269
           ) [] jids
270
  return QueryResult { qresFields = fdefs, qresData = reverse fdata }
271

  
200 272
-- | Helper for 'queryFields'.
201 273
fieldsExtractor :: FieldMap a b -> [FilterField] -> QueryFieldsResult
202 274
fieldsExtractor fieldsMap fields =
......
213 285
queryFields (QueryFields (ItemTypeOpCode QRGroup) fields) =
214 286
  Ok $ fieldsExtractor groupFieldsMap fields
215 287

  
288
queryFields (QueryFields (ItemTypeLuxi QRJob) fields) =
289
  Ok $ fieldsExtractor Query.Job.fieldsMap fields
290

  
216 291
queryFields (QueryFields qkind _) =
217 292
  Bad . GenericError $ "QueryFields '" ++ show qkind ++ "' not supported"
218 293

  
b/htools/Ganeti/Query/Server.hs
163 163
  handleClassicQuery cfg (Qlang.ItemTypeOpCode Qlang.QRGroup)
164 164
    (map Left names) fields lock
165 165

  
166
handleCall cfg (QueryJobs names fields) =
167
  handleClassicQuery cfg (Qlang.ItemTypeLuxi Qlang.QRJob)
168
    (map (Right . fromIntegral . fromJobId) names)  fields False
169

  
166 170
handleCall _ op =
167 171
  return . Bad $
168 172
    GenericError ("Luxi call '" ++ strOfOp op ++ "' not implemented")

Also available in: Unified diff