Fix old-style import
[ganeti-local] / htools / Ganeti / HTools / QC.hs
index 231baf5..d056a9c 100644 (file)
@@ -32,23 +32,34 @@ module Ganeti.HTools.QC
   , testInstance
   , testNode
   , testText
   , testInstance
   , testNode
   , testText
+  , testSimu
   , testOpCodes
   , testJobs
   , testCluster
   , testLoader
   , testTypes
   , testOpCodes
   , testJobs
   , testCluster
   , testLoader
   , testTypes
+  , testCLI
+  , testJSON
+  , testLUXI
+  , testSsconf
   ) where
 
 import Test.QuickCheck
   ) where
 
 import Test.QuickCheck
+import Text.Printf (printf)
 import Data.List (findIndex, intercalate, nub, isPrefixOf)
 import Data.List (findIndex, intercalate, nub, isPrefixOf)
+import qualified Data.Set as Set
 import Data.Maybe
 import Control.Monad
 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 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.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
 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.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.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.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
 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
 
 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
 -- | 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.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.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.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
        }
 
        , 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)
 -- | 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
 
 -- | 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]
 
            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.
 -- * Arbitrary instances
 
 -- | Defines a DNS name.
@@ -230,16 +273,31 @@ instance Arbitrary DNSChar where
 getName :: Gen String
 getName = do
   n <- choose (1, 64)
 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)
   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
 
   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 }
 
 -- | Defines a tag type.
 newtype TagChar = TagChar { tagGetChar :: Char }
 
@@ -275,18 +333,29 @@ genTags = do
 instance Arbitrary Types.InstanceStatus where
     arbitrary = elements [minBound..maxBound]
 
 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
 -- 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
 
 -- | 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
   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
 
       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" ->
                       ]
     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" ->
       "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" ->
       "OP_INSTANCE_FAILOVER" ->
-        liftM3 OpCodes.OpInstanceFailover arbitrary arbitrary
-          arbitrary
+        OpCodes.OpInstanceFailover <$> getFQDN <*> arbitrary <*>
+          getMaybe getFQDN
       "OP_INSTANCE_MIGRATE" ->
       "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
       _ -> 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
 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
 
 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)
     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.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
 
 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
     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
 
                          }
 
 -- * 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 =
 -- | 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.
   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) =
           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"
 
 -- | 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.
 -- | 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 ->
   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)
                $ 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
 
 testSuite "Container"
             [ 'prop_Container_addTwo
@@ -689,17 +784,20 @@ testSuite "Instance"
             , 'prop_Instance_setMovable
             ]
 
             , '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
 
 -- 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
   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)]
       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,
       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]
       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 &&
                                       then Node.noSecondary
                                       else sdx) &&
                Instance.autoBalance i == autobal &&
+               Instance.spindleUse i == su &&
                Types.isBad fail1
 
 prop_Text_Load_InstanceFail ktn fields =
                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) $
     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_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 ',' .
 
 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 ->
 -- 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 (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 ->
   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
      of
        Types.Bad msg -> failTest $ "Failed to allocate: " ++ msg
        Types.Ok (_, _, _, [], _) -> printTestCase
@@ -840,6 +940,57 @@ testSuite "Text"
             , 'prop_Text_CreateSerialise
             ]
 
             , '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 =
 -- ** 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 =
     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) &&
     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
   case Node.addPri node inst'' of
     Types.OpFail Types.FailMem -> True
     _ -> False
@@ -872,18 +1023,24 @@ prop_Node_addPriFM node inst =
         inst' = setInstanceSmallerThanNode node inst
         inst'' = inst' { Instance.mem = Instance.mem 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 =
 prop_Node_addPriFD node inst =
+  forAll (elements Instance.localStorageTemplates) $ \dt ->
   Instance.dsk inst >= Node.fDsk node && not (Node.failN1 node) ==>
   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 }
+  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
 
 
-prop_Node_addPriFC (Positive extra) =
+-- | 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 genOnlineNode $ \node ->
-  forAll (arbitrary `suchThat` Instance.instanceNotOffline) $ \inst ->
+  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
   let inst' = setInstanceSmallerThanNode node inst
       inst'' = inst' { Instance.vcpus = Node.availCpu node + extra }
   in case Node.addPri node inst'' of
@@ -894,38 +1051,45 @@ prop_Node_addPriFC (Positive extra) =
 -- rejected.
 prop_Node_addSec node inst pdx =
   ((Instance.mem inst >= (Node.fMem node - Node.rMem node) &&
 -- 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)
 
    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 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 =
 
 -- | 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
   -- 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
       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
           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
 
   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 ->
 
 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))
 
      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
 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_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_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
             ]
 
 -- ** 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
   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 ->
      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
 
 -- | 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 ->
 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
   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
       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
 
 -- | 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 ->
      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 ->
 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
   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
         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 ->
 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
   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
 
 -- | 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 ->
 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') ->
   case genClusterAlloc count node inst of
     Types.Bad msg -> failTest msg
     Types.Ok (nl, il, inst') ->
@@ -1388,3 +1594,159 @@ testSuite "Types"
             , 'prop_Types_opToResult
             , 'prop_Types_eitherToResult
             ]
             , '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
+  ]