diff --git a/Makefile.am b/Makefile.am index 5bdcd939322324eada902ee6d5bf5f58a83c46a5..26c5b86b176f6fef31c3f6a12ee1b1b212f18352 100644 --- a/Makefile.am +++ b/Makefile.am @@ -364,7 +364,8 @@ HS_LIB_SRCS = \ htools/Ganeti/HTools/Program/Hspace.hs \ htools/Ganeti/Jobs.hs \ htools/Ganeti/Luxi.hs \ - htools/Ganeti/OpCodes.hs + htools/Ganeti/OpCodes.hs \ + htools/Ganeti/THH.hs HS_BUILT_SRCS = htools/Ganeti/HTools/Version.hs htools/Ganeti/Constants.hs HS_BUILT_SRCS_IN = $(patsubst %,%.in,$(HS_BUILT_SRCS)) diff --git a/htools/Ganeti/HTools/Types.hs b/htools/Ganeti/HTools/Types.hs index 75dbce59ae2c72b4b7252bedd00c3371ad8f8ce5..8e6b6a4dcb44a14a664ba4d88f0c246d7ef11ac3 100644 --- a/htools/Ganeti/HTools/Types.hs +++ b/htools/Ganeti/HTools/Types.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE TemplateHaskell #-} + {-| Some common types. -} @@ -72,6 +74,7 @@ import qualified Data.Map as M import qualified Text.JSON as JSON import qualified Ganeti.Constants as C +import qualified Ganeti.THH as THH -- | The instance index type. type Idx = Int @@ -100,36 +103,12 @@ type GroupID = String -- Ord instance will order them in the order they are defined, so when -- changing this data type be careful about the interaction with the -- desired sorting order. -data AllocPolicy - = AllocPreferred -- ^ This is the normal status, the group - -- should be used normally during allocations - | AllocLastResort -- ^ This group should be used only as - -- last-resort, after the preferred groups - | AllocUnallocable -- ^ This group must not be used for new - -- allocations - deriving (Show, Read, Eq, Ord, Enum, Bounded) - --- | Convert a string to an alloc policy. -allocPolicyFromString :: (Monad m) => String -> m AllocPolicy -allocPolicyFromString s = - case () of - _ | s == C.allocPolicyPreferred -> return AllocPreferred - | s == C.allocPolicyLastResort -> return AllocLastResort - | s == C.allocPolicyUnallocable -> return AllocUnallocable - | otherwise -> fail $ "Invalid alloc policy mode: " ++ s - --- | Convert an alloc policy to the Ganeti string equivalent. -allocPolicyToString :: AllocPolicy -> String -allocPolicyToString AllocPreferred = C.allocPolicyPreferred -allocPolicyToString AllocLastResort = C.allocPolicyLastResort -allocPolicyToString AllocUnallocable = C.allocPolicyUnallocable - -instance JSON.JSON AllocPolicy where - showJSON = JSON.showJSON . allocPolicyToString - readJSON s = case JSON.readJSON s of - JSON.Ok s' -> allocPolicyFromString s' - JSON.Error e -> JSON.Error $ - "Can't parse alloc_policy: " ++ e +$(THH.declareSADT "AllocPolicy" + [ ("AllocPreferred", 'C.allocPolicyPreferred) + , ("AllocLastResort", 'C.allocPolicyLastResort) + , ("AllocUnallocable", 'C.allocPolicyUnallocable) + ]) +$(THH.makeJSONInstance ''AllocPolicy) -- | The resource spec type. data RSpec = RSpec @@ -182,41 +161,15 @@ data IMove = Failover -- ^ Failover the instance (f) deriving (Show, Read) -- | Instance disk template type. -data DiskTemplate = DTDiskless - | DTFile - | DTSharedFile - | DTPlain - | DTBlock - | DTDrbd8 - deriving (Show, Read, Eq, Enum, Bounded) - --- | Converts a DiskTemplate to String. -diskTemplateToString :: DiskTemplate -> String -diskTemplateToString DTDiskless = C.dtDiskless -diskTemplateToString DTFile = C.dtFile -diskTemplateToString DTSharedFile = C.dtSharedFile -diskTemplateToString DTPlain = C.dtPlain -diskTemplateToString DTBlock = C.dtBlock -diskTemplateToString DTDrbd8 = C.dtDrbd8 - --- | Converts a DiskTemplate from String. -diskTemplateFromString :: (Monad m) => String -> m DiskTemplate -diskTemplateFromString s = - case () of - _ | s == C.dtDiskless -> return DTDiskless - | s == C.dtFile -> return DTFile - | s == C.dtSharedFile -> return DTSharedFile - | s == C.dtPlain -> return DTPlain - | s == C.dtBlock -> return DTBlock - | s == C.dtDrbd8 -> return DTDrbd8 - | otherwise -> fail $ "Invalid disk template: " ++ s - -instance JSON.JSON DiskTemplate where - showJSON = JSON.showJSON . diskTemplateToString - readJSON s = case JSON.readJSON s of - JSON.Ok s' -> diskTemplateFromString s' - JSON.Error e -> JSON.Error $ - "Can't parse disk_template as string: " ++ e +$(THH.declareSADT "DiskTemplate" + [ ("DTDiskless", 'C.dtDiskless) + , ("DTFile", 'C.dtFile) + , ("DTSharedFile", 'C.dtSharedFile) + , ("DTPlain", 'C.dtPlain) + , ("DTBlock", 'C.dtBlock) + , ("DTDrbd8", 'C.dtDrbd8) + ]) +$(THH.makeJSONInstance ''DiskTemplate) -- | Formatted solution output for one move (involved nodes and -- commands. @@ -347,21 +300,9 @@ class Element a where setIdx :: a -> Int -> a -- | The iallocator node-evacuate evac_mode type. -data EvacMode = ChangePrimary - | ChangeSecondary - | ChangeAll - deriving (Show, Read) - -instance JSON.JSON EvacMode where - showJSON mode = case mode of - ChangeAll -> JSON.showJSON C.iallocatorNevacAll - ChangePrimary -> JSON.showJSON C.iallocatorNevacPri - ChangeSecondary -> JSON.showJSON C.iallocatorNevacSec - readJSON v = - case JSON.readJSON v of - JSON.Ok s | s == C.iallocatorNevacAll -> return ChangeAll - | s == C.iallocatorNevacPri -> return ChangePrimary - | s == C.iallocatorNevacSec -> return ChangeSecondary - | otherwise -> fail $ "Invalid evacuate mode " ++ s - JSON.Error e -> JSON.Error $ - "Can't parse evacuate mode as string: " ++ e +$(THH.declareSADT "EvacMode" + [ ("ChangePrimary", 'C.iallocatorNevacPri) + , ("ChangeSecondary", 'C.iallocatorNevacSec) + , ("ChangeAll", 'C.iallocatorNevacAll) + ]) +$(THH.makeJSONInstance ''EvacMode) diff --git a/htools/Ganeti/Jobs.hs b/htools/Ganeti/Jobs.hs index ed7bc7d8f34cdfb7823804a85c423a01b4f21f58..1e2cebecccc14eea92cd9c85ccf9e8a21248a5dc 100644 --- a/htools/Ganeti/Jobs.hs +++ b/htools/Ganeti/Jobs.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE TemplateHaskell #-} + {-| Implementation of the job information. -} @@ -32,67 +34,30 @@ import Text.JSON (readJSON, showJSON, JSON) import qualified Text.JSON as J import qualified Ganeti.Constants as C +import qualified Ganeti.THH as THH -- | Our ADT for the OpCode status at runtime (while in a job). -data OpStatus = OP_STATUS_QUEUED - | OP_STATUS_WAITING - | OP_STATUS_CANCELING - | OP_STATUS_RUNNING - | OP_STATUS_CANCELED - | OP_STATUS_SUCCESS - | OP_STATUS_ERROR - deriving (Eq, Enum, Bounded, Show, Read) - -instance JSON OpStatus where - showJSON os = showJSON w - where w = case os of - OP_STATUS_QUEUED -> C.opStatusQueued - OP_STATUS_WAITING -> C.opStatusWaiting - OP_STATUS_CANCELING -> C.opStatusCanceling - OP_STATUS_RUNNING -> C.opStatusRunning - OP_STATUS_CANCELED -> C.opStatusCanceled - OP_STATUS_SUCCESS -> C.opStatusSuccess - OP_STATUS_ERROR -> C.opStatusError - readJSON s = case readJSON s of - J.Ok v | v == C.opStatusQueued -> J.Ok OP_STATUS_QUEUED - | v == C.opStatusWaiting -> J.Ok OP_STATUS_WAITING - | v == C.opStatusCanceling -> J.Ok OP_STATUS_CANCELING - | v == C.opStatusRunning -> J.Ok OP_STATUS_RUNNING - | v == C.opStatusCanceled -> J.Ok OP_STATUS_CANCELED - | v == C.opStatusSuccess -> J.Ok OP_STATUS_SUCCESS - | v == C.opStatusError -> J.Ok OP_STATUS_ERROR - | otherwise -> J.Error ("Unknown opcode status " ++ v) - _ -> J.Error ("Cannot parse opcode status " ++ show s) +$(THH.declareSADT "OpStatus" + [ ("OP_STATUS_QUEUED", 'C.opStatusQueued) + , ("OP_STATUS_WAITING", 'C.opStatusWaiting) + , ("OP_STATUS_CANCELING", 'C.opStatusCanceling) + , ("OP_STATUS_RUNNING", 'C.opStatusRunning) + , ("OP_STATUS_CANCELED", 'C.opStatusCanceled) + , ("OP_STATUS_SUCCESS", 'C.opStatusSuccess) + , ("OP_STATUS_ERROR", 'C.opStatusError) + ]) +$(THH.makeJSONInstance ''OpStatus) -- | The JobStatus data type. Note that this is ordered especially -- such that greater\/lesser comparison on values of this type makes -- sense. -data JobStatus = JOB_STATUS_QUEUED - | JOB_STATUS_WAITING - | JOB_STATUS_RUNNING - | JOB_STATUS_SUCCESS - | JOB_STATUS_CANCELING - | JOB_STATUS_CANCELED - | JOB_STATUS_ERROR - deriving (Eq, Enum, Ord, Bounded, Show, Read) - -instance JSON JobStatus where - showJSON js = showJSON w - where w = case js of - JOB_STATUS_QUEUED -> C.jobStatusQueued - JOB_STATUS_WAITING -> C.jobStatusWaiting - JOB_STATUS_CANCELING -> C.jobStatusCanceling - JOB_STATUS_RUNNING -> C.jobStatusRunning - JOB_STATUS_CANCELED -> C.jobStatusCanceled - JOB_STATUS_SUCCESS -> C.jobStatusSuccess - JOB_STATUS_ERROR -> C.jobStatusError - readJSON s = case readJSON s of - J.Ok v | v == C.jobStatusQueued -> J.Ok JOB_STATUS_QUEUED - | v == C.jobStatusWaiting -> J.Ok JOB_STATUS_WAITING - | v == C.jobStatusCanceling -> J.Ok JOB_STATUS_CANCELING - | v == C.jobStatusRunning -> J.Ok JOB_STATUS_RUNNING - | v == C.jobStatusSuccess -> J.Ok JOB_STATUS_SUCCESS - | v == C.jobStatusCanceled -> J.Ok JOB_STATUS_CANCELED - | v == C.jobStatusError -> J.Ok JOB_STATUS_ERROR - | otherwise -> J.Error ("Unknown job status " ++ v) - _ -> J.Error ("Unknown job status " ++ show s) +$(THH.declareSADT "JobStatus" + [ ("JOB_STATUS_QUEUED", 'C.jobStatusQueued) + , ("JOB_STATUS_WAITING", 'C.jobStatusWaiting) + , ("JOB_STATUS_CANCELING", 'C.jobStatusCanceling) + , ("JOB_STATUS_RUNNING", 'C.jobStatusRunning) + , ("JOB_STATUS_CANCELED", 'C.jobStatusCanceled) + , ("JOB_STATUS_SUCCESS", 'C.jobStatusSuccess) + , ("JOB_STATUS_ERROR", 'C.jobStatusError) + ]) +$(THH.makeJSONInstance ''JobStatus) diff --git a/htools/Ganeti/OpCodes.hs b/htools/Ganeti/OpCodes.hs index 09305983bc64b43414b664c2bd1962ad341f2c8b..b3fc1f33aaeaa145a79dc8fe460b8d79929d4ab3 100644 --- a/htools/Ganeti/OpCodes.hs +++ b/htools/Ganeti/OpCodes.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE TemplateHaskell #-} + {-| Implementation of the opcodes. -} @@ -34,27 +36,19 @@ import Text.JSON (readJSON, showJSON, makeObj, JSON) import qualified Text.JSON as J import Text.JSON.Types +import qualified Ganeti.Constants as C +import qualified Ganeti.THH as THH + import Ganeti.HTools.Utils -- | Replace disks type. -data ReplaceDisksMode = ReplaceOnPrimary - | ReplaceOnSecondary - | ReplaceNewSecondary - | ReplaceAuto - deriving (Show, Read, Eq) - -instance JSON ReplaceDisksMode where - showJSON m = case m of - ReplaceOnPrimary -> showJSON "replace_on_primary" - ReplaceOnSecondary -> showJSON "replace_on_secondary" - ReplaceNewSecondary -> showJSON "replace_new_secondary" - ReplaceAuto -> showJSON "replace_auto" - readJSON s = case readJSON s of - J.Ok "replace_on_primary" -> J.Ok ReplaceOnPrimary - J.Ok "replace_on_secondary" -> J.Ok ReplaceOnSecondary - J.Ok "replace_new_secondary" -> J.Ok ReplaceNewSecondary - J.Ok "replace_auto" -> J.Ok ReplaceAuto - _ -> J.Error "Can't parse a valid ReplaceDisksMode" +$(THH.declareSADT "ReplaceDisksMode" + [ ("ReplaceOnPrimary", 'C.replaceDiskPri) + , ("ReplaceOnSecondary", 'C.replaceDiskSec) + , ("ReplaceNewSecondary", 'C.replaceDiskChg) + , ("ReplaceAuto", 'C.replaceDiskAuto) + ]) +$(THH.makeJSONInstance ''ReplaceDisksMode) -- | OpCode representation. -- diff --git a/htools/Ganeti/THH.hs b/htools/Ganeti/THH.hs new file mode 100644 index 0000000000000000000000000000000000000000..6e539a43d4d023d1bb388213aca3279d47faa8a6 --- /dev/null +++ b/htools/Ganeti/THH.hs @@ -0,0 +1,186 @@ +{-# LANGUAGE TemplateHaskell, QuasiQuotes #-} + +{-| TemplateHaskell helper for HTools. + +As TemplateHaskell require that splices be defined in a separate +module, we combine all the TemplateHaskell functionality that HTools +needs in this module (except the one for unittests). + +-} + +{- + +Copyright (C) 2011 Google Inc. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +02110-1301, USA. + +-} + +module Ganeti.THH ( declareSADT + , makeJSONInstance + ) where + +import Data.Char +import Language.Haskell.TH + +import qualified Text.JSON as JSON + +-- | Ensure first letter is lowercase. +-- +-- Used to convert type name to function prefix, e.g. in @data Aa -> +-- aaToString@. +ensureLower :: String -> String +ensureLower [] = [] +ensureLower (x:xs) = toLower x:xs + +-- | ToString function name. +toStrName :: String -> Name +toStrName = mkName . (++ "ToString") . ensureLower + +-- | FromString function name. +fromStrName :: String -> Name +fromStrName = mkName . (++ "FromString") . ensureLower + +-- | Generates a data type declaration. +-- +-- The type will have a fixed list of instances. +strADTDecl :: Name -> [String] -> Dec +strADTDecl name constructors = + DataD [] name [] + (map (flip NormalC [] . mkName) constructors) + [''Show, ''Read, ''Eq, ''Enum, ''Bounded, ''Ord] + +-- | Generates a toString function. +-- +-- This generates a simple function of the form: +-- +-- @ +-- nameToString :: Name -> String +-- nameToString Cons1 = var1 +-- nameToString Cons2 = \"value2\" +-- @ +genToString :: Name -> Name -> [(String, Name)] -> Q [Dec] +genToString fname tname constructors = do + sigt <- [t| $(conT tname) -> String |] + -- the body clauses, matching on the constructor and returning the + -- string value + clauses <- mapM (\(c, v) -> clause [recP (mkName c) []] + (normalB (varE v)) []) constructors + return [SigD fname sigt, FunD fname clauses] + +-- | Generates a fromString function. +-- +-- The function generated is monadic and can fail parsing the +-- string. It is of the form: +-- +-- @ +-- nameFromString :: (Monad m) => String -> m Name +-- nameFromString s | s == var1 = Cons1 +-- | s == \"value2\" = Cons2 +-- | otherwise = fail /.../ +-- @ +genFromString :: Name -> Name -> [(String, Name)] -> Q [Dec] +genFromString fname tname constructors = do + -- signature of form (Monad m) => String -> m $name + sigt <- [t| (Monad m) => String -> m $(conT tname) |] + -- clauses for a guarded pattern + let varp = mkName "s" + varpe = varE varp + clauses <- mapM (\(c, v) -> do + -- the clause match condition + g <- normalG [| $varpe == $(varE v) |] + -- the clause result + r <- [| return $(conE (mkName c)) |] + return (g, r)) constructors + -- the otherwise clause (fallback) + oth_clause <- do + g <- normalG [| otherwise |] + r <- [|fail ("Invalid string value for type " ++ + $(litE (stringL (nameBase tname))) ++ ": " ++ $varpe) |] + return (g, r) + let fun = FunD fname [Clause [VarP varp] + (GuardedB (clauses++[oth_clause])) []] + return [SigD fname sigt, fun] + +-- | Generates a data type from a given string format. +-- +-- The format is expected to multiline. The first line contains the +-- type name, and the rest of the lines must contain two words: the +-- constructor name and then the string representation of the +-- respective constructor. +-- +-- The function will generate the data type declaration, and then two +-- functions: +-- +-- * /name/ToString, which converts the type to a string +-- +-- * /name/FromString, which (monadically) converts from a string to the type +-- +-- Note that this is basically just a custom show/read instance, +-- nothing else. +declareSADT :: String -> [(String, Name)] -> Q [Dec] +declareSADT sname cons = do + let name = mkName sname + ddecl = strADTDecl name (map fst cons) + tostr <- genToString (toStrName sname) name cons + fromstr <- genFromString (fromStrName sname) name cons + return $ ddecl:tostr ++ fromstr + + +-- | Creates the showJSON member of a JSON instance declaration. +-- +-- This will create what is the equivalent of: +-- +-- @ +-- showJSON = showJSON . /name/ToString +-- @ +-- +-- in an instance JSON /name/ declaration +genShowJSON :: String -> Q [Dec] +genShowJSON name = [d| showJSON = JSON.showJSON . $(varE (toStrName name)) |] + +-- | Creates the readJSON member of a JSON instance declaration. +-- +-- This will create what is the equivalent of: +-- +-- @ +-- readJSON s = case readJSON s of +-- Ok s' -> /name/FromString s' +-- Error e -> Error /description/ +-- @ +-- +-- in an instance JSON /name/ declaration +genReadJSON :: String -> Q Dec +genReadJSON name = do + let s = mkName "s" + body <- [| case JSON.readJSON $(varE s) of + JSON.Ok s' -> $(varE (fromStrName name)) s' + JSON.Error e -> + JSON.Error $ "Can't parse string value for type " ++ + $(litE (StringL name)) ++ ": " ++ e + |] + return $ FunD (mkName "readJSON") [Clause [VarP s] (NormalB body) []] + +-- | Generates a JSON instance for a given type. +-- +-- This assumes that the /name/ToString and /name/FromString functions +-- have been defined as by the 'declareSADT' function. +makeJSONInstance :: Name -> Q [Dec] +makeJSONInstance name = do + let base = nameBase name + showJ <- genShowJSON base + readJ <- genReadJSON base + return [InstanceD [] (AppT (ConT ''JSON.JSON) (ConT name)) (readJ:showJ)]