Statistics
| Branch: | Tag: | Revision:

root / htools / Ganeti / Rpc.hs @ 47163f0f

History | View | Annotate | Download (13.2 kB)

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

    
4
{-| Implementation of the RPC client.
5

    
6
-}
7

    
8
{-
9

    
10
Copyright (C) 2012 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
  , executeRpcCall
35

    
36
  , rpcCallName
37
  , rpcCallTimeout
38
  , rpcCallData
39
  , rpcCallAcceptOffline
40

    
41
  , rpcResultFill
42

    
43
  , InstanceInfo(..)
44
  , RpcCallAllInstancesInfo(..)
45
  , RpcResultAllInstancesInfo(..)
46

    
47
  , RpcCallInstanceList(..)
48
  , RpcResultInstanceList(..)
49

    
50
  , HvInfo(..)
51
  , VgInfo(..)
52
  , RpcCallNodeInfo(..)
53
  , RpcResultNodeInfo(..)
54

    
55
  , RpcCallVersion(..)
56
  , RpcResultVersion(..)
57

    
58
  , StorageType(..)
59
  , StorageField(..)
60
  , RpcCallStorageList(..)
61
  , RpcResultStorageList(..)
62

    
63
  , rpcTimeoutFromRaw -- FIXME: Not used anywhere
64
  ) where
65

    
66
import Control.Arrow (second)
67
import qualified Text.JSON as J
68
import Text.JSON.Pretty (pp_value)
69
import Text.JSON (makeObj)
70

    
71
#ifndef NO_CURL
72
import Network.Curl
73
import qualified Ganeti.Path as P
74
#endif
75

    
76
import qualified Ganeti.Constants as C
77
import Ganeti.Objects
78
import Ganeti.THH
79
import Ganeti.Compat
80
import Ganeti.JSON
81

    
82
#ifndef NO_CURL
83
-- | The curl options used for RPC.
84
curlOpts :: [CurlOption]
85
curlOpts = [ CurlFollowLocation False
86
           , CurlCAInfo P.nodedCertFile
87
           , CurlSSLVerifyHost 0
88
           , CurlSSLVerifyPeer True
89
           , CurlSSLCertType "PEM"
90
           , CurlSSLCert P.nodedCertFile
91
           , CurlSSLKeyType "PEM"
92
           , CurlSSLKey P.nodedCertFile
93
           , CurlConnectTimeout (fromIntegral C.rpcConnectTimeout)
94
           ]
95
#endif
96

    
97
-- | Data type for RPC error reporting.
98
data RpcError
99
  = CurlDisabledError
100
  | CurlLayerError Node String
101
  | JsonDecodeError String
102
  | RpcResultError String
103
  | OfflineNodeError Node
104
  deriving Eq
105

    
106
instance Show RpcError where
107
  show CurlDisabledError =
108
    "RPC/curl backend disabled at compile time"
109
  show (CurlLayerError node code) =
110
    "Curl error for " ++ nodeName node ++ ", " ++ code
111
  show (JsonDecodeError msg) =
112
    "Error while decoding JSON from HTTP response: " ++ msg
113
  show (RpcResultError msg) =
114
    "Error reponse received from RPC server: " ++ msg
115
  show (OfflineNodeError node) =
116
    "Node " ++ nodeName node ++ " is marked as offline"
117

    
118
type ERpcError = Either RpcError
119

    
120
-- | Basic timeouts for RPC calls.
121
$(declareIADT "RpcTimeout"
122
  [ ( "Urgent",    'C.rpcTmoUrgent )
123
  , ( "Fast",      'C.rpcTmoFast )
124
  , ( "Normal",    'C.rpcTmoNormal )
125
  , ( "Slow",      'C.rpcTmoSlow )
126
  , ( "FourHours", 'C.rpcTmo4hrs )
127
  , ( "OneDay",    'C.rpcTmo1day )
128
  ])
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 :: (Monad m) => a -> J.JSValue -> m (ERpcError b)
146

    
147
-- | Http Request definition.
148
data HttpClientRequest = HttpClientRequest
149
  { requestTimeout :: Int
150
  , requestUrl :: String
151
  , requestPostData :: String
152
  }
153

    
154
-- | Execute the request and return the result as a plain String. When
155
-- curl reports an error, we propagate it.
156
executeHttpRequest :: Node -> ERpcError HttpClientRequest
157
                   -> IO (ERpcError String)
158

    
159
executeHttpRequest _ (Left rpc_err) = return $ Left rpc_err
160
#ifdef NO_CURL
161
executeHttpRequest _ _ = return $ Left CurlDisabledError
162
#else
163
executeHttpRequest node (Right request) = do
164
  let reqOpts = [ CurlTimeout (fromIntegral $ requestTimeout request)
165
                , CurlPostFields [requestPostData request]
166
                ]
167
      url = requestUrl request
168
  -- FIXME: This is very similar to getUrl in Htools/Rapi.hs
169
  (code, !body) <- curlGetString url $ curlOpts ++ reqOpts
170
  return $ case code of
171
             CurlOK -> Right body
172
             _ -> Left $ CurlLayerError node (show code)
173
#endif
174

    
175
-- | Prepare url for the HTTP request.
176
prepareUrl :: (RpcCall a) => Node -> a -> String
177
prepareUrl node call =
178
  let node_ip = nodePrimaryIp node
179
      port = snd C.daemonsPortsGanetiNoded
180
      path_prefix = "https://" ++ node_ip ++ ":" ++ show port
181
  in path_prefix ++ "/" ++ rpcCallName call
182

    
183
-- | Create HTTP request for a given node provided it is online,
184
-- otherwise create empty response.
185
prepareHttpRequest ::  (RpcCall a) => Node -> a
186
                   -> ERpcError HttpClientRequest
187
prepareHttpRequest node call
188
  | rpcCallAcceptOffline call || not (nodeOffline node) =
189
      Right HttpClientRequest { requestTimeout = rpcCallTimeout call
190
                              , requestUrl = prepareUrl node call
191
                              , requestPostData = rpcCallData node call
192
                              }
193
  | otherwise = Left $ OfflineNodeError node
194

    
195
-- | Parse a result based on the received HTTP response.
196
rpcResultParse :: (Monad m, Rpc a b) => a -> String -> m (ERpcError b)
197
rpcResultParse call res = do
198
  res' <- fromJResult "Reading JSON response" $ J.decode res
199
  case res' of
200
    (True, res'') ->
201
       rpcResultFill call res''
202
    (False, jerr) -> case jerr of
203
       J.JSString msg -> return . Left $ RpcResultError (J.fromJSString msg)
204
       _ -> (return . Left) . JsonDecodeError $ show (pp_value jerr)
205

    
206
-- | Parse the response or propagate the error.
207
parseHttpResponse :: (Rpc a b) => a -> ERpcError String -> IO (ERpcError b)
208
parseHttpResponse _ (Left err) = return $ Left err
209
parseHttpResponse call (Right response) = rpcResultParse call response
210

    
211
-- | Execute RPC call for a sigle node.
212
executeSingleRpcCall :: (Rpc a b) => Node -> a -> IO (Node, ERpcError b)
213
executeSingleRpcCall node call = do
214
  let request = prepareHttpRequest node call
215
  response <- executeHttpRequest node request
216
  result <- parseHttpResponse call response
217
  return (node, result)
218

    
219
-- | Execute RPC call for many nodes in parallel.
220
executeRpcCall :: (Rpc a b) => [Node] -> a -> IO [(Node, ERpcError b)]
221
executeRpcCall nodes call =
222
  sequence $ parMap rwhnf (uncurry executeSingleRpcCall)
223
               (zip nodes $ repeat call)
224

    
225
-- | Helper function that is used to read dictionaries of values.
226
sanitizeDictResults :: [(String, J.Result a)] -> ERpcError [(String, a)]
227
sanitizeDictResults [] = Right []
228
sanitizeDictResults ((_, J.Error err):_) = Left $ JsonDecodeError err
229
sanitizeDictResults ((name, J.Ok val):xs) =
230
  case sanitizeDictResults xs of
231
    Left err -> Left err
232
    Right res' -> Right $ (name, val):res'
233

    
234
-- * RPC calls and results
235

    
236
-- | AllInstancesInfo
237
--   Returns information about all running instances on the given nodes.
238
$(buildObject "RpcCallAllInstancesInfo" "rpcCallAllInstInfo"
239
  [ simpleField "hypervisors" [t| [Hypervisor] |] ])
240

    
241
$(buildObject "InstanceInfo" "instInfo"
242
  [ simpleField "memory" [t| Int|]
243
  , simpleField "state"  [t| String |] -- It depends on hypervisor :(
244
  , simpleField "vcpus"  [t| Int |]
245
  , simpleField "time"   [t| Int |]
246
  ])
247

    
248
$(buildObject "RpcResultAllInstancesInfo" "rpcResAllInstInfo"
249
  [ simpleField "instances" [t| [(String, InstanceInfo)] |] ])
250

    
251
instance RpcCall RpcCallAllInstancesInfo where
252
  rpcCallName _ = "all_instances_info"
253
  rpcCallTimeout _ = rpcTimeoutToRaw Urgent
254
  rpcCallAcceptOffline _ = False
255
  rpcCallData _ call = J.encode [rpcCallAllInstInfoHypervisors call]
256

    
257
instance Rpc RpcCallAllInstancesInfo RpcResultAllInstancesInfo where
258
  -- FIXME: Is there a simpler way to do it?
259
  rpcResultFill _ res =
260
    return $ case res of
261
      J.JSObject res' -> do
262
        let res'' = map (second J.readJSON) (J.fromJSObject res')
263
                        :: [(String, J.Result InstanceInfo)]
264
        case sanitizeDictResults res'' of
265
          Left err -> Left err
266
          Right insts -> Right $ RpcResultAllInstancesInfo insts
267
      _ -> Left $ JsonDecodeError
268
           ("Expected JSObject, got " ++ show res)
269

    
270
-- | InstanceList
271
-- Returns the list of running instances on the given nodes.
272
$(buildObject "RpcCallInstanceList" "rpcCallInstList"
273
  [ simpleField "hypervisors" [t| [Hypervisor] |] ])
274

    
275
$(buildObject "RpcResultInstanceList" "rpcResInstList"
276
  [ simpleField "instances" [t| [String] |] ])
277

    
278
instance RpcCall RpcCallInstanceList where
279
  rpcCallName _ = "instance_list"
280
  rpcCallTimeout _ = rpcTimeoutToRaw Urgent
281
  rpcCallAcceptOffline _ = False
282
  rpcCallData _ call = J.encode [rpcCallInstListHypervisors call]
283

    
284

    
285
instance Rpc RpcCallInstanceList RpcResultInstanceList where
286
  rpcResultFill _ res =
287
    return $ case J.readJSON res of
288
      J.Error err -> Left $ JsonDecodeError err
289
      J.Ok insts -> Right $ RpcResultInstanceList insts
290

    
291
-- | NodeInfo
292
-- Return node information.
293
$(buildObject "RpcCallNodeInfo" "rpcCallNodeInfo"
294
  [ simpleField "volume_groups" [t| [String] |]
295
  , simpleField "hypervisors" [t| [Hypervisor] |]
296
  ])
297

    
298
$(buildObject "VgInfo" "vgInfo"
299
  [ simpleField "name" [t| String |]
300
  , optionalField $ simpleField "vg_free" [t| Int |]
301
  , optionalField $ simpleField "vg_size" [t| Int |]
302
  ])
303

    
304
-- | We only provide common fields as described in hv_base.py.
305
$(buildObject "HvInfo" "hvInfo"
306
  [ simpleField "memory_total" [t| Int |]
307
  , simpleField "memory_free" [t| Int |]
308
  , simpleField "memory_dom0" [t| Int |]
309
  , simpleField "cpu_total" [t| Int |]
310
  , simpleField "cpu_nodes" [t| Int |]
311
  , simpleField "cpu_sockets" [t| Int |]
312
  ])
313

    
314
$(buildObject "RpcResultNodeInfo" "rpcResNodeInfo"
315
  [ simpleField "boot_id" [t| String |]
316
  , simpleField "vg_info" [t| [VgInfo] |]
317
  , simpleField "hv_info" [t| [HvInfo] |]
318
  ])
319

    
320
instance RpcCall RpcCallNodeInfo where
321
  rpcCallName _ = "node_info"
322
  rpcCallTimeout _ = rpcTimeoutToRaw Urgent
323
  rpcCallAcceptOffline _ = False
324
  rpcCallData _ call = J.encode ( rpcCallNodeInfoVolumeGroups call
325
                                , rpcCallNodeInfoHypervisors call
326
                                )
327

    
328
instance Rpc RpcCallNodeInfo RpcResultNodeInfo where
329
  rpcResultFill _ res =
330
    return $ case J.readJSON res of
331
      J.Error err -> Left $ JsonDecodeError err
332
      J.Ok (boot_id, vg_info, hv_info) ->
333
          Right $ RpcResultNodeInfo boot_id vg_info hv_info
334

    
335
-- | Version
336
-- Query node version.
337
-- Note: We can't use THH as it does not know what to do with empty dict
338
data RpcCallVersion = RpcCallVersion {}
339
  deriving (Show, Read, Eq)
340

    
341
instance J.JSON RpcCallVersion where
342
  showJSON _ = J.JSNull
343
  readJSON J.JSNull = return RpcCallVersion
344
  readJSON _ = fail "Unable to read RpcCallVersion"
345

    
346
$(buildObject "RpcResultVersion" "rpcResultVersion"
347
  [ simpleField "version" [t| Int |]
348
  ])
349

    
350
instance RpcCall RpcCallVersion where
351
  rpcCallName _ = "version"
352
  rpcCallTimeout _ = rpcTimeoutToRaw Urgent
353
  rpcCallAcceptOffline _ = True
354
  rpcCallData call _ = J.encode [call]
355

    
356
instance Rpc RpcCallVersion RpcResultVersion where
357
  rpcResultFill _ res =
358
    return $ case J.readJSON res of
359
      J.Error err -> Left $ JsonDecodeError err
360
      J.Ok ver -> Right $ RpcResultVersion ver
361

    
362
-- | StorageList
363
-- Get list of storage units.
364
-- FIXME: This may be moved to Objects
365
$(declareSADT "StorageType"
366
  [ ( "STLvmPv", 'C.stLvmPv )
367
  , ( "STFile",  'C.stFile )
368
  , ( "STLvmVg", 'C.stLvmVg )
369
  ])
370
$(makeJSONInstance ''StorageType)
371

    
372
-- FIXME: This may be moved to Objects
373
$(declareSADT "StorageField"
374
  [ ( "SFUsed",        'C.sfUsed)
375
  , ( "SFName",        'C.sfName)
376
  , ( "SFAllocatable", 'C.sfAllocatable)
377
  , ( "SFFree",        'C.sfFree)
378
  , ( "SFSize",        'C.sfSize)
379
  ])
380
$(makeJSONInstance ''StorageField)
381

    
382
$(buildObject "RpcCallStorageList" "rpcCallStorageList"
383
  [ simpleField "su_name" [t| StorageType |]
384
  , simpleField "su_args" [t| [String] |]
385
  , simpleField "name"    [t| String |]
386
  , simpleField "fields"  [t| [StorageField] |]
387
  ])
388

    
389
-- FIXME: The resulting JSValues should have types appropriate for their
390
-- StorageField value: Used -> Bool, Name -> String etc
391
$(buildObject "RpcResultStorageList" "rpcResStorageList"
392
  [ simpleField "storage" [t| [[(StorageField, J.JSValue)]] |] ])
393

    
394
instance RpcCall RpcCallStorageList where
395
  rpcCallName _ = "storage_list"
396
  rpcCallTimeout _ = rpcTimeoutToRaw Normal
397
  rpcCallAcceptOffline _ = False
398
  rpcCallData _ call = J.encode
399
    ( rpcCallStorageListSuName call
400
    , rpcCallStorageListSuArgs call
401
    , rpcCallStorageListName call
402
    , rpcCallStorageListFields call
403
    )
404

    
405
instance Rpc RpcCallStorageList RpcResultStorageList where
406
  rpcResultFill call res =
407
    let sfields = rpcCallStorageListFields call in
408
    return $ case J.readJSON res of
409
      J.Error err -> Left $ JsonDecodeError err
410
      J.Ok res_lst -> Right $ RpcResultStorageList (map (zip sfields) res_lst)
411