Statistics
| Branch: | Tag: | Revision:

root / src / Ganeti / Luxi.hs @ 1c474f2b

History | View | Annotate | Download (11.8 kB)

1
{-# LANGUAGE TemplateHaskell #-}
2

    
3
{-| Implementation of the Ganeti LUXI interface.
4

    
5
-}
6

    
7
{-
8

    
9
Copyright (C) 2009, 2010, 2011, 2012, 2013 Google Inc.
10

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

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

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

    
26
-}
27

    
28
module Ganeti.Luxi
29
  ( LuxiOp(..)
30
  , LuxiReq(..)
31
  , Client
32
  , Server
33
  , JobId
34
  , fromJobId
35
  , makeJobId
36
  , RecvResult(..)
37
  , strOfOp
38
  , getLuxiClient
39
  , getLuxiServer
40
  , acceptClient
41
  , closeClient
42
  , closeServer
43
  , callMethod
44
  , submitManyJobs
45
  , queryJobsStatus
46
  , buildCall
47
  , buildResponse
48
  , decodeLuxiCall
49
  , recvMsg
50
  , recvMsgExt
51
  , sendMsg
52
  , allLuxiCalls
53
  ) where
54

    
55
import Control.Applicative (optional)
56
import Control.Monad
57
import qualified Data.ByteString.UTF8 as UTF8
58
import Text.JSON (encodeStrict, decodeStrict)
59
import qualified Text.JSON as J
60
import Text.JSON.Pretty (pp_value)
61
import Text.JSON.Types
62

    
63
import Ganeti.BasicTypes
64
import Ganeti.Constants
65
import Ganeti.Errors
66
import Ganeti.JSON
67
import Ganeti.UDSServer
68
import Ganeti.OpParams (pTagsObject)
69
import Ganeti.OpCodes
70
import qualified Ganeti.Query.Language as Qlang
71
import Ganeti.Runtime (GanetiDaemon(..))
72
import Ganeti.THH
73
import Ganeti.THH.Field
74
import Ganeti.Types
75

    
76

    
77
-- | Currently supported Luxi operations and JSON serialization.
78
$(genLuxiOp "LuxiOp"
79
  [ (luxiReqQuery,
80
    [ simpleField "what"    [t| Qlang.ItemType |]
81
    , simpleField "fields"  [t| [String]  |]
82
    , simpleField "qfilter" [t| Qlang.Filter Qlang.FilterField |]
83
    ])
84
  , (luxiReqQueryFields,
85
    [ simpleField "what"    [t| Qlang.ItemType |]
86
    , simpleField "fields"  [t| [String]  |]
87
    ])
88
  , (luxiReqQueryNodes,
89
     [ simpleField "names"  [t| [String] |]
90
     , simpleField "fields" [t| [String] |]
91
     , simpleField "lock"   [t| Bool     |]
92
     ])
93
  , (luxiReqQueryGroups,
94
     [ simpleField "names"  [t| [String] |]
95
     , simpleField "fields" [t| [String] |]
96
     , simpleField "lock"   [t| Bool     |]
97
     ])
98
  , (luxiReqQueryNetworks,
99
     [ simpleField "names"  [t| [String] |]
100
     , simpleField "fields" [t| [String] |]
101
     , simpleField "lock"   [t| Bool     |]
102
     ])
103
  , (luxiReqQueryInstances,
104
     [ simpleField "names"  [t| [String] |]
105
     , simpleField "fields" [t| [String] |]
106
     , simpleField "lock"   [t| Bool     |]
107
     ])
108
  , (luxiReqQueryJobs,
109
     [ simpleField "ids"    [t| [JobId]  |]
110
     , simpleField "fields" [t| [String] |]
111
     ])
112
  , (luxiReqQueryExports,
113
     [ simpleField "nodes" [t| [String] |]
114
     , simpleField "lock"  [t| Bool     |]
115
     ])
116
  , (luxiReqQueryConfigValues,
117
     [ simpleField "fields" [t| [String] |] ]
118
    )
119
  , (luxiReqQueryClusterInfo, [])
120
  , (luxiReqQueryTags,
121
     [ pTagsObject
122
     , simpleField "name" [t| String |]
123
     ])
124
  , (luxiReqSubmitJob,
125
     [ simpleField "job" [t| [MetaOpCode] |] ]
126
    )
127
  , (luxiReqSubmitJobToDrainedQueue,
128
     [ simpleField "job" [t| [MetaOpCode] |] ]
129
    )
130
  , (luxiReqSubmitManyJobs,
131
     [ simpleField "ops" [t| [[MetaOpCode]] |] ]
132
    )
133
  , (luxiReqWaitForJobChange,
134
     [ simpleField "job"      [t| JobId   |]
135
     , simpleField "fields"   [t| [String]|]
136
     , simpleField "prev_job" [t| JSValue |]
137
     , simpleField "prev_log" [t| JSValue |]
138
     , simpleField "tmout"    [t| Int     |]
139
     ])
140
  , (luxiReqPickupJob,
141
     [ simpleField "job" [t| JobId |] ]
142
    )
143
  , (luxiReqArchiveJob,
144
     [ simpleField "job" [t| JobId |] ]
145
    )
146
  , (luxiReqAutoArchiveJobs,
147
     [ simpleField "age"   [t| Int |]
148
     , simpleField "tmout" [t| Int |]
149
     ])
150
  , (luxiReqCancelJob,
151
     [ simpleField "job" [t| JobId |] ]
152
    )
153
  , (luxiReqChangeJobPriority,
154
     [ simpleField "job"      [t| JobId |]
155
     , simpleField "priority" [t| Int |] ]
156
    )
157
  , (luxiReqSetDrainFlag,
158
     [ simpleField "flag" [t| Bool |] ]
159
    )
160
  , (luxiReqSetWatcherPause,
161
     [ optionalNullSerField
162
         $ timeAsDoubleField "duration" ]
163
    )
164
  ])
165

    
166
$(makeJSONInstance ''LuxiReq)
167

    
168
-- | List of all defined Luxi calls.
169
$(genAllConstr (drop 3) ''LuxiReq "allLuxiCalls")
170

    
171
-- | The serialisation of LuxiOps into strings in messages.
172
$(genStrOfOp ''LuxiOp "strOfOp")
173

    
174

    
175
luxiConnectConfig :: ConnectConfig
176
luxiConnectConfig = ConnectConfig { connDaemon = GanetiLuxid
177
                                  , recvTmo    = luxiDefRwto
178
                                  , sendTmo    = luxiDefRwto
179
                                  }
180

    
181
-- | Connects to the master daemon and returns a luxi Client.
182
getLuxiClient :: String -> IO Client
183
getLuxiClient = connectClient luxiConnectConfig luxiDefCtmo
184

    
185
-- | Creates and returns a server endpoint.
186
getLuxiServer :: Bool -> FilePath -> IO Server
187
getLuxiServer = connectServer luxiConnectConfig
188

    
189
-- | Serialize a request to String.
190
buildCall :: LuxiOp  -- ^ The method
191
          -> String  -- ^ The serialized form
192
buildCall lo =
193
  let ja = [ (strOfKey Method, J.showJSON $ strOfOp lo)
194
           , (strOfKey Args, opToArgs lo)
195
           ]
196
      jo = toJSObject ja
197
  in encodeStrict jo
198

    
199

    
200
-- | Converts Luxi call arguments into a 'LuxiOp' data structure.
201
-- This is used for building a Luxi 'Handler'.
202
--
203
-- This is currently hand-coded until we make it more uniform so that
204
-- it can be generated using TH.
205
decodeLuxiCall :: JSValue -> JSValue -> Result LuxiOp
206
decodeLuxiCall method args = do
207
  call <- fromJResult "Unable to parse LUXI request method" $ J.readJSON method
208
  case call of
209
    ReqQueryJobs -> do
210
              (jids, jargs) <- fromJVal args
211
              jids' <- case jids of
212
                         JSNull -> return []
213
                         _ -> fromJVal jids
214
              return $ QueryJobs jids' jargs
215
    ReqQueryInstances -> do
216
              (names, fields, locking) <- fromJVal args
217
              return $ QueryInstances names fields locking
218
    ReqQueryNodes -> do
219
              (names, fields, locking) <- fromJVal args
220
              return $ QueryNodes names fields locking
221
    ReqQueryGroups -> do
222
              (names, fields, locking) <- fromJVal args
223
              return $ QueryGroups names fields locking
224
    ReqQueryClusterInfo ->
225
              return QueryClusterInfo
226
    ReqQueryNetworks -> do
227
              (names, fields, locking) <- fromJVal args
228
              return $ QueryNetworks names fields locking
229
    ReqQuery -> do
230
              (what, fields, qfilter) <- fromJVal args
231
              return $ Query what fields qfilter
232
    ReqQueryFields -> do
233
              (what, fields) <- fromJVal args
234
              fields' <- case fields of
235
                           JSNull -> return []
236
                           _ -> fromJVal fields
237
              return $ QueryFields what fields'
238
    ReqSubmitJob -> do
239
              [ops1] <- fromJVal args
240
              ops2 <- mapM (fromJResult (luxiReqToRaw call) . J.readJSON) ops1
241
              return $ SubmitJob ops2
242
    ReqSubmitJobToDrainedQueue -> do
243
              [ops1] <- fromJVal args
244
              ops2 <- mapM (fromJResult (luxiReqToRaw call) . J.readJSON) ops1
245
              return $ SubmitJobToDrainedQueue ops2
246
    ReqSubmitManyJobs -> do
247
              [ops1] <- fromJVal args
248
              ops2 <- mapM (fromJResult (luxiReqToRaw call) . J.readJSON) ops1
249
              return $ SubmitManyJobs ops2
250
    ReqWaitForJobChange -> do
251
              (jid, fields, pinfo, pidx, wtmout) <-
252
                -- No instance for 5-tuple, code copied from the
253
                -- json sources and adapted
254
                fromJResult "Parsing WaitForJobChange message" $
255
                case args of
256
                  JSArray [a, b, c, d, e] ->
257
                    (,,,,) `fmap`
258
                    J.readJSON a `ap`
259
                    J.readJSON b `ap`
260
                    J.readJSON c `ap`
261
                    J.readJSON d `ap`
262
                    J.readJSON e
263
                  _ -> J.Error "Not enough values"
264
              return $ WaitForJobChange jid fields pinfo pidx wtmout
265
    ReqPickupJob -> do
266
              [jid] <- fromJVal args
267
              return $ PickupJob jid
268
    ReqArchiveJob -> do
269
              [jid] <- fromJVal args
270
              return $ ArchiveJob jid
271
    ReqAutoArchiveJobs -> do
272
              (age, tmout) <- fromJVal args
273
              return $ AutoArchiveJobs age tmout
274
    ReqQueryExports -> do
275
              (nodes, lock) <- fromJVal args
276
              return $ QueryExports nodes lock
277
    ReqQueryConfigValues -> do
278
              [fields] <- fromJVal args
279
              return $ QueryConfigValues fields
280
    ReqQueryTags -> do
281
              (kind, name) <- fromJVal args
282
              return $ QueryTags kind name
283
    ReqCancelJob -> do
284
              [jid] <- fromJVal args
285
              return $ CancelJob jid
286
    ReqChangeJobPriority -> do
287
              (jid, priority) <- fromJVal args
288
              return $ ChangeJobPriority jid priority
289
    ReqSetDrainFlag -> do
290
              [flag] <- fromJVal args
291
              return $ SetDrainFlag flag
292
    ReqSetWatcherPause -> do
293
              duration <- optional $ do
294
                [x] <- fromJVal args
295
                liftM unTimeAsDoubleJSON $ fromJVal x
296
              return $ SetWatcherPause duration
297

    
298
-- | Check that luxi responses contain the required keys and that the
299
-- call was successful.
300
validateResult :: String -> ErrorResult JSValue
301
validateResult s = do
302
  when (UTF8.replacement_char `elem` s) $
303
       fail "Failed to decode UTF-8, detected replacement char after decoding"
304
  oarr <- fromJResult "Parsing LUXI response" (decodeStrict s)
305
  let arr = J.fromJSObject oarr
306
  status <- fromObj arr (strOfKey Success)
307
  result <- fromObj arr (strOfKey Result)
308
  if status
309
    then return result
310
    else decodeError result
311

    
312
-- | Try to decode an error from the server response. This function
313
-- will always fail, since it's called only on the error path (when
314
-- status is False).
315
decodeError :: JSValue -> ErrorResult JSValue
316
decodeError val =
317
  case fromJVal val of
318
    Ok e -> Bad e
319
    Bad msg -> Bad $ GenericError msg
320

    
321
-- | Generic luxi method call.
322
callMethod :: LuxiOp -> Client -> IO (ErrorResult JSValue)
323
callMethod method s = do
324
  sendMsg s $ buildCall method
325
  result <- recvMsg s
326
  let rval = validateResult result
327
  return rval
328

    
329
-- | Parse job submission result.
330
parseSubmitJobResult :: JSValue -> ErrorResult JobId
331
parseSubmitJobResult (JSArray [JSBool True, v]) =
332
  case J.readJSON v of
333
    J.Error msg -> Bad $ LuxiError msg
334
    J.Ok v' -> Ok v'
335
parseSubmitJobResult (JSArray [JSBool False, JSString x]) =
336
  Bad . LuxiError $ fromJSString x
337
parseSubmitJobResult v =
338
  Bad . LuxiError $ "Unknown result from the master daemon: " ++
339
      show (pp_value v)
340

    
341
-- | Specialized submitManyJobs call.
342
submitManyJobs :: Client -> [[MetaOpCode]] -> IO (ErrorResult [JobId])
343
submitManyJobs s jobs = do
344
  rval <- callMethod (SubmitManyJobs jobs) s
345
  -- map each result (status, payload) pair into a nice Result ADT
346
  return $ case rval of
347
             Bad x -> Bad x
348
             Ok (JSArray r) -> mapM parseSubmitJobResult r
349
             x -> Bad . LuxiError $
350
                  "Cannot parse response from Ganeti: " ++ show x
351

    
352
-- | Custom queryJobs call.
353
queryJobsStatus :: Client -> [JobId] -> IO (ErrorResult [JobStatus])
354
queryJobsStatus s jids = do
355
  rval <- callMethod (QueryJobs jids ["status"]) s
356
  return $ case rval of
357
             Bad x -> Bad x
358
             Ok y -> case J.readJSON y::(J.Result [[JobStatus]]) of
359
                       J.Ok vals -> if any null vals
360
                                    then Bad $
361
                                         LuxiError "Missing job status field"
362
                                    else Ok (map head vals)
363
                       J.Error x -> Bad $ LuxiError x