1 {-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies, CPP,
2 BangPatterns, TemplateHaskell #-}
4 {-| Implementation of the RPC client.
10 Copyright (C) 2012 Google Inc.
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.
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.
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
40 , rpcCallAcceptOffline
45 , RpcCallInstanceInfo(..)
46 , RpcResultInstanceInfo(..)
48 , RpcCallAllInstancesInfo(..)
49 , RpcResultAllInstancesInfo(..)
51 , RpcCallInstanceList(..)
52 , RpcResultInstanceList(..)
57 , RpcResultNodeInfo(..)
60 , RpcResultVersion(..)
64 , RpcCallStorageList(..)
65 , RpcResultStorageList(..)
67 , RpcCallTestDelay(..)
68 , RpcResultTestDelay(..)
70 , rpcTimeoutFromRaw -- FIXME: Not used anywhere
73 import Control.Arrow (second)
74 import qualified Text.JSON as J
75 import Text.JSON.Pretty (pp_value)
79 import qualified Ganeti.Path as P
82 import qualified Ganeti.Constants as C
88 -- * Base RPC functionality and types
91 -- | The curl options used for RPC.
92 curlOpts :: [CurlOption]
93 curlOpts = [ CurlFollowLocation False
95 , CurlSSLVerifyPeer True
96 , CurlSSLCertType "PEM"
97 , CurlSSLKeyType "PEM"
98 , CurlConnectTimeout (fromIntegral C.rpcConnectTimeout)
102 -- | Data type for RPC error reporting.
105 | CurlLayerError Node String
106 | JsonDecodeError String
107 | RpcResultError String
108 | OfflineNodeError Node
111 -- | Provide explanation to RPC errors.
112 explainRpcError :: RpcError -> String
113 explainRpcError CurlDisabledError =
114 "RPC/curl backend disabled at compile time"
115 explainRpcError (CurlLayerError node code) =
116 "Curl error for " ++ nodeName node ++ ", " ++ code
117 explainRpcError (JsonDecodeError msg) =
118 "Error while decoding JSON from HTTP response: " ++ msg
119 explainRpcError (RpcResultError msg) =
120 "Error reponse received from RPC server: " ++ msg
121 explainRpcError (OfflineNodeError node) =
122 "Node " ++ nodeName node ++ " is marked as offline"
124 type ERpcError = Either RpcError
126 -- | Basic timeouts for RPC calls.
127 $(declareIADT "RpcTimeout"
128 [ ( "Urgent", 'C.rpcTmoUrgent )
129 , ( "Fast", 'C.rpcTmoFast )
130 , ( "Normal", 'C.rpcTmoNormal )
131 , ( "Slow", 'C.rpcTmoSlow )
132 , ( "FourHours", 'C.rpcTmo4hrs )
133 , ( "OneDay", 'C.rpcTmo1day )
136 -- | A generic class for RPC calls.
137 class (J.JSON a) => RpcCall a where
138 -- | Give the (Python) name of the procedure.
139 rpcCallName :: a -> String
140 -- | Calculate the timeout value for the call execution.
141 rpcCallTimeout :: a -> Int
142 -- | Prepare arguments of the call to be send as POST.
143 rpcCallData :: Node -> a -> String
144 -- | Whether we accept offline nodes when making a call.
145 rpcCallAcceptOffline :: a -> Bool
147 -- | Generic class that ensures matching RPC call with its respective
149 class (RpcCall a, J.JSON b) => Rpc a b | a -> b, b -> a where
150 -- | Create a result based on the received HTTP response.
151 rpcResultFill :: a -> J.JSValue -> ERpcError b
153 -- | Http Request definition.
154 data HttpClientRequest = HttpClientRequest
155 { requestTimeout :: Int
156 , requestUrl :: String
157 , requestPostData :: String
160 -- | Execute the request and return the result as a plain String. When
161 -- curl reports an error, we propagate it.
162 executeHttpRequest :: Node -> ERpcError HttpClientRequest
163 -> IO (ERpcError String)
165 executeHttpRequest _ (Left rpc_err) = return $ Left rpc_err
167 executeHttpRequest _ _ = return $ Left CurlDisabledError
169 executeHttpRequest node (Right request) = do
170 cert_file <- P.nodedCertFile
171 let reqOpts = [ CurlTimeout (fromIntegral $ requestTimeout request)
172 , CurlPostFields [requestPostData request]
173 , CurlSSLCert cert_file
174 , CurlSSLKey cert_file
175 , CurlCAInfo cert_file
177 url = requestUrl request
178 -- FIXME: This is very similar to getUrl in Htools/Rapi.hs
179 (code, !body) <- curlGetString url $ curlOpts ++ reqOpts
180 return $ case code of
182 _ -> Left $ CurlLayerError node (show code)
185 -- | Prepare url for the HTTP request.
186 prepareUrl :: (RpcCall a) => Node -> a -> String
187 prepareUrl node call =
188 let node_ip = nodePrimaryIp node
189 port = snd C.daemonsPortsGanetiNoded
190 path_prefix = "https://" ++ node_ip ++ ":" ++ show port
191 in path_prefix ++ "/" ++ rpcCallName call
193 -- | Create HTTP request for a given node provided it is online,
194 -- otherwise create empty response.
195 prepareHttpRequest :: (RpcCall a) => Node -> a
196 -> ERpcError HttpClientRequest
197 prepareHttpRequest node call
198 | rpcCallAcceptOffline call || not (nodeOffline node) =
199 Right HttpClientRequest { requestTimeout = rpcCallTimeout call
200 , requestUrl = prepareUrl node call
201 , requestPostData = rpcCallData node call
203 | otherwise = Left $ OfflineNodeError node
205 -- | Parse a result based on the received HTTP response.
206 parseHttpResponse :: (Rpc a b) => a -> ERpcError String -> ERpcError b
207 parseHttpResponse _ (Left err) = Left err
208 parseHttpResponse call (Right res) =
210 J.Error val -> Left $ JsonDecodeError val
211 J.Ok (True, res'') -> rpcResultFill call res''
212 J.Ok (False, jerr) -> case jerr of
213 J.JSString msg -> Left $ RpcResultError (J.fromJSString msg)
214 _ -> Left . JsonDecodeError $ show (pp_value jerr)
216 -- | Execute RPC call for a sigle node.
217 executeSingleRpcCall :: (Rpc a b) => Node -> a -> IO (Node, ERpcError b)
218 executeSingleRpcCall node call = do
219 let request = prepareHttpRequest node call
220 response <- executeHttpRequest node request
221 let result = parseHttpResponse call response
222 return (node, result)
224 -- | Execute RPC call for many nodes in parallel.
225 executeRpcCall :: (Rpc a b) => [Node] -> a -> IO [(Node, ERpcError b)]
226 executeRpcCall nodes call =
227 sequence $ parMap rwhnf (uncurry executeSingleRpcCall)
228 (zip nodes $ repeat call)
230 -- | Helper function that is used to read dictionaries of values.
231 sanitizeDictResults :: [(String, J.Result a)] -> ERpcError [(String, a)]
232 sanitizeDictResults =
233 foldr sanitize1 (Right [])
235 sanitize1 _ (Left e) = Left e
236 sanitize1 (_, J.Error e) _ = Left $ JsonDecodeError e
237 sanitize1 (name, J.Ok v) (Right res) = Right $ (name, v) : res
239 -- | Helper function to tranform JSON Result to Either RpcError b.
240 -- Note: For now we really only use it for b s.t. Rpc c b for some c
241 fromJResultToRes :: J.Result a -> (a -> b) -> ERpcError b
242 fromJResultToRes (J.Error v) _ = Left $ JsonDecodeError v
243 fromJResultToRes (J.Ok v) f = Right $ f v
245 -- | Helper function transforming JSValue to Rpc result type.
246 fromJSValueToRes :: (J.JSON a) => J.JSValue -> (a -> b) -> ERpcError b
247 fromJSValueToRes val = fromJResultToRes (J.readJSON val)
249 -- * RPC calls and results
254 -- Returns information about a single instance.
256 $(buildObject "RpcCallInstanceInfo" "rpcCallInstInfo"
257 [ simpleField "instance" [t| String |]
258 , simpleField "hname" [t| Hypervisor |]
261 $(buildObject "InstanceInfo" "instInfo"
262 [ simpleField "memory" [t| Int|]
263 , simpleField "state" [t| String |] -- It depends on hypervisor :(
264 , simpleField "vcpus" [t| Int |]
265 , simpleField "time" [t| Int |]
268 -- This is optional here because the result may be empty if instance is
269 -- not on a node - and this is not considered an error.
270 $(buildObject "RpcResultInstanceInfo" "rpcResInstInfo"
271 [ optionalField $ simpleField "inst_info" [t| InstanceInfo |]])
273 instance RpcCall RpcCallInstanceInfo where
274 rpcCallName _ = "instance_info"
275 rpcCallTimeout _ = rpcTimeoutToRaw Urgent
276 rpcCallAcceptOffline _ = False
277 rpcCallData _ call = J.encode
278 ( rpcCallInstInfoInstance call
279 , rpcCallInstInfoHname call
282 instance Rpc RpcCallInstanceInfo RpcResultInstanceInfo where
283 rpcResultFill _ res =
286 case J.fromJSObject res' of
287 [] -> Right $ RpcResultInstanceInfo Nothing
288 _ -> fromJSValueToRes res (RpcResultInstanceInfo . Just)
289 _ -> Left $ JsonDecodeError
290 ("Expected JSObject, got " ++ show (pp_value res))
292 -- ** AllInstancesInfo
294 -- | AllInstancesInfo
295 -- Returns information about all running instances on the given nodes
296 $(buildObject "RpcCallAllInstancesInfo" "rpcCallAllInstInfo"
297 [ simpleField "hypervisors" [t| [Hypervisor] |] ])
299 $(buildObject "RpcResultAllInstancesInfo" "rpcResAllInstInfo"
300 [ simpleField "instances" [t| [(String, InstanceInfo)] |] ])
302 instance RpcCall RpcCallAllInstancesInfo where
303 rpcCallName _ = "all_instances_info"
304 rpcCallTimeout _ = rpcTimeoutToRaw Urgent
305 rpcCallAcceptOffline _ = False
306 rpcCallData _ call = J.encode [rpcCallAllInstInfoHypervisors call]
308 instance Rpc RpcCallAllInstancesInfo RpcResultAllInstancesInfo where
309 -- FIXME: Is there a simpler way to do it?
310 rpcResultFill _ res =
313 let res'' = map (second J.readJSON) (J.fromJSObject res')
314 :: [(String, J.Result InstanceInfo)] in
315 case sanitizeDictResults res'' of
317 Right insts -> Right $ RpcResultAllInstancesInfo insts
318 _ -> Left $ JsonDecodeError
319 ("Expected JSObject, got " ++ show (pp_value res))
324 -- Returns the list of running instances on the given nodes.
325 $(buildObject "RpcCallInstanceList" "rpcCallInstList"
326 [ simpleField "hypervisors" [t| [Hypervisor] |] ])
328 $(buildObject "RpcResultInstanceList" "rpcResInstList"
329 [ simpleField "instances" [t| [String] |] ])
331 instance RpcCall RpcCallInstanceList where
332 rpcCallName _ = "instance_list"
333 rpcCallTimeout _ = rpcTimeoutToRaw Urgent
334 rpcCallAcceptOffline _ = False
335 rpcCallData _ call = J.encode [rpcCallInstListHypervisors call]
337 instance Rpc RpcCallInstanceList RpcResultInstanceList where
338 rpcResultFill _ res = fromJSValueToRes res RpcResultInstanceList
343 -- Return node information.
344 $(buildObject "RpcCallNodeInfo" "rpcCallNodeInfo"
345 [ simpleField "volume_groups" [t| [String] |]
346 , simpleField "hypervisors" [t| [Hypervisor] |]
349 $(buildObject "VgInfo" "vgInfo"
350 [ simpleField "name" [t| String |]
351 , optionalField $ simpleField "vg_free" [t| Int |]
352 , optionalField $ simpleField "vg_size" [t| Int |]
355 -- | We only provide common fields as described in hv_base.py.
356 $(buildObject "HvInfo" "hvInfo"
357 [ simpleField "memory_total" [t| Int |]
358 , simpleField "memory_free" [t| Int |]
359 , simpleField "memory_dom0" [t| Int |]
360 , simpleField "cpu_total" [t| Int |]
361 , simpleField "cpu_nodes" [t| Int |]
362 , simpleField "cpu_sockets" [t| Int |]
365 $(buildObject "RpcResultNodeInfo" "rpcResNodeInfo"
366 [ simpleField "boot_id" [t| String |]
367 , simpleField "vg_info" [t| [VgInfo] |]
368 , simpleField "hv_info" [t| [HvInfo] |]
371 instance RpcCall RpcCallNodeInfo where
372 rpcCallName _ = "node_info"
373 rpcCallTimeout _ = rpcTimeoutToRaw Urgent
374 rpcCallAcceptOffline _ = False
375 rpcCallData _ call = J.encode
376 ( rpcCallNodeInfoVolumeGroups call
377 , rpcCallNodeInfoHypervisors call
380 instance Rpc RpcCallNodeInfo RpcResultNodeInfo where
381 rpcResultFill _ res =
382 fromJSValueToRes res (\(b, vg, hv) -> RpcResultNodeInfo b vg hv)
387 -- Query node version.
388 -- Note: We can't use THH as it does not know what to do with empty dict
389 data RpcCallVersion = RpcCallVersion {}
390 deriving (Show, Read, Eq)
392 instance J.JSON RpcCallVersion where
393 showJSON _ = J.JSNull
394 readJSON J.JSNull = return RpcCallVersion
395 readJSON _ = fail "Unable to read RpcCallVersion"
397 $(buildObject "RpcResultVersion" "rpcResultVersion"
398 [ simpleField "version" [t| Int |]
401 instance RpcCall RpcCallVersion where
402 rpcCallName _ = "version"
403 rpcCallTimeout _ = rpcTimeoutToRaw Urgent
404 rpcCallAcceptOffline _ = True
405 rpcCallData _ = J.encode
407 instance Rpc RpcCallVersion RpcResultVersion where
408 rpcResultFill _ res = fromJSValueToRes res RpcResultVersion
413 -- Get list of storage units.
414 -- FIXME: This may be moved to Objects
415 $(declareSADT "StorageType"
416 [ ( "STLvmPv", 'C.stLvmPv )
417 , ( "STFile", 'C.stFile )
418 , ( "STLvmVg", 'C.stLvmVg )
420 $(makeJSONInstance ''StorageType)
422 -- FIXME: This may be moved to Objects
423 $(declareSADT "StorageField"
424 [ ( "SFUsed", 'C.sfUsed)
425 , ( "SFName", 'C.sfName)
426 , ( "SFAllocatable", 'C.sfAllocatable)
427 , ( "SFFree", 'C.sfFree)
428 , ( "SFSize", 'C.sfSize)
430 $(makeJSONInstance ''StorageField)
432 $(buildObject "RpcCallStorageList" "rpcCallStorageList"
433 [ simpleField "su_name" [t| StorageType |]
434 , simpleField "su_args" [t| [String] |]
435 , simpleField "name" [t| String |]
436 , simpleField "fields" [t| [StorageField] |]
439 -- FIXME: The resulting JSValues should have types appropriate for their
440 -- StorageField value: Used -> Bool, Name -> String etc
441 $(buildObject "RpcResultStorageList" "rpcResStorageList"
442 [ simpleField "storage" [t| [[(StorageField, J.JSValue)]] |] ])
444 instance RpcCall RpcCallStorageList where
445 rpcCallName _ = "storage_list"
446 rpcCallTimeout _ = rpcTimeoutToRaw Normal
447 rpcCallAcceptOffline _ = False
448 rpcCallData _ call = J.encode
449 ( rpcCallStorageListSuName call
450 , rpcCallStorageListSuArgs call
451 , rpcCallStorageListName call
452 , rpcCallStorageListFields call
455 instance Rpc RpcCallStorageList RpcResultStorageList where
456 rpcResultFill call res =
457 let sfields = rpcCallStorageListFields call in
458 fromJSValueToRes res (RpcResultStorageList . map (zip sfields))
463 -- | Call definition for test delay.
464 $(buildObject "RpcCallTestDelay" "rpcCallTestDelay"
465 [ simpleField "duration" [t| Double |]
468 -- | Result definition for test delay.
469 data RpcResultTestDelay = RpcResultTestDelay
472 -- | Custom JSON instance for null result.
473 instance J.JSON RpcResultTestDelay where
474 showJSON _ = J.JSNull
475 readJSON J.JSNull = return RpcResultTestDelay
476 readJSON _ = fail "Unable to read RpcResultTestDelay"
478 instance RpcCall RpcCallTestDelay where
479 rpcCallName _ = "test_delay"
480 rpcCallTimeout = ceiling . (+ 5) . rpcCallTestDelayDuration
481 rpcCallAcceptOffline _ = False
482 rpcCallData _ call = J.encode [rpcCallTestDelayDuration call]
484 instance Rpc RpcCallTestDelay RpcResultTestDelay where
485 rpcResultFill _ res = fromJSValueToRes res id