Statistics
| Branch: | Tag: | Revision:

root / src / Ganeti / Rpc.hs @ b9e12624

History | View | Annotate | Download (17.6 kB)

1
{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies,
2
  BangPatterns, TemplateHaskell #-}
3

    
4
{-| Implementation of the RPC client.
5

    
6
-}
7

    
8
{-
9

    
10
Copyright (C) 2012, 2013 Google Inc.
11

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

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

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

    
27
-}
28

    
29
module Ganeti.Rpc
30
  ( RpcCall
31
  , Rpc
32
  , RpcError(..)
33
  , ERpcError
34
  , explainRpcError
35
  , executeRpcCall
36
  , logRpcErrors
37

    
38
  , rpcCallName
39
  , rpcCallTimeout
40
  , rpcCallData
41
  , rpcCallAcceptOffline
42

    
43
  , rpcResultFill
44

    
45
  , InstanceInfo(..)
46
  , RpcCallInstanceInfo(..)
47
  , RpcResultInstanceInfo(..)
48

    
49
  , RpcCallAllInstancesInfo(..)
50
  , RpcResultAllInstancesInfo(..)
51

    
52
  , InstanceConsoleInfoParams(..)
53
  , InstanceConsoleInfo(..)
54
  , RpcCallInstanceConsoleInfo(..)
55
  , RpcResultInstanceConsoleInfo(..)
56

    
57
  , RpcCallInstanceList(..)
58
  , RpcResultInstanceList(..)
59

    
60
  , HvInfo(..)
61
  , StorageInfo(..)
62
  , RpcCallNodeInfo(..)
63
  , RpcResultNodeInfo(..)
64

    
65
  , RpcCallVersion(..)
66
  , RpcResultVersion(..)
67

    
68
  , RpcCallStorageList(..)
69
  , RpcResultStorageList(..)
70

    
71
  , RpcCallTestDelay(..)
72
  , RpcResultTestDelay(..)
73

    
74
  , RpcCallExportList(..)
75
  , RpcResultExportList(..)
76
  ) where
77

    
78
import Control.Arrow (second)
79
import qualified Data.Map as Map
80
import Data.Maybe (fromMaybe)
81
import qualified Text.JSON as J
82
import Text.JSON.Pretty (pp_value)
83

    
84
import Network.Curl
85
import qualified Ganeti.Path as P
86

    
87
import Ganeti.BasicTypes
88
import qualified Ganeti.Constants as C
89
import Ganeti.JSON
90
import Ganeti.Logging
91
import Ganeti.Objects
92
import Ganeti.THH
93
import Ganeti.Types
94
import Ganeti.Curl.Multi
95
import Ganeti.Utils
96

    
97
-- * Base RPC functionality and types
98

    
99
-- | The curl options used for RPC.
100
curlOpts :: [CurlOption]
101
curlOpts = [ CurlFollowLocation False
102
           , CurlSSLVerifyHost 0
103
           , CurlSSLVerifyPeer True
104
           , CurlSSLCertType "PEM"
105
           , CurlSSLKeyType "PEM"
106
           , CurlConnectTimeout (fromIntegral C.rpcConnectTimeout)
107
           ]
108

    
109
-- | Data type for RPC error reporting.
110
data RpcError
111
  = CurlLayerError String
112
  | JsonDecodeError String
113
  | RpcResultError String
114
  | OfflineNodeError
115
  deriving (Show, Eq)
116

    
117
-- | Provide explanation to RPC errors.
118
explainRpcError :: RpcError -> String
119
explainRpcError (CurlLayerError code) =
120
    "Curl error:" ++ code
121
explainRpcError (JsonDecodeError msg) =
122
    "Error while decoding JSON from HTTP response: " ++ msg
123
explainRpcError (RpcResultError msg) =
124
    "Error reponse received from RPC server: " ++ msg
125
explainRpcError OfflineNodeError =
126
    "Node is marked offline"
127

    
128
type ERpcError = Either RpcError
129

    
130
-- | A generic class for RPC calls.
131
class (J.JSON a) => RpcCall a where
132
  -- | Give the (Python) name of the procedure.
133
  rpcCallName :: a -> String
134
  -- | Calculate the timeout value for the call execution.
135
  rpcCallTimeout :: a -> Int
136
  -- | Prepare arguments of the call to be send as POST.
137
  rpcCallData :: Node -> a -> String
138
  -- | Whether we accept offline nodes when making a call.
139
  rpcCallAcceptOffline :: a -> Bool
140

    
141
-- | Generic class that ensures matching RPC call with its respective
142
-- result.
143
class (RpcCall a, J.JSON b) => Rpc a b  | a -> b, b -> a where
144
  -- | Create a result based on the received HTTP response.
145
  rpcResultFill :: a -> J.JSValue -> ERpcError b
146

    
147
-- | Http Request definition.
148
data HttpClientRequest = HttpClientRequest
149
  { requestUrl  :: String       -- ^ The actual URL for the node endpoint
150
  , requestData :: String       -- ^ The arguments for the call
151
  , requestOpts :: [CurlOption] -- ^ The various curl options
152
  }
153

    
154
-- | Prepare url for the HTTP request.
155
prepareUrl :: (RpcCall a) => Node -> a -> String
156
prepareUrl node call =
157
  let node_ip = nodePrimaryIp node
158
      port = C.defaultNodedPort
159
      path_prefix = "https://" ++ node_ip ++ ":" ++ show port
160
  in path_prefix ++ "/" ++ rpcCallName call
161

    
162
-- | Create HTTP request for a given node provided it is online,
163
-- otherwise create empty response.
164
prepareHttpRequest :: (RpcCall a) => [CurlOption] -> Node -> a
165
                   -> ERpcError HttpClientRequest
166
prepareHttpRequest opts node call
167
  | rpcCallAcceptOffline call || not (nodeOffline node) =
168
      Right HttpClientRequest { requestUrl  = prepareUrl node call
169
                              , requestData = rpcCallData node call
170
                              , requestOpts = opts ++ curlOpts
171
                              }
172
  | otherwise = Left OfflineNodeError
173

    
174
-- | Parse an HTTP reply.
175
parseHttpReply :: (Rpc a b) =>
176
                  a -> ERpcError (CurlCode, String) -> ERpcError b
177
parseHttpReply _ (Left e) = Left e
178
parseHttpReply call (Right (CurlOK, body)) = parseHttpResponse call body
179
parseHttpReply _ (Right (code, err)) =
180
  Left . CurlLayerError $ "code: " ++ show code ++ ", explanation: " ++ err
181

    
182
-- | Parse a result based on the received HTTP response.
183
parseHttpResponse :: (Rpc a b) => a -> String -> ERpcError b
184
parseHttpResponse call res =
185
  case J.decode res of
186
    J.Error val -> Left $ JsonDecodeError val
187
    J.Ok (True, res'') -> rpcResultFill call res''
188
    J.Ok (False, jerr) -> case jerr of
189
       J.JSString msg -> Left $ RpcResultError (J.fromJSString msg)
190
       _ -> Left . JsonDecodeError $ show (pp_value jerr)
191

    
192
-- | Scan the list of results produced by executeRpcCall and log all the RPC
193
-- errors.
194
logRpcErrors :: [(a, ERpcError b)] -> IO ()
195
logRpcErrors allElems =
196
  let logOneRpcErr (_, Right _) = return ()
197
      logOneRpcErr (_, Left err) =
198
        logError $ "Error in the RPC HTTP reply: " ++ show err
199
  in mapM_ logOneRpcErr allElems
200

    
201
-- | Execute RPC call for many nodes in parallel.
202
executeRpcCall :: (Rpc a b) => [Node] -> a -> IO [(Node, ERpcError b)]
203
executeRpcCall nodes call = do
204
  cert_file <- P.nodedCertFile
205
  let opts = [ CurlTimeout (fromIntegral $ rpcCallTimeout call)
206
             , CurlSSLCert cert_file
207
             , CurlSSLKey cert_file
208
             , CurlCAInfo cert_file
209
             ]
210
      opts_urls = map (\n ->
211
                         case prepareHttpRequest opts n call of
212
                           Left v -> Left v
213
                           Right request ->
214
                             Right (CurlPostFields [requestData request]:
215
                                    requestOpts request,
216
                                    requestUrl request)
217
                      ) nodes
218
  -- split the opts_urls list; we don't want to pass the
219
  -- failed-already nodes to Curl
220
  let (lefts, rights, trail) = splitEithers opts_urls
221
  results <- execMultiCall rights
222
  results' <- case recombineEithers lefts results trail of
223
                Bad msg -> error msg
224
                Ok r -> return r
225
  -- now parse the replies
226
  let results'' = map (parseHttpReply call) results'
227
      pairedList = zip nodes results''
228
  logRpcErrors pairedList
229
  return pairedList
230

    
231
-- | Helper function that is used to read dictionaries of values.
232
sanitizeDictResults :: [(String, J.Result a)] -> ERpcError [(String, a)]
233
sanitizeDictResults =
234
  foldr sanitize1 (Right [])
235
  where
236
    sanitize1 _ (Left e) = Left e
237
    sanitize1 (_, J.Error e) _ = Left $ JsonDecodeError e
238
    sanitize1 (name, J.Ok v) (Right res) = Right $ (name, v) : res
239

    
240
-- | Helper function to tranform JSON Result to Either RpcError b.
241
-- Note: For now we really only use it for b s.t. Rpc c b for some c
242
fromJResultToRes :: J.Result a -> (a -> b) -> ERpcError b
243
fromJResultToRes (J.Error v) _ = Left $ JsonDecodeError v
244
fromJResultToRes (J.Ok v) f = Right $ f v
245

    
246
-- | Helper function transforming JSValue to Rpc result type.
247
fromJSValueToRes :: (J.JSON a) => J.JSValue -> (a -> b) -> ERpcError b
248
fromJSValueToRes val = fromJResultToRes (J.readJSON val)
249

    
250
-- * RPC calls and results
251

    
252
-- ** Instance info
253

    
254
-- | Returns information about a single instance
255
$(buildObject "RpcCallInstanceInfo" "rpcCallInstInfo"
256
  [ simpleField "instance" [t| String |]
257
  , simpleField "hname" [t| Hypervisor |]
258
  ])
259

    
260
$(buildObject "InstanceInfo" "instInfo"
261
  [ simpleField "memory" [t| Int|]
262
  , simpleField "state"  [t| String |] -- It depends on hypervisor :(
263
  , simpleField "vcpus"  [t| Int |]
264
  , simpleField "time"   [t| Int |]
265
  ])
266

    
267
-- This is optional here because the result may be empty if instance is
268
-- not on a node - and this is not considered an error.
269
$(buildObject "RpcResultInstanceInfo" "rpcResInstInfo"
270
  [ optionalField $ simpleField "inst_info" [t| InstanceInfo |]])
271

    
272
instance RpcCall RpcCallInstanceInfo where
273
  rpcCallName _          = "instance_info"
274
  rpcCallTimeout _       = rpcTimeoutToRaw Urgent
275
  rpcCallAcceptOffline _ = False
276
  rpcCallData _ call     = J.encode
277
    ( rpcCallInstInfoInstance call
278
    , rpcCallInstInfoHname call
279
    )
280

    
281
instance Rpc RpcCallInstanceInfo RpcResultInstanceInfo where
282
  rpcResultFill _ res =
283
    case res of
284
      J.JSObject res' ->
285
        case J.fromJSObject res' of
286
          [] -> Right $ RpcResultInstanceInfo Nothing
287
          _ -> fromJSValueToRes res (RpcResultInstanceInfo . Just)
288
      _ -> Left $ JsonDecodeError
289
           ("Expected JSObject, got " ++ show (pp_value res))
290

    
291
-- ** AllInstancesInfo
292

    
293
-- | Returns information about all running instances on the given nodes
294
$(buildObject "RpcCallAllInstancesInfo" "rpcCallAllInstInfo"
295
  [ simpleField "hypervisors" [t| [(Hypervisor, HvParams)] |] ])
296

    
297
$(buildObject "RpcResultAllInstancesInfo" "rpcResAllInstInfo"
298
  [ simpleField "instances" [t| [(String, InstanceInfo)] |] ])
299

    
300
instance RpcCall RpcCallAllInstancesInfo where
301
  rpcCallName _          = "all_instances_info"
302
  rpcCallTimeout _       = rpcTimeoutToRaw Urgent
303
  rpcCallAcceptOffline _ = False
304
  rpcCallData _ call     = J.encode (
305
    map fst $ rpcCallAllInstInfoHypervisors call,
306
    GenericContainer . Map.fromList $ rpcCallAllInstInfoHypervisors call)
307

    
308
instance Rpc RpcCallAllInstancesInfo RpcResultAllInstancesInfo where
309
  -- FIXME: Is there a simpler way to do it?
310
  rpcResultFill _ res =
311
    case res of
312
      J.JSObject res' ->
313
        let res'' = map (second J.readJSON) (J.fromJSObject res')
314
                        :: [(String, J.Result InstanceInfo)] in
315
        case sanitizeDictResults res'' of
316
          Left err -> Left err
317
          Right insts -> Right $ RpcResultAllInstancesInfo insts
318
      _ -> Left $ JsonDecodeError
319
           ("Expected JSObject, got " ++ show (pp_value res))
320

    
321
-- ** InstanceConsoleInfo
322

    
323
-- | Returns information about how to access instances on the given node
324
$(buildObject "InstanceConsoleInfoParams" "instConsInfoParams"
325
  [ simpleField "instance"    [t| Instance |]
326
  , simpleField "node"        [t| Node |]
327
  , simpleField "hvParams"    [t| HvParams |]
328
  , simpleField "beParams"    [t| FilledBeParams |]
329
  ])
330

    
331
$(buildObject "RpcCallInstanceConsoleInfo" "rpcCallInstConsInfo"
332
  [ simpleField "instanceInfo" [t| [(String, InstanceConsoleInfoParams)] |] ])
333

    
334
$(buildObject "InstanceConsoleInfo" "instConsInfo"
335
  [ simpleField "instance"    [t| String |]
336
  , simpleField "kind"        [t| String |]
337
  , optionalField $
338
    simpleField "message"     [t| String |]
339
  , optionalField $
340
    simpleField "host"        [t| String |]
341
  , optionalField $
342
    simpleField "port"        [t| Int |]
343
  , optionalField $
344
    simpleField "user"        [t| String |]
345
  , optionalField $
346
    simpleField "command"     [t| [String] |]
347
  , optionalField $
348
    simpleField "display"     [t| String |]
349
  ])
350

    
351
$(buildObject "RpcResultInstanceConsoleInfo" "rpcResInstConsInfo"
352
  [ simpleField "instancesInfo" [t| [(String, InstanceConsoleInfo)] |] ])
353

    
354
instance RpcCall RpcCallInstanceConsoleInfo where
355
  rpcCallName _          = "instance_console_info"
356
  rpcCallTimeout _       = rpcTimeoutToRaw Urgent
357
  rpcCallAcceptOffline _ = False
358
  rpcCallData _ call     = J.encode .
359
    GenericContainer $ Map.fromList (rpcCallInstConsInfoInstanceInfo call)
360

    
361
instance Rpc RpcCallInstanceConsoleInfo RpcResultInstanceConsoleInfo where
362
  rpcResultFill _ res =
363
    case res of
364
      J.JSObject res' ->
365
        let res'' = map (second J.readJSON) (J.fromJSObject res')
366
                        :: [(String, J.Result InstanceConsoleInfo)] in
367
        case sanitizeDictResults res'' of
368
          Left err -> Left err
369
          Right instInfos -> Right $ RpcResultInstanceConsoleInfo instInfos
370
      _ -> Left $ JsonDecodeError
371
           ("Expected JSObject, got " ++ show (pp_value res))
372

    
373
-- ** InstanceList
374

    
375
-- | Returns the list of running instances on the given nodes
376
$(buildObject "RpcCallInstanceList" "rpcCallInstList"
377
  [ simpleField "hypervisors" [t| [Hypervisor] |] ])
378

    
379
$(buildObject "RpcResultInstanceList" "rpcResInstList"
380
  [ simpleField "instances" [t| [String] |] ])
381

    
382
instance RpcCall RpcCallInstanceList where
383
  rpcCallName _          = "instance_list"
384
  rpcCallTimeout _       = rpcTimeoutToRaw Urgent
385
  rpcCallAcceptOffline _ = False
386
  rpcCallData _ call     = J.encode [rpcCallInstListHypervisors call]
387

    
388
instance Rpc RpcCallInstanceList RpcResultInstanceList where
389
  rpcResultFill _ res = fromJSValueToRes res RpcResultInstanceList
390

    
391
-- ** NodeInfo
392

    
393
-- | Returns node information
394
$(buildObject "RpcCallNodeInfo" "rpcCallNodeInfo"
395
  [ simpleField "storage_units" [t| Map.Map String [StorageUnit] |]
396
  , simpleField "hypervisors" [t| [ (Hypervisor, HvParams) ] |]
397
  ])
398

    
399
$(buildObject "StorageInfo" "storageInfo"
400
  [ simpleField "name" [t| String |]
401
  , simpleField "type" [t| String |]
402
  , optionalField $ simpleField "storage_free" [t| Int |]
403
  , optionalField $ simpleField "storage_size" [t| Int |]
404
  ])
405

    
406
-- | We only provide common fields as described in hv_base.py.
407
$(buildObject "HvInfo" "hvInfo"
408
  [ simpleField "memory_total" [t| Int |]
409
  , simpleField "memory_free" [t| Int |]
410
  , simpleField "memory_dom0" [t| Int |]
411
  , simpleField "cpu_total" [t| Int |]
412
  , simpleField "cpu_nodes" [t| Int |]
413
  , simpleField "cpu_sockets" [t| Int |]
414
  , simpleField "cpu_dom0" [t| Int |]
415
  ])
416

    
417
$(buildObject "RpcResultNodeInfo" "rpcResNodeInfo"
418
  [ simpleField "boot_id" [t| String |]
419
  , simpleField "storage_info" [t| [StorageInfo] |]
420
  , simpleField "hv_info" [t| [HvInfo] |]
421
  ])
422

    
423
instance RpcCall RpcCallNodeInfo where
424
  rpcCallName _          = "node_info"
425
  rpcCallTimeout _       = rpcTimeoutToRaw Urgent
426
  rpcCallAcceptOffline _ = False
427
  rpcCallData n call     = J.encode
428
    ( fromMaybe (error $ "Programmer error: missing parameter for node named "
429
                         ++ nodeName n)
430
          $ Map.lookup (nodeUuid n) (rpcCallNodeInfoStorageUnits call)
431
    , rpcCallNodeInfoHypervisors call
432
    )
433

    
434
instance Rpc RpcCallNodeInfo RpcResultNodeInfo where
435
  rpcResultFill _ res =
436
    fromJSValueToRes res (\(b, vg, hv) -> RpcResultNodeInfo b vg hv)
437

    
438
-- ** Version
439

    
440
-- | Query node version.
441
$(buildObject "RpcCallVersion" "rpcCallVersion" [])
442

    
443
-- | Query node reply.
444
$(buildObject "RpcResultVersion" "rpcResultVersion"
445
  [ simpleField "version" [t| Int |]
446
  ])
447

    
448
instance RpcCall RpcCallVersion where
449
  rpcCallName _          = "version"
450
  rpcCallTimeout _       = rpcTimeoutToRaw Urgent
451
  rpcCallAcceptOffline _ = True
452
  rpcCallData _          = J.encode
453

    
454
instance Rpc RpcCallVersion RpcResultVersion where
455
  rpcResultFill _ res = fromJSValueToRes res RpcResultVersion
456

    
457
-- ** StorageList
458

    
459
$(buildObject "RpcCallStorageList" "rpcCallStorageList"
460
  [ simpleField "su_name" [t| StorageType |]
461
  , simpleField "su_args" [t| [String] |]
462
  , simpleField "name"    [t| String |]
463
  , simpleField "fields"  [t| [StorageField] |]
464
  ])
465

    
466
-- FIXME: The resulting JSValues should have types appropriate for their
467
-- StorageField value: Used -> Bool, Name -> String etc
468
$(buildObject "RpcResultStorageList" "rpcResStorageList"
469
  [ simpleField "storage" [t| [[(StorageField, J.JSValue)]] |] ])
470

    
471
instance RpcCall RpcCallStorageList where
472
  rpcCallName _          = "storage_list"
473
  rpcCallTimeout _       = rpcTimeoutToRaw Normal
474
  rpcCallAcceptOffline _ = False
475
  rpcCallData _ call     = J.encode
476
    ( rpcCallStorageListSuName call
477
    , rpcCallStorageListSuArgs call
478
    , rpcCallStorageListName call
479
    , rpcCallStorageListFields call
480
    )
481

    
482
instance Rpc RpcCallStorageList RpcResultStorageList where
483
  rpcResultFill call res =
484
    let sfields = rpcCallStorageListFields call in
485
    fromJSValueToRes res (RpcResultStorageList . map (zip sfields))
486

    
487
-- ** TestDelay
488

    
489
-- | Call definition for test delay.
490
$(buildObject "RpcCallTestDelay" "rpcCallTestDelay"
491
  [ simpleField "duration" [t| Double |]
492
  ])
493

    
494
-- | Result definition for test delay.
495
data RpcResultTestDelay = RpcResultTestDelay
496
                          deriving Show
497

    
498
-- | Custom JSON instance for null result.
499
instance J.JSON RpcResultTestDelay where
500
  showJSON _        = J.JSNull
501
  readJSON J.JSNull = return RpcResultTestDelay
502
  readJSON _        = fail "Unable to read RpcResultTestDelay"
503

    
504
instance RpcCall RpcCallTestDelay where
505
  rpcCallName _          = "test_delay"
506
  rpcCallTimeout         = ceiling . (+ 5) . rpcCallTestDelayDuration
507
  rpcCallAcceptOffline _ = False
508
  rpcCallData _ call     = J.encode [rpcCallTestDelayDuration call]
509

    
510
instance Rpc RpcCallTestDelay RpcResultTestDelay where
511
  rpcResultFill _ res = fromJSValueToRes res id
512

    
513
-- ** ExportList
514

    
515
-- | Call definition for export list.
516

    
517
$(buildObject "RpcCallExportList" "rpcCallExportList" [])
518

    
519
-- | Result definition for export list.
520
$(buildObject "RpcResultExportList" "rpcResExportList"
521
  [ simpleField "exports" [t| [String] |]
522
  ])
523

    
524
instance RpcCall RpcCallExportList where
525
  rpcCallName _          = "export_list"
526
  rpcCallTimeout _       = rpcTimeoutToRaw Fast
527
  rpcCallAcceptOffline _ = False
528
  rpcCallData _          = J.encode
529

    
530
instance Rpc RpcCallExportList RpcResultExportList where
531
  rpcResultFill _ res = fromJSValueToRes res RpcResultExportList