1 {-| Unittests for ganeti-htools.
7 Copyright (C) 2009, 2010, 2011 Google Inc.
9 This program is free software; you can redistribute it and/or modify
10 it under the terms of the GNU General Public License as published by
11 the Free Software Foundation; either version 2 of the License, or
12 (at your option) any later version.
14 This program is distributed in the hope that it will be useful, but
15 WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17 General Public License for more details.
19 You should have received a copy of the GNU General Public License
20 along with this program; if not, write to the Free Software
21 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 module Ganeti.HTools.QC
40 import Test.QuickCheck
41 import Data.List (findIndex, intercalate, nub, isPrefixOf)
44 import qualified Text.JSON as J
45 import qualified Data.Map
46 import qualified Data.IntMap as IntMap
47 import qualified Ganeti.OpCodes as OpCodes
48 import qualified Ganeti.Jobs as Jobs
49 import qualified Ganeti.Luxi
50 import qualified Ganeti.HTools.CLI as CLI
51 import qualified Ganeti.HTools.Cluster as Cluster
52 import qualified Ganeti.HTools.Container as Container
53 import qualified Ganeti.HTools.ExtLoader
54 import qualified Ganeti.HTools.IAlloc as IAlloc
55 import qualified Ganeti.HTools.Instance as Instance
56 import qualified Ganeti.HTools.Loader as Loader
57 import qualified Ganeti.HTools.Luxi
58 import qualified Ganeti.HTools.Node as Node
59 import qualified Ganeti.HTools.Group as Group
60 import qualified Ganeti.HTools.PeerMap as PeerMap
61 import qualified Ganeti.HTools.Rapi
62 import qualified Ganeti.HTools.Simu
63 import qualified Ganeti.HTools.Text as Text
64 import qualified Ganeti.HTools.Types as Types
65 import qualified Ganeti.HTools.Utils as Utils
66 import qualified Ganeti.HTools.Version
67 import qualified Ganeti.Constants as C
69 run :: Testable prop => prop -> Args -> IO Result
70 run = flip quickCheckWithResult
74 -- | Maximum memory (1TiB, somewhat random value).
78 -- | Maximum disk (8TiB, somewhat random value).
80 maxDsk = 1024 * 1024 * 8
82 -- | Max CPUs (1024, somewhat random value).
86 defGroup :: Group.Group
87 defGroup = flip Group.setIdx 0 $
88 Group.create "default" Utils.defaultGroupID
91 defGroupList :: Group.List
92 defGroupList = Container.fromList [(Group.idx defGroup, defGroup)]
94 defGroupAssoc :: Data.Map.Map String Types.Gdx
95 defGroupAssoc = Data.Map.singleton (Group.uuid defGroup) (Group.idx defGroup)
99 -- | Simple checker for whether OpResult is fail or pass.
100 isFailure :: Types.OpResult a -> Bool
101 isFailure (Types.OpFail _) = True
104 -- | Update an instance to be smaller than a node.
105 setInstanceSmallerThanNode node inst =
106 inst { Instance.mem = Node.availMem node `div` 2
107 , Instance.dsk = Node.availDisk node `div` 2
108 , Instance.vcpus = Node.availCpu node `div` 2
111 -- | Create an instance given its spec.
112 createInstance mem dsk vcpus =
113 Instance.create "inst-unnamed" mem dsk vcpus "running" [] True (-1) (-1)
116 -- | Create a small cluster by repeating a node spec.
117 makeSmallCluster :: Node.Node -> Int -> Node.List
118 makeSmallCluster node count =
119 let fn = Node.buildPeers node Container.empty
120 namelst = map (\n -> (Node.name n, n)) (replicate count fn)
121 (_, nlst) = Loader.assignIndices namelst
124 -- | Checks if a node is "big" enough.
125 isNodeBig :: Node.Node -> Int -> Bool
126 isNodeBig node size = Node.availDisk node > size * Types.unitDsk
127 && Node.availMem node > size * Types.unitMem
128 && Node.availCpu node > size * Types.unitCpu
130 canBalance :: Cluster.Table -> Bool -> Bool -> Bool -> Bool
131 canBalance tbl dm im evac = isJust $ Cluster.tryBalance tbl dm im evac 0 0
133 -- | Assigns a new fresh instance to a cluster; this is not
134 -- allocation, so no resource checks are done.
135 assignInstance :: Node.List -> Instance.List -> Instance.Instance ->
136 Types.Idx -> Types.Idx ->
137 (Node.List, Instance.List)
138 assignInstance nl il inst pdx sdx =
139 let pnode = Container.find pdx nl
140 snode = Container.find sdx nl
141 maxiidx = if Container.null il
143 else fst (Container.findMax il) + 1
144 inst' = inst { Instance.idx = maxiidx,
145 Instance.pNode = pdx, Instance.sNode = sdx }
146 pnode' = Node.setPri pnode inst'
147 snode' = Node.setSec snode inst'
148 nl' = Container.addTwo pdx pnode' sdx snode' nl
149 il' = Container.add maxiidx inst' il
152 -- * Arbitrary instances
154 -- | Defines a DNS name.
155 newtype DNSChar = DNSChar { dnsGetChar::Char }
157 instance Arbitrary DNSChar where
159 x <- elements (['a'..'z'] ++ ['0'..'9'] ++ "_-")
162 getName :: Gen String
165 dn <- vector n::Gen [DNSChar]
166 return (map dnsGetChar dn)
169 getFQDN :: Gen String
172 ncomps <- choose (1, 4)
173 frest <- vector ncomps::Gen [[DNSChar]]
174 let frest' = map (map dnsGetChar) frest
175 return (felem ++ "." ++ intercalate "." frest')
177 -- let's generate a random instance
178 instance Arbitrary Instance.Instance where
181 mem <- choose (0, maxMem)
182 dsk <- choose (0, maxDsk)
183 run_st <- elements [ C.inststErrorup
187 , C.inststNodeoffline
193 vcpus <- choose (0, maxCpu)
194 return $ Instance.create name mem dsk vcpus run_st [] True pn sn
197 -- | Generas an arbitrary node based on sizing information.
198 genNode :: Maybe Int -- ^ Minimum node size in terms of units
199 -> Maybe Int -- ^ Maximum node size (when Nothing, bounded
200 -- just by the max... constants)
202 genNode min_multiplier max_multiplier = do
203 let (base_mem, base_dsk, base_cpu) =
204 case min_multiplier of
205 Just mm -> (mm * Types.unitMem,
209 (top_mem, top_dsk, top_cpu) =
210 case max_multiplier of
211 Just mm -> (mm * Types.unitMem,
214 Nothing -> (maxMem, maxDsk, maxCpu)
216 mem_t <- choose (base_mem, top_mem)
217 mem_f <- choose (base_mem, mem_t)
218 mem_n <- choose (0, mem_t - mem_f)
219 dsk_t <- choose (base_dsk, top_dsk)
220 dsk_f <- choose (base_dsk, dsk_t)
221 cpu_t <- choose (base_cpu, top_cpu)
223 let n = Node.create name (fromIntegral mem_t) mem_n mem_f
224 (fromIntegral dsk_t) dsk_f (fromIntegral cpu_t) offl 0
225 return $ Node.buildPeers n Container.empty
228 instance Arbitrary Node.Node where
229 arbitrary = genNode Nothing Nothing
232 instance Arbitrary OpCodes.ReplaceDisksMode where
233 arbitrary = elements [ OpCodes.ReplaceOnPrimary
234 , OpCodes.ReplaceOnSecondary
235 , OpCodes.ReplaceNewSecondary
236 , OpCodes.ReplaceAuto
239 instance Arbitrary OpCodes.OpCode where
241 op_id <- elements [ "OP_TEST_DELAY"
242 , "OP_INSTANCE_REPLACE_DISKS"
243 , "OP_INSTANCE_FAILOVER"
244 , "OP_INSTANCE_MIGRATE"
248 liftM3 OpCodes.OpTestDelay arbitrary arbitrary arbitrary
249 "OP_INSTANCE_REPLACE_DISKS" ->
250 liftM5 OpCodes.OpInstanceReplaceDisks arbitrary arbitrary
251 arbitrary arbitrary arbitrary
252 "OP_INSTANCE_FAILOVER" ->
253 liftM2 OpCodes.OpInstanceFailover arbitrary arbitrary
254 "OP_INSTANCE_MIGRATE" ->
255 liftM4 OpCodes.OpInstanceMigrate arbitrary arbitrary arbitrary
257 _ -> fail "Wrong opcode")
259 instance Arbitrary Jobs.OpStatus where
260 arbitrary = elements [minBound..maxBound]
262 instance Arbitrary Jobs.JobStatus where
263 arbitrary = elements [minBound..maxBound]
265 newtype SmallRatio = SmallRatio Double deriving Show
266 instance Arbitrary SmallRatio where
269 return $ SmallRatio v
271 instance Arbitrary Types.AllocPolicy where
272 arbitrary = elements [minBound..maxBound]
274 instance Arbitrary Types.DiskTemplate where
275 arbitrary = elements [minBound..maxBound]
281 -- | If the list is not just an empty element, and if the elements do
282 -- not contain commas, then join+split should be idempotent.
283 prop_Utils_commaJoinSplit =
284 forAll (arbitrary `suchThat`
285 (\l -> l /= [""] && all (not . elem ',') l )) $ \lst ->
286 Utils.sepSplit ',' (Utils.commaJoin lst) == lst
288 -- | Split and join should always be idempotent.
289 prop_Utils_commaSplitJoin s = Utils.commaJoin (Utils.sepSplit ',' s) == s
291 -- | fromObjWithDefault, we test using the Maybe monad and an integer
293 prop_Utils_fromObjWithDefault def_value random_key =
294 -- a missing key will be returned with the default
295 Utils.fromObjWithDefault [] random_key def_value == Just def_value &&
296 -- a found key will be returned as is, not with default
297 Utils.fromObjWithDefault [(random_key, J.showJSON def_value)]
298 random_key (def_value+1) == Just def_value
299 where _types = def_value :: Integer
301 -- | Test that functional if' behaves like the syntactic sugar if.
302 prop_Utils_if'if :: Bool -> Int -> Int -> Bool
303 prop_Utils_if'if cnd a b = Utils.if' cnd a b == if cnd then a else b
305 -- | Test basic select functionality
306 prop_Utils_select :: Int -- ^ Default result
307 -> [Int] -- ^ List of False values
308 -> [Int] -- ^ List of True values
309 -> Bool -- ^ Test result
310 prop_Utils_select def lst1 lst2 =
311 Utils.select def cndlist == expectedresult
312 where expectedresult = Utils.if' (null lst2) def (head lst2)
313 flist = map (\e -> (False, e)) lst1
314 tlist = map (\e -> (True, e)) lst2
315 cndlist = flist ++ tlist
317 -- | Test basic select functionality with undefined default
318 prop_Utils_select_undefd :: [Int] -- ^ List of False values
319 -> NonEmptyList Int -- ^ List of True values
320 -> Bool -- ^ Test result
321 prop_Utils_select_undefd lst1 (NonEmpty lst2) =
322 Utils.select undefined cndlist == head lst2
323 where flist = map (\e -> (False, e)) lst1
324 tlist = map (\e -> (True, e)) lst2
325 cndlist = flist ++ tlist
327 -- | Test basic select functionality with undefined list values
328 prop_Utils_select_undefv :: [Int] -- ^ List of False values
329 -> NonEmptyList Int -- ^ List of True values
330 -> Bool -- ^ Test result
331 prop_Utils_select_undefv lst1 (NonEmpty lst2) =
332 Utils.select undefined cndlist == head lst2
333 where flist = map (\e -> (False, e)) lst1
334 tlist = map (\e -> (True, e)) lst2
335 cndlist = flist ++ tlist ++ [undefined]
337 prop_Utils_parseUnit (NonNegative n) =
338 Utils.parseUnit (show n) == Types.Ok n &&
339 Utils.parseUnit (show n ++ "m") == Types.Ok n &&
340 (case Utils.parseUnit (show n ++ "M") of
341 Types.Ok m -> if n > 0
342 then m < n -- for positive values, X MB is less than X MiB
343 else m == 0 -- but for 0, 0 MB == 0 MiB
344 Types.Bad _ -> False) &&
345 Utils.parseUnit (show n ++ "g") == Types.Ok (n*1024) &&
346 Utils.parseUnit (show n ++ "t") == Types.Ok (n*1048576) &&
347 Types.isBad (Utils.parseUnit (show n ++ "x")::Types.Result Int)
348 where _types = (n::Int)
350 -- | Test list for the Utils module.
352 [ run prop_Utils_commaJoinSplit
353 , run prop_Utils_commaSplitJoin
354 , run prop_Utils_fromObjWithDefault
355 , run prop_Utils_if'if
356 , run prop_Utils_select
357 , run prop_Utils_select_undefd
358 , run prop_Utils_select_undefv
359 , run prop_Utils_parseUnit
364 -- | Make sure add is idempotent.
365 prop_PeerMap_addIdempotent pmap key em =
366 fn puniq == fn (fn puniq)
367 where _types = (pmap::PeerMap.PeerMap,
368 key::PeerMap.Key, em::PeerMap.Elem)
369 fn = PeerMap.add key em
370 puniq = PeerMap.accumArray const pmap
372 -- | Make sure remove is idempotent.
373 prop_PeerMap_removeIdempotent pmap key =
374 fn puniq == fn (fn puniq)
375 where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
376 fn = PeerMap.remove key
377 puniq = PeerMap.accumArray const pmap
379 -- | Make sure a missing item returns 0.
380 prop_PeerMap_findMissing pmap key =
381 PeerMap.find key (PeerMap.remove key puniq) == 0
382 where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
383 puniq = PeerMap.accumArray const pmap
385 -- | Make sure an added item is found.
386 prop_PeerMap_addFind pmap key em =
387 PeerMap.find key (PeerMap.add key em puniq) == em
388 where _types = (pmap::PeerMap.PeerMap,
389 key::PeerMap.Key, em::PeerMap.Elem)
390 puniq = PeerMap.accumArray const pmap
392 -- | Manual check that maxElem returns the maximum indeed, or 0 for null.
393 prop_PeerMap_maxElem pmap =
394 PeerMap.maxElem puniq == if null puniq then 0
395 else (maximum . snd . unzip) puniq
396 where _types = pmap::PeerMap.PeerMap
397 puniq = PeerMap.accumArray const pmap
399 -- | List of tests for the PeerMap module.
401 [ run prop_PeerMap_addIdempotent
402 , run prop_PeerMap_removeIdempotent
403 , run prop_PeerMap_maxElem
404 , run prop_PeerMap_addFind
405 , run prop_PeerMap_findMissing
408 -- ** Container tests
410 prop_Container_addTwo cdata i1 i2 =
411 fn i1 i2 cont == fn i2 i1 cont &&
412 fn i1 i2 cont == fn i1 i2 (fn i1 i2 cont)
413 where _types = (cdata::[Int],
415 cont = foldl (\c x -> Container.add x x c) Container.empty cdata
416 fn x1 x2 = Container.addTwo x1 x1 x2 x2
418 prop_Container_nameOf node =
419 let nl = makeSmallCluster node 1
420 fnode = head (Container.elems nl)
421 in Container.nameOf nl (Node.idx fnode) == Node.name fnode
423 -- | We test that in a cluster, given a random node, we can find it by
424 -- its name and alias, as long as all names and aliases are unique,
425 -- and that we fail to find a non-existing name.
426 prop_Container_findByName node othername =
427 forAll (choose (1, 20)) $ \ cnt ->
428 forAll (choose (0, cnt - 1)) $ \ fidx ->
429 forAll (vector cnt) $ \ names ->
430 (length . nub) (map fst names ++ map snd names) ==
432 not (othername `elem` (map fst names ++ map snd names)) ==>
433 let nl = makeSmallCluster node cnt
434 nodes = Container.elems nl
435 nodes' = map (\((name, alias), nn) -> (Node.idx nn,
436 nn { Node.name = name,
437 Node.alias = alias }))
439 nl' = Container.fromList nodes'
440 target = snd (nodes' !! fidx)
441 in Container.findByName nl' (Node.name target) == Just target &&
442 Container.findByName nl' (Node.alias target) == Just target &&
443 Container.findByName nl' othername == Nothing
446 [ run prop_Container_addTwo
447 , run prop_Container_nameOf
448 , run prop_Container_findByName
453 -- Simple instance tests, we only have setter/getters
455 prop_Instance_creat inst =
456 Instance.name inst == Instance.alias inst
458 prop_Instance_setIdx inst idx =
459 Instance.idx (Instance.setIdx inst idx) == idx
460 where _types = (inst::Instance.Instance, idx::Types.Idx)
462 prop_Instance_setName inst name =
463 Instance.name newinst == name &&
464 Instance.alias newinst == name
465 where _types = (inst::Instance.Instance, name::String)
466 newinst = Instance.setName inst name
468 prop_Instance_setAlias inst name =
469 Instance.name newinst == Instance.name inst &&
470 Instance.alias newinst == name
471 where _types = (inst::Instance.Instance, name::String)
472 newinst = Instance.setAlias inst name
474 prop_Instance_setPri inst pdx =
475 Instance.pNode (Instance.setPri inst pdx) == pdx
476 where _types = (inst::Instance.Instance, pdx::Types.Ndx)
478 prop_Instance_setSec inst sdx =
479 Instance.sNode (Instance.setSec inst sdx) == sdx
480 where _types = (inst::Instance.Instance, sdx::Types.Ndx)
482 prop_Instance_setBoth inst pdx sdx =
483 Instance.pNode si == pdx && Instance.sNode si == sdx
484 where _types = (inst::Instance.Instance, pdx::Types.Ndx, sdx::Types.Ndx)
485 si = Instance.setBoth inst pdx sdx
487 prop_Instance_runStatus_True =
488 forAll (arbitrary `suchThat`
489 ((`elem` Instance.runningStates) . Instance.runSt))
492 prop_Instance_runStatus_False inst =
493 let run_st = Instance.running inst
494 run_tx = Instance.runSt inst
496 run_tx `notElem` Instance.runningStates ==> not run_st
498 prop_Instance_shrinkMG inst =
499 Instance.mem inst >= 2 * Types.unitMem ==>
500 case Instance.shrinkByType inst Types.FailMem of
502 Instance.mem inst' == Instance.mem inst - Types.unitMem
505 prop_Instance_shrinkMF inst =
506 forAll (choose (0, 2 * Types.unitMem - 1)) $ \mem ->
507 let inst' = inst { Instance.mem = mem}
508 in Types.isBad $ Instance.shrinkByType inst' Types.FailMem
510 prop_Instance_shrinkCG inst =
511 Instance.vcpus inst >= 2 * Types.unitCpu ==>
512 case Instance.shrinkByType inst Types.FailCPU of
514 Instance.vcpus inst' == Instance.vcpus inst - Types.unitCpu
517 prop_Instance_shrinkCF inst =
518 forAll (choose (0, 2 * Types.unitCpu - 1)) $ \vcpus ->
519 let inst' = inst { Instance.vcpus = vcpus }
520 in Types.isBad $ Instance.shrinkByType inst' Types.FailCPU
522 prop_Instance_shrinkDG inst =
523 Instance.dsk inst >= 2 * Types.unitDsk ==>
524 case Instance.shrinkByType inst Types.FailDisk of
526 Instance.dsk inst' == Instance.dsk inst - Types.unitDsk
529 prop_Instance_shrinkDF inst =
530 forAll (choose (0, 2 * Types.unitDsk - 1)) $ \dsk ->
531 let inst' = inst { Instance.dsk = dsk }
532 in Types.isBad $ Instance.shrinkByType inst' Types.FailDisk
534 prop_Instance_setMovable inst m =
535 Instance.movable inst' == m
536 where inst' = Instance.setMovable inst m
539 [ run prop_Instance_creat
540 , run prop_Instance_setIdx
541 , run prop_Instance_setName
542 , run prop_Instance_setAlias
543 , run prop_Instance_setPri
544 , run prop_Instance_setSec
545 , run prop_Instance_setBoth
546 , run prop_Instance_runStatus_True
547 , run prop_Instance_runStatus_False
548 , run prop_Instance_shrinkMG
549 , run prop_Instance_shrinkMF
550 , run prop_Instance_shrinkCG
551 , run prop_Instance_shrinkCF
552 , run prop_Instance_shrinkDG
553 , run prop_Instance_shrinkDF
554 , run prop_Instance_setMovable
557 -- ** Text backend tests
559 -- Instance text loader tests
561 prop_Text_Load_Instance name mem dsk vcpus status
562 (NonEmpty pnode) snode
563 (NonNegative pdx) (NonNegative sdx) autobal dt =
564 pnode /= snode && pdx /= sdx ==>
565 let vcpus_s = show vcpus
570 else [(pnode, pdx), (snode, sdx)]
571 nl = Data.Map.fromList ndx
573 sbal = if autobal then "Y" else "N"
574 sdt = Types.dtToString dt
575 inst = Text.loadInst nl
576 [name, mem_s, dsk_s, vcpus_s, status,
577 sbal, pnode, snode, sdt, tags]
578 fail1 = Text.loadInst nl
579 [name, mem_s, dsk_s, vcpus_s, status,
580 sbal, pnode, pnode, tags]
581 _types = ( name::String, mem::Int, dsk::Int
582 , vcpus::Int, status::String
587 Types.Bad msg -> printTestCase ("Failed to load instance: " ++ msg)
589 Types.Ok (_, i) -> printTestCase ("Mismatch in some field while\
590 \ loading the instance") $
591 Instance.name i == name &&
592 Instance.vcpus i == vcpus &&
593 Instance.mem i == mem &&
594 Instance.pNode i == pdx &&
595 Instance.sNode i == (if null snode
596 then Node.noSecondary
598 Instance.autoBalance i == autobal &&
601 prop_Text_Load_InstanceFail ktn fields =
602 length fields /= 10 ==>
603 case Text.loadInst nl fields of
604 Types.Ok _ -> printTestCase "Managed to load instance from invalid\
606 Types.Bad msg -> printTestCase ("Unrecognised error message: " ++ msg) $
607 "Invalid/incomplete instance data: '" `isPrefixOf` msg
608 where nl = Data.Map.fromList ktn
610 prop_Text_Load_Node name tm nm fm td fd tc fo =
611 let conv v = if v < 0
623 any_broken = any (< 0) [tm, nm, fm, td, fd, tc]
624 gid = Group.uuid defGroup
625 in case Text.loadNode defGroupAssoc
626 [name, tm_s, nm_s, fm_s, td_s, fd_s, tc_s, fo_s, gid] of
628 Just (name', node) ->
630 then Node.offline node
631 else Node.name node == name' && name' == name &&
632 Node.alias node == name &&
633 Node.tMem node == fromIntegral tm &&
634 Node.nMem node == nm &&
635 Node.fMem node == fm &&
636 Node.tDsk node == fromIntegral td &&
637 Node.fDsk node == fd &&
638 Node.tCpu node == fromIntegral tc
640 prop_Text_Load_NodeFail fields =
641 length fields /= 8 ==> isNothing $ Text.loadNode Data.Map.empty fields
643 prop_Text_NodeLSIdempotent node =
644 (Text.loadNode defGroupAssoc.
645 Utils.sepSplit '|' . Text.serializeNode defGroupList) n ==
646 Just (Node.name n, n)
647 -- override failN1 to what loadNode returns by default
648 where n = node { Node.failN1 = True, Node.offline = False }
651 [ run prop_Text_Load_Instance
652 , run prop_Text_Load_InstanceFail
653 , run prop_Text_Load_Node
654 , run prop_Text_Load_NodeFail
655 , run prop_Text_NodeLSIdempotent
660 prop_Node_setAlias node name =
661 Node.name newnode == Node.name node &&
662 Node.alias newnode == name
663 where _types = (node::Node.Node, name::String)
664 newnode = Node.setAlias node name
666 prop_Node_setOffline node status =
667 Node.offline newnode == status
668 where newnode = Node.setOffline node status
670 prop_Node_setXmem node xm =
671 Node.xMem newnode == xm
672 where newnode = Node.setXmem node xm
674 prop_Node_setMcpu node mc =
675 Node.mCpu newnode == mc
676 where newnode = Node.setMcpu node mc
678 -- | Check that an instance add with too high memory or disk will be
680 prop_Node_addPriFM node inst = Instance.mem inst >= Node.fMem node &&
681 not (Node.failN1 node)
683 case Node.addPri node inst'' of
684 Types.OpFail Types.FailMem -> True
686 where _types = (node::Node.Node, inst::Instance.Instance)
687 inst' = setInstanceSmallerThanNode node inst
688 inst'' = inst' { Instance.mem = Instance.mem inst }
690 prop_Node_addPriFD node inst = Instance.dsk inst >= Node.fDsk node &&
691 not (Node.failN1 node)
693 case Node.addPri node inst'' of
694 Types.OpFail Types.FailDisk -> True
696 where _types = (node::Node.Node, inst::Instance.Instance)
697 inst' = setInstanceSmallerThanNode node inst
698 inst'' = inst' { Instance.dsk = Instance.dsk inst }
700 prop_Node_addPriFC node inst (Positive extra) =
701 not (Node.failN1 node) ==>
702 case Node.addPri node inst'' of
703 Types.OpFail Types.FailCPU -> True
705 where _types = (node::Node.Node, inst::Instance.Instance)
706 inst' = setInstanceSmallerThanNode node inst
707 inst'' = inst' { Instance.vcpus = Node.availCpu node + extra }
709 -- | Check that an instance add with too high memory or disk will be
711 prop_Node_addSec node inst pdx =
712 (Instance.mem inst >= (Node.fMem node - Node.rMem node) ||
713 Instance.dsk inst >= Node.fDsk node) &&
714 not (Node.failN1 node)
715 ==> isFailure (Node.addSec node inst pdx)
716 where _types = (node::Node.Node, inst::Instance.Instance, pdx::Int)
718 -- | Checks for memory reservation changes.
719 prop_Node_rMem inst =
720 forAll (arbitrary `suchThat` ((> Types.unitMem) . Node.fMem)) $ \node ->
721 -- ab = auto_balance, nb = non-auto_balance
722 -- we use -1 as the primary node of the instance
723 let inst' = inst { Instance.pNode = -1, Instance.autoBalance = True }
724 inst_ab = setInstanceSmallerThanNode node inst'
725 inst_nb = inst_ab { Instance.autoBalance = False }
726 -- now we have the two instances, identical except the
727 -- autoBalance attribute
728 orig_rmem = Node.rMem node
729 inst_idx = Instance.idx inst_ab
730 node_add_ab = Node.addSec node inst_ab (-1)
731 node_add_nb = Node.addSec node inst_nb (-1)
732 node_del_ab = liftM (flip Node.removeSec inst_ab) node_add_ab
733 node_del_nb = liftM (flip Node.removeSec inst_nb) node_add_nb
734 in case (node_add_ab, node_add_nb, node_del_ab, node_del_nb) of
735 (Types.OpGood a_ab, Types.OpGood a_nb,
736 Types.OpGood d_ab, Types.OpGood d_nb) ->
737 printTestCase "Consistency checks failed" $
738 Node.rMem a_ab > orig_rmem &&
739 Node.rMem a_ab - orig_rmem == Instance.mem inst_ab &&
740 Node.rMem a_nb == orig_rmem &&
741 Node.rMem d_ab == orig_rmem &&
742 Node.rMem d_nb == orig_rmem &&
743 -- this is not related to rMem, but as good a place to
745 inst_idx `elem` Node.sList a_ab &&
746 not (inst_idx `elem` Node.sList d_ab)
747 x -> printTestCase ("Failed to add/remove instances: " ++ show x)
750 -- | Check mdsk setting.
751 prop_Node_setMdsk node mx =
752 Node.loDsk node' >= 0 &&
753 fromIntegral (Node.loDsk node') <= Node.tDsk node &&
754 Node.availDisk node' >= 0 &&
755 Node.availDisk node' <= Node.fDsk node' &&
756 fromIntegral (Node.availDisk node') <= Node.tDsk node' &&
757 Node.mDsk node' == mx'
758 where _types = (node::Node.Node, mx::SmallRatio)
759 node' = Node.setMdsk node mx'
763 prop_Node_tagMaps_idempotent tags =
764 Node.delTags (Node.addTags m tags) tags == m
765 where m = Data.Map.empty
767 prop_Node_tagMaps_reject tags =
769 any (\t -> Node.rejectAddTags m [t]) tags
770 where m = Node.addTags Data.Map.empty tags
772 prop_Node_showField node =
773 forAll (elements Node.defaultFields) $ \ field ->
774 fst (Node.showHeader field) /= Types.unknownField &&
775 Node.showField node field /= Types.unknownField
778 prop_Node_computeGroups nodes =
779 let ng = Node.computeGroups nodes
780 onlyuuid = map fst ng
781 in length nodes == sum (map (length . snd) ng) &&
782 all (\(guuid, ns) -> all ((== guuid) . Node.group) ns) ng &&
783 length (nub onlyuuid) == length onlyuuid &&
784 (null nodes || not (null ng))
787 [ run prop_Node_setAlias
788 , run prop_Node_setOffline
789 , run prop_Node_setMcpu
790 , run prop_Node_setXmem
791 , run prop_Node_addPriFM
792 , run prop_Node_addPriFD
793 , run prop_Node_addPriFC
794 , run prop_Node_addSec
796 , run prop_Node_setMdsk
797 , run prop_Node_tagMaps_idempotent
798 , run prop_Node_tagMaps_reject
799 , run prop_Node_showField
800 , run prop_Node_computeGroups
806 -- | Check that the cluster score is close to zero for a homogeneous
808 prop_Score_Zero node =
809 forAll (choose (1, 1024)) $ \count ->
810 (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
811 (Node.tDsk node > 0) && (Node.tMem node > 0)) ==>
812 let fn = Node.buildPeers node Container.empty
813 nlst = replicate count fn
814 score = Cluster.compCVNodes nlst
815 -- we can't say == 0 here as the floating point errors accumulate;
816 -- this should be much lower than the default score in CLI.hs
819 -- | Check that cluster stats are sane.
820 prop_CStats_sane node =
821 forAll (choose (1, 1024)) $ \count ->
822 (not (Node.offline node) && not (Node.failN1 node) &&
823 (Node.availDisk node > 0) && (Node.availMem node > 0)) ==>
824 let fn = Node.buildPeers node Container.empty
825 nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
826 nl = Container.fromList nlst
827 cstats = Cluster.totalResources nl
828 in Cluster.csAdsk cstats >= 0 &&
829 Cluster.csAdsk cstats <= Cluster.csFdsk cstats
831 -- | Check that one instance is allocated correctly, without
832 -- rebalances needed.
833 prop_ClusterAlloc_sane node inst =
834 forAll (choose (5, 20)) $ \count ->
835 not (Node.offline node)
836 && not (Node.failN1 node)
837 && Node.availDisk node > 0
838 && Node.availMem node > 0
840 let nl = makeSmallCluster node count
842 inst' = setInstanceSmallerThanNode node inst
843 in case Cluster.genAllocNodes defGroupList nl 2 True >>=
844 Cluster.tryAlloc nl il inst' of
847 case Cluster.asSolutions as of
849 (xnl, xi, _, cv):[] ->
850 let il' = Container.add (Instance.idx xi) xi il
851 tbl = Cluster.Table xnl il' cv []
852 in not (canBalance tbl True True False)
855 -- | Checks that on a 2-5 node cluster, we can allocate a random
856 -- instance spec via tiered allocation (whatever the original instance
857 -- spec), on either one or two nodes.
858 prop_ClusterCanTieredAlloc node inst =
859 forAll (choose (2, 5)) $ \count ->
860 forAll (choose (1, 2)) $ \rqnodes ->
861 not (Node.offline node)
862 && not (Node.failN1 node)
865 let nl = makeSmallCluster node count
867 allocnodes = Cluster.genAllocNodes defGroupList nl rqnodes True
868 in case allocnodes >>= \allocnodes' ->
869 Cluster.tieredAlloc nl il inst allocnodes' [] [] of
871 Types.Ok (_, _, il', ixes, cstats) -> not (null ixes) &&
872 IntMap.size il' == length ixes &&
873 length ixes == length cstats
875 -- | Checks that on a 4-8 node cluster, once we allocate an instance,
876 -- we can also evacuate it.
877 prop_ClusterAllocEvac node inst =
878 forAll (choose (4, 8)) $ \count ->
879 not (Node.offline node)
880 && not (Node.failN1 node)
883 let nl = makeSmallCluster node count
885 inst' = setInstanceSmallerThanNode node inst
886 in case Cluster.genAllocNodes defGroupList nl 2 True >>=
887 Cluster.tryAlloc nl il inst' of
890 case Cluster.asSolutions as of
892 (xnl, xi, _, _):[] ->
893 let sdx = Instance.sNode xi
894 il' = Container.add (Instance.idx xi) xi il
895 in case Cluster.tryEvac xnl il' [Instance.idx xi] [sdx] of
900 -- | Check that allocating multiple instances on a cluster, then
901 -- adding an empty node, results in a valid rebalance.
902 prop_ClusterAllocBalance =
903 forAll (genNode (Just 5) (Just 128)) $ \node ->
904 forAll (choose (3, 5)) $ \count ->
905 not (Node.offline node) && not (Node.failN1 node) ==>
906 let nl = makeSmallCluster node count
907 (hnode, nl') = IntMap.deleteFindMax nl
909 allocnodes = Cluster.genAllocNodes defGroupList nl' 2 True
910 i_templ = createInstance Types.unitMem Types.unitDsk Types.unitCpu
911 in case allocnodes >>= \allocnodes' ->
912 Cluster.iterateAlloc nl' il i_templ allocnodes' [] [] of
914 Types.Ok (_, xnl, il', _, _) ->
915 let ynl = Container.add (Node.idx hnode) hnode xnl
916 cv = Cluster.compCV ynl
917 tbl = Cluster.Table ynl il' cv []
918 in canBalance tbl True True False
920 -- | Checks consistency.
921 prop_ClusterCheckConsistency node inst =
922 let nl = makeSmallCluster node 3
923 [node1, node2, node3] = Container.elems nl
924 node3' = node3 { Node.group = 1 }
925 nl' = Container.add (Node.idx node3') node3' nl
926 inst1 = Instance.setBoth inst (Node.idx node1) (Node.idx node2)
927 inst2 = Instance.setBoth inst (Node.idx node1) Node.noSecondary
928 inst3 = Instance.setBoth inst (Node.idx node1) (Node.idx node3)
929 ccheck = Cluster.findSplitInstances nl' . Container.fromList
930 in null (ccheck [(0, inst1)]) &&
931 null (ccheck [(0, inst2)]) &&
932 (not . null $ ccheck [(0, inst3)])
934 -- | For now, we only test that we don't lose instances during the split.
935 prop_ClusterSplitCluster node inst =
936 forAll (choose (0, 100)) $ \icnt ->
937 let nl = makeSmallCluster node 2
938 (nl', il') = foldl (\(ns, is) _ -> assignInstance ns is inst 0 1)
939 (nl, Container.empty) [1..icnt]
940 gni = Cluster.splitCluster nl' il'
941 in sum (map (Container.size . snd . snd) gni) == icnt &&
942 all (\(guuid, (nl'', _)) -> all ((== guuid) . Node.group)
943 (Container.elems nl'')) gni
946 [ run prop_Score_Zero
947 , run prop_CStats_sane
948 , run prop_ClusterAlloc_sane
949 , run prop_ClusterCanTieredAlloc
950 , run prop_ClusterAllocEvac
951 , run prop_ClusterAllocBalance
952 , run prop_ClusterCheckConsistency
953 , run prop_ClusterSplitCluster
958 -- | Check that opcode serialization is idempotent.
959 prop_OpCodes_serialization op =
960 case J.readJSON (J.showJSON op) of
962 J.Ok op' -> op == op'
963 where _types = op::OpCodes.OpCode
966 [ run prop_OpCodes_serialization
971 -- | Check that (queued) job\/opcode status serialization is idempotent.
972 prop_OpStatus_serialization os =
973 case J.readJSON (J.showJSON os) of
975 J.Ok os' -> os == os'
976 where _types = os::Jobs.OpStatus
978 prop_JobStatus_serialization js =
979 case J.readJSON (J.showJSON js) of
981 J.Ok js' -> js == js'
982 where _types = js::Jobs.JobStatus
985 [ run prop_OpStatus_serialization
986 , run prop_JobStatus_serialization
991 prop_Loader_lookupNode ktn inst node =
992 Loader.lookupNode nl inst node == Data.Map.lookup node nl
993 where nl = Data.Map.fromList ktn
995 prop_Loader_lookupInstance kti inst =
996 Loader.lookupInstance il inst == Data.Map.lookup inst il
997 where il = Data.Map.fromList kti
999 prop_Loader_assignIndices nodes =
1000 Data.Map.size nassoc == length nodes &&
1001 Container.size kt == length nodes &&
1002 (if not (null nodes)
1003 then maximum (IntMap.keys kt) == length nodes - 1
1005 where (nassoc, kt) = Loader.assignIndices (map (\n -> (Node.name n, n)) nodes)
1007 -- | Checks that the number of primary instances recorded on the nodes
1009 prop_Loader_mergeData ns =
1010 let na = Container.fromList $ map (\n -> (Node.idx n, n)) ns
1011 in case Loader.mergeData [] [] [] []
1012 (Loader.emptyCluster {Loader.cdNodes = na}) of
1013 Types.Bad _ -> False
1014 Types.Ok (Loader.ClusterData _ nl il _) ->
1015 let nodes = Container.elems nl
1016 instances = Container.elems il
1017 in (sum . map (length . Node.pList)) nodes == 0 &&
1020 -- | Check that compareNameComponent on equal strings works.
1021 prop_Loader_compareNameComponent_equal :: String -> Bool
1022 prop_Loader_compareNameComponent_equal s =
1023 Loader.compareNameComponent s s ==
1024 Loader.LookupResult Loader.ExactMatch s
1026 -- | Check that compareNameComponent on prefix strings works.
1027 prop_Loader_compareNameComponent_prefix :: NonEmptyList Char -> String -> Bool
1028 prop_Loader_compareNameComponent_prefix (NonEmpty s1) s2 =
1029 Loader.compareNameComponent (s1 ++ "." ++ s2) s1 ==
1030 Loader.LookupResult Loader.PartialMatch s1
1033 [ run prop_Loader_lookupNode
1034 , run prop_Loader_lookupInstance
1035 , run prop_Loader_assignIndices
1036 , run prop_Loader_mergeData
1037 , run prop_Loader_compareNameComponent_equal
1038 , run prop_Loader_compareNameComponent_prefix
1043 prop_AllocPolicy_serialisation apol =
1044 case Types.apolFromString (Types.apolToString apol) of
1045 Types.Ok p -> printTestCase ("invalid deserialisation " ++ show p) $
1047 Types.Bad s -> printTestCase ("failed to deserialise: " ++ s) False
1049 prop_DiskTemplate_serialisation dt =
1050 case Types.dtFromString (Types.dtToString dt) of
1051 Types.Ok p -> printTestCase ("invalid deserialisation " ++ show p) $
1053 Types.Bad s -> printTestCase ("failed to deserialise: " ++ s) False
1056 [ run prop_AllocPolicy_serialisation
1057 , run prop_DiskTemplate_serialisation