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)
115 -- | Create a small cluster by repeating a node spec.
116 makeSmallCluster :: Node.Node -> Int -> Node.List
117 makeSmallCluster node count =
118 let fn = Node.buildPeers node Container.empty
119 namelst = map (\n -> (Node.name n, n)) (replicate count fn)
120 (_, nlst) = Loader.assignIndices namelst
123 -- | Checks if a node is "big" enough.
124 isNodeBig :: Node.Node -> Int -> Bool
125 isNodeBig node size = Node.availDisk node > size * Types.unitDsk
126 && Node.availMem node > size * Types.unitMem
127 && Node.availCpu node > size * Types.unitCpu
129 canBalance :: Cluster.Table -> Bool -> Bool -> Bool -> Bool
130 canBalance tbl dm im evac = isJust $ Cluster.tryBalance tbl dm im evac 0 0
132 -- | Assigns a new fresh instance to a cluster; this is not
133 -- allocation, so no resource checks are done.
134 assignInstance :: Node.List -> Instance.List -> Instance.Instance ->
135 Types.Idx -> Types.Idx ->
136 (Node.List, Instance.List)
137 assignInstance nl il inst pdx sdx =
138 let pnode = Container.find pdx nl
139 snode = Container.find sdx nl
140 maxiidx = if Container.null il
142 else fst (Container.findMax il) + 1
143 inst' = inst { Instance.idx = maxiidx,
144 Instance.pNode = pdx, Instance.sNode = sdx }
145 pnode' = Node.setPri pnode inst'
146 snode' = Node.setSec snode inst'
147 nl' = Container.addTwo pdx pnode' sdx snode' nl
148 il' = Container.add maxiidx inst' il
151 -- * Arbitrary instances
153 -- | Defines a DNS name.
154 newtype DNSChar = DNSChar { dnsGetChar::Char }
156 instance Arbitrary DNSChar where
158 x <- elements (['a'..'z'] ++ ['0'..'9'] ++ "_-")
161 getName :: Gen String
164 dn <- vector n::Gen [DNSChar]
165 return (map dnsGetChar dn)
168 getFQDN :: Gen String
171 ncomps <- choose (1, 4)
172 frest <- vector ncomps::Gen [[DNSChar]]
173 let frest' = map (map dnsGetChar) frest
174 return (felem ++ "." ++ intercalate "." frest')
176 -- let's generate a random instance
177 instance Arbitrary Instance.Instance where
180 mem <- choose (0, maxMem)
181 dsk <- choose (0, maxDsk)
182 run_st <- elements [ C.inststErrorup
186 , C.inststNodeoffline
192 vcpus <- choose (0, maxCpu)
193 return $ Instance.create name mem dsk vcpus run_st [] True pn sn
195 -- | Generas an arbitrary node based on sizing information.
196 genNode :: Maybe Int -- ^ Minimum node size in terms of units
197 -> Maybe Int -- ^ Maximum node size (when Nothing, bounded
198 -- just by the max... constants)
200 genNode min_multiplier max_multiplier = do
201 let (base_mem, base_dsk, base_cpu) =
202 case min_multiplier of
203 Just mm -> (mm * Types.unitMem,
207 (top_mem, top_dsk, top_cpu) =
208 case max_multiplier of
209 Just mm -> (mm * Types.unitMem,
212 Nothing -> (maxMem, maxDsk, maxCpu)
214 mem_t <- choose (base_mem, top_mem)
215 mem_f <- choose (base_mem, mem_t)
216 mem_n <- choose (0, mem_t - mem_f)
217 dsk_t <- choose (base_dsk, top_dsk)
218 dsk_f <- choose (base_dsk, dsk_t)
219 cpu_t <- choose (base_cpu, top_cpu)
221 let n = Node.create name (fromIntegral mem_t) mem_n mem_f
222 (fromIntegral dsk_t) dsk_f (fromIntegral cpu_t) offl 0
223 return $ Node.buildPeers n Container.empty
226 instance Arbitrary Node.Node where
227 arbitrary = genNode Nothing Nothing
230 instance Arbitrary OpCodes.ReplaceDisksMode where
231 arbitrary = elements [ OpCodes.ReplaceOnPrimary
232 , OpCodes.ReplaceOnSecondary
233 , OpCodes.ReplaceNewSecondary
234 , OpCodes.ReplaceAuto
237 instance Arbitrary OpCodes.OpCode where
239 op_id <- elements [ "OP_TEST_DELAY"
240 , "OP_INSTANCE_REPLACE_DISKS"
241 , "OP_INSTANCE_FAILOVER"
242 , "OP_INSTANCE_MIGRATE"
246 liftM3 OpCodes.OpTestDelay arbitrary arbitrary arbitrary
247 "OP_INSTANCE_REPLACE_DISKS" ->
248 liftM5 OpCodes.OpInstanceReplaceDisks arbitrary arbitrary
249 arbitrary arbitrary arbitrary
250 "OP_INSTANCE_FAILOVER" ->
251 liftM2 OpCodes.OpInstanceFailover arbitrary arbitrary
252 "OP_INSTANCE_MIGRATE" ->
253 liftM4 OpCodes.OpInstanceMigrate arbitrary arbitrary arbitrary
255 _ -> fail "Wrong opcode")
257 instance Arbitrary Jobs.OpStatus where
258 arbitrary = elements [minBound..maxBound]
260 instance Arbitrary Jobs.JobStatus where
261 arbitrary = elements [minBound..maxBound]
263 newtype SmallRatio = SmallRatio Double deriving Show
264 instance Arbitrary SmallRatio where
267 return $ SmallRatio v
269 instance Arbitrary Types.AllocPolicy where
270 arbitrary = elements [minBound..maxBound]
272 instance Arbitrary Types.DiskTemplate where
273 arbitrary = elements [minBound..maxBound]
279 -- | If the list is not just an empty element, and if the elements do
280 -- not contain commas, then join+split should be idempotent.
281 prop_Utils_commaJoinSplit =
282 forAll (arbitrary `suchThat`
283 (\l -> l /= [""] && all (not . elem ',') l )) $ \lst ->
284 Utils.sepSplit ',' (Utils.commaJoin lst) == lst
286 -- | Split and join should always be idempotent.
287 prop_Utils_commaSplitJoin s = Utils.commaJoin (Utils.sepSplit ',' s) == s
289 -- | fromObjWithDefault, we test using the Maybe monad and an integer
291 prop_Utils_fromObjWithDefault def_value random_key =
292 -- a missing key will be returned with the default
293 Utils.fromObjWithDefault [] random_key def_value == Just def_value &&
294 -- a found key will be returned as is, not with default
295 Utils.fromObjWithDefault [(random_key, J.showJSON def_value)]
296 random_key (def_value+1) == Just def_value
297 where _types = def_value :: Integer
299 -- | Test list for the Utils module.
301 [ run prop_Utils_commaJoinSplit
302 , run prop_Utils_commaSplitJoin
303 , run prop_Utils_fromObjWithDefault
308 -- | Make sure add is idempotent.
309 prop_PeerMap_addIdempotent pmap key em =
310 fn puniq == fn (fn puniq)
311 where _types = (pmap::PeerMap.PeerMap,
312 key::PeerMap.Key, em::PeerMap.Elem)
313 fn = PeerMap.add key em
314 puniq = PeerMap.accumArray const pmap
316 -- | Make sure remove is idempotent.
317 prop_PeerMap_removeIdempotent pmap key =
318 fn puniq == fn (fn puniq)
319 where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
320 fn = PeerMap.remove key
321 puniq = PeerMap.accumArray const pmap
323 -- | Make sure a missing item returns 0.
324 prop_PeerMap_findMissing pmap key =
325 PeerMap.find key (PeerMap.remove key puniq) == 0
326 where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
327 puniq = PeerMap.accumArray const pmap
329 -- | Make sure an added item is found.
330 prop_PeerMap_addFind pmap key em =
331 PeerMap.find key (PeerMap.add key em puniq) == em
332 where _types = (pmap::PeerMap.PeerMap,
333 key::PeerMap.Key, em::PeerMap.Elem)
334 puniq = PeerMap.accumArray const pmap
336 -- | Manual check that maxElem returns the maximum indeed, or 0 for null.
337 prop_PeerMap_maxElem pmap =
338 PeerMap.maxElem puniq == if null puniq then 0
339 else (maximum . snd . unzip) puniq
340 where _types = pmap::PeerMap.PeerMap
341 puniq = PeerMap.accumArray const pmap
343 -- | List of tests for the PeerMap module.
345 [ run prop_PeerMap_addIdempotent
346 , run prop_PeerMap_removeIdempotent
347 , run prop_PeerMap_maxElem
348 , run prop_PeerMap_addFind
349 , run prop_PeerMap_findMissing
352 -- ** Container tests
354 prop_Container_addTwo cdata i1 i2 =
355 fn i1 i2 cont == fn i2 i1 cont &&
356 fn i1 i2 cont == fn i1 i2 (fn i1 i2 cont)
357 where _types = (cdata::[Int],
359 cont = foldl (\c x -> Container.add x x c) Container.empty cdata
360 fn x1 x2 = Container.addTwo x1 x1 x2 x2
362 prop_Container_nameOf node =
363 let nl = makeSmallCluster node 1
364 fnode = head (Container.elems nl)
365 in Container.nameOf nl (Node.idx fnode) == Node.name fnode
367 -- | We test that in a cluster, given a random node, we can find it by
368 -- its name and alias, as long as all names and aliases are unique,
369 -- and that we fail to find a non-existing name.
370 prop_Container_findByName node othername =
371 forAll (choose (1, 20)) $ \ cnt ->
372 forAll (choose (0, cnt - 1)) $ \ fidx ->
373 forAll (vector cnt) $ \ names ->
374 (length . nub) (map fst names ++ map snd names) ==
376 not (othername `elem` (map fst names ++ map snd names)) ==>
377 let nl = makeSmallCluster node cnt
378 nodes = Container.elems nl
379 nodes' = map (\((name, alias), nn) -> (Node.idx nn,
380 nn { Node.name = name,
381 Node.alias = alias }))
383 nl' = Container.fromList nodes'
384 target = snd (nodes' !! fidx)
385 in Container.findByName nl' (Node.name target) == Just target &&
386 Container.findByName nl' (Node.alias target) == Just target &&
387 Container.findByName nl' othername == Nothing
390 [ run prop_Container_addTwo
391 , run prop_Container_nameOf
392 , run prop_Container_findByName
397 -- Simple instance tests, we only have setter/getters
399 prop_Instance_creat inst =
400 Instance.name inst == Instance.alias inst
402 prop_Instance_setIdx inst idx =
403 Instance.idx (Instance.setIdx inst idx) == idx
404 where _types = (inst::Instance.Instance, idx::Types.Idx)
406 prop_Instance_setName inst name =
407 Instance.name newinst == name &&
408 Instance.alias newinst == name
409 where _types = (inst::Instance.Instance, name::String)
410 newinst = Instance.setName inst name
412 prop_Instance_setAlias inst name =
413 Instance.name newinst == Instance.name inst &&
414 Instance.alias newinst == name
415 where _types = (inst::Instance.Instance, name::String)
416 newinst = Instance.setAlias inst name
418 prop_Instance_setPri inst pdx =
419 Instance.pNode (Instance.setPri inst pdx) == pdx
420 where _types = (inst::Instance.Instance, pdx::Types.Ndx)
422 prop_Instance_setSec inst sdx =
423 Instance.sNode (Instance.setSec inst sdx) == sdx
424 where _types = (inst::Instance.Instance, sdx::Types.Ndx)
426 prop_Instance_setBoth inst pdx sdx =
427 Instance.pNode si == pdx && Instance.sNode si == sdx
428 where _types = (inst::Instance.Instance, pdx::Types.Ndx, sdx::Types.Ndx)
429 si = Instance.setBoth inst pdx sdx
431 prop_Instance_runStatus_True =
432 forAll (arbitrary `suchThat`
433 ((`elem` Instance.runningStates) . Instance.runSt))
436 prop_Instance_runStatus_False inst =
437 let run_st = Instance.running inst
438 run_tx = Instance.runSt inst
440 run_tx `notElem` Instance.runningStates ==> not run_st
442 prop_Instance_shrinkMG inst =
443 Instance.mem inst >= 2 * Types.unitMem ==>
444 case Instance.shrinkByType inst Types.FailMem of
446 Instance.mem inst' == Instance.mem inst - Types.unitMem
449 prop_Instance_shrinkMF inst =
450 forAll (choose (0, 2 * Types.unitMem - 1)) $ \mem ->
451 let inst' = inst { Instance.mem = mem}
452 in Types.isBad $ Instance.shrinkByType inst' Types.FailMem
454 prop_Instance_shrinkCG inst =
455 Instance.vcpus inst >= 2 * Types.unitCpu ==>
456 case Instance.shrinkByType inst Types.FailCPU of
458 Instance.vcpus inst' == Instance.vcpus inst - Types.unitCpu
461 prop_Instance_shrinkCF inst =
462 forAll (choose (0, 2 * Types.unitCpu - 1)) $ \vcpus ->
463 let inst' = inst { Instance.vcpus = vcpus }
464 in Types.isBad $ Instance.shrinkByType inst' Types.FailCPU
466 prop_Instance_shrinkDG inst =
467 Instance.dsk inst >= 2 * Types.unitDsk ==>
468 case Instance.shrinkByType inst Types.FailDisk of
470 Instance.dsk inst' == Instance.dsk inst - Types.unitDsk
473 prop_Instance_shrinkDF inst =
474 forAll (choose (0, 2 * Types.unitDsk - 1)) $ \dsk ->
475 let inst' = inst { Instance.dsk = dsk }
476 in Types.isBad $ Instance.shrinkByType inst' Types.FailDisk
478 prop_Instance_setMovable inst m =
479 Instance.movable inst' == m
480 where inst' = Instance.setMovable inst m
483 [ run prop_Instance_creat
484 , run prop_Instance_setIdx
485 , run prop_Instance_setName
486 , run prop_Instance_setAlias
487 , run prop_Instance_setPri
488 , run prop_Instance_setSec
489 , run prop_Instance_setBoth
490 , run prop_Instance_runStatus_True
491 , run prop_Instance_runStatus_False
492 , run prop_Instance_shrinkMG
493 , run prop_Instance_shrinkMF
494 , run prop_Instance_shrinkCG
495 , run prop_Instance_shrinkCF
496 , run prop_Instance_shrinkDG
497 , run prop_Instance_shrinkDF
498 , run prop_Instance_setMovable
501 -- ** Text backend tests
503 -- Instance text loader tests
505 prop_Text_Load_Instance name mem dsk vcpus status
506 (NonEmpty pnode) snode
507 (NonNegative pdx) (NonNegative sdx) autobal =
508 pnode /= snode && pdx /= sdx ==>
509 let vcpus_s = show vcpus
514 else [(pnode, pdx), (snode, sdx)]
515 nl = Data.Map.fromList ndx
517 sbal = if autobal then "Y" else "N"
518 inst = Text.loadInst nl
519 [name, mem_s, dsk_s, vcpus_s, status,
520 sbal, pnode, snode, tags]:: Maybe (String, Instance.Instance)
521 fail1 = Text.loadInst nl
522 [name, mem_s, dsk_s, vcpus_s, status,
523 sbal, pnode, pnode, tags]:: Maybe (String, Instance.Instance)
524 _types = ( name::String, mem::Int, dsk::Int
525 , vcpus::Int, status::String
532 Instance.name i == name &&
533 Instance.vcpus i == vcpus &&
534 Instance.mem i == mem &&
535 Instance.pNode i == pdx &&
536 Instance.sNode i == (if null snode
537 then Node.noSecondary
539 Instance.autoBalance i == autobal &&
542 prop_Text_Load_InstanceFail ktn fields =
543 length fields /= 9 ==>
544 case Text.loadInst nl fields of
546 Types.Bad msg -> "Invalid/incomplete instance data: '" `isPrefixOf` msg
547 where nl = Data.Map.fromList ktn
549 prop_Text_Load_Node name tm nm fm td fd tc fo =
550 let conv v = if v < 0
562 any_broken = any (< 0) [tm, nm, fm, td, fd, tc]
563 gid = Group.uuid defGroup
564 in case Text.loadNode defGroupAssoc
565 [name, tm_s, nm_s, fm_s, td_s, fd_s, tc_s, fo_s, gid] of
567 Just (name', node) ->
569 then Node.offline node
570 else Node.name node == name' && name' == name &&
571 Node.alias node == name &&
572 Node.tMem node == fromIntegral tm &&
573 Node.nMem node == nm &&
574 Node.fMem node == fm &&
575 Node.tDsk node == fromIntegral td &&
576 Node.fDsk node == fd &&
577 Node.tCpu node == fromIntegral tc
579 prop_Text_Load_NodeFail fields =
580 length fields /= 8 ==> isNothing $ Text.loadNode Data.Map.empty fields
582 prop_Text_NodeLSIdempotent node =
583 (Text.loadNode defGroupAssoc.
584 Utils.sepSplit '|' . Text.serializeNode defGroupList) n ==
585 Just (Node.name n, n)
586 -- override failN1 to what loadNode returns by default
587 where n = node { Node.failN1 = True, Node.offline = False }
590 [ run prop_Text_Load_Instance
591 , run prop_Text_Load_InstanceFail
592 , run prop_Text_Load_Node
593 , run prop_Text_Load_NodeFail
594 , run prop_Text_NodeLSIdempotent
599 prop_Node_setAlias node name =
600 Node.name newnode == Node.name node &&
601 Node.alias newnode == name
602 where _types = (node::Node.Node, name::String)
603 newnode = Node.setAlias node name
605 prop_Node_setOffline node status =
606 Node.offline newnode == status
607 where newnode = Node.setOffline node status
609 prop_Node_setXmem node xm =
610 Node.xMem newnode == xm
611 where newnode = Node.setXmem node xm
613 prop_Node_setMcpu node mc =
614 Node.mCpu newnode == mc
615 where newnode = Node.setMcpu node mc
617 -- | Check that an instance add with too high memory or disk will be
619 prop_Node_addPriFM node inst = Instance.mem inst >= Node.fMem node &&
620 not (Node.failN1 node)
622 case Node.addPri node inst'' of
623 Types.OpFail Types.FailMem -> True
625 where _types = (node::Node.Node, inst::Instance.Instance)
626 inst' = setInstanceSmallerThanNode node inst
627 inst'' = inst' { Instance.mem = Instance.mem inst }
629 prop_Node_addPriFD node inst = Instance.dsk inst >= Node.fDsk node &&
630 not (Node.failN1 node)
632 case Node.addPri node inst'' of
633 Types.OpFail Types.FailDisk -> True
635 where _types = (node::Node.Node, inst::Instance.Instance)
636 inst' = setInstanceSmallerThanNode node inst
637 inst'' = inst' { Instance.dsk = Instance.dsk inst }
639 prop_Node_addPriFC node inst (Positive extra) =
640 not (Node.failN1 node) ==>
641 case Node.addPri node inst'' of
642 Types.OpFail Types.FailCPU -> True
644 where _types = (node::Node.Node, inst::Instance.Instance)
645 inst' = setInstanceSmallerThanNode node inst
646 inst'' = inst' { Instance.vcpus = Node.availCpu node + extra }
648 -- | Check that an instance add with too high memory or disk will be
650 prop_Node_addSec node inst pdx =
651 (Instance.mem inst >= (Node.fMem node - Node.rMem node) ||
652 Instance.dsk inst >= Node.fDsk node) &&
653 not (Node.failN1 node)
654 ==> isFailure (Node.addSec node inst pdx)
655 where _types = (node::Node.Node, inst::Instance.Instance, pdx::Int)
657 -- | Checks for memory reservation changes.
658 prop_Node_rMem inst =
659 forAll (arbitrary `suchThat` ((> 0) . Node.fMem)) $ \node ->
660 -- ab = auto_balance, nb = non-auto_balance
661 -- we use -1 as the primary node of the instance
662 let inst' = inst { Instance.pNode = -1, Instance.autoBalance = True }
663 inst_ab = setInstanceSmallerThanNode node inst'
664 inst_nb = inst_ab { Instance.autoBalance = False }
665 -- now we have the two instances, identical except the
666 -- autoBalance attribute
667 orig_rmem = Node.rMem node
668 inst_idx = Instance.idx inst_ab
669 node_add_ab = Node.addSec node inst_ab (-1)
670 node_add_nb = Node.addSec node inst_nb (-1)
671 node_del_ab = liftM (flip Node.removeSec inst_ab) node_add_ab
672 node_del_nb = liftM (flip Node.removeSec inst_nb) node_add_nb
673 in case (node_add_ab, node_add_nb, node_del_ab, node_del_nb) of
674 (Types.OpGood a_ab, Types.OpGood a_nb,
675 Types.OpGood d_ab, Types.OpGood d_nb) ->
676 printTestCase "Consistency checks failed" $
677 Node.rMem a_ab > orig_rmem &&
678 Node.rMem a_ab - orig_rmem == Instance.mem inst_ab &&
679 Node.rMem a_nb == orig_rmem &&
680 Node.rMem d_ab == orig_rmem &&
681 Node.rMem d_nb == orig_rmem &&
682 -- this is not related to rMem, but as good a place to
684 inst_idx `elem` Node.sList a_ab &&
685 not (inst_idx `elem` Node.sList d_ab)
686 x -> printTestCase ("Failed to add/remove instances: " ++ show x)
689 -- | Check mdsk setting.
690 prop_Node_setMdsk node mx =
691 Node.loDsk node' >= 0 &&
692 fromIntegral (Node.loDsk node') <= Node.tDsk node &&
693 Node.availDisk node' >= 0 &&
694 Node.availDisk node' <= Node.fDsk node' &&
695 fromIntegral (Node.availDisk node') <= Node.tDsk node' &&
696 Node.mDsk node' == mx'
697 where _types = (node::Node.Node, mx::SmallRatio)
698 node' = Node.setMdsk node mx'
702 prop_Node_tagMaps_idempotent tags =
703 Node.delTags (Node.addTags m tags) tags == m
704 where m = Data.Map.empty
706 prop_Node_tagMaps_reject tags =
708 any (\t -> Node.rejectAddTags m [t]) tags
709 where m = Node.addTags Data.Map.empty tags
711 prop_Node_showField node =
712 forAll (elements Node.defaultFields) $ \ field ->
713 fst (Node.showHeader field) /= Types.unknownField &&
714 Node.showField node field /= Types.unknownField
717 prop_Node_computeGroups nodes =
718 let ng = Node.computeGroups nodes
719 onlyuuid = map fst ng
720 in length nodes == sum (map (length . snd) ng) &&
721 all (\(guuid, ns) -> all ((== guuid) . Node.group) ns) ng &&
722 length (nub onlyuuid) == length onlyuuid &&
723 (null nodes || not (null ng))
726 [ run prop_Node_setAlias
727 , run prop_Node_setOffline
728 , run prop_Node_setMcpu
729 , run prop_Node_setXmem
730 , run prop_Node_addPriFM
731 , run prop_Node_addPriFD
732 , run prop_Node_addPriFC
733 , run prop_Node_addSec
735 , run prop_Node_setMdsk
736 , run prop_Node_tagMaps_idempotent
737 , run prop_Node_tagMaps_reject
738 , run prop_Node_showField
739 , run prop_Node_computeGroups
745 -- | Check that the cluster score is close to zero for a homogeneous
747 prop_Score_Zero node =
748 forAll (choose (1, 1024)) $ \count ->
749 (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
750 (Node.tDsk node > 0) && (Node.tMem node > 0)) ==>
751 let fn = Node.buildPeers node Container.empty
752 nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
753 nl = Container.fromList nlst
754 score = Cluster.compCV nl
755 -- we can't say == 0 here as the floating point errors accumulate;
756 -- this should be much lower than the default score in CLI.hs
759 -- | Check that cluster stats are sane.
760 prop_CStats_sane node =
761 forAll (choose (1, 1024)) $ \count ->
762 (not (Node.offline node) && not (Node.failN1 node) &&
763 (Node.availDisk node > 0) && (Node.availMem node > 0)) ==>
764 let fn = Node.buildPeers node Container.empty
765 nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
766 nl = Container.fromList nlst
767 cstats = Cluster.totalResources nl
768 in Cluster.csAdsk cstats >= 0 &&
769 Cluster.csAdsk cstats <= Cluster.csFdsk cstats
771 -- | Check that one instance is allocated correctly, without
772 -- rebalances needed.
773 prop_ClusterAlloc_sane node inst =
774 forAll (choose (5, 20)) $ \count ->
775 not (Node.offline node)
776 && not (Node.failN1 node)
777 && Node.availDisk node > 0
778 && Node.availMem node > 0
780 let nl = makeSmallCluster node count
782 inst' = setInstanceSmallerThanNode node inst
783 in case Cluster.genAllocNodes defGroupList nl 2 True >>=
784 Cluster.tryAlloc nl il inst' of
787 case Cluster.asSolutions as of
789 (xnl, xi, _, cv):[] ->
790 let il' = Container.add (Instance.idx xi) xi il
791 tbl = Cluster.Table xnl il' cv []
792 in not (canBalance tbl True True False)
795 -- | Checks that on a 2-5 node cluster, we can allocate a random
796 -- instance spec via tiered allocation (whatever the original instance
797 -- spec), on either one or two nodes.
798 prop_ClusterCanTieredAlloc node inst =
799 forAll (choose (2, 5)) $ \count ->
800 forAll (choose (1, 2)) $ \rqnodes ->
801 not (Node.offline node)
802 && not (Node.failN1 node)
805 let nl = makeSmallCluster node count
807 allocnodes = Cluster.genAllocNodes defGroupList nl rqnodes True
808 in case allocnodes >>= \allocnodes' ->
809 Cluster.tieredAlloc nl il inst allocnodes' [] [] of
811 Types.Ok (_, _, il', ixes, cstats) -> not (null ixes) &&
812 IntMap.size il' == length ixes &&
813 length ixes == length cstats
815 -- | Checks that on a 4-8 node cluster, once we allocate an instance,
816 -- we can also evacuate it.
817 prop_ClusterAllocEvac node inst =
818 forAll (choose (4, 8)) $ \count ->
819 not (Node.offline node)
820 && not (Node.failN1 node)
823 let nl = makeSmallCluster node count
825 inst' = setInstanceSmallerThanNode node inst
826 in case Cluster.genAllocNodes defGroupList nl 2 True >>=
827 Cluster.tryAlloc nl il inst' of
830 case Cluster.asSolutions as of
832 (xnl, xi, _, _):[] ->
833 let sdx = Instance.sNode xi
834 il' = Container.add (Instance.idx xi) xi il
835 in case Cluster.tryEvac xnl il' [Instance.idx xi] [sdx] of
840 -- | Check that allocating multiple instances on a cluster, then
841 -- adding an empty node, results in a valid rebalance.
842 prop_ClusterAllocBalance =
843 forAll (genNode (Just 5) (Just 128)) $ \node ->
844 forAll (choose (3, 5)) $ \count ->
845 not (Node.offline node) && not (Node.failN1 node) ==>
846 let nl = makeSmallCluster node count
847 (hnode, nl') = IntMap.deleteFindMax nl
849 allocnodes = Cluster.genAllocNodes defGroupList nl' 2 True
850 i_templ = createInstance Types.unitMem Types.unitDsk Types.unitCpu
851 in case allocnodes >>= \allocnodes' ->
852 Cluster.iterateAlloc nl' il i_templ allocnodes' [] [] of
854 Types.Ok (_, xnl, il', _, _) ->
855 let ynl = Container.add (Node.idx hnode) hnode xnl
856 cv = Cluster.compCV ynl
857 tbl = Cluster.Table ynl il' cv []
858 in canBalance tbl True True False
860 -- | Checks consistency.
861 prop_ClusterCheckConsistency node inst =
862 let nl = makeSmallCluster node 3
863 [node1, node2, node3] = Container.elems nl
864 node3' = node3 { Node.group = 1 }
865 nl' = Container.add (Node.idx node3') node3' nl
866 inst1 = Instance.setBoth inst (Node.idx node1) (Node.idx node2)
867 inst2 = Instance.setBoth inst (Node.idx node1) Node.noSecondary
868 inst3 = Instance.setBoth inst (Node.idx node1) (Node.idx node3)
869 ccheck = Cluster.findSplitInstances nl' . Container.fromList
870 in null (ccheck [(0, inst1)]) &&
871 null (ccheck [(0, inst2)]) &&
872 (not . null $ ccheck [(0, inst3)])
874 -- | For now, we only test that we don't lose instances during the split.
875 prop_ClusterSplitCluster node inst =
876 forAll (choose (0, 100)) $ \icnt ->
877 let nl = makeSmallCluster node 2
878 (nl', il') = foldl (\(ns, is) _ -> assignInstance ns is inst 0 1)
879 (nl, Container.empty) [1..icnt]
880 gni = Cluster.splitCluster nl' il'
881 in sum (map (Container.size . snd . snd) gni) == icnt &&
882 all (\(guuid, (nl'', _)) -> all ((== guuid) . Node.group)
883 (Container.elems nl'')) gni
886 [ run prop_Score_Zero
887 , run prop_CStats_sane
888 , run prop_ClusterAlloc_sane
889 , run prop_ClusterCanTieredAlloc
890 , run prop_ClusterAllocEvac
891 , run prop_ClusterAllocBalance
892 , run prop_ClusterCheckConsistency
893 , run prop_ClusterSplitCluster
898 -- | Check that opcode serialization is idempotent.
899 prop_OpCodes_serialization op =
900 case J.readJSON (J.showJSON op) of
902 J.Ok op' -> op == op'
903 where _types = op::OpCodes.OpCode
906 [ run prop_OpCodes_serialization
911 -- | Check that (queued) job\/opcode status serialization is idempotent.
912 prop_OpStatus_serialization os =
913 case J.readJSON (J.showJSON os) of
915 J.Ok os' -> os == os'
916 where _types = os::Jobs.OpStatus
918 prop_JobStatus_serialization js =
919 case J.readJSON (J.showJSON js) of
921 J.Ok js' -> js == js'
922 where _types = js::Jobs.JobStatus
925 [ run prop_OpStatus_serialization
926 , run prop_JobStatus_serialization
931 prop_Loader_lookupNode ktn inst node =
932 Loader.lookupNode nl inst node == Data.Map.lookup node nl
933 where nl = Data.Map.fromList ktn
935 prop_Loader_lookupInstance kti inst =
936 Loader.lookupInstance il inst == Data.Map.lookup inst il
937 where il = Data.Map.fromList kti
939 prop_Loader_assignIndices nodes =
940 Data.Map.size nassoc == length nodes &&
941 Container.size kt == length nodes &&
943 then maximum (IntMap.keys kt) == length nodes - 1
945 where (nassoc, kt) = Loader.assignIndices (map (\n -> (Node.name n, n)) nodes)
947 -- | Checks that the number of primary instances recorded on the nodes
949 prop_Loader_mergeData ns =
950 let na = Container.fromList $ map (\n -> (Node.idx n, n)) ns
951 in case Loader.mergeData [] [] [] []
952 (Loader.emptyCluster {Loader.cdNodes = na}) of
954 Types.Ok (Loader.ClusterData _ nl il _) ->
955 let nodes = Container.elems nl
956 instances = Container.elems il
957 in (sum . map (length . Node.pList)) nodes == 0 &&
961 [ run prop_Loader_lookupNode
962 , run prop_Loader_lookupInstance
963 , run prop_Loader_assignIndices
964 , run prop_Loader_mergeData
969 prop_AllocPolicy_serialisation apol =
970 case Types.apolFromString (Types.apolToString apol) of
971 Types.Ok p -> printTestCase ("invalid deserialisation " ++ show p) $
973 Types.Bad s -> printTestCase ("failed to deserialise: " ++ s) False
975 prop_DiskTemplate_serialisation dt =
976 case Types.dtFromString (Types.dtToString dt) of
977 Types.Ok p -> printTestCase ("invalid deserialisation " ++ show p) $
979 Types.Bad s -> printTestCase ("failed to deserialise: " ++ s) False
982 [ run prop_AllocPolicy_serialisation
983 , run prop_DiskTemplate_serialisation