1 {-# LANGUAGE TemplateHaskell #-}
3 {-| Unittests for ganeti-htools.
9 Copyright (C) 2009, 2010, 2011, 2012 Google Inc.
11 This program is free software; you can redistribute it and/or modify
12 it under the terms of the GNU General Public License as published by
13 the Free Software Foundation; either version 2 of the License, or
14 (at your option) any later version.
16 This program is distributed in the hope that it will be useful, but
17 WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19 General Public License for more details.
21 You should have received a copy of the GNU General Public License
22 along with this program; if not, write to the Free Software
23 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
28 module Ganeti.HTools.QC
42 import Test.QuickCheck
43 import Data.List (findIndex, intercalate, nub, isPrefixOf)
46 import qualified Text.JSON as J
47 import qualified Data.Map
48 import qualified Data.IntMap as IntMap
49 import qualified Ganeti.OpCodes as OpCodes
50 import qualified Ganeti.Jobs as Jobs
51 import qualified Ganeti.Luxi
52 import qualified Ganeti.HTools.CLI as CLI
53 import qualified Ganeti.HTools.Cluster as Cluster
54 import qualified Ganeti.HTools.Container as Container
55 import qualified Ganeti.HTools.ExtLoader
56 import qualified Ganeti.HTools.IAlloc as IAlloc
57 import qualified Ganeti.HTools.Instance as Instance
58 import qualified Ganeti.HTools.JSON as JSON
59 import qualified Ganeti.HTools.Loader as Loader
60 import qualified Ganeti.HTools.Luxi
61 import qualified Ganeti.HTools.Node as Node
62 import qualified Ganeti.HTools.Group as Group
63 import qualified Ganeti.HTools.PeerMap as PeerMap
64 import qualified Ganeti.HTools.Rapi
65 import qualified Ganeti.HTools.Simu
66 import qualified Ganeti.HTools.Text as Text
67 import qualified Ganeti.HTools.Types as Types
68 import qualified Ganeti.HTools.Utils as Utils
69 import qualified Ganeti.HTools.Version
70 import qualified Ganeti.Constants as C
72 import qualified Ganeti.HTools.Program.Hail
73 import qualified Ganeti.HTools.Program.Hbal
74 import qualified Ganeti.HTools.Program.Hscan
75 import qualified Ganeti.HTools.Program.Hspace
77 import Ganeti.HTools.QCHelper (testSuite)
81 -- | Maximum memory (1TiB, somewhat random value).
85 -- | Maximum disk (8TiB, somewhat random value).
87 maxDsk = 1024 * 1024 * 8
89 -- | Max CPUs (1024, somewhat random value).
93 -- | Null iPolicy, and by null we mean very liberal.
94 nullIPolicy = Types.IPolicy
95 { Types.iPolicyMinSpec = Types.ISpec { Types.iSpecMemorySize = 0
96 , Types.iSpecCpuCount = 0
97 , Types.iSpecDiskSize = 0
98 , Types.iSpecDiskCount = 0
99 , Types.iSpecNicCount = 0
101 , Types.iPolicyMaxSpec = Types.ISpec { Types.iSpecMemorySize = maxBound
102 , Types.iSpecCpuCount = maxBound
103 , Types.iSpecDiskSize = maxBound
104 , Types.iSpecDiskCount = C.maxDisks
105 , Types.iSpecNicCount = C.maxNics
107 , Types.iPolicyStdSpec = Types.ISpec { Types.iSpecMemorySize = Types.unitMem
108 , Types.iSpecCpuCount = Types.unitCpu
109 , Types.iSpecDiskSize = Types.unitDsk
110 , Types.iSpecDiskCount = 1
111 , Types.iSpecNicCount = 1
113 , Types.iPolicyDiskTemplates = [Types.DTDrbd8, Types.DTPlain]
117 defGroup :: Group.Group
118 defGroup = flip Group.setIdx 0 $
119 Group.create "default" Types.defaultGroupID Types.AllocPreferred
122 defGroupList :: Group.List
123 defGroupList = Container.fromList [(Group.idx defGroup, defGroup)]
125 defGroupAssoc :: Data.Map.Map String Types.Gdx
126 defGroupAssoc = Data.Map.singleton (Group.uuid defGroup) (Group.idx defGroup)
128 -- * Helper functions
130 -- | Simple checker for whether OpResult is fail or pass.
131 isFailure :: Types.OpResult a -> Bool
132 isFailure (Types.OpFail _) = True
135 -- | Checks for equality with proper annotation.
136 (==?) :: (Show a, Eq a) => a -> a -> Property
137 (==?) x y = printTestCase
138 ("Expected equality, but '" ++
139 show x ++ "' /= '" ++ show y ++ "'") (x == y)
142 -- | Show a message and fail the test.
143 failTest :: String -> Property
144 failTest msg = printTestCase msg False
146 -- | Update an instance to be smaller than a node.
147 setInstanceSmallerThanNode node inst =
148 inst { Instance.mem = Node.availMem node `div` 2
149 , Instance.dsk = Node.availDisk node `div` 2
150 , Instance.vcpus = Node.availCpu node `div` 2
153 -- | Check if an instance is smaller than a node.
154 isInstanceSmallerThanNode node inst =
155 Instance.mem inst <= Node.availMem node `div` 2 &&
156 Instance.dsk inst <= Node.availDisk node `div` 2 &&
157 Instance.vcpus inst <= Node.availCpu node `div` 2
159 -- | Create an instance given its spec.
160 createInstance mem dsk vcpus =
161 Instance.create "inst-unnamed" mem dsk vcpus Types.Running [] True (-1) (-1)
164 -- | Create a small cluster by repeating a node spec.
165 makeSmallCluster :: Node.Node -> Int -> Node.List
166 makeSmallCluster node count =
167 let origname = Node.name node
168 origalias = Node.alias node
169 nodes = map (\idx -> node { Node.name = origname ++ "-" ++ show idx
170 , Node.alias = origalias ++ "-" ++ show idx })
172 fn = flip Node.buildPeers Container.empty
173 namelst = map (\n -> (Node.name n, fn n)) nodes
174 (_, nlst) = Loader.assignIndices namelst
177 -- | Make a small cluster, both nodes and instances.
178 makeSmallEmptyCluster :: Node.Node -> Int -> Instance.Instance
179 -> (Node.List, Instance.List, Instance.Instance)
180 makeSmallEmptyCluster node count inst =
181 (makeSmallCluster node count, Container.empty,
182 setInstanceSmallerThanNode node inst)
184 -- | Checks if a node is "big" enough.
185 isNodeBig :: Int -> Node.Node -> Bool
186 isNodeBig size node = Node.availDisk node > size * Types.unitDsk
187 && Node.availMem node > size * Types.unitMem
188 && Node.availCpu node > size * Types.unitCpu
190 canBalance :: Cluster.Table -> Bool -> Bool -> Bool -> Bool
191 canBalance tbl dm im evac = isJust $ Cluster.tryBalance tbl dm im evac 0 0
193 -- | Assigns a new fresh instance to a cluster; this is not
194 -- allocation, so no resource checks are done.
195 assignInstance :: Node.List -> Instance.List -> Instance.Instance ->
196 Types.Idx -> Types.Idx ->
197 (Node.List, Instance.List)
198 assignInstance nl il inst pdx sdx =
199 let pnode = Container.find pdx nl
200 snode = Container.find sdx nl
201 maxiidx = if Container.null il
203 else fst (Container.findMax il) + 1
204 inst' = inst { Instance.idx = maxiidx,
205 Instance.pNode = pdx, Instance.sNode = sdx }
206 pnode' = Node.setPri pnode inst'
207 snode' = Node.setSec snode inst'
208 nl' = Container.addTwo pdx pnode' sdx snode' nl
209 il' = Container.add maxiidx inst' il
212 -- | Generates a list of a given size with non-duplicate elements.
213 genUniquesList :: (Eq a, Arbitrary a) => Int -> Gen [a]
216 newelem <- arbitrary `suchThat` (`notElem` lst)
217 return (newelem:lst)) [] [1..cnt]
219 -- * Arbitrary instances
221 -- | Defines a DNS name.
222 newtype DNSChar = DNSChar { dnsGetChar::Char }
224 instance Arbitrary DNSChar where
226 x <- elements (['a'..'z'] ++ ['0'..'9'] ++ "_-")
229 -- | Generates a single name component.
230 getName :: Gen String
233 dn <- vector n::Gen [DNSChar]
234 return (map dnsGetChar dn)
236 -- | Generates an entire FQDN.
237 getFQDN :: Gen String
239 ncomps <- choose (1, 4)
240 names <- mapM (const getName) [1..ncomps::Int]
241 return $ intercalate "." names
243 -- | Defines a tag type.
244 newtype TagChar = TagChar { tagGetChar :: Char }
246 -- | All valid tag chars. This doesn't need to match _exactly_
247 -- Ganeti's own tag regex, just enough for it to be close.
249 tagChar = ['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9'] ++ ".+*/:@-"
251 instance Arbitrary TagChar where
253 c <- elements tagChar
257 genTag :: Gen [TagChar]
259 -- the correct value would be C.maxTagLen, but that's way too
260 -- verbose in unittests, and at the moment I don't see any possible
261 -- bugs with longer tags and the way we use tags in htools
265 -- | Generates a list of tags (correctly upper bounded).
266 genTags :: Gen [String]
268 -- the correct value would be C.maxTagsPerObj, but per the comment
269 -- in genTag, we don't use tags enough in htools to warrant testing
271 n <- choose (0, 10::Int)
272 tags <- mapM (const genTag) [1..n]
273 return $ map (map tagGetChar) tags
275 instance Arbitrary Types.InstanceStatus where
276 arbitrary = elements [minBound..maxBound]
278 -- let's generate a random instance
279 instance Arbitrary Instance.Instance where
282 mem <- choose (0, maxMem)
283 dsk <- choose (0, maxDsk)
287 vcpus <- choose (0, maxCpu)
288 return $ Instance.create name mem dsk vcpus run_st [] True pn sn
291 -- | Generas an arbitrary node based on sizing information.
292 genNode :: Maybe Int -- ^ Minimum node size in terms of units
293 -> Maybe Int -- ^ Maximum node size (when Nothing, bounded
294 -- just by the max... constants)
296 genNode min_multiplier max_multiplier = do
297 let (base_mem, base_dsk, base_cpu) =
298 case min_multiplier of
299 Just mm -> (mm * Types.unitMem,
303 (top_mem, top_dsk, top_cpu) =
304 case max_multiplier of
305 Just mm -> (mm * Types.unitMem,
308 Nothing -> (maxMem, maxDsk, maxCpu)
310 mem_t <- choose (base_mem, top_mem)
311 mem_f <- choose (base_mem, mem_t)
312 mem_n <- choose (0, mem_t - mem_f)
313 dsk_t <- choose (base_dsk, top_dsk)
314 dsk_f <- choose (base_dsk, dsk_t)
315 cpu_t <- choose (base_cpu, top_cpu)
317 let n = Node.create name (fromIntegral mem_t) mem_n mem_f
318 (fromIntegral dsk_t) dsk_f (fromIntegral cpu_t) offl 0
319 n' = Node.setPolicy nullIPolicy n
320 return $ Node.buildPeers n' Container.empty
322 -- | Helper function to generate a sane node.
323 genOnlineNode :: Gen Node.Node
325 arbitrary `suchThat` (\n -> not (Node.offline n) &&
326 not (Node.failN1 n) &&
327 Node.availDisk n > 0 &&
328 Node.availMem n > 0 &&
332 instance Arbitrary Node.Node where
333 arbitrary = genNode Nothing Nothing
336 instance Arbitrary OpCodes.ReplaceDisksMode where
337 arbitrary = elements [minBound..maxBound]
339 instance Arbitrary OpCodes.OpCode where
341 op_id <- elements [ "OP_TEST_DELAY"
342 , "OP_INSTANCE_REPLACE_DISKS"
343 , "OP_INSTANCE_FAILOVER"
344 , "OP_INSTANCE_MIGRATE"
348 liftM3 OpCodes.OpTestDelay arbitrary arbitrary arbitrary
349 "OP_INSTANCE_REPLACE_DISKS" ->
350 liftM5 OpCodes.OpInstanceReplaceDisks arbitrary arbitrary
351 arbitrary arbitrary arbitrary
352 "OP_INSTANCE_FAILOVER" ->
353 liftM3 OpCodes.OpInstanceFailover arbitrary arbitrary
355 "OP_INSTANCE_MIGRATE" ->
356 liftM5 OpCodes.OpInstanceMigrate arbitrary arbitrary
357 arbitrary arbitrary arbitrary
358 _ -> fail "Wrong opcode"
360 instance Arbitrary Jobs.OpStatus where
361 arbitrary = elements [minBound..maxBound]
363 instance Arbitrary Jobs.JobStatus where
364 arbitrary = elements [minBound..maxBound]
366 newtype SmallRatio = SmallRatio Double deriving Show
367 instance Arbitrary SmallRatio where
370 return $ SmallRatio v
372 instance Arbitrary Types.AllocPolicy where
373 arbitrary = elements [minBound..maxBound]
375 instance Arbitrary Types.DiskTemplate where
376 arbitrary = elements [minBound..maxBound]
378 instance Arbitrary Types.FailMode where
379 arbitrary = elements [minBound..maxBound]
381 instance Arbitrary Types.EvacMode where
382 arbitrary = elements [minBound..maxBound]
384 instance Arbitrary a => Arbitrary (Types.OpResult a) where
385 arbitrary = arbitrary >>= \c ->
387 then liftM Types.OpGood arbitrary
388 else liftM Types.OpFail arbitrary
390 instance Arbitrary Types.ISpec where
392 mem <- arbitrary::Gen (NonNegative Int)
393 dsk_c <- arbitrary::Gen (NonNegative Int)
394 dsk_s <- arbitrary::Gen (NonNegative Int)
395 cpu <- arbitrary::Gen (NonNegative Int)
396 nic <- arbitrary::Gen (NonNegative Int)
397 return Types.ISpec { Types.iSpecMemorySize = fromIntegral mem
398 , Types.iSpecCpuCount = fromIntegral cpu
399 , Types.iSpecDiskSize = fromIntegral dsk_s
400 , Types.iSpecDiskCount = fromIntegral dsk_c
401 , Types.iSpecNicCount = fromIntegral nic
404 -- | Helper function to check whether a spec is LTE than another
405 iSpecSmaller :: Types.ISpec -> Types.ISpec -> Bool
406 iSpecSmaller imin imax =
407 Types.iSpecMemorySize imin <= Types.iSpecMemorySize imax &&
408 Types.iSpecCpuCount imin <= Types.iSpecCpuCount imax &&
409 Types.iSpecDiskSize imin <= Types.iSpecDiskSize imax &&
410 Types.iSpecDiskCount imin <= Types.iSpecDiskCount imax &&
411 Types.iSpecNicCount imin <= Types.iSpecNicCount imax
413 instance Arbitrary Types.IPolicy where
416 istd <- arbitrary `suchThat` (iSpecSmaller imin)
417 imax <- arbitrary `suchThat` (iSpecSmaller istd)
419 return Types.IPolicy { Types.iPolicyMinSpec = imin
420 , Types.iPolicyStdSpec = istd
421 , Types.iPolicyMaxSpec = imax
422 , Types.iPolicyDiskTemplates = dts
429 -- | If the list is not just an empty element, and if the elements do
430 -- not contain commas, then join+split should be idempotent.
431 prop_Utils_commaJoinSplit =
432 forAll (arbitrary `suchThat`
433 (\l -> l /= [""] && all (notElem ',') l )) $ \lst ->
434 Utils.sepSplit ',' (Utils.commaJoin lst) ==? lst
436 -- | Split and join should always be idempotent.
437 prop_Utils_commaSplitJoin s =
438 Utils.commaJoin (Utils.sepSplit ',' s) ==? s
440 -- | fromObjWithDefault, we test using the Maybe monad and an integer
442 prop_Utils_fromObjWithDefault def_value random_key =
443 -- a missing key will be returned with the default
444 JSON.fromObjWithDefault [] random_key def_value == Just def_value &&
445 -- a found key will be returned as is, not with default
446 JSON.fromObjWithDefault [(random_key, J.showJSON def_value)]
447 random_key (def_value+1) == Just def_value
448 where _types = def_value :: Integer
450 -- | Test that functional if' behaves like the syntactic sugar if.
451 prop_Utils_if'if :: Bool -> Int -> Int -> Gen Prop
452 prop_Utils_if'if cnd a b =
453 Utils.if' cnd a b ==? if cnd then a else b
455 -- | Test basic select functionality
456 prop_Utils_select :: Int -- ^ Default result
457 -> [Int] -- ^ List of False values
458 -> [Int] -- ^ List of True values
459 -> Gen Prop -- ^ Test result
460 prop_Utils_select def lst1 lst2 =
461 Utils.select def (flist ++ tlist) ==? expectedresult
462 where expectedresult = Utils.if' (null lst2) def (head lst2)
463 flist = zip (repeat False) lst1
464 tlist = zip (repeat True) lst2
466 -- | Test basic select functionality with undefined default
467 prop_Utils_select_undefd :: [Int] -- ^ List of False values
468 -> NonEmptyList Int -- ^ List of True values
469 -> Gen Prop -- ^ Test result
470 prop_Utils_select_undefd lst1 (NonEmpty lst2) =
471 Utils.select undefined (flist ++ tlist) ==? head lst2
472 where flist = zip (repeat False) lst1
473 tlist = zip (repeat True) lst2
475 -- | Test basic select functionality with undefined list values
476 prop_Utils_select_undefv :: [Int] -- ^ List of False values
477 -> NonEmptyList Int -- ^ List of True values
478 -> Gen Prop -- ^ Test result
479 prop_Utils_select_undefv lst1 (NonEmpty lst2) =
480 Utils.select undefined cndlist ==? head lst2
481 where flist = zip (repeat False) lst1
482 tlist = zip (repeat True) lst2
483 cndlist = flist ++ tlist ++ [undefined]
485 prop_Utils_parseUnit (NonNegative n) =
486 Utils.parseUnit (show n) == Types.Ok n &&
487 Utils.parseUnit (show n ++ "m") == Types.Ok n &&
488 (case Utils.parseUnit (show n ++ "M") of
489 Types.Ok m -> if n > 0
490 then m < n -- for positive values, X MB is < than X MiB
491 else m == 0 -- but for 0, 0 MB == 0 MiB
492 Types.Bad _ -> False) &&
493 Utils.parseUnit (show n ++ "g") == Types.Ok (n*1024) &&
494 Utils.parseUnit (show n ++ "t") == Types.Ok (n*1048576) &&
495 Types.isBad (Utils.parseUnit (show n ++ "x")::Types.Result Int)
496 where _types = n::Int
498 -- | Test list for the Utils module.
500 [ 'prop_Utils_commaJoinSplit
501 , 'prop_Utils_commaSplitJoin
502 , 'prop_Utils_fromObjWithDefault
505 , 'prop_Utils_select_undefd
506 , 'prop_Utils_select_undefv
507 , 'prop_Utils_parseUnit
512 -- | Make sure add is idempotent.
513 prop_PeerMap_addIdempotent pmap key em =
514 fn puniq ==? fn (fn puniq)
515 where _types = (pmap::PeerMap.PeerMap,
516 key::PeerMap.Key, em::PeerMap.Elem)
517 fn = PeerMap.add key em
518 puniq = PeerMap.accumArray const pmap
520 -- | Make sure remove is idempotent.
521 prop_PeerMap_removeIdempotent pmap key =
522 fn puniq ==? fn (fn puniq)
523 where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
524 fn = PeerMap.remove key
525 puniq = PeerMap.accumArray const pmap
527 -- | Make sure a missing item returns 0.
528 prop_PeerMap_findMissing pmap key =
529 PeerMap.find key (PeerMap.remove key puniq) ==? 0
530 where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
531 puniq = PeerMap.accumArray const pmap
533 -- | Make sure an added item is found.
534 prop_PeerMap_addFind pmap key em =
535 PeerMap.find key (PeerMap.add key em puniq) ==? em
536 where _types = (pmap::PeerMap.PeerMap,
537 key::PeerMap.Key, em::PeerMap.Elem)
538 puniq = PeerMap.accumArray const pmap
540 -- | Manual check that maxElem returns the maximum indeed, or 0 for null.
541 prop_PeerMap_maxElem pmap =
542 PeerMap.maxElem puniq ==? if null puniq then 0
543 else (maximum . snd . unzip) puniq
544 where _types = pmap::PeerMap.PeerMap
545 puniq = PeerMap.accumArray const pmap
547 -- | List of tests for the PeerMap module.
549 [ 'prop_PeerMap_addIdempotent
550 , 'prop_PeerMap_removeIdempotent
551 , 'prop_PeerMap_maxElem
552 , 'prop_PeerMap_addFind
553 , 'prop_PeerMap_findMissing
556 -- ** Container tests
558 -- we silence the following due to hlint bug fixed in later versions
559 {-# ANN prop_Container_addTwo "HLint: ignore Avoid lambda" #-}
560 prop_Container_addTwo cdata i1 i2 =
561 fn i1 i2 cont == fn i2 i1 cont &&
562 fn i1 i2 cont == fn i1 i2 (fn i1 i2 cont)
563 where _types = (cdata::[Int],
565 cont = foldl (\c x -> Container.add x x c) Container.empty cdata
566 fn x1 x2 = Container.addTwo x1 x1 x2 x2
568 prop_Container_nameOf node =
569 let nl = makeSmallCluster node 1
570 fnode = head (Container.elems nl)
571 in Container.nameOf nl (Node.idx fnode) ==? Node.name fnode
573 -- | We test that in a cluster, given a random node, we can find it by
574 -- its name and alias, as long as all names and aliases are unique,
575 -- and that we fail to find a non-existing name.
576 prop_Container_findByName node =
577 forAll (choose (1, 20)) $ \ cnt ->
578 forAll (choose (0, cnt - 1)) $ \ fidx ->
579 forAll (genUniquesList (cnt * 2)) $ \ allnames ->
580 forAll (arbitrary `suchThat` (`notElem` allnames)) $ \ othername ->
581 let names = zip (take cnt allnames) (drop cnt allnames)
582 nl = makeSmallCluster node cnt
583 nodes = Container.elems nl
584 nodes' = map (\((name, alias), nn) -> (Node.idx nn,
585 nn { Node.name = name,
586 Node.alias = alias }))
588 nl' = Container.fromList nodes'
589 target = snd (nodes' !! fidx)
590 in Container.findByName nl' (Node.name target) == Just target &&
591 Container.findByName nl' (Node.alias target) == Just target &&
592 isNothing (Container.findByName nl' othername)
594 testSuite "Container"
595 [ 'prop_Container_addTwo
596 , 'prop_Container_nameOf
597 , 'prop_Container_findByName
602 -- Simple instance tests, we only have setter/getters
604 prop_Instance_creat inst =
605 Instance.name inst ==? Instance.alias inst
607 prop_Instance_setIdx inst idx =
608 Instance.idx (Instance.setIdx inst idx) ==? idx
609 where _types = (inst::Instance.Instance, idx::Types.Idx)
611 prop_Instance_setName inst name =
612 Instance.name newinst == name &&
613 Instance.alias newinst == name
614 where _types = (inst::Instance.Instance, name::String)
615 newinst = Instance.setName inst name
617 prop_Instance_setAlias inst name =
618 Instance.name newinst == Instance.name inst &&
619 Instance.alias newinst == name
620 where _types = (inst::Instance.Instance, name::String)
621 newinst = Instance.setAlias inst name
623 prop_Instance_setPri inst pdx =
624 Instance.pNode (Instance.setPri inst pdx) ==? pdx
625 where _types = (inst::Instance.Instance, pdx::Types.Ndx)
627 prop_Instance_setSec inst sdx =
628 Instance.sNode (Instance.setSec inst sdx) ==? sdx
629 where _types = (inst::Instance.Instance, sdx::Types.Ndx)
631 prop_Instance_setBoth inst pdx sdx =
632 Instance.pNode si == pdx && Instance.sNode si == sdx
633 where _types = (inst::Instance.Instance, pdx::Types.Ndx, sdx::Types.Ndx)
634 si = Instance.setBoth inst pdx sdx
636 prop_Instance_shrinkMG inst =
637 Instance.mem inst >= 2 * Types.unitMem ==>
638 case Instance.shrinkByType inst Types.FailMem of
639 Types.Ok inst' -> Instance.mem inst' == Instance.mem inst - Types.unitMem
642 prop_Instance_shrinkMF inst =
643 forAll (choose (0, 2 * Types.unitMem - 1)) $ \mem ->
644 let inst' = inst { Instance.mem = mem}
645 in Types.isBad $ Instance.shrinkByType inst' Types.FailMem
647 prop_Instance_shrinkCG inst =
648 Instance.vcpus inst >= 2 * Types.unitCpu ==>
649 case Instance.shrinkByType inst Types.FailCPU of
651 Instance.vcpus inst' == Instance.vcpus inst - Types.unitCpu
654 prop_Instance_shrinkCF inst =
655 forAll (choose (0, 2 * Types.unitCpu - 1)) $ \vcpus ->
656 let inst' = inst { Instance.vcpus = vcpus }
657 in Types.isBad $ Instance.shrinkByType inst' Types.FailCPU
659 prop_Instance_shrinkDG inst =
660 Instance.dsk inst >= 2 * Types.unitDsk ==>
661 case Instance.shrinkByType inst Types.FailDisk of
663 Instance.dsk inst' == Instance.dsk inst - Types.unitDsk
666 prop_Instance_shrinkDF inst =
667 forAll (choose (0, 2 * Types.unitDsk - 1)) $ \dsk ->
668 let inst' = inst { Instance.dsk = dsk }
669 in Types.isBad $ Instance.shrinkByType inst' Types.FailDisk
671 prop_Instance_setMovable inst m =
672 Instance.movable inst' ==? m
673 where inst' = Instance.setMovable inst m
676 [ 'prop_Instance_creat
677 , 'prop_Instance_setIdx
678 , 'prop_Instance_setName
679 , 'prop_Instance_setAlias
680 , 'prop_Instance_setPri
681 , 'prop_Instance_setSec
682 , 'prop_Instance_setBoth
683 , 'prop_Instance_shrinkMG
684 , 'prop_Instance_shrinkMF
685 , 'prop_Instance_shrinkCG
686 , 'prop_Instance_shrinkCF
687 , 'prop_Instance_shrinkDG
688 , 'prop_Instance_shrinkDF
689 , 'prop_Instance_setMovable
692 -- ** Text backend tests
694 -- Instance text loader tests
696 prop_Text_Load_Instance name mem dsk vcpus status
697 (NonEmpty pnode) snode
698 (NonNegative pdx) (NonNegative sdx) autobal dt =
699 pnode /= snode && pdx /= sdx ==>
700 let vcpus_s = show vcpus
703 status_s = Types.instanceStatusToRaw status
706 else [(pnode, pdx), (snode, sdx)]
707 nl = Data.Map.fromList ndx
709 sbal = if autobal then "Y" else "N"
710 sdt = Types.diskTemplateToRaw dt
711 inst = Text.loadInst nl
712 [name, mem_s, dsk_s, vcpus_s, status_s,
713 sbal, pnode, snode, sdt, tags]
714 fail1 = Text.loadInst nl
715 [name, mem_s, dsk_s, vcpus_s, status_s,
716 sbal, pnode, pnode, tags]
717 _types = ( name::String, mem::Int, dsk::Int
718 , vcpus::Int, status::Types.InstanceStatus
722 Types.Bad msg -> failTest $ "Failed to load instance: " ++ msg
723 Types.Ok (_, i) -> printTestCase "Mismatch in some field while\
724 \ loading the instance" $
725 Instance.name i == name &&
726 Instance.vcpus i == vcpus &&
727 Instance.mem i == mem &&
728 Instance.pNode i == pdx &&
729 Instance.sNode i == (if null snode
730 then Node.noSecondary
732 Instance.autoBalance i == autobal &&
735 prop_Text_Load_InstanceFail ktn fields =
736 length fields /= 10 ==>
737 case Text.loadInst nl fields of
738 Types.Ok _ -> failTest "Managed to load instance from invalid data"
739 Types.Bad msg -> printTestCase ("Unrecognised error message: " ++ msg) $
740 "Invalid/incomplete instance data: '" `isPrefixOf` msg
741 where nl = Data.Map.fromList ktn
743 prop_Text_Load_Node name tm nm fm td fd tc fo =
744 let conv v = if v < 0
756 any_broken = any (< 0) [tm, nm, fm, td, fd, tc]
757 gid = Group.uuid defGroup
758 in case Text.loadNode defGroupAssoc
759 [name, tm_s, nm_s, fm_s, td_s, fd_s, tc_s, fo_s, gid] of
761 Just (name', node) ->
763 then Node.offline node
764 else Node.name node == name' && name' == name &&
765 Node.alias node == name &&
766 Node.tMem node == fromIntegral tm &&
767 Node.nMem node == nm &&
768 Node.fMem node == fm &&
769 Node.tDsk node == fromIntegral td &&
770 Node.fDsk node == fd &&
771 Node.tCpu node == fromIntegral tc
773 prop_Text_Load_NodeFail fields =
774 length fields /= 8 ==> isNothing $ Text.loadNode Data.Map.empty fields
776 prop_Text_NodeLSIdempotent node =
777 (Text.loadNode defGroupAssoc.
778 Utils.sepSplit '|' . Text.serializeNode defGroupList) n ==
779 Just (Node.name n, n)
780 -- override failN1 to what loadNode returns by default
781 where n = node { Node.failN1 = True, Node.offline = False
782 , Node.iPolicy = Types.defIPolicy }
784 prop_Text_ISpecIdempotent ispec =
785 case Text.loadISpec "dummy" . Utils.sepSplit ',' .
786 Text.serializeISpec $ ispec of
787 Types.Bad msg -> failTest $ "Failed to load ispec: " ++ msg
788 Types.Ok ispec' -> ispec ==? ispec'
790 prop_Text_IPolicyIdempotent ipol =
791 case Text.loadIPolicy . Utils.sepSplit '|' $
792 Text.serializeIPolicy owner ipol of
793 Types.Bad msg -> failTest $ "Failed to load ispec: " ++ msg
794 Types.Ok res -> (owner, ipol) ==? res
795 where owner = "dummy"
797 -- | This property, while being in the text tests, does more than just
798 -- test end-to-end the serialisation and loading back workflow; it
799 -- also tests the Loader.mergeData and the actuall
800 -- Cluster.iterateAlloc (for well-behaving w.r.t. instance
801 -- allocations, not for the business logic). As such, it's a quite
802 -- complex and slow test, and that's the reason we restrict it to
803 -- small cluster sizes.
804 prop_Text_CreateSerialise =
805 forAll genTags $ \ctags ->
806 forAll (choose (1, 2)) $ \reqnodes ->
807 forAll (choose (1, 20)) $ \maxiter ->
808 forAll (choose (2, 10)) $ \count ->
809 forAll genOnlineNode $ \node ->
810 forAll (arbitrary `suchThat` isInstanceSmallerThanNode node) $ \inst ->
811 let inst' = Instance.setMovable inst $ Utils.if' (reqnodes == 2) True False
812 nl = makeSmallCluster node count
813 in case Cluster.genAllocNodes defGroupList nl reqnodes True >>= \allocn ->
814 Cluster.iterateAlloc nl Container.empty (Just maxiter) inst' allocn [] []
816 Types.Bad msg -> failTest $ "Failed to allocate: " ++ msg
817 Types.Ok (_, _, _, [], _) -> printTestCase
818 "Failed to allocate: no allocations" False
819 Types.Ok (_, nl', il', _, _) ->
820 let cdata = Loader.ClusterData defGroupList nl' il' ctags
822 saved = Text.serializeCluster cdata
823 in case Text.parseData saved >>= Loader.mergeData [] [] [] [] of
824 Types.Bad msg -> failTest $ "Failed to load/merge: " ++ msg
825 Types.Ok (Loader.ClusterData gl2 nl2 il2 ctags2 cpol2) ->
826 ctags ==? ctags2 .&&.
827 Types.defIPolicy ==? cpol2 .&&.
829 defGroupList ==? gl2 .&&.
833 [ 'prop_Text_Load_Instance
834 , 'prop_Text_Load_InstanceFail
835 , 'prop_Text_Load_Node
836 , 'prop_Text_Load_NodeFail
837 , 'prop_Text_NodeLSIdempotent
838 , 'prop_Text_ISpecIdempotent
839 , 'prop_Text_IPolicyIdempotent
840 , 'prop_Text_CreateSerialise
845 prop_Node_setAlias node name =
846 Node.name newnode == Node.name node &&
847 Node.alias newnode == name
848 where _types = (node::Node.Node, name::String)
849 newnode = Node.setAlias node name
851 prop_Node_setOffline node status =
852 Node.offline newnode ==? status
853 where newnode = Node.setOffline node status
855 prop_Node_setXmem node xm =
856 Node.xMem newnode ==? xm
857 where newnode = Node.setXmem node xm
859 prop_Node_setMcpu node mc =
860 Node.mCpu newnode ==? mc
861 where newnode = Node.setMcpu node mc
863 -- | Check that an instance add with too high memory or disk will be
865 prop_Node_addPriFM node inst =
866 Instance.mem inst >= Node.fMem node && not (Node.failN1 node) &&
867 not (Instance.instanceOffline inst) ==>
868 case Node.addPri node inst'' of
869 Types.OpFail Types.FailMem -> True
871 where _types = (node::Node.Node, inst::Instance.Instance)
872 inst' = setInstanceSmallerThanNode node inst
873 inst'' = inst' { Instance.mem = Instance.mem inst }
875 prop_Node_addPriFD node inst =
876 Instance.dsk inst >= Node.fDsk node && not (Node.failN1 node) ==>
877 case Node.addPri node inst'' of
878 Types.OpFail Types.FailDisk -> True
880 where _types = (node::Node.Node, inst::Instance.Instance)
881 inst' = setInstanceSmallerThanNode node inst
882 inst'' = inst' { Instance.dsk = Instance.dsk inst }
884 prop_Node_addPriFC node inst (Positive extra) =
885 not (Node.failN1 node) && not (Instance.instanceOffline inst) ==>
886 case Node.addPri node inst'' of
887 Types.OpFail Types.FailCPU -> True
889 where _types = (node::Node.Node, inst::Instance.Instance)
890 inst' = setInstanceSmallerThanNode node inst
891 inst'' = inst' { Instance.vcpus = Node.availCpu node + extra }
893 -- | Check that an instance add with too high memory or disk will be
895 prop_Node_addSec node inst pdx =
896 ((Instance.mem inst >= (Node.fMem node - Node.rMem node) &&
897 not (Instance.instanceOffline inst)) ||
898 Instance.dsk inst >= Node.fDsk node) &&
899 not (Node.failN1 node) ==>
900 isFailure (Node.addSec node inst pdx)
901 where _types = (node::Node.Node, inst::Instance.Instance, pdx::Int)
903 -- | Check that an offline instance with reasonable disk size can always
905 prop_Node_addPriOffline =
906 forAll (arbitrary `suchThat` ((> 0) . Node.fMem)) $ \node ->
907 forAll (arbitrary `suchThat`
908 (\ x -> (Instance.dsk x < Node.fDsk node) &&
909 Instance.instanceOffline x)) $ \inst ->
910 case Node.addPri node inst of
911 Types.OpGood _ -> True
914 prop_Node_addSecOffline pdx =
915 forAll genOnlineNode $ \node ->
916 forAll (arbitrary `suchThat`
917 (\ inst -> Instance.dsk inst < Node.availDisk node)) $ \inst ->
918 case Node.addSec node (inst { Instance.runSt = Types.AdminOffline }) pdx of
919 Types.OpGood _ -> True
922 -- | Checks for memory reservation changes.
923 prop_Node_rMem inst =
924 not (Instance.instanceOffline inst) ==>
925 forAll (arbitrary `suchThat` ((> Types.unitMem) . Node.fMem)) $ \node ->
926 -- ab = auto_balance, nb = non-auto_balance
927 -- we use -1 as the primary node of the instance
928 let inst' = inst { Instance.pNode = -1, Instance.autoBalance = True }
929 inst_ab = setInstanceSmallerThanNode node inst'
930 inst_nb = inst_ab { Instance.autoBalance = False }
931 -- now we have the two instances, identical except the
932 -- autoBalance attribute
933 orig_rmem = Node.rMem node
934 inst_idx = Instance.idx inst_ab
935 node_add_ab = Node.addSec node inst_ab (-1)
936 node_add_nb = Node.addSec node inst_nb (-1)
937 node_del_ab = liftM (`Node.removeSec` inst_ab) node_add_ab
938 node_del_nb = liftM (`Node.removeSec` inst_nb) node_add_nb
939 in case (node_add_ab, node_add_nb, node_del_ab, node_del_nb) of
940 (Types.OpGood a_ab, Types.OpGood a_nb,
941 Types.OpGood d_ab, Types.OpGood d_nb) ->
942 printTestCase "Consistency checks failed" $
943 Node.rMem a_ab > orig_rmem &&
944 Node.rMem a_ab - orig_rmem == Instance.mem inst_ab &&
945 Node.rMem a_nb == orig_rmem &&
946 Node.rMem d_ab == orig_rmem &&
947 Node.rMem d_nb == orig_rmem &&
948 -- this is not related to rMem, but as good a place to
950 inst_idx `elem` Node.sList a_ab &&
951 inst_idx `notElem` Node.sList d_ab
952 x -> failTest $ "Failed to add/remove instances: " ++ show x
954 -- | Check mdsk setting.
955 prop_Node_setMdsk node mx =
956 Node.loDsk node' >= 0 &&
957 fromIntegral (Node.loDsk node') <= Node.tDsk node &&
958 Node.availDisk node' >= 0 &&
959 Node.availDisk node' <= Node.fDsk node' &&
960 fromIntegral (Node.availDisk node') <= Node.tDsk node' &&
961 Node.mDsk node' == mx'
962 where _types = (node::Node.Node, mx::SmallRatio)
963 node' = Node.setMdsk node mx'
967 prop_Node_tagMaps_idempotent tags =
968 Node.delTags (Node.addTags m tags) tags ==? m
969 where m = Data.Map.empty
971 prop_Node_tagMaps_reject tags =
973 all (\t -> Node.rejectAddTags m [t]) tags
974 where m = Node.addTags Data.Map.empty tags
976 prop_Node_showField node =
977 forAll (elements Node.defaultFields) $ \ field ->
978 fst (Node.showHeader field) /= Types.unknownField &&
979 Node.showField node field /= Types.unknownField
981 prop_Node_computeGroups nodes =
982 let ng = Node.computeGroups nodes
983 onlyuuid = map fst ng
984 in length nodes == sum (map (length . snd) ng) &&
985 all (\(guuid, ns) -> all ((== guuid) . Node.group) ns) ng &&
986 length (nub onlyuuid) == length onlyuuid &&
987 (null nodes || not (null ng))
990 [ 'prop_Node_setAlias
991 , 'prop_Node_setOffline
994 , 'prop_Node_addPriFM
995 , 'prop_Node_addPriFD
996 , 'prop_Node_addPriFC
998 , 'prop_Node_addPriOffline
999 , 'prop_Node_addSecOffline
1001 , 'prop_Node_setMdsk
1002 , 'prop_Node_tagMaps_idempotent
1003 , 'prop_Node_tagMaps_reject
1004 , 'prop_Node_showField
1005 , 'prop_Node_computeGroups
1010 -- | Check that the cluster score is close to zero for a homogeneous
1012 prop_Score_Zero node =
1013 forAll (choose (1, 1024)) $ \count ->
1014 (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
1015 (Node.tDsk node > 0) && (Node.tMem node > 0)) ==>
1016 let fn = Node.buildPeers node Container.empty
1017 nlst = replicate count fn
1018 score = Cluster.compCVNodes nlst
1019 -- we can't say == 0 here as the floating point errors accumulate;
1020 -- this should be much lower than the default score in CLI.hs
1023 -- | Check that cluster stats are sane.
1025 forAll (choose (1, 1024)) $ \count ->
1026 forAll genOnlineNode $ \node ->
1027 let fn = Node.buildPeers node Container.empty
1028 nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
1029 nl = Container.fromList nlst
1030 cstats = Cluster.totalResources nl
1031 in Cluster.csAdsk cstats >= 0 &&
1032 Cluster.csAdsk cstats <= Cluster.csFdsk cstats
1034 -- | Check that one instance is allocated correctly, without
1035 -- rebalances needed.
1036 prop_ClusterAlloc_sane inst =
1037 forAll (choose (5, 20)) $ \count ->
1038 forAll genOnlineNode $ \node ->
1039 let (nl, il, inst') = makeSmallEmptyCluster node count inst
1040 in case Cluster.genAllocNodes defGroupList nl 2 True >>=
1041 Cluster.tryAlloc nl il inst' of
1042 Types.Bad _ -> False
1044 case Cluster.asSolution as of
1046 Just (xnl, xi, _, cv) ->
1047 let il' = Container.add (Instance.idx xi) xi il
1048 tbl = Cluster.Table xnl il' cv []
1049 in not (canBalance tbl True True False)
1051 -- | Checks that on a 2-5 node cluster, we can allocate a random
1052 -- instance spec via tiered allocation (whatever the original instance
1053 -- spec), on either one or two nodes.
1054 prop_ClusterCanTieredAlloc inst =
1055 forAll (choose (2, 5)) $ \count ->
1056 forAll (choose (1, 2)) $ \rqnodes ->
1057 forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1058 let nl = makeSmallCluster node count
1059 il = Container.empty
1060 allocnodes = Cluster.genAllocNodes defGroupList nl rqnodes True
1061 in case allocnodes >>= \allocnodes' ->
1062 Cluster.tieredAlloc nl il (Just 1) inst allocnodes' [] [] of
1063 Types.Bad _ -> False
1064 Types.Ok (_, _, il', ixes, cstats) -> not (null ixes) &&
1065 IntMap.size il' == length ixes &&
1066 length ixes == length cstats
1068 -- | Helper function to create a cluster with the given range of nodes
1069 -- and allocate an instance on it.
1070 genClusterAlloc count node inst =
1071 let nl = makeSmallCluster node count
1072 in case Cluster.genAllocNodes defGroupList nl 2 True >>=
1073 Cluster.tryAlloc nl Container.empty inst of
1074 Types.Bad _ -> Types.Bad "Can't allocate"
1076 case Cluster.asSolution as of
1077 Nothing -> Types.Bad "Empty solution?"
1078 Just (xnl, xi, _, _) ->
1079 let xil = Container.add (Instance.idx xi) xi Container.empty
1080 in Types.Ok (xnl, xil, xi)
1082 -- | Checks that on a 4-8 node cluster, once we allocate an instance,
1083 -- we can also relocate it.
1084 prop_ClusterAllocRelocate =
1085 forAll (choose (4, 8)) $ \count ->
1086 forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1087 forAll (arbitrary `suchThat` (isInstanceSmallerThanNode node)) $ \inst ->
1088 case genClusterAlloc count node inst of
1089 Types.Bad msg -> failTest msg
1090 Types.Ok (nl, il, inst') ->
1091 case IAlloc.processRelocate defGroupList nl il
1092 (Instance.idx inst) 1 [Instance.sNode inst'] of
1093 Types.Ok _ -> printTestCase "??" True -- huh, how to make
1095 Types.Bad msg -> failTest $ "Failed to relocate: " ++ msg
1097 -- | Helper property checker for the result of a nodeEvac or
1098 -- changeGroup operation.
1099 check_EvacMode grp inst result =
1101 Types.Bad msg -> failTest $ "Couldn't evacuate/change group:" ++ msg
1102 Types.Ok (_, _, es) ->
1103 let moved = Cluster.esMoved es
1104 failed = Cluster.esFailed es
1105 opcodes = not . null $ Cluster.esOpCodes es
1106 in failmsg ("'failed' not empty: " ++ show failed) (null failed) .&&.
1107 failmsg "'opcodes' is null" opcodes .&&.
1109 [(idx', gdx, _)] -> failmsg "invalid instance moved" (idx == idx')
1111 failmsg "wrong target group"
1112 (gdx == Group.idx grp)
1113 v -> failmsg ("invalid solution: " ++ show v) False
1114 where failmsg = \msg -> printTestCase ("Failed to evacuate: " ++ msg)
1115 idx = Instance.idx inst
1117 -- | Checks that on a 4-8 node cluster, once we allocate an instance,
1118 -- we can also node-evacuate it.
1119 prop_ClusterAllocEvacuate =
1120 forAll (choose (4, 8)) $ \count ->
1121 forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1122 forAll (arbitrary `suchThat` (isInstanceSmallerThanNode node)) $ \inst ->
1123 case genClusterAlloc count node inst of
1124 Types.Bad msg -> failTest msg
1125 Types.Ok (nl, il, inst') ->
1126 conjoin $ map (\mode -> check_EvacMode defGroup inst' $
1127 Cluster.tryNodeEvac defGroupList nl il mode
1128 [Instance.idx inst']) [minBound..maxBound]
1130 -- | Checks that on a 4-8 node cluster with two node groups, once we
1131 -- allocate an instance on the first node group, we can also change
1133 prop_ClusterAllocChangeGroup =
1134 forAll (choose (4, 8)) $ \count ->
1135 forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1136 forAll (arbitrary `suchThat` (isInstanceSmallerThanNode node)) $ \inst ->
1137 case genClusterAlloc count node inst of
1138 Types.Bad msg -> failTest msg
1139 Types.Ok (nl, il, inst') ->
1140 -- we need to add a second node group and nodes to the cluster
1141 let nl2 = Container.elems $ makeSmallCluster node count
1142 grp2 = Group.setIdx defGroup (Group.idx defGroup + 1)
1143 maxndx = maximum . map Node.idx $ nl2
1144 nl3 = map (\n -> n { Node.group = Group.idx grp2
1145 , Node.idx = Node.idx n + maxndx }) nl2
1146 nl4 = Container.fromList . map (\n -> (Node.idx n, n)) $ nl3
1147 gl' = Container.add (Group.idx grp2) grp2 defGroupList
1148 nl' = IntMap.union nl nl4
1149 in check_EvacMode grp2 inst' $
1150 Cluster.tryChangeGroup gl' nl' il [] [Instance.idx inst']
1152 -- | Check that allocating multiple instances on a cluster, then
1153 -- adding an empty node, results in a valid rebalance.
1154 prop_ClusterAllocBalance =
1155 forAll (genNode (Just 5) (Just 128)) $ \node ->
1156 forAll (choose (3, 5)) $ \count ->
1157 not (Node.offline node) && not (Node.failN1 node) ==>
1158 let nl = makeSmallCluster node count
1159 (hnode, nl') = IntMap.deleteFindMax nl
1160 il = Container.empty
1161 allocnodes = Cluster.genAllocNodes defGroupList nl' 2 True
1162 i_templ = createInstance Types.unitMem Types.unitDsk Types.unitCpu
1163 in case allocnodes >>= \allocnodes' ->
1164 Cluster.iterateAlloc nl' il (Just 5) i_templ allocnodes' [] [] of
1165 Types.Bad msg -> failTest $ "Failed to allocate: " ++ msg
1166 Types.Ok (_, _, _, [], _) -> failTest "Failed to allocate: no instances"
1167 Types.Ok (_, xnl, il', _, _) ->
1168 let ynl = Container.add (Node.idx hnode) hnode xnl
1169 cv = Cluster.compCV ynl
1170 tbl = Cluster.Table ynl il' cv []
1171 in printTestCase "Failed to rebalance" $
1172 canBalance tbl True True False
1174 -- | Checks consistency.
1175 prop_ClusterCheckConsistency node inst =
1176 let nl = makeSmallCluster node 3
1177 [node1, node2, node3] = Container.elems nl
1178 node3' = node3 { Node.group = 1 }
1179 nl' = Container.add (Node.idx node3') node3' nl
1180 inst1 = Instance.setBoth inst (Node.idx node1) (Node.idx node2)
1181 inst2 = Instance.setBoth inst (Node.idx node1) Node.noSecondary
1182 inst3 = Instance.setBoth inst (Node.idx node1) (Node.idx node3)
1183 ccheck = Cluster.findSplitInstances nl' . Container.fromList
1184 in null (ccheck [(0, inst1)]) &&
1185 null (ccheck [(0, inst2)]) &&
1186 (not . null $ ccheck [(0, inst3)])
1188 -- | For now, we only test that we don't lose instances during the split.
1189 prop_ClusterSplitCluster node inst =
1190 forAll (choose (0, 100)) $ \icnt ->
1191 let nl = makeSmallCluster node 2
1192 (nl', il') = foldl (\(ns, is) _ -> assignInstance ns is inst 0 1)
1193 (nl, Container.empty) [1..icnt]
1194 gni = Cluster.splitCluster nl' il'
1195 in sum (map (Container.size . snd . snd) gni) == icnt &&
1196 all (\(guuid, (nl'', _)) -> all ((== guuid) . Node.group)
1197 (Container.elems nl'')) gni
1199 -- | Helper function to check if we can allocate an instance on a
1201 canAllocOn :: Node.List -> Int -> Instance.Instance -> Bool
1202 canAllocOn nl reqnodes inst =
1203 case Cluster.genAllocNodes defGroupList nl reqnodes True >>=
1204 Cluster.tryAlloc nl (Container.empty) inst of
1205 Types.Bad _ -> False
1207 case Cluster.asSolution as of
1211 -- | Checks that allocation obeys minimum and maximum instance
1212 -- policies. The unittest generates a random node, duplicates it count
1213 -- times, and generates a random instance that can be allocated on
1214 -- this mini-cluster; it then checks that after applying a policy that
1215 -- the instance doesn't fits, the allocation fails.
1216 prop_ClusterAllocPolicy node =
1217 -- rqn is the required nodes (1 or 2)
1218 forAll (choose (1, 2)) $ \rqn ->
1219 forAll (choose (5, 20)) $ \count ->
1220 forAll (arbitrary `suchThat` (canAllocOn (makeSmallCluster node count) rqn))
1222 forAll (arbitrary `suchThat` (isFailure .
1223 Instance.instMatchesPolicy inst)) $ \ipol ->
1224 let node' = Node.setPolicy ipol node
1225 nl = makeSmallCluster node' count
1226 in not $ canAllocOn nl rqn inst
1231 , 'prop_ClusterAlloc_sane
1232 , 'prop_ClusterCanTieredAlloc
1233 , 'prop_ClusterAllocRelocate
1234 , 'prop_ClusterAllocEvacuate
1235 , 'prop_ClusterAllocChangeGroup
1236 , 'prop_ClusterAllocBalance
1237 , 'prop_ClusterCheckConsistency
1238 , 'prop_ClusterSplitCluster
1239 , 'prop_ClusterAllocPolicy
1244 -- | Check that opcode serialization is idempotent.
1245 prop_OpCodes_serialization op =
1246 case J.readJSON (J.showJSON op) of
1247 J.Error e -> failTest $ "Cannot deserialise: " ++ e
1248 J.Ok op' -> op ==? op'
1249 where _types = op::OpCodes.OpCode
1252 [ 'prop_OpCodes_serialization ]
1256 -- | Check that (queued) job\/opcode status serialization is idempotent.
1257 prop_OpStatus_serialization os =
1258 case J.readJSON (J.showJSON os) of
1259 J.Error e -> failTest $ "Cannot deserialise: " ++ e
1260 J.Ok os' -> os ==? os'
1261 where _types = os::Jobs.OpStatus
1263 prop_JobStatus_serialization js =
1264 case J.readJSON (J.showJSON js) of
1265 J.Error e -> failTest $ "Cannot deserialise: " ++ e
1266 J.Ok js' -> js ==? js'
1267 where _types = js::Jobs.JobStatus
1270 [ 'prop_OpStatus_serialization
1271 , 'prop_JobStatus_serialization
1276 prop_Loader_lookupNode ktn inst node =
1277 Loader.lookupNode nl inst node ==? Data.Map.lookup node nl
1278 where nl = Data.Map.fromList ktn
1280 prop_Loader_lookupInstance kti inst =
1281 Loader.lookupInstance il inst ==? Data.Map.lookup inst il
1282 where il = Data.Map.fromList kti
1284 prop_Loader_assignIndices =
1285 -- generate nodes with unique names
1286 forAll (arbitrary `suchThat`
1288 let names = map Node.name nodes
1289 in length names == length (nub names))) $ \nodes ->
1291 Loader.assignIndices (map (\n -> (Node.name n, n)) nodes)
1292 in Data.Map.size nassoc == length nodes &&
1293 Container.size kt == length nodes &&
1295 then maximum (IntMap.keys kt) == length nodes - 1
1298 -- | Checks that the number of primary instances recorded on the nodes
1300 prop_Loader_mergeData ns =
1301 let na = Container.fromList $ map (\n -> (Node.idx n, n)) ns
1302 in case Loader.mergeData [] [] [] []
1303 (Loader.emptyCluster {Loader.cdNodes = na}) of
1304 Types.Bad _ -> False
1305 Types.Ok (Loader.ClusterData _ nl il _ _) ->
1306 let nodes = Container.elems nl
1307 instances = Container.elems il
1308 in (sum . map (length . Node.pList)) nodes == 0 &&
1311 -- | Check that compareNameComponent on equal strings works.
1312 prop_Loader_compareNameComponent_equal :: String -> Bool
1313 prop_Loader_compareNameComponent_equal s =
1314 Loader.compareNameComponent s s ==
1315 Loader.LookupResult Loader.ExactMatch s
1317 -- | Check that compareNameComponent on prefix strings works.
1318 prop_Loader_compareNameComponent_prefix :: NonEmptyList Char -> String -> Bool
1319 prop_Loader_compareNameComponent_prefix (NonEmpty s1) s2 =
1320 Loader.compareNameComponent (s1 ++ "." ++ s2) s1 ==
1321 Loader.LookupResult Loader.PartialMatch s1
1324 [ 'prop_Loader_lookupNode
1325 , 'prop_Loader_lookupInstance
1326 , 'prop_Loader_assignIndices
1327 , 'prop_Loader_mergeData
1328 , 'prop_Loader_compareNameComponent_equal
1329 , 'prop_Loader_compareNameComponent_prefix
1334 prop_Types_AllocPolicy_serialisation apol =
1335 case J.readJSON (J.showJSON apol) of
1336 J.Ok p -> p ==? apol
1337 J.Error s -> failTest $ "Failed to deserialise: " ++ s
1338 where _types = apol::Types.AllocPolicy
1340 prop_Types_DiskTemplate_serialisation dt =
1341 case J.readJSON (J.showJSON dt) of
1343 J.Error s -> failTest $ "Failed to deserialise: " ++ s
1344 where _types = dt::Types.DiskTemplate
1346 prop_Types_ISpec_serialisation ispec =
1347 case J.readJSON (J.showJSON ispec) of
1348 J.Ok p -> p ==? ispec
1349 J.Error s -> failTest $ "Failed to deserialise: " ++ s
1350 where _types = ispec::Types.ISpec
1352 prop_Types_IPolicy_serialisation ipol =
1353 case J.readJSON (J.showJSON ipol) of
1354 J.Ok p -> p ==? ipol
1355 J.Error s -> failTest $ "Failed to deserialise: " ++ s
1356 where _types = ipol::Types.IPolicy
1358 prop_Types_EvacMode_serialisation em =
1359 case J.readJSON (J.showJSON em) of
1361 J.Error s -> failTest $ "Failed to deserialise: " ++ s
1362 where _types = em::Types.EvacMode
1364 prop_Types_opToResult op =
1366 Types.OpFail _ -> Types.isBad r
1367 Types.OpGood v -> case r of
1368 Types.Bad _ -> False
1369 Types.Ok v' -> v == v'
1370 where r = Types.opToResult op
1371 _types = op::Types.OpResult Int
1373 prop_Types_eitherToResult ei =
1375 Left _ -> Types.isBad r
1376 Right v -> case r of
1377 Types.Bad _ -> False
1378 Types.Ok v' -> v == v'
1379 where r = Types.eitherToResult ei
1380 _types = ei::Either String Int
1383 [ 'prop_Types_AllocPolicy_serialisation
1384 , 'prop_Types_DiskTemplate_serialisation
1385 , 'prop_Types_ISpec_serialisation
1386 , 'prop_Types_IPolicy_serialisation
1387 , 'prop_Types_EvacMode_serialisation
1388 , 'prop_Types_opToResult
1389 , 'prop_Types_eitherToResult