On Wed, Sep 26, 2012 at 9:31 AM, Iustin Pop <[email protected]> wrote:
> On Tue, Sep 25, 2012 at 06:43:45PM +0200, Agata Murawska wrote:
>> Previous version of RPC calls implementation in Haskell did not take
>> into account that the actual result type for queries is a list, not
>> a dictionary.
>>
>> This patch aims at fixing the problem "for now" - it is not a pretty
>> solution, but it does work. Note that parsing of the result is now
>> split into two parts - first, we check if server's aswer is positive,
>> then if it is, we procede with decoding the actual result.
>>
>> Values and order of some fields in the result type were changed to
>> reflect actual order of arguments from server responses.
>>
>> AllInstancesInfo call was particularly tricky, because it returns a
>> dictionary where keys are instance names - and the response from
>> a given node is correct if all the instances were deserialized, not
>> just some.
>>
>> Signed-off-by: Agata Murawska <[email protected]>
>> ---
>> htools/Ganeti/Rpc.hs | 90
>> +++++++++++++++++++++++++++++++++++---------------
>> 1 files changed, 63 insertions(+), 27 deletions(-)
>>
>> diff --git a/htools/Ganeti/Rpc.hs b/htools/Ganeti/Rpc.hs
>> index 377800c..d21b834 100644
>> --- a/htools/Ganeti/Rpc.hs
>> +++ b/htools/Ganeti/Rpc.hs
>> @@ -56,6 +56,7 @@ module Ganeti.Rpc
>> , rpcTimeoutFromRaw -- FIXME: Not used anywhere
>> ) where
>>
>> +import Control.Arrow (second)
>> import qualified Text.JSON as J
>> import Text.JSON (makeObj)
>>
>> @@ -108,10 +109,6 @@ instance Show RpcError where
>>
>> type ERpcError = Either RpcError
>>
>> -rpcErrorJsonReport :: (Monad m) => J.Result a -> m (ERpcError a)
>> -rpcErrorJsonReport (J.Error x) = return . Left $ JsonDecodeError x
>> -rpcErrorJsonReport (J.Ok x) = return $ Right x
>> -
>> -- | Basic timeouts for RPC calls.
>> $(declareIADT "RpcTimeout"
>> [ ( "Urgent", 'C.rpcTmoUrgent )
>> @@ -133,14 +130,10 @@ class (J.JSON a) => RpcCall a where
>> -- | Whether we accept offline nodes when making a call.
>> rpcCallAcceptOffline :: a -> Bool
>>
>> - rpcCallData _ = J.encode
>> -
>> -- | A generic class for RPC results with default implementation.
>> class (J.JSON a) => RpcResult a where
>> -- | Create a result based on the received HTTP response.
>> - rpcResultFill :: (Monad m) => String -> m (ERpcError a)
>> -
>> - rpcResultFill res = rpcErrorJsonReport $ J.decode res
>> + rpcResultFill :: (Monad m) => J.JSValue -> m (ERpcError a)
>>
>> -- | Generic class that ensures matching RPC call with its respective
>> -- result.
>> @@ -194,11 +187,22 @@ prepareHttpRequest node call
>> }
>> | otherwise = Left $ OfflineNodeError node
>>
>> +-- | Parse a result based on the received HTTP response.
>> +rpcResultParse :: (Monad m, RpcResult a) => String -> m (ERpcError a)
>> +rpcResultParse res = do
>> + res' <- fromJResult "Reading JSON response" $ J.decode res
>> + case res' of
>> + (True, res'') ->
>> + rpcResultFill res''
>> + (False, jerr) -> case jerr of
>> + J.JSString msg -> return . Left $ RpcResultError (J.fromJSString msg)
>> + _ -> (return . Left) . JsonDecodeError $ show jerr
>
> "show jerr" is an ugly way to format JSON objects; please use pp_value, per
> commit c12a68e2.
Forgot about this one; fixed.
>
>> +
>> -- | Parse the response or propagate the error.
>> parseHttpResponse :: (Monad m, RpcResult a) => ERpcError String
>> -> m (ERpcError a)
>> parseHttpResponse (Left err) = return $ Left err
>> -parseHttpResponse (Right response) = rpcResultFill response
>> +parseHttpResponse (Right response) = rpcResultParse response
>>
>> -- | Execute RPC call for a sigle node.
>> executeSingleRpcCall :: (Rpc a b) => Node -> a -> IO (Node, ERpcError b)
>> @@ -214,30 +218,51 @@ executeRpcCall nodes call =
>> sequence $ parMap rwhnf (uncurry executeSingleRpcCall)
>> (zip nodes $ repeat call)
>>
>> +-- | Helper function that is used to read dictionaries of values.
>> +sanitizeDictResults :: [(String, J.Result a)] ->
>> + ERpcError [(String, a)]
>
> Style issue: '->' go always at the beginning of the line, not at the
> end.
>
>> +sanitizeDictResults [] = Right []
>> +sanitizeDictResults ((_, J.Error err):_) = Left $ JsonDecodeError err
>> +sanitizeDictResults ((name, J.Ok val):xs) =
>> + case sanitizeDictResults xs of
>> + Left err -> Left err
>> + Right res' -> Right $ (name, val):res'
>
> Mmm… this smells so much of foldM…
>
>> -- * RPC calls and results
>>
>> -- | AllInstancesInfo
>> --- Returns information about all instances on the given nodes
>> +-- Returns information about all running instances on the given nodes.
>> $(buildObject "RpcCallAllInstancesInfo" "rpcCallAllInstInfo"
>> [ simpleField "hypervisors" [t| [Hypervisor] |] ])
>>
>> $(buildObject "InstanceInfo" "instInfo"
>> - [ simpleField "name" [t| String |]
>> - , simpleField "memory" [t| Int|]
>> - , simpleField "state" [t| AdminState |]
>> + [ simpleField "memory" [t| Int|]
>> + , simpleField "state" [t| String |] -- It depends on hypervisor :(
>> , simpleField "vcpus" [t| Int |]
>> , simpleField "time" [t| Int |]
>> ])
>>
>> $(buildObject "RpcResultAllInstancesInfo" "rpcResAllInstInfo"
>> - [ simpleField "instances" [t| [InstanceInfo] |] ])
>> + [ simpleField "instances" [t| [(String, InstanceInfo)] |] ])
>>
>> instance RpcCall RpcCallAllInstancesInfo where
>> rpcCallName _ = "all_instances_info"
>> rpcCallTimeout _ = rpcTimeoutToRaw Urgent
>> rpcCallAcceptOffline _ = False
>> -
>> -instance RpcResult RpcResultAllInstancesInfo
>> + rpcCallData _ call = J.encode [rpcCallAllInstInfoHypervisors call]
>> +
>> +instance RpcResult RpcResultAllInstancesInfo where
>> + -- FIXME: Is there a simpler way to do it?
>> + rpcResultFill res =
>> + return $ case res of
>> + J.JSObject res' -> do
>> + let res'' = map (second J.readJSON) (J.fromJSObject res')
>> + :: [(String, J.Result InstanceInfo)]
>> + case sanitizeDictResults res'' of
>> + Left err -> Left err
>> + Right insts -> Right $ RpcResultAllInstancesInfo insts
>> + _ -> Left $ JsonDecodeError
>> + ("Expected JSObject, got " ++ show res)
>
> Hmm, not nice indeed. Let's go with this version and see later if we can
> improve it. It's most likely doable with a newtype+custom JSON
> instance.
>
>> instance Rpc RpcCallAllInstancesInfo RpcResultAllInstancesInfo
>>
>> @@ -247,30 +272,33 @@ $(buildObject "RpcCallInstanceList" "rpcCallInstList"
>> [ simpleField "hypervisors" [t| [Hypervisor] |] ])
>>
>> $(buildObject "RpcResultInstanceList" "rpcResInstList"
>> - [ simpleField "node" [t| Node |]
>> - , simpleField "instances" [t| [String] |]
>> - ])
>> + [ simpleField "instances" [t| [String] |] ])
>>
>> instance RpcCall RpcCallInstanceList where
>> rpcCallName _ = "instance_list"
>> rpcCallTimeout _ = rpcTimeoutToRaw Urgent
>> rpcCallAcceptOffline _ = False
>> + rpcCallData _ call = J.encode [rpcCallInstListHypervisors call]
>>
>> -instance RpcResult RpcResultInstanceList
>> +instance RpcResult RpcResultInstanceList where
>> + rpcResultFill res =
>> + return $ case J.readJSON res of
>> + J.Error err -> Left $ JsonDecodeError err
>> + J.Ok insts -> Right $ RpcResultInstanceList insts
>>
>> instance Rpc RpcCallInstanceList RpcResultInstanceList
>>
>> -- | NodeInfo
>> -- Return node information.
>> $(buildObject "RpcCallNodeInfo" "rpcCallNodeInfo"
>> - [ simpleField "hypervisors" [t| [Hypervisor] |]
>> - , simpleField "volume_groups" [t| [String] |]
>> + [ simpleField "volume_groups" [t| [String] |]
>> + , simpleField "hypervisors" [t| [Hypervisor] |]
>> ])
>
> Is there a reason for this or is it just cleanup?
>
>> $(buildObject "VgInfo" "vgInfo"
>> [ simpleField "name" [t| String |]
>> - , simpleField "free" [t| Int |]
>> - , simpleField "size" [t| Int |]
>> + , optionalField $ simpleField "vg_free" [t| Int |]
>> + , optionalField $ simpleField "vg_size" [t| Int |]
>> ])
>>
>> -- | We only provide common fields as described in hv_base.py.
>> @@ -293,7 +321,15 @@ instance RpcCall RpcCallNodeInfo where
>> rpcCallName _ = "node_info"
>> rpcCallTimeout _ = rpcTimeoutToRaw Urgent
>> rpcCallAcceptOffline _ = False
>> -
>> -instance RpcResult RpcResultNodeInfo
>> + rpcCallData _ call = J.encode ( rpcCallNodeInfoVolumeGroups call
>> + , rpcCallNodeInfoHypervisors call
>> + )
>> +
>> +instance RpcResult RpcResultNodeInfo where
>> + rpcResultFill res =
>> + return $ case J.readJSON res of
>> + J.Error err -> Left $ JsonDecodeError err
>> + J.Ok (boot_id, vg_info, hv_info) ->
>> + Right $ RpcResultNodeInfo boot_id vg_info hv_info
>
> Rest LGTM, thanks.
>
> iustin
Interdiff for both changes:
diff --git a/htools/Ganeti/Rpc.hs b/htools/Ganeti/Rpc.hs
index d21b834..0fac0a2 100644
--- a/htools/Ganeti/Rpc.hs
+++ b/htools/Ganeti/Rpc.hs
@@ -58,6 +58,7 @@ module Ganeti.Rpc
import Control.Arrow (second)
import qualified Text.JSON as J
+import Text.JSON.Pretty (pp_value)
import Text.JSON (makeObj)
#ifndef NO_CURL
@@ -196,7 +197,7 @@ rpcResultParse res = do
rpcResultFill res''
(False, jerr) -> case jerr of
J.JSString msg -> return . Left $ RpcResultError (J.fromJSString msg)
- _ -> (return . Left) . JsonDecodeError $ show jerr
+ _ -> (return . Left) . JsonDecodeError $ show (pp_value jerr)
-- | Parse the response or propagate the error.
parseHttpResponse :: (Monad m, RpcResult a) => ERpcError String
@@ -219,8 +220,7 @@ executeRpcCall nodes call =
(zip nodes $ repeat call)
-- | Helper function that is used to read dictionaries of values.
-sanitizeDictResults :: [(String, J.Result a)] ->
- ERpcError [(String, a)]
+sanitizeDictResults :: [(String, J.Result a)] -> ERpcError [(String, a)]
sanitizeDictResults [] = Right []