Fix old-style import
[ganeti-local] / htools / Ganeti / HTools / QC.hs
index 23da865..d056a9c 100644 (file)
@@ -32,23 +32,34 @@ module Ganeti.HTools.QC
   , testInstance
   , testNode
   , testText
+  , testSimu
   , testOpCodes
   , testJobs
   , testCluster
   , testLoader
   , testTypes
+  , testCLI
+  , testJSON
+  , testLUXI
+  , testSsconf
   ) where
 
 import Test.QuickCheck
+import Text.Printf (printf)
 import Data.List (findIndex, intercalate, nub, isPrefixOf)
+import qualified Data.Set as Set
 import Data.Maybe
 import Control.Monad
+import Control.Applicative
+import qualified System.Console.GetOpt as GetOpt
 import qualified Text.JSON as J
 import qualified Data.Map
 import qualified Data.IntMap as IntMap
+
 import qualified Ganeti.OpCodes as OpCodes
 import qualified Ganeti.Jobs as Jobs
-import qualified Ganeti.Luxi
+import qualified Ganeti.Luxi as Luxi
+import qualified Ganeti.Ssconf as Ssconf
 import qualified Ganeti.HTools.CLI as CLI
 import qualified Ganeti.HTools.Cluster as Cluster
 import qualified Ganeti.HTools.Container as Container
@@ -57,18 +68,19 @@ import qualified Ganeti.HTools.IAlloc as IAlloc
 import qualified Ganeti.HTools.Instance as Instance
 import qualified Ganeti.HTools.JSON as JSON
 import qualified Ganeti.HTools.Loader as Loader
-import qualified Ganeti.HTools.Luxi
+import qualified Ganeti.HTools.Luxi as HTools.Luxi
 import qualified Ganeti.HTools.Node as Node
 import qualified Ganeti.HTools.Group as Group
 import qualified Ganeti.HTools.PeerMap as PeerMap
 import qualified Ganeti.HTools.Rapi
-import qualified Ganeti.HTools.Simu
+import qualified Ganeti.HTools.Simu as Simu
 import qualified Ganeti.HTools.Text as Text
 import qualified Ganeti.HTools.Types as Types
 import qualified Ganeti.HTools.Utils as Utils
 import qualified Ganeti.HTools.Version
 import qualified Ganeti.Constants as C
 
+import qualified Ganeti.HTools.Program as Program
 import qualified Ganeti.HTools.Program.Hail
 import qualified Ganeti.HTools.Program.Hbal
 import qualified Ganeti.HTools.Program.Hscan
@@ -90,6 +102,27 @@ maxDsk = 1024 * 1024 * 8
 maxCpu :: Int
 maxCpu = 1024
 
+-- | Max vcpu ratio (random value).
+maxVcpuRatio :: Double
+maxVcpuRatio = 1024.0
+
+-- | Max spindle ratio (random value).
+maxSpindleRatio :: Double
+maxSpindleRatio = 1024.0
+
+-- | Max nodes, used just to limit arbitrary instances for smaller
+-- opcode definitions (e.g. list of nodes in OpTestDelay).
+maxNodes :: Int
+maxNodes = 32
+
+-- | Max opcodes or jobs in a submit job and submit many jobs.
+maxOpCodes :: Int
+maxOpCodes = 16
+
+-- | All disk templates (used later)
+allDiskTemplates :: [Types.DiskTemplate]
+allDiskTemplates = [minBound..maxBound]
+
 -- | Null iPolicy, and by null we mean very liberal.
 nullIPolicy = Types.IPolicy
   { Types.iPolicyMinSpec = Types.ISpec { Types.iSpecMemorySize = 0
@@ -97,20 +130,26 @@ nullIPolicy = Types.IPolicy
                                        , Types.iSpecDiskSize   = 0
                                        , Types.iSpecDiskCount  = 0
                                        , Types.iSpecNicCount   = 0
+                                       , Types.iSpecSpindleUse = 0
                                        }
   , Types.iPolicyMaxSpec = Types.ISpec { Types.iSpecMemorySize = maxBound
                                        , Types.iSpecCpuCount   = maxBound
                                        , Types.iSpecDiskSize   = maxBound
                                        , Types.iSpecDiskCount  = C.maxDisks
                                        , Types.iSpecNicCount   = C.maxNics
+                                       , Types.iSpecSpindleUse = maxBound
                                        }
   , Types.iPolicyStdSpec = Types.ISpec { Types.iSpecMemorySize = Types.unitMem
                                        , Types.iSpecCpuCount   = Types.unitCpu
                                        , Types.iSpecDiskSize   = Types.unitDsk
                                        , Types.iSpecDiskCount  = 1
                                        , Types.iSpecNicCount   = 1
+                                       , Types.iSpecSpindleUse = 1
                                        }
-  , Types.iPolicyDiskTemplates = [Types.DTDrbd8, Types.DTPlain]
+  , Types.iPolicyDiskTemplates = [minBound..maxBound]
+  , Types.iPolicyVcpuRatio = maxVcpuRatio -- somewhat random value, high
+                                          -- enough to not impact us
+  , Types.iPolicySpindleRatio = maxSpindleRatio
   }
 
 
@@ -150,16 +189,10 @@ setInstanceSmallerThanNode node inst =
        , Instance.vcpus = Node.availCpu node `div` 2
        }
 
--- | Check if an instance is smaller than a node.
-isInstanceSmallerThanNode node inst =
-  Instance.mem inst   <= Node.availMem node `div` 2 &&
-  Instance.dsk inst   <= Node.availDisk node `div` 2 &&
-  Instance.vcpus inst <= Node.availCpu node `div` 2
-
 -- | Create an instance given its spec.
 createInstance mem dsk vcpus =
   Instance.create "inst-unnamed" mem dsk vcpus Types.Running [] True (-1) (-1)
-    Types.DTDrbd8
+    Types.DTDrbd8 1
 
 -- | Create a small cluster by repeating a node spec.
 makeSmallCluster :: Node.Node -> Int -> Node.List
@@ -216,6 +249,16 @@ genUniquesList cnt =
            newelem <- arbitrary `suchThat` (`notElem` lst)
            return (newelem:lst)) [] [1..cnt]
 
+-- | Checks if an instance is mirrored.
+isMirrored :: Instance.Instance -> Bool
+isMirrored = (/= Types.MirrorNone) . Instance.mirrorType
+
+-- | Returns the possible change node types for a disk template.
+evacModeOptions :: Types.MirrorType -> [Types.EvacMode]
+evacModeOptions Types.MirrorNone     = []
+evacModeOptions Types.MirrorInternal = [minBound..maxBound] -- DRBD can do all
+evacModeOptions Types.MirrorExternal = [Types.ChangePrimary, Types.ChangeAll]
+
 -- * Arbitrary instances
 
 -- | Defines a DNS name.
@@ -230,16 +273,31 @@ instance Arbitrary DNSChar where
 getName :: Gen String
 getName = do
   n <- choose (1, 64)
-  dn <- vector n::Gen [DNSChar]
+  dn <- vector n
   return (map dnsGetChar dn)
 
 -- | Generates an entire FQDN.
 getFQDN :: Gen String
 getFQDN = do
   ncomps <- choose (1, 4)
-  names <- mapM (const getName) [1..ncomps::Int]
+  names <- vectorOf ncomps getName
   return $ intercalate "." names
 
+-- | Combinator that generates a 'Maybe' using a sub-combinator.
+getMaybe :: Gen a -> Gen (Maybe a)
+getMaybe subgen = do
+  bool <- arbitrary
+  if bool
+    then Just <$> subgen
+    else return Nothing
+
+-- | Generates a fields list. This uses the same character set as a
+-- DNS name (just for simplicity).
+getFields :: Gen [String]
+getFields = do
+  n <- choose (1, 32)
+  vectorOf n getName
+
 -- | Defines a tag type.
 newtype TagChar = TagChar { tagGetChar :: Char }
 
@@ -275,18 +333,29 @@ genTags = do
 instance Arbitrary Types.InstanceStatus where
     arbitrary = elements [minBound..maxBound]
 
+-- | Generates a random instance with maximum disk/mem/cpu values.
+genInstanceSmallerThan :: Int -> Int -> Int -> Gen Instance.Instance
+genInstanceSmallerThan lim_mem lim_dsk lim_cpu = do
+  name <- getFQDN
+  mem <- choose (0, lim_mem)
+  dsk <- choose (0, lim_dsk)
+  run_st <- arbitrary
+  pn <- arbitrary
+  sn <- arbitrary
+  vcpus <- choose (0, lim_cpu)
+  dt <- arbitrary
+  return $ Instance.create name mem dsk vcpus run_st [] True pn sn dt 1
+
+-- | Generates an instance smaller than a node.
+genInstanceSmallerThanNode :: Node.Node -> Gen Instance.Instance
+genInstanceSmallerThanNode node =
+  genInstanceSmallerThan (Node.availMem node `div` 2)
+                         (Node.availDisk node `div` 2)
+                         (Node.availCpu node `div` 2)
+
 -- let's generate a random instance
 instance Arbitrary Instance.Instance where
-  arbitrary = do
-    name <- getFQDN
-    mem <- choose (0, maxMem)
-    dsk <- choose (0, maxDsk)
-    run_st <- arbitrary
-    pn <- arbitrary
-    sn <- arbitrary
-    vcpus <- choose (0, maxCpu)
-    return $ Instance.create name mem dsk vcpus run_st [] True pn sn
-              Types.DTDrbd8
+  arbitrary = genInstanceSmallerThan maxMem maxDsk maxCpu
 
 -- | Generas an arbitrary node based on sizing information.
 genNode :: Maybe Int -- ^ Minimum node size in terms of units
@@ -315,7 +384,7 @@ genNode min_multiplier max_multiplier = do
   cpu_t <- choose (base_cpu, top_cpu)
   offl  <- arbitrary
   let n = Node.create name (fromIntegral mem_t) mem_n mem_f
-          (fromIntegral dsk_t) dsk_f (fromIntegral cpu_t) offl 0
+          (fromIntegral dsk_t) dsk_f (fromIntegral cpu_t) offl 1 0
       n' = Node.setPolicy nullIPolicy n
   return $ Node.buildPeers n' Container.empty
 
@@ -345,16 +414,17 @@ instance Arbitrary OpCodes.OpCode where
                       ]
     case op_id of
       "OP_TEST_DELAY" ->
-        liftM3 OpCodes.OpTestDelay arbitrary arbitrary arbitrary
+        OpCodes.OpTestDelay <$> arbitrary <*> arbitrary
+                 <*> resize maxNodes (listOf getFQDN)
       "OP_INSTANCE_REPLACE_DISKS" ->
-        liftM5 OpCodes.OpInstanceReplaceDisks arbitrary arbitrary
-          arbitrary arbitrary arbitrary
+        OpCodes.OpInstanceReplaceDisks <$> getFQDN <*> getMaybe getFQDN <*>
+          arbitrary <*> resize C.maxDisks arbitrary <*> getMaybe getName
       "OP_INSTANCE_FAILOVER" ->
-        liftM3 OpCodes.OpInstanceFailover arbitrary arbitrary
-          arbitrary
+        OpCodes.OpInstanceFailover <$> getFQDN <*> arbitrary <*>
+          getMaybe getFQDN
       "OP_INSTANCE_MIGRATE" ->
-        liftM5 OpCodes.OpInstanceMigrate arbitrary arbitrary
-          arbitrary arbitrary arbitrary
+        OpCodes.OpInstanceMigrate <$> getFQDN <*> arbitrary <*>
+          arbitrary <*> arbitrary <*> getMaybe getFQDN
       _ -> fail "Wrong opcode"
 
 instance Arbitrary Jobs.OpStatus where
@@ -384,53 +454,73 @@ instance Arbitrary Types.EvacMode where
 instance Arbitrary a => Arbitrary (Types.OpResult a) where
   arbitrary = arbitrary >>= \c ->
               if c
-                then liftM Types.OpGood arbitrary
-                else liftM Types.OpFail arbitrary
+                then Types.OpGood <$> arbitrary
+                else Types.OpFail <$> arbitrary
 
 instance Arbitrary Types.ISpec where
   arbitrary = do
-    mem <- arbitrary::Gen (NonNegative Int)
+    mem_s <- arbitrary::Gen (NonNegative Int)
     dsk_c <- arbitrary::Gen (NonNegative Int)
     dsk_s <- arbitrary::Gen (NonNegative Int)
-    cpu <- arbitrary::Gen (NonNegative Int)
-    nic <- arbitrary::Gen (NonNegative Int)
-    return Types.ISpec { Types.iSpecMemorySize = fromIntegral mem
-                       , Types.iSpecCpuCount   = fromIntegral cpu
+    cpu_c <- arbitrary::Gen (NonNegative Int)
+    nic_c <- arbitrary::Gen (NonNegative Int)
+    su    <- arbitrary::Gen (NonNegative Int)
+    return Types.ISpec { Types.iSpecMemorySize = fromIntegral mem_s
+                       , Types.iSpecCpuCount   = fromIntegral cpu_c
                        , Types.iSpecDiskSize   = fromIntegral dsk_s
                        , Types.iSpecDiskCount  = fromIntegral dsk_c
-                       , Types.iSpecNicCount   = fromIntegral nic
+                       , Types.iSpecNicCount   = fromIntegral nic_c
+                       , Types.iSpecSpindleUse = fromIntegral su
                        }
 
--- | Helper function to check whether a spec is LTE than another
-iSpecSmaller :: Types.ISpec -> Types.ISpec -> Bool
-iSpecSmaller imin imax =
-  Types.iSpecMemorySize imin <= Types.iSpecMemorySize imax &&
-  Types.iSpecCpuCount imin   <= Types.iSpecCpuCount imax &&
-  Types.iSpecDiskSize imin   <= Types.iSpecDiskSize imax &&
-  Types.iSpecDiskCount imin  <= Types.iSpecDiskCount imax &&
-  Types.iSpecNicCount imin   <= Types.iSpecNicCount imax
+-- | Generates an ispec bigger than the given one.
+genBiggerISpec :: Types.ISpec -> Gen Types.ISpec
+genBiggerISpec imin = do
+  mem_s <- choose (Types.iSpecMemorySize imin, maxBound)
+  dsk_c <- choose (Types.iSpecDiskCount imin, maxBound)
+  dsk_s <- choose (Types.iSpecDiskSize imin, maxBound)
+  cpu_c <- choose (Types.iSpecCpuCount imin, maxBound)
+  nic_c <- choose (Types.iSpecNicCount imin, maxBound)
+  su    <- choose (Types.iSpecSpindleUse imin, maxBound)
+  return Types.ISpec { Types.iSpecMemorySize = fromIntegral mem_s
+                     , Types.iSpecCpuCount   = fromIntegral cpu_c
+                     , Types.iSpecDiskSize   = fromIntegral dsk_s
+                     , Types.iSpecDiskCount  = fromIntegral dsk_c
+                     , Types.iSpecNicCount   = fromIntegral nic_c
+                     , Types.iSpecSpindleUse = fromIntegral su
+                     }
 
 instance Arbitrary Types.IPolicy where
   arbitrary = do
     imin <- arbitrary
-    istd <- arbitrary `suchThat` (iSpecSmaller imin)
-    imax <- arbitrary `suchThat` (iSpecSmaller istd)
-    dts  <- arbitrary
+    istd <- genBiggerISpec imin
+    imax <- genBiggerISpec istd
+    num_tmpl <- choose (0, length allDiskTemplates)
+    dts  <- genUniquesList num_tmpl
+    vcpu_ratio <- choose (1.0, maxVcpuRatio)
+    spindle_ratio <- choose (1.0, maxSpindleRatio)
     return Types.IPolicy { Types.iPolicyMinSpec = imin
                          , Types.iPolicyStdSpec = istd
                          , Types.iPolicyMaxSpec = imax
                          , Types.iPolicyDiskTemplates = dts
+                         , Types.iPolicyVcpuRatio = vcpu_ratio
+                         , Types.iPolicySpindleRatio = spindle_ratio
                          }
 
 -- * Actual tests
 
 -- ** Utils tests
 
+-- | Helper to generate a small string that doesn't contain commas.
+genNonCommaString = do
+  size <- choose (0, 20) -- arbitrary max size
+  vectorOf size (arbitrary `suchThat` ((/=) ','))
+
 -- | If the list is not just an empty element, and if the elements do
 -- not contain commas, then join+split should be idempotent.
 prop_Utils_commaJoinSplit =
-  forAll (arbitrary `suchThat`
-          (\l -> l /= [""] && all (notElem ',') l )) $ \lst ->
+  forAll (choose (0, 20)) $ \llen ->
+  forAll (vectorOf llen genNonCommaString `suchThat` ((/=) [""])) $ \lst ->
   Utils.sepSplit ',' (Utils.commaJoin lst) ==? lst
 
 -- | Split and join should always be idempotent.
@@ -483,17 +573,20 @@ prop_Utils_select_undefv lst1 (NonEmpty lst2) =
           cndlist = flist ++ tlist ++ [undefined]
 
 prop_Utils_parseUnit (NonNegative n) =
-  Utils.parseUnit (show n) == Types.Ok n &&
-  Utils.parseUnit (show n ++ "m") == Types.Ok n &&
-  (case Utils.parseUnit (show n ++ "M") of
-     Types.Ok m -> if n > 0
-                     then m < n  -- for positive values, X MB is < than X MiB
-                     else m == 0 -- but for 0, 0 MB == 0 MiB
-     Types.Bad _ -> False) &&
-  Utils.parseUnit (show n ++ "g") == Types.Ok (n*1024) &&
-  Utils.parseUnit (show n ++ "t") == Types.Ok (n*1048576) &&
-  Types.isBad (Utils.parseUnit (show n ++ "x")::Types.Result Int)
-    where _types = n::Int
+  Utils.parseUnit (show n) ==? Types.Ok n .&&.
+  Utils.parseUnit (show n ++ "m") ==? Types.Ok n .&&.
+  Utils.parseUnit (show n ++ "M") ==? Types.Ok (truncate n_mb::Int) .&&.
+  Utils.parseUnit (show n ++ "g") ==? Types.Ok (n*1024) .&&.
+  Utils.parseUnit (show n ++ "G") ==? Types.Ok (truncate n_gb::Int) .&&.
+  Utils.parseUnit (show n ++ "t") ==? Types.Ok (n*1048576) .&&.
+  Utils.parseUnit (show n ++ "T") ==? Types.Ok (truncate n_tb::Int) .&&.
+  printTestCase "Internal error/overflow?"
+    (n_mb >=0 && n_gb >= 0 && n_tb >= 0) .&&.
+  property (Types.isBad (Utils.parseUnit (show n ++ "x")::Types.Result Int))
+  where _types = (n::Int)
+        n_mb = (fromIntegral n::Rational) * 1000 * 1000 / 1024 / 1024
+        n_gb = n_mb * 1000
+        n_tb = n_gb * 1000
 
 -- | Test list for the Utils module.
 testSuite "Utils"
@@ -573,7 +666,8 @@ prop_Container_nameOf node =
 -- | We test that in a cluster, given a random node, we can find it by
 -- its name and alias, as long as all names and aliases are unique,
 -- and that we fail to find a non-existing name.
-prop_Container_findByName node =
+prop_Container_findByName =
+  forAll (genNode (Just 1) Nothing) $ \node ->
   forAll (choose (1, 20)) $ \ cnt ->
   forAll (choose (0, cnt - 1)) $ \ fidx ->
   forAll (genUniquesList (cnt * 2)) $ \ allnames ->
@@ -587,9 +681,10 @@ prop_Container_findByName node =
                $ zip names nodes
       nl' = Container.fromList nodes'
       target = snd (nodes' !! fidx)
-  in Container.findByName nl' (Node.name target) == Just target &&
-     Container.findByName nl' (Node.alias target) == Just target &&
-     isNothing (Container.findByName nl' othername)
+  in Container.findByName nl' (Node.name target) ==? Just target .&&.
+     Container.findByName nl' (Node.alias target) ==? Just target .&&.
+     printTestCase "Found non-existing name"
+       (isNothing (Container.findByName nl' othername))
 
 testSuite "Container"
             [ 'prop_Container_addTwo
@@ -689,17 +784,20 @@ testSuite "Instance"
             , 'prop_Instance_setMovable
             ]
 
--- ** Text backend tests
+-- ** Backends
+
+-- *** Text backend tests
 
 -- Instance text loader tests
 
 prop_Text_Load_Instance name mem dsk vcpus status
                         (NonEmpty pnode) snode
-                        (NonNegative pdx) (NonNegative sdx) autobal dt =
+                        (NonNegative pdx) (NonNegative sdx) autobal dt su =
   pnode /= snode && pdx /= sdx ==>
   let vcpus_s = show vcpus
       dsk_s = show dsk
       mem_s = show mem
+      su_s = show su
       status_s = Types.instanceStatusToRaw status
       ndx = if null snode
               then [(pnode, pdx)]
@@ -710,7 +808,7 @@ prop_Text_Load_Instance name mem dsk vcpus status
       sdt = Types.diskTemplateToRaw dt
       inst = Text.loadInst nl
              [name, mem_s, dsk_s, vcpus_s, status_s,
-              sbal, pnode, snode, sdt, tags]
+              sbal, pnode, snode, sdt, tags, su_s]
       fail1 = Text.loadInst nl
               [name, mem_s, dsk_s, vcpus_s, status_s,
                sbal, pnode, pnode, tags]
@@ -730,10 +828,11 @@ prop_Text_Load_Instance name mem dsk vcpus status
                                       then Node.noSecondary
                                       else sdx) &&
                Instance.autoBalance i == autobal &&
+               Instance.spindleUse i == su &&
                Types.isBad fail1
 
 prop_Text_Load_InstanceFail ktn fields =
-  length fields /= 10 ==>
+  length fields /= 10 && length fields /= 11 ==>
     case Text.loadInst nl fields of
       Types.Ok _ -> failTest "Managed to load instance from invalid data"
       Types.Bad msg -> printTestCase ("Unrecognised error message: " ++ msg) $
@@ -773,13 +872,15 @@ prop_Text_Load_Node name tm nm fm td fd tc fo =
 prop_Text_Load_NodeFail fields =
   length fields /= 8 ==> isNothing $ Text.loadNode Data.Map.empty fields
 
-prop_Text_NodeLSIdempotent node =
-  (Text.loadNode defGroupAssoc.
-       Utils.sepSplit '|' . Text.serializeNode defGroupList) n ==
-  Just (Node.name n, n)
-    -- override failN1 to what loadNode returns by default
-    where n = node { Node.failN1 = True, Node.offline = False
-                   , Node.iPolicy = Types.defIPolicy }
+prop_Text_NodeLSIdempotent =
+  forAll (genNode (Just 1) Nothing) $ \node ->
+  -- override failN1 to what loadNode returns by default
+  let n = Node.setPolicy Types.defIPolicy $
+          node { Node.failN1 = True, Node.offline = False }
+  in
+    (Text.loadNode defGroupAssoc.
+         Utils.sepSplit '|' . Text.serializeNode defGroupList) n ==?
+    Just (Node.name n, n)
 
 prop_Text_ISpecIdempotent ispec =
   case Text.loadISpec "dummy" . Utils.sepSplit ',' .
@@ -803,15 +904,14 @@ prop_Text_IPolicyIdempotent ipol =
 -- small cluster sizes.
 prop_Text_CreateSerialise =
   forAll genTags $ \ctags ->
-  forAll (choose (1, 2)) $ \reqnodes ->
   forAll (choose (1, 20)) $ \maxiter ->
   forAll (choose (2, 10)) $ \count ->
   forAll genOnlineNode $ \node ->
-  forAll (arbitrary `suchThat` isInstanceSmallerThanNode node) $ \inst ->
-  let inst' = Instance.setMovable inst $ Utils.if' (reqnodes == 2) True False
-      nl = makeSmallCluster node count
+  forAll (genInstanceSmallerThanNode node) $ \inst ->
+  let nl = makeSmallCluster node count
+      reqnodes = Instance.requiredNodes $ Instance.diskTemplate inst
   in case Cluster.genAllocNodes defGroupList nl reqnodes True >>= \allocn ->
-     Cluster.iterateAlloc nl Container.empty (Just maxiter) inst' allocn [] []
+     Cluster.iterateAlloc nl Container.empty (Just maxiter) inst allocn [] []
      of
        Types.Bad msg -> failTest $ "Failed to allocate: " ++ msg
        Types.Ok (_, _, _, [], _) -> printTestCase
@@ -840,6 +940,57 @@ testSuite "Text"
             , 'prop_Text_CreateSerialise
             ]
 
+-- *** Simu backend
+
+-- | Generates a tuple of specs for simulation.
+genSimuSpec :: Gen (String, Int, Int, Int, Int)
+genSimuSpec = do
+  pol <- elements [C.allocPolicyPreferred,
+                   C.allocPolicyLastResort, C.allocPolicyUnallocable,
+                  "p", "a", "u"]
+ -- should be reasonable (nodes/group), bigger values only complicate
+ -- the display of failed tests, and we don't care (in this particular
+ -- test) about big node groups
+  nodes <- choose (0, 20)
+  dsk <- choose (0, maxDsk)
+  mem <- choose (0, maxMem)
+  cpu <- choose (0, maxCpu)
+  return (pol, nodes, dsk, mem, cpu)
+
+-- | Checks that given a set of corrects specs, we can load them
+-- successfully, and that at high-level the values look right.
+prop_SimuLoad =
+  forAll (choose (0, 10)) $ \ngroups ->
+  forAll (replicateM ngroups genSimuSpec) $ \specs ->
+  let strspecs = map (\(p, n, d, m, c) -> printf "%s,%d,%d,%d,%d"
+                                          p n d m c::String) specs
+      totnodes = sum $ map (\(_, n, _, _, _) -> n) specs
+      mdc_in = concatMap (\(_, n, d, m, c) ->
+                            replicate n (fromIntegral m, fromIntegral d,
+                                         fromIntegral c,
+                                         fromIntegral m, fromIntegral d)) specs
+  in case Simu.parseData strspecs of
+       Types.Bad msg -> failTest $ "Failed to load specs: " ++ msg
+       Types.Ok (Loader.ClusterData gl nl il tags ipol) ->
+         let nodes = map snd $ IntMap.toAscList nl
+             nidx = map Node.idx nodes
+             mdc_out = map (\n -> (Node.tMem n, Node.tDsk n, Node.tCpu n,
+                                   Node.fMem n, Node.fDsk n)) nodes
+         in
+         Container.size gl ==? ngroups .&&.
+         Container.size nl ==? totnodes .&&.
+         Container.size il ==? 0 .&&.
+         length tags ==? 0 .&&.
+         ipol ==? Types.defIPolicy .&&.
+         nidx ==? [1..totnodes] .&&.
+         mdc_in ==? mdc_out .&&.
+         map Group.iPolicy (Container.elems gl) ==?
+             replicate ngroups Types.defIPolicy
+
+testSuite "Simu"
+            [ 'prop_SimuLoad
+            ]
+
 -- ** Node tests
 
 prop_Node_setAlias node name =
@@ -857,14 +1008,14 @@ prop_Node_setXmem node xm =
     where newnode = Node.setXmem node xm
 
 prop_Node_setMcpu node mc =
-  Node.mCpu newnode ==? mc
+  Types.iPolicyVcpuRatio (Node.iPolicy newnode) ==? mc
     where newnode = Node.setMcpu node mc
 
 -- | Check that an instance add with too high memory or disk will be
 -- rejected.
 prop_Node_addPriFM node inst =
   Instance.mem inst >= Node.fMem node && not (Node.failN1 node) &&
-  not (Instance.instanceOffline inst) ==>
+  not (Instance.isOffline inst) ==>
   case Node.addPri node inst'' of
     Types.OpFail Types.FailMem -> True
     _ -> False
@@ -872,60 +1023,73 @@ prop_Node_addPriFM node inst =
         inst' = setInstanceSmallerThanNode node inst
         inst'' = inst' { Instance.mem = Instance.mem inst }
 
+-- | Check that adding a primary instance with too much disk fails
+-- with type FailDisk.
 prop_Node_addPriFD node inst =
+  forAll (elements Instance.localStorageTemplates) $ \dt ->
   Instance.dsk inst >= Node.fDsk node && not (Node.failN1 node) ==>
-    case Node.addPri node inst'' of
-      Types.OpFail Types.FailDisk -> True
-      _ -> False
-    where _types = (node::Node.Node, inst::Instance.Instance)
-          inst' = setInstanceSmallerThanNode node inst
-          inst'' = inst' { Instance.dsk = Instance.dsk inst }
-
-prop_Node_addPriFC node inst (Positive extra) =
-  not (Node.failN1 node) && not (Instance.instanceOffline inst) ==>
-      case Node.addPri node inst'' of
-        Types.OpFail Types.FailCPU -> True
-        _ -> False
-    where _types = (node::Node.Node, inst::Instance.Instance)
-          inst' = setInstanceSmallerThanNode node inst
-          inst'' = inst' { Instance.vcpus = Node.availCpu node + extra }
+  let inst' = setInstanceSmallerThanNode node inst
+      inst'' = inst' { Instance.dsk = Instance.dsk inst
+                     , Instance.diskTemplate = dt }
+  in case Node.addPri node inst'' of
+       Types.OpFail Types.FailDisk -> True
+       _ -> False
+
+-- | Check that adding a primary instance with too many VCPUs fails
+-- with type FailCPU.
+prop_Node_addPriFC =
+  forAll (choose (1, maxCpu)) $ \extra ->
+  forAll genOnlineNode $ \node ->
+  forAll (arbitrary `suchThat` Instance.notOffline) $ \inst ->
+  let inst' = setInstanceSmallerThanNode node inst
+      inst'' = inst' { Instance.vcpus = Node.availCpu node + extra }
+  in case Node.addPri node inst'' of
+       Types.OpFail Types.FailCPU -> property True
+       v -> failTest $ "Expected OpFail FailCPU, but got " ++ show v
 
 -- | Check that an instance add with too high memory or disk will be
 -- rejected.
 prop_Node_addSec node inst pdx =
   ((Instance.mem inst >= (Node.fMem node - Node.rMem node) &&
-    not (Instance.instanceOffline inst)) ||
+    not (Instance.isOffline inst)) ||
    Instance.dsk inst >= Node.fDsk node) &&
   not (Node.failN1 node) ==>
       isFailure (Node.addSec node inst pdx)
         where _types = (node::Node.Node, inst::Instance.Instance, pdx::Int)
 
--- | Check that an offline instance with reasonable disk size can always
--- be added.
-prop_Node_addPriOffline =
-  forAll (arbitrary `suchThat` ((> 0) . Node.fMem)) $ \node ->
-  forAll (arbitrary `suchThat`
-          (\ x ->  (Instance.dsk x  < Node.fDsk node) &&
-                   Instance.instanceOffline x)) $ \inst ->
-  case Node.addPri node inst of
-    Types.OpGood _ -> True
-    _ -> False
-
-prop_Node_addSecOffline pdx =
+-- | Check that an offline instance with reasonable disk size but
+-- extra mem/cpu can always be added.
+prop_Node_addOfflinePri (NonNegative extra_mem) (NonNegative extra_cpu) =
   forAll genOnlineNode $ \node ->
-  forAll (arbitrary `suchThat`
-          (\ inst -> Instance.dsk inst  < Node.availDisk node)) $ \inst ->
-  case Node.addSec node (inst { Instance.runSt = Types.AdminOffline }) pdx of
-    Types.OpGood _ -> True
-    _ -> False
+  forAll (genInstanceSmallerThanNode node) $ \inst ->
+  let inst' = inst { Instance.runSt = Types.AdminOffline
+                   , Instance.mem = Node.availMem node + extra_mem
+                   , Instance.vcpus = Node.availCpu node + extra_cpu }
+  in case Node.addPri node inst' of
+       Types.OpGood _ -> property True
+       v -> failTest $ "Expected OpGood, but got: " ++ show v
+
+-- | Check that an offline instance with reasonable disk size but
+-- extra mem/cpu can always be added.
+prop_Node_addOfflineSec (NonNegative extra_mem) (NonNegative extra_cpu) pdx =
+  forAll genOnlineNode $ \node ->
+  forAll (genInstanceSmallerThanNode node) $ \inst ->
+  let inst' = inst { Instance.runSt = Types.AdminOffline
+                   , Instance.mem = Node.availMem node + extra_mem
+                   , Instance.vcpus = Node.availCpu node + extra_cpu
+                   , Instance.diskTemplate = Types.DTDrbd8 }
+  in case Node.addSec node inst' pdx of
+       Types.OpGood _ -> property True
+       v -> failTest $ "Expected OpGood/OpGood, but got: " ++ show v
 
 -- | Checks for memory reservation changes.
 prop_Node_rMem inst =
-  not (Instance.instanceOffline inst) ==>
-  forAll (arbitrary `suchThat` ((> Types.unitMem) . Node.fMem)) $ \node ->
+  not (Instance.isOffline inst) ==>
+  forAll (genOnlineNode `suchThat` ((> Types.unitMem) . Node.fMem)) $ \node ->
   -- ab = auto_balance, nb = non-auto_balance
   -- we use -1 as the primary node of the instance
-  let inst' = inst { Instance.pNode = -1, Instance.autoBalance = True }
+  let inst' = inst { Instance.pNode = -1, Instance.autoBalance = True
+                   , Instance.diskTemplate = Types.DTDrbd8 }
       inst_ab = setInstanceSmallerThanNode node inst'
       inst_nb = inst_ab { Instance.autoBalance = False }
       -- now we have the two instances, identical except the
@@ -964,14 +1128,15 @@ prop_Node_setMdsk node mx =
           SmallRatio mx' = mx
 
 -- Check tag maps
-prop_Node_tagMaps_idempotent tags =
+prop_Node_tagMaps_idempotent =
+  forAll genTags $ \tags ->
   Node.delTags (Node.addTags m tags) tags ==? m
     where m = Data.Map.empty
 
-prop_Node_tagMaps_reject tags =
-  not (null tags) ==>
-  all (\t -> Node.rejectAddTags m [t]) tags
-    where m = Node.addTags Data.Map.empty tags
+prop_Node_tagMaps_reject =
+  forAll (genTags `suchThat` (not . null)) $ \tags ->
+  let m = Node.addTags Data.Map.empty tags
+  in all (\t -> Node.rejectAddTags m [t]) tags
 
 prop_Node_showField node =
   forAll (elements Node.defaultFields) $ \ field ->
@@ -986,6 +1151,24 @@ prop_Node_computeGroups nodes =
      length (nub onlyuuid) == length onlyuuid &&
      (null nodes || not (null ng))
 
+-- Check idempotence of add/remove operations
+prop_Node_addPri_idempotent =
+  forAll genOnlineNode $ \node ->
+  forAll (genInstanceSmallerThanNode node) $ \inst ->
+  case Node.addPri node inst of
+    Types.OpGood node' -> Node.removePri node' inst ==? node
+    _ -> failTest "Can't add instance"
+
+prop_Node_addSec_idempotent =
+  forAll genOnlineNode $ \node ->
+  forAll (genInstanceSmallerThanNode node) $ \inst ->
+  let pdx = Node.idx node + 1
+      inst' = Instance.setPri inst pdx
+      inst'' = inst' { Instance.diskTemplate = Types.DTDrbd8 }
+  in case Node.addSec node inst'' pdx of
+       Types.OpGood node' -> Node.removeSec node' inst'' ==? node
+       _ -> failTest "Can't add instance"
+
 testSuite "Node"
             [ 'prop_Node_setAlias
             , 'prop_Node_setOffline
@@ -995,14 +1178,16 @@ testSuite "Node"
             , 'prop_Node_addPriFD
             , 'prop_Node_addPriFC
             , 'prop_Node_addSec
-            , 'prop_Node_addPriOffline
-            , 'prop_Node_addSecOffline
+            , 'prop_Node_addOfflinePri
+            , 'prop_Node_addOfflineSec
             , 'prop_Node_rMem
             , 'prop_Node_setMdsk
             , 'prop_Node_tagMaps_idempotent
             , 'prop_Node_tagMaps_reject
             , 'prop_Node_showField
             , 'prop_Node_computeGroups
+            , 'prop_Node_addPri_idempotent
+            , 'prop_Node_addSec_idempotent
             ]
 
 -- ** Cluster tests
@@ -1037,7 +1222,8 @@ prop_ClusterAlloc_sane inst =
   forAll (choose (5, 20)) $ \count ->
   forAll genOnlineNode $ \node ->
   let (nl, il, inst') = makeSmallEmptyCluster node count inst
-  in case Cluster.genAllocNodes defGroupList nl 2 True >>=
+      reqnodes = Instance.requiredNodes $ Instance.diskTemplate inst
+  in case Cluster.genAllocNodes defGroupList nl reqnodes True >>=
      Cluster.tryAlloc nl il inst' of
        Types.Bad _ -> False
        Types.Ok as ->
@@ -1050,26 +1236,42 @@ prop_ClusterAlloc_sane inst =
 
 -- | Checks that on a 2-5 node cluster, we can allocate a random
 -- instance spec via tiered allocation (whatever the original instance
--- spec), on either one or two nodes.
+-- spec), on either one or two nodes. Furthermore, we test that
+-- computed allocation statistics are correct.
 prop_ClusterCanTieredAlloc inst =
   forAll (choose (2, 5)) $ \count ->
-  forAll (choose (1, 2)) $ \rqnodes ->
   forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
   let nl = makeSmallCluster node count
       il = Container.empty
+      rqnodes = Instance.requiredNodes $ Instance.diskTemplate inst
       allocnodes = Cluster.genAllocNodes defGroupList nl rqnodes True
   in case allocnodes >>= \allocnodes' ->
     Cluster.tieredAlloc nl il (Just 1) inst allocnodes' [] [] of
-       Types.Bad _ -> False
-       Types.Ok (_, _, il', ixes, cstats) -> not (null ixes) &&
-                                             IntMap.size il' == length ixes &&
-                                             length ixes == length cstats
+       Types.Bad msg -> failTest $ "Failed to tiered alloc: " ++ msg
+       Types.Ok (_, nl', il', ixes, cstats) ->
+         let (ai_alloc, ai_pool, ai_unav) =
+               Cluster.computeAllocationDelta
+                (Cluster.totalResources nl)
+                (Cluster.totalResources nl')
+             all_nodes = Container.elems nl
+         in property (not (null ixes)) .&&.
+            IntMap.size il' ==? length ixes .&&.
+            length ixes ==? length cstats .&&.
+            sum (map Types.allocInfoVCpus [ai_alloc, ai_pool, ai_unav]) ==?
+              sum (map Node.hiCpu all_nodes) .&&.
+            sum (map Types.allocInfoNCpus [ai_alloc, ai_pool, ai_unav]) ==?
+              sum (map Node.tCpu all_nodes) .&&.
+            sum (map Types.allocInfoMem [ai_alloc, ai_pool, ai_unav]) ==?
+              truncate (sum (map Node.tMem all_nodes)) .&&.
+            sum (map Types.allocInfoDisk [ai_alloc, ai_pool, ai_unav]) ==?
+              truncate (sum (map Node.tDsk all_nodes))
 
 -- | Helper function to create a cluster with the given range of nodes
 -- and allocate an instance on it.
 genClusterAlloc count node inst =
   let nl = makeSmallCluster node count
-  in case Cluster.genAllocNodes defGroupList nl 2 True >>=
+      reqnodes = Instance.requiredNodes $ Instance.diskTemplate inst
+  in case Cluster.genAllocNodes defGroupList nl reqnodes True >>=
      Cluster.tryAlloc nl Container.empty inst of
        Types.Bad _ -> Types.Bad "Can't allocate"
        Types.Ok as ->
@@ -1084,14 +1286,16 @@ genClusterAlloc count node inst =
 prop_ClusterAllocRelocate =
   forAll (choose (4, 8)) $ \count ->
   forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
-  forAll (arbitrary `suchThat` (isInstanceSmallerThanNode node)) $ \inst ->
+  forAll (genInstanceSmallerThanNode node `suchThat` isMirrored) $ \inst ->
   case genClusterAlloc count node inst of
     Types.Bad msg -> failTest msg
     Types.Ok (nl, il, inst') ->
       case IAlloc.processRelocate defGroupList nl il
-             (Instance.idx inst) 1 [Instance.sNode inst'] of
-        Types.Ok _ -> printTestCase "??" True  -- huh, how to make
-                                               -- this nicer...
+             (Instance.idx inst) 1
+             [(if Instance.diskTemplate inst' == Types.DTDrbd8
+                 then Instance.sNode
+                 else Instance.pNode) inst'] of
+        Types.Ok _ -> property True
         Types.Bad msg -> failTest $ "Failed to relocate: " ++ msg
 
 -- | Helper property checker for the result of a nodeEvac or
@@ -1119,13 +1323,15 @@ check_EvacMode grp inst result =
 prop_ClusterAllocEvacuate =
   forAll (choose (4, 8)) $ \count ->
   forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
-  forAll (arbitrary `suchThat` (isInstanceSmallerThanNode node)) $ \inst ->
+  forAll (genInstanceSmallerThanNode node `suchThat` isMirrored) $ \inst ->
   case genClusterAlloc count node inst of
     Types.Bad msg -> failTest msg
     Types.Ok (nl, il, inst') ->
       conjoin $ map (\mode -> check_EvacMode defGroup inst' $
                               Cluster.tryNodeEvac defGroupList nl il mode
-                                [Instance.idx inst']) [minBound..maxBound]
+                                [Instance.idx inst']) .
+                              evacModeOptions .
+                              Instance.mirrorType $ inst'
 
 -- | Checks that on a 4-8 node cluster with two node groups, once we
 -- allocate an instance on the first node group, we can also change
@@ -1133,7 +1339,7 @@ prop_ClusterAllocEvacuate =
 prop_ClusterAllocChangeGroup =
   forAll (choose (4, 8)) $ \count ->
   forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
-  forAll (arbitrary `suchThat` (isInstanceSmallerThanNode node)) $ \inst ->
+  forAll (genInstanceSmallerThanNode node `suchThat` isMirrored) $ \inst ->
   case genClusterAlloc count node inst of
     Types.Bad msg -> failTest msg
     Types.Ok (nl, il, inst') ->
@@ -1281,14 +1487,19 @@ prop_Loader_lookupInstance kti inst =
   Loader.lookupInstance il inst ==? Data.Map.lookup inst il
     where il = Data.Map.fromList kti
 
-prop_Loader_assignIndices nodes =
-  Data.Map.size nassoc == length nodes &&
-  Container.size kt == length nodes &&
-  (if not (null nodes)
-   then maximum (IntMap.keys kt) == length nodes - 1
-   else True)
-    where (nassoc, kt) =
-            Loader.assignIndices (map (\n -> (Node.name n, n)) nodes)
+prop_Loader_assignIndices =
+  -- generate nodes with unique names
+  forAll (arbitrary `suchThat`
+          (\nodes ->
+             let names = map Node.name nodes
+             in length names == length (nub names))) $ \nodes ->
+  let (nassoc, kt) =
+        Loader.assignIndices (map (\n -> (Node.name n, n)) nodes)
+  in Data.Map.size nassoc == length nodes &&
+     Container.size kt == length nodes &&
+     if not (null nodes)
+       then maximum (IntMap.keys kt) == length nodes - 1
+       else True
 
 -- | Checks that the number of primary instances recorded on the nodes
 -- is zero.
@@ -1383,3 +1594,159 @@ testSuite "Types"
             , 'prop_Types_opToResult
             , 'prop_Types_eitherToResult
             ]
+
+-- ** CLI tests
+
+-- | Test correct parsing.
+prop_CLI_parseISpec descr dsk mem cpu =
+  let str = printf "%d,%d,%d" dsk mem cpu
+  in CLI.parseISpecString descr str ==? Types.Ok (Types.RSpec cpu mem dsk)
+
+-- | Test parsing failure due to wrong section count.
+prop_CLI_parseISpecFail descr =
+  forAll (choose (0,100) `suchThat` ((/=) 3)) $ \nelems ->
+  forAll (replicateM nelems arbitrary) $ \values ->
+  let str = intercalate "," $ map show (values::[Int])
+  in case CLI.parseISpecString descr str of
+       Types.Ok v -> failTest $ "Expected failure, got " ++ show v
+       _ -> property True
+
+-- | Test parseYesNo.
+prop_CLI_parseYesNo def testval val =
+  forAll (elements [val, "yes", "no"]) $ \actual_val ->
+  if testval
+    then CLI.parseYesNo def Nothing ==? Types.Ok def
+    else let result = CLI.parseYesNo def (Just actual_val)
+         in if actual_val `elem` ["yes", "no"]
+              then result ==? Types.Ok (actual_val == "yes")
+              else property $ Types.isBad result
+
+-- | Helper to check for correct parsing of string arg.
+checkStringArg val (opt, fn) =
+  let GetOpt.Option _ longs _ _ = opt
+  in case longs of
+       [] -> failTest "no long options?"
+       cmdarg:_ ->
+         case CLI.parseOptsInner ["--" ++ cmdarg ++ "=" ++ val] "prog" [opt] of
+           Left e -> failTest $ "Failed to parse option: " ++ show e
+           Right (options, _) -> fn options ==? Just val
+
+-- | Test a few string arguments.
+prop_CLI_StringArg argument =
+  let args = [ (CLI.oDataFile,      CLI.optDataFile)
+             , (CLI.oDynuFile,      CLI.optDynuFile)
+             , (CLI.oSaveCluster,   CLI.optSaveCluster)
+             , (CLI.oReplay,        CLI.optReplay)
+             , (CLI.oPrintCommands, CLI.optShowCmds)
+             , (CLI.oLuxiSocket,    CLI.optLuxi)
+             ]
+  in conjoin $ map (checkStringArg argument) args
+
+-- | Helper to test that a given option is accepted OK with quick exit.
+checkEarlyExit name options param =
+  case CLI.parseOptsInner [param] name options of
+    Left (code, _) -> if code == 0
+                          then property True
+                          else failTest $ "Program " ++ name ++
+                                 " returns invalid code " ++ show code ++
+                                 " for option " ++ param
+    _ -> failTest $ "Program " ++ name ++ " doesn't consider option " ++
+         param ++ " as early exit one"
+
+-- | Test that all binaries support some common options. There is
+-- nothing actually random about this test...
+prop_CLI_stdopts =
+  let params = ["-h", "--help", "-V", "--version"]
+      opts = map (\(name, (_, o)) -> (name, o)) Program.personalities
+      -- apply checkEarlyExit across the cartesian product of params and opts
+  in conjoin [checkEarlyExit n o p | p <- params, (n, o) <- opts]
+
+testSuite "CLI"
+          [ 'prop_CLI_parseISpec
+          , 'prop_CLI_parseISpecFail
+          , 'prop_CLI_parseYesNo
+          , 'prop_CLI_StringArg
+          , 'prop_CLI_stdopts
+          ]
+
+-- * JSON tests
+
+prop_JSON_toArray :: [Int] -> Property
+prop_JSON_toArray intarr =
+  let arr = map J.showJSON intarr in
+  case JSON.toArray (J.JSArray arr) of
+    Types.Ok arr' -> arr ==? arr'
+    Types.Bad err -> failTest $ "Failed to parse array: " ++ err
+
+prop_JSON_toArrayFail :: Int -> String -> Bool -> Property
+prop_JSON_toArrayFail i s b =
+  -- poor man's instance Arbitrary JSValue
+  forAll (elements [J.showJSON i, J.showJSON s, J.showJSON b]) $ \item ->
+  case JSON.toArray item of
+    Types.Bad _ -> property True
+    Types.Ok result -> failTest $ "Unexpected parse, got " ++ show result
+
+testSuite "JSON"
+          [ 'prop_JSON_toArray
+          , 'prop_JSON_toArrayFail
+          ]
+
+-- * Luxi tests
+
+instance Arbitrary Luxi.LuxiReq where
+  arbitrary = elements [minBound..maxBound]
+
+instance Arbitrary Luxi.QrViaLuxi where
+  arbitrary = elements [minBound..maxBound]
+
+instance Arbitrary Luxi.LuxiOp where
+  arbitrary = do
+    lreq <- arbitrary
+    case lreq of
+      Luxi.ReqQuery -> Luxi.Query <$> arbitrary <*> getFields <*> arbitrary
+      Luxi.ReqQueryNodes -> Luxi.QueryNodes <$> (listOf getFQDN) <*>
+                            getFields <*> arbitrary
+      Luxi.ReqQueryGroups -> Luxi.QueryGroups <$> arbitrary <*>
+                             arbitrary <*> arbitrary
+      Luxi.ReqQueryInstances -> Luxi.QueryInstances <$> (listOf getFQDN) <*>
+                                getFields <*> arbitrary
+      Luxi.ReqQueryJobs -> Luxi.QueryJobs <$> arbitrary <*> getFields
+      Luxi.ReqQueryExports -> Luxi.QueryExports <$>
+                              (listOf getFQDN) <*> arbitrary
+      Luxi.ReqQueryConfigValues -> Luxi.QueryConfigValues <$> getFields
+      Luxi.ReqQueryClusterInfo -> pure Luxi.QueryClusterInfo
+      Luxi.ReqQueryTags -> Luxi.QueryTags <$> getName <*> getFQDN
+      Luxi.ReqSubmitJob -> Luxi.SubmitJob <$> (resize maxOpCodes arbitrary)
+      Luxi.ReqSubmitManyJobs -> Luxi.SubmitManyJobs <$>
+                                (resize maxOpCodes arbitrary)
+      Luxi.ReqWaitForJobChange -> Luxi.WaitForJobChange <$> arbitrary <*>
+                                  getFields <*> pure J.JSNull <*>
+                                  pure J.JSNull <*> arbitrary
+      Luxi.ReqArchiveJob -> Luxi.ArchiveJob <$> arbitrary
+      Luxi.ReqAutoArchiveJobs -> Luxi.AutoArchiveJobs <$> arbitrary <*>
+                                 arbitrary
+      Luxi.ReqCancelJob -> Luxi.CancelJob <$> arbitrary
+      Luxi.ReqSetDrainFlag -> Luxi.SetDrainFlag <$> arbitrary
+      Luxi.ReqSetWatcherPause -> Luxi.SetWatcherPause <$> arbitrary
+
+-- | Simple check that encoding/decoding of LuxiOp works.
+prop_Luxi_CallEncoding :: Luxi.LuxiOp -> Property
+prop_Luxi_CallEncoding op =
+  (Luxi.validateCall (Luxi.buildCall op) >>= Luxi.decodeCall) ==? Types.Ok op
+
+testSuite "LUXI"
+          [ 'prop_Luxi_CallEncoding
+          ]
+
+-- * Ssconf tests
+
+instance Arbitrary Ssconf.SSKey where
+  arbitrary = elements [minBound..maxBound]
+
+prop_Ssconf_filename key =
+  printTestCase "Key doesn't start with correct prefix" $
+    Ssconf.sSFilePrefix `isPrefixOf` Ssconf.keyToFilename (Just "") key
+
+testSuite "Ssconf"
+  [ 'prop_Ssconf_filename
+  ]