1 {-# LANGUAGE TemplateHaskell #-}
3 {-| Unittests for ganeti-htools.
9 Copyright (C) 2009, 2010, 2011 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 defGroup :: Group.Group
94 defGroup = flip Group.setIdx 0 $
95 Group.create "default" Types.defaultGroupID Types.AllocPreferred
97 defGroupList :: Group.List
98 defGroupList = Container.fromList [(Group.idx defGroup, defGroup)]
100 defGroupAssoc :: Data.Map.Map String Types.Gdx
101 defGroupAssoc = Data.Map.singleton (Group.uuid defGroup) (Group.idx defGroup)
103 -- * Helper functions
105 -- | Simple checker for whether OpResult is fail or pass.
106 isFailure :: Types.OpResult a -> Bool
107 isFailure (Types.OpFail _) = True
110 -- | Checks for equality with proper annotation.
111 (==?) :: (Show a, Eq a) => a -> a -> Property
112 (==?) x y = printTestCase
113 ("Expected equality, but '" ++
114 show x ++ "' /= '" ++ show y ++ "'") (x == y)
117 -- | Update an instance to be smaller than a node.
118 setInstanceSmallerThanNode node inst =
119 inst { Instance.mem = Node.availMem node `div` 2
120 , Instance.dsk = Node.availDisk node `div` 2
121 , Instance.vcpus = Node.availCpu node `div` 2
124 -- | Create an instance given its spec.
125 createInstance mem dsk vcpus =
126 Instance.create "inst-unnamed" mem dsk vcpus Types.Running [] True (-1) (-1)
129 -- | Create a small cluster by repeating a node spec.
130 makeSmallCluster :: Node.Node -> Int -> Node.List
131 makeSmallCluster node count =
132 let fn = Node.buildPeers node Container.empty
133 namelst = map (\n -> (Node.name n, n)) (replicate count fn)
134 (_, nlst) = Loader.assignIndices namelst
137 -- | Make a small cluster, both nodes and instances.
138 makeSmallEmptyCluster :: Node.Node -> Int -> Instance.Instance
139 -> (Node.List, Instance.List, Instance.Instance)
140 makeSmallEmptyCluster node count inst =
141 (makeSmallCluster node count, Container.empty,
142 setInstanceSmallerThanNode node inst)
144 -- | Checks if a node is "big" enough.
145 isNodeBig :: Node.Node -> Int -> Bool
146 isNodeBig node size = Node.availDisk node > size * Types.unitDsk
147 && Node.availMem node > size * Types.unitMem
148 && Node.availCpu node > size * Types.unitCpu
150 canBalance :: Cluster.Table -> Bool -> Bool -> Bool -> Bool
151 canBalance tbl dm im evac = isJust $ Cluster.tryBalance tbl dm im evac 0 0
153 -- | Assigns a new fresh instance to a cluster; this is not
154 -- allocation, so no resource checks are done.
155 assignInstance :: Node.List -> Instance.List -> Instance.Instance ->
156 Types.Idx -> Types.Idx ->
157 (Node.List, Instance.List)
158 assignInstance nl il inst pdx sdx =
159 let pnode = Container.find pdx nl
160 snode = Container.find sdx nl
161 maxiidx = if Container.null il
163 else fst (Container.findMax il) + 1
164 inst' = inst { Instance.idx = maxiidx,
165 Instance.pNode = pdx, Instance.sNode = sdx }
166 pnode' = Node.setPri pnode inst'
167 snode' = Node.setSec snode inst'
168 nl' = Container.addTwo pdx pnode' sdx snode' nl
169 il' = Container.add maxiidx inst' il
172 -- * Arbitrary instances
174 -- | Defines a DNS name.
175 newtype DNSChar = DNSChar { dnsGetChar::Char }
177 instance Arbitrary DNSChar where
179 x <- elements (['a'..'z'] ++ ['0'..'9'] ++ "_-")
182 getName :: Gen String
185 dn <- vector n::Gen [DNSChar]
186 return (map dnsGetChar dn)
188 getFQDN :: Gen String
191 ncomps <- choose (1, 4)
192 frest <- vector ncomps::Gen [[DNSChar]]
193 let frest' = map (map dnsGetChar) frest
194 return (felem ++ "." ++ intercalate "." frest')
196 instance Arbitrary Types.InstanceStatus where
197 arbitrary = elements [minBound..maxBound]
199 -- let's generate a random instance
200 instance Arbitrary Instance.Instance where
203 mem <- choose (0, maxMem)
204 dsk <- choose (0, maxDsk)
208 vcpus <- choose (0, maxCpu)
209 return $ Instance.create name mem dsk vcpus run_st [] True pn sn
212 -- | Generas an arbitrary node based on sizing information.
213 genNode :: Maybe Int -- ^ Minimum node size in terms of units
214 -> Maybe Int -- ^ Maximum node size (when Nothing, bounded
215 -- just by the max... constants)
217 genNode min_multiplier max_multiplier = do
218 let (base_mem, base_dsk, base_cpu) =
219 case min_multiplier of
220 Just mm -> (mm * Types.unitMem,
224 (top_mem, top_dsk, top_cpu) =
225 case max_multiplier of
226 Just mm -> (mm * Types.unitMem,
229 Nothing -> (maxMem, maxDsk, maxCpu)
231 mem_t <- choose (base_mem, top_mem)
232 mem_f <- choose (base_mem, mem_t)
233 mem_n <- choose (0, mem_t - mem_f)
234 dsk_t <- choose (base_dsk, top_dsk)
235 dsk_f <- choose (base_dsk, dsk_t)
236 cpu_t <- choose (base_cpu, top_cpu)
238 let n = Node.create name (fromIntegral mem_t) mem_n mem_f
239 (fromIntegral dsk_t) dsk_f (fromIntegral cpu_t) offl 0
240 return $ Node.buildPeers n Container.empty
243 instance Arbitrary Node.Node where
244 arbitrary = genNode Nothing Nothing
247 instance Arbitrary OpCodes.ReplaceDisksMode where
248 arbitrary = elements [minBound..maxBound]
250 instance Arbitrary OpCodes.OpCode where
252 op_id <- elements [ "OP_TEST_DELAY"
253 , "OP_INSTANCE_REPLACE_DISKS"
254 , "OP_INSTANCE_FAILOVER"
255 , "OP_INSTANCE_MIGRATE"
259 liftM3 OpCodes.OpTestDelay arbitrary arbitrary arbitrary
260 "OP_INSTANCE_REPLACE_DISKS" ->
261 liftM5 OpCodes.OpInstanceReplaceDisks arbitrary arbitrary
262 arbitrary arbitrary arbitrary
263 "OP_INSTANCE_FAILOVER" ->
264 liftM3 OpCodes.OpInstanceFailover arbitrary arbitrary
266 "OP_INSTANCE_MIGRATE" ->
267 liftM5 OpCodes.OpInstanceMigrate arbitrary arbitrary
268 arbitrary arbitrary arbitrary
269 _ -> fail "Wrong opcode"
271 instance Arbitrary Jobs.OpStatus where
272 arbitrary = elements [minBound..maxBound]
274 instance Arbitrary Jobs.JobStatus where
275 arbitrary = elements [minBound..maxBound]
277 newtype SmallRatio = SmallRatio Double deriving Show
278 instance Arbitrary SmallRatio where
281 return $ SmallRatio v
283 instance Arbitrary Types.AllocPolicy where
284 arbitrary = elements [minBound..maxBound]
286 instance Arbitrary Types.DiskTemplate where
287 arbitrary = elements [minBound..maxBound]
289 instance Arbitrary Types.FailMode where
290 arbitrary = elements [minBound..maxBound]
292 instance Arbitrary a => Arbitrary (Types.OpResult a) where
293 arbitrary = arbitrary >>= \c ->
295 then liftM Types.OpGood arbitrary
296 else liftM Types.OpFail arbitrary
302 -- | If the list is not just an empty element, and if the elements do
303 -- not contain commas, then join+split should be idempotent.
304 prop_Utils_commaJoinSplit =
305 forAll (arbitrary `suchThat`
306 (\l -> l /= [""] && all (notElem ',') l )) $ \lst ->
307 Utils.sepSplit ',' (Utils.commaJoin lst) ==? lst
309 -- | Split and join should always be idempotent.
310 prop_Utils_commaSplitJoin s =
311 Utils.commaJoin (Utils.sepSplit ',' s) ==? s
313 -- | fromObjWithDefault, we test using the Maybe monad and an integer
315 prop_Utils_fromObjWithDefault def_value random_key =
316 -- a missing key will be returned with the default
317 JSON.fromObjWithDefault [] random_key def_value == Just def_value &&
318 -- a found key will be returned as is, not with default
319 JSON.fromObjWithDefault [(random_key, J.showJSON def_value)]
320 random_key (def_value+1) == Just def_value
321 where _types = def_value :: Integer
323 -- | Test that functional if' behaves like the syntactic sugar if.
324 prop_Utils_if'if :: Bool -> Int -> Int -> Gen Prop
325 prop_Utils_if'if cnd a b =
326 Utils.if' cnd a b ==? if cnd then a else b
328 -- | Test basic select functionality
329 prop_Utils_select :: Int -- ^ Default result
330 -> [Int] -- ^ List of False values
331 -> [Int] -- ^ List of True values
332 -> Gen Prop -- ^ Test result
333 prop_Utils_select def lst1 lst2 =
334 Utils.select def (flist ++ tlist) ==? expectedresult
335 where expectedresult = Utils.if' (null lst2) def (head lst2)
336 flist = zip (repeat False) lst1
337 tlist = zip (repeat True) lst2
339 -- | Test basic select functionality with undefined default
340 prop_Utils_select_undefd :: [Int] -- ^ List of False values
341 -> NonEmptyList Int -- ^ List of True values
342 -> Gen Prop -- ^ Test result
343 prop_Utils_select_undefd lst1 (NonEmpty lst2) =
344 Utils.select undefined (flist ++ tlist) ==? head lst2
345 where flist = zip (repeat False) lst1
346 tlist = zip (repeat True) lst2
348 -- | Test basic select functionality with undefined list values
349 prop_Utils_select_undefv :: [Int] -- ^ List of False values
350 -> NonEmptyList Int -- ^ List of True values
351 -> Gen Prop -- ^ Test result
352 prop_Utils_select_undefv lst1 (NonEmpty lst2) =
353 Utils.select undefined cndlist ==? head lst2
354 where flist = zip (repeat False) lst1
355 tlist = zip (repeat True) lst2
356 cndlist = flist ++ tlist ++ [undefined]
358 prop_Utils_parseUnit (NonNegative n) =
359 Utils.parseUnit (show n) == Types.Ok n &&
360 Utils.parseUnit (show n ++ "m") == Types.Ok n &&
361 (case Utils.parseUnit (show n ++ "M") of
362 Types.Ok m -> if n > 0
363 then m < n -- for positive values, X MB is < than X MiB
364 else m == 0 -- but for 0, 0 MB == 0 MiB
365 Types.Bad _ -> False) &&
366 Utils.parseUnit (show n ++ "g") == Types.Ok (n*1024) &&
367 Utils.parseUnit (show n ++ "t") == Types.Ok (n*1048576) &&
368 Types.isBad (Utils.parseUnit (show n ++ "x")::Types.Result Int)
369 where _types = n::Int
371 -- | Test list for the Utils module.
373 [ 'prop_Utils_commaJoinSplit
374 , 'prop_Utils_commaSplitJoin
375 , 'prop_Utils_fromObjWithDefault
378 , 'prop_Utils_select_undefd
379 , 'prop_Utils_select_undefv
380 , 'prop_Utils_parseUnit
385 -- | Make sure add is idempotent.
386 prop_PeerMap_addIdempotent pmap key em =
387 fn puniq ==? fn (fn puniq)
388 where _types = (pmap::PeerMap.PeerMap,
389 key::PeerMap.Key, em::PeerMap.Elem)
390 fn = PeerMap.add key em
391 puniq = PeerMap.accumArray const pmap
393 -- | Make sure remove is idempotent.
394 prop_PeerMap_removeIdempotent pmap key =
395 fn puniq ==? fn (fn puniq)
396 where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
397 fn = PeerMap.remove key
398 puniq = PeerMap.accumArray const pmap
400 -- | Make sure a missing item returns 0.
401 prop_PeerMap_findMissing pmap key =
402 PeerMap.find key (PeerMap.remove key puniq) ==? 0
403 where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
404 puniq = PeerMap.accumArray const pmap
406 -- | Make sure an added item is found.
407 prop_PeerMap_addFind pmap key em =
408 PeerMap.find key (PeerMap.add key em puniq) ==? em
409 where _types = (pmap::PeerMap.PeerMap,
410 key::PeerMap.Key, em::PeerMap.Elem)
411 puniq = PeerMap.accumArray const pmap
413 -- | Manual check that maxElem returns the maximum indeed, or 0 for null.
414 prop_PeerMap_maxElem pmap =
415 PeerMap.maxElem puniq ==? if null puniq then 0
416 else (maximum . snd . unzip) puniq
417 where _types = pmap::PeerMap.PeerMap
418 puniq = PeerMap.accumArray const pmap
420 -- | List of tests for the PeerMap module.
422 [ 'prop_PeerMap_addIdempotent
423 , 'prop_PeerMap_removeIdempotent
424 , 'prop_PeerMap_maxElem
425 , 'prop_PeerMap_addFind
426 , 'prop_PeerMap_findMissing
429 -- ** Container tests
431 -- we silence the following due to hlint bug fixed in later versions
432 {-# ANN prop_Container_addTwo "HLint: ignore Avoid lambda" #-}
433 prop_Container_addTwo cdata i1 i2 =
434 fn i1 i2 cont == fn i2 i1 cont &&
435 fn i1 i2 cont == fn i1 i2 (fn i1 i2 cont)
436 where _types = (cdata::[Int],
438 cont = foldl (\c x -> Container.add x x c) Container.empty cdata
439 fn x1 x2 = Container.addTwo x1 x1 x2 x2
441 prop_Container_nameOf node =
442 let nl = makeSmallCluster node 1
443 fnode = head (Container.elems nl)
444 in Container.nameOf nl (Node.idx fnode) ==? Node.name fnode
446 -- | We test that in a cluster, given a random node, we can find it by
447 -- its name and alias, as long as all names and aliases are unique,
448 -- and that we fail to find a non-existing name.
449 prop_Container_findByName node othername =
450 forAll (choose (1, 20)) $ \ cnt ->
451 forAll (choose (0, cnt - 1)) $ \ fidx ->
452 forAll (vector cnt) $ \ names ->
453 (length . nub) (map fst names ++ map snd names) ==
455 othername `notElem` (map fst names ++ map snd names) ==>
456 let nl = makeSmallCluster node cnt
457 nodes = Container.elems nl
458 nodes' = map (\((name, alias), nn) -> (Node.idx nn,
459 nn { Node.name = name,
460 Node.alias = alias }))
462 nl' = Container.fromList nodes'
463 target = snd (nodes' !! fidx)
464 in Container.findByName nl' (Node.name target) == Just target &&
465 Container.findByName nl' (Node.alias target) == Just target &&
466 isNothing (Container.findByName nl' othername)
468 testSuite "Container"
469 [ 'prop_Container_addTwo
470 , 'prop_Container_nameOf
471 , 'prop_Container_findByName
476 -- Simple instance tests, we only have setter/getters
478 prop_Instance_creat inst =
479 Instance.name inst ==? Instance.alias inst
481 prop_Instance_setIdx inst idx =
482 Instance.idx (Instance.setIdx inst idx) ==? idx
483 where _types = (inst::Instance.Instance, idx::Types.Idx)
485 prop_Instance_setName inst name =
486 Instance.name newinst == name &&
487 Instance.alias newinst == name
488 where _types = (inst::Instance.Instance, name::String)
489 newinst = Instance.setName inst name
491 prop_Instance_setAlias inst name =
492 Instance.name newinst == Instance.name inst &&
493 Instance.alias newinst == name
494 where _types = (inst::Instance.Instance, name::String)
495 newinst = Instance.setAlias inst name
497 prop_Instance_setPri inst pdx =
498 Instance.pNode (Instance.setPri inst pdx) ==? pdx
499 where _types = (inst::Instance.Instance, pdx::Types.Ndx)
501 prop_Instance_setSec inst sdx =
502 Instance.sNode (Instance.setSec inst sdx) ==? sdx
503 where _types = (inst::Instance.Instance, sdx::Types.Ndx)
505 prop_Instance_setBoth inst pdx sdx =
506 Instance.pNode si == pdx && Instance.sNode si == sdx
507 where _types = (inst::Instance.Instance, pdx::Types.Ndx, sdx::Types.Ndx)
508 si = Instance.setBoth inst pdx sdx
510 prop_Instance_shrinkMG inst =
511 Instance.mem inst >= 2 * Types.unitMem ==>
512 case Instance.shrinkByType inst Types.FailMem of
513 Types.Ok inst' -> Instance.mem inst' == Instance.mem inst - Types.unitMem
516 prop_Instance_shrinkMF inst =
517 forAll (choose (0, 2 * Types.unitMem - 1)) $ \mem ->
518 let inst' = inst { Instance.mem = mem}
519 in Types.isBad $ Instance.shrinkByType inst' Types.FailMem
521 prop_Instance_shrinkCG inst =
522 Instance.vcpus inst >= 2 * Types.unitCpu ==>
523 case Instance.shrinkByType inst Types.FailCPU of
525 Instance.vcpus inst' == Instance.vcpus inst - Types.unitCpu
528 prop_Instance_shrinkCF inst =
529 forAll (choose (0, 2 * Types.unitCpu - 1)) $ \vcpus ->
530 let inst' = inst { Instance.vcpus = vcpus }
531 in Types.isBad $ Instance.shrinkByType inst' Types.FailCPU
533 prop_Instance_shrinkDG inst =
534 Instance.dsk inst >= 2 * Types.unitDsk ==>
535 case Instance.shrinkByType inst Types.FailDisk of
537 Instance.dsk inst' == Instance.dsk inst - Types.unitDsk
540 prop_Instance_shrinkDF inst =
541 forAll (choose (0, 2 * Types.unitDsk - 1)) $ \dsk ->
542 let inst' = inst { Instance.dsk = dsk }
543 in Types.isBad $ Instance.shrinkByType inst' Types.FailDisk
545 prop_Instance_setMovable inst m =
546 Instance.movable inst' ==? m
547 where inst' = Instance.setMovable inst m
550 [ 'prop_Instance_creat
551 , 'prop_Instance_setIdx
552 , 'prop_Instance_setName
553 , 'prop_Instance_setAlias
554 , 'prop_Instance_setPri
555 , 'prop_Instance_setSec
556 , 'prop_Instance_setBoth
557 , 'prop_Instance_shrinkMG
558 , 'prop_Instance_shrinkMF
559 , 'prop_Instance_shrinkCG
560 , 'prop_Instance_shrinkCF
561 , 'prop_Instance_shrinkDG
562 , 'prop_Instance_shrinkDF
563 , 'prop_Instance_setMovable
566 -- ** Text backend tests
568 -- Instance text loader tests
570 prop_Text_Load_Instance name mem dsk vcpus status
571 (NonEmpty pnode) snode
572 (NonNegative pdx) (NonNegative sdx) autobal dt =
573 pnode /= snode && pdx /= sdx ==>
574 let vcpus_s = show vcpus
577 status_s = Types.instanceStatusToRaw status
580 else [(pnode, pdx), (snode, sdx)]
581 nl = Data.Map.fromList ndx
583 sbal = if autobal then "Y" else "N"
584 sdt = Types.diskTemplateToRaw dt
585 inst = Text.loadInst nl
586 [name, mem_s, dsk_s, vcpus_s, status_s,
587 sbal, pnode, snode, sdt, tags]
588 fail1 = Text.loadInst nl
589 [name, mem_s, dsk_s, vcpus_s, status_s,
590 sbal, pnode, pnode, tags]
591 _types = ( name::String, mem::Int, dsk::Int
592 , vcpus::Int, status::Types.InstanceStatus
596 Types.Bad msg -> printTestCase ("Failed to load instance: " ++ msg)
598 Types.Ok (_, i) -> printTestCase "Mismatch in some field while\
599 \ loading the instance" $
600 Instance.name i == name &&
601 Instance.vcpus i == vcpus &&
602 Instance.mem i == mem &&
603 Instance.pNode i == pdx &&
604 Instance.sNode i == (if null snode
605 then Node.noSecondary
607 Instance.autoBalance i == autobal &&
610 prop_Text_Load_InstanceFail ktn fields =
611 length fields /= 10 ==>
612 case Text.loadInst nl fields of
613 Types.Ok _ -> printTestCase "Managed to load instance from invalid\
615 Types.Bad msg -> printTestCase ("Unrecognised error message: " ++ msg) $
616 "Invalid/incomplete instance data: '" `isPrefixOf` msg
617 where nl = Data.Map.fromList ktn
619 prop_Text_Load_Node name tm nm fm td fd tc fo =
620 let conv v = if v < 0
632 any_broken = any (< 0) [tm, nm, fm, td, fd, tc]
633 gid = Group.uuid defGroup
634 in case Text.loadNode defGroupAssoc
635 [name, tm_s, nm_s, fm_s, td_s, fd_s, tc_s, fo_s, gid] of
637 Just (name', node) ->
639 then Node.offline node
640 else Node.name node == name' && name' == name &&
641 Node.alias node == name &&
642 Node.tMem node == fromIntegral tm &&
643 Node.nMem node == nm &&
644 Node.fMem node == fm &&
645 Node.tDsk node == fromIntegral td &&
646 Node.fDsk node == fd &&
647 Node.tCpu node == fromIntegral tc
649 prop_Text_Load_NodeFail fields =
650 length fields /= 8 ==> isNothing $ Text.loadNode Data.Map.empty fields
652 prop_Text_NodeLSIdempotent node =
653 (Text.loadNode defGroupAssoc.
654 Utils.sepSplit '|' . Text.serializeNode defGroupList) n ==
655 Just (Node.name n, n)
656 -- override failN1 to what loadNode returns by default
657 where n = node { Node.failN1 = True, Node.offline = False }
660 [ 'prop_Text_Load_Instance
661 , 'prop_Text_Load_InstanceFail
662 , 'prop_Text_Load_Node
663 , 'prop_Text_Load_NodeFail
664 , 'prop_Text_NodeLSIdempotent
669 prop_Node_setAlias node name =
670 Node.name newnode == Node.name node &&
671 Node.alias newnode == name
672 where _types = (node::Node.Node, name::String)
673 newnode = Node.setAlias node name
675 prop_Node_setOffline node status =
676 Node.offline newnode ==? status
677 where newnode = Node.setOffline node status
679 prop_Node_setXmem node xm =
680 Node.xMem newnode ==? xm
681 where newnode = Node.setXmem node xm
683 prop_Node_setMcpu node mc =
684 Node.mCpu newnode ==? mc
685 where newnode = Node.setMcpu node mc
687 -- | Check that an instance add with too high memory or disk will be
689 prop_Node_addPriFM node inst =
690 Instance.mem inst >= Node.fMem node && not (Node.failN1 node) &&
691 not (Instance.instanceOffline inst) ==>
692 case Node.addPri node inst'' of
693 Types.OpFail Types.FailMem -> True
695 where _types = (node::Node.Node, inst::Instance.Instance)
696 inst' = setInstanceSmallerThanNode node inst
697 inst'' = inst' { Instance.mem = Instance.mem inst }
699 prop_Node_addPriFD node inst =
700 Instance.dsk inst >= Node.fDsk node && not (Node.failN1 node) ==>
701 case Node.addPri node inst'' of
702 Types.OpFail Types.FailDisk -> True
704 where _types = (node::Node.Node, inst::Instance.Instance)
705 inst' = setInstanceSmallerThanNode node inst
706 inst'' = inst' { Instance.dsk = Instance.dsk inst }
708 prop_Node_addPriFC node inst (Positive extra) =
709 not (Node.failN1 node) && not (Instance.instanceOffline inst) ==>
710 case Node.addPri node inst'' of
711 Types.OpFail Types.FailCPU -> True
713 where _types = (node::Node.Node, inst::Instance.Instance)
714 inst' = setInstanceSmallerThanNode node inst
715 inst'' = inst' { Instance.vcpus = Node.availCpu node + extra }
717 -- | Check that an instance add with too high memory or disk will be
719 prop_Node_addSec node inst pdx =
720 ((Instance.mem inst >= (Node.fMem node - Node.rMem node) &&
721 not (Instance.instanceOffline inst)) ||
722 Instance.dsk inst >= Node.fDsk node) &&
723 not (Node.failN1 node) ==>
724 isFailure (Node.addSec node inst pdx)
725 where _types = (node::Node.Node, inst::Instance.Instance, pdx::Int)
727 -- | Check that an offline instance with reasonable disk size can always
729 prop_Node_addPriOffline =
730 forAll (arbitrary `suchThat` ((> 0) . Node.fMem)) $ \node ->
731 forAll (arbitrary `suchThat`
732 (\ x -> (Instance.dsk x < Node.fDsk node) &&
733 Instance.instanceOffline x)) $ \inst ->
734 case Node.addPri node inst of
735 Types.OpGood _ -> True
738 prop_Node_addSecOffline pdx =
739 forAll (arbitrary `suchThat` ((> 0) . Node.fMem)) $ \node ->
740 forAll (arbitrary `suchThat`
741 (\ x -> (Instance.dsk x < Node.fDsk node) &&
742 Instance.instanceOffline x)) $ \inst ->
743 case Node.addSec node inst pdx of
744 Types.OpGood _ -> True
747 -- | Checks for memory reservation changes.
748 prop_Node_rMem inst =
749 not (Instance.instanceOffline inst) ==>
750 forAll (arbitrary `suchThat` ((> Types.unitMem) . Node.fMem)) $ \node ->
751 -- ab = auto_balance, nb = non-auto_balance
752 -- we use -1 as the primary node of the instance
753 let inst' = inst { Instance.pNode = -1, Instance.autoBalance = True }
754 inst_ab = setInstanceSmallerThanNode node inst'
755 inst_nb = inst_ab { Instance.autoBalance = False }
756 -- now we have the two instances, identical except the
757 -- autoBalance attribute
758 orig_rmem = Node.rMem node
759 inst_idx = Instance.idx inst_ab
760 node_add_ab = Node.addSec node inst_ab (-1)
761 node_add_nb = Node.addSec node inst_nb (-1)
762 node_del_ab = liftM (`Node.removeSec` inst_ab) node_add_ab
763 node_del_nb = liftM (`Node.removeSec` inst_nb) node_add_nb
764 in case (node_add_ab, node_add_nb, node_del_ab, node_del_nb) of
765 (Types.OpGood a_ab, Types.OpGood a_nb,
766 Types.OpGood d_ab, Types.OpGood d_nb) ->
767 printTestCase "Consistency checks failed" $
768 Node.rMem a_ab > orig_rmem &&
769 Node.rMem a_ab - orig_rmem == Instance.mem inst_ab &&
770 Node.rMem a_nb == orig_rmem &&
771 Node.rMem d_ab == orig_rmem &&
772 Node.rMem d_nb == orig_rmem &&
773 -- this is not related to rMem, but as good a place to
775 inst_idx `elem` Node.sList a_ab &&
776 inst_idx `notElem` Node.sList d_ab
777 x -> printTestCase ("Failed to add/remove instances: " ++ show x) False
779 -- | Check mdsk setting.
780 prop_Node_setMdsk node mx =
781 Node.loDsk node' >= 0 &&
782 fromIntegral (Node.loDsk node') <= Node.tDsk node &&
783 Node.availDisk node' >= 0 &&
784 Node.availDisk node' <= Node.fDsk node' &&
785 fromIntegral (Node.availDisk node') <= Node.tDsk node' &&
786 Node.mDsk node' == mx'
787 where _types = (node::Node.Node, mx::SmallRatio)
788 node' = Node.setMdsk node mx'
792 prop_Node_tagMaps_idempotent tags =
793 Node.delTags (Node.addTags m tags) tags ==? m
794 where m = Data.Map.empty
796 prop_Node_tagMaps_reject tags =
798 all (\t -> Node.rejectAddTags m [t]) tags
799 where m = Node.addTags Data.Map.empty tags
801 prop_Node_showField node =
802 forAll (elements Node.defaultFields) $ \ field ->
803 fst (Node.showHeader field) /= Types.unknownField &&
804 Node.showField node field /= Types.unknownField
806 prop_Node_computeGroups nodes =
807 let ng = Node.computeGroups nodes
808 onlyuuid = map fst ng
809 in length nodes == sum (map (length . snd) ng) &&
810 all (\(guuid, ns) -> all ((== guuid) . Node.group) ns) ng &&
811 length (nub onlyuuid) == length onlyuuid &&
812 (null nodes || not (null ng))
815 [ 'prop_Node_setAlias
816 , 'prop_Node_setOffline
819 , 'prop_Node_addPriFM
820 , 'prop_Node_addPriFD
821 , 'prop_Node_addPriFC
823 , 'prop_Node_addPriOffline
824 , 'prop_Node_addSecOffline
827 , 'prop_Node_tagMaps_idempotent
828 , 'prop_Node_tagMaps_reject
829 , 'prop_Node_showField
830 , 'prop_Node_computeGroups
835 -- | Check that the cluster score is close to zero for a homogeneous
837 prop_Score_Zero node =
838 forAll (choose (1, 1024)) $ \count ->
839 (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
840 (Node.tDsk node > 0) && (Node.tMem node > 0)) ==>
841 let fn = Node.buildPeers node Container.empty
842 nlst = replicate count fn
843 score = Cluster.compCVNodes nlst
844 -- we can't say == 0 here as the floating point errors accumulate;
845 -- this should be much lower than the default score in CLI.hs
848 -- | Check that cluster stats are sane.
849 prop_CStats_sane node =
850 forAll (choose (1, 1024)) $ \count ->
851 (not (Node.offline node) && not (Node.failN1 node) &&
852 (Node.availDisk node > 0) && (Node.availMem node > 0)) ==>
853 let fn = Node.buildPeers node Container.empty
854 nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
855 nl = Container.fromList nlst
856 cstats = Cluster.totalResources nl
857 in Cluster.csAdsk cstats >= 0 &&
858 Cluster.csAdsk cstats <= Cluster.csFdsk cstats
860 -- | Check that one instance is allocated correctly, without
861 -- rebalances needed.
862 prop_ClusterAlloc_sane node inst =
863 forAll (choose (5, 20)) $ \count ->
864 not (Node.offline node)
865 && not (Node.failN1 node)
866 && Node.availDisk node > 0
867 && Node.availMem node > 0
869 let (nl, il, inst') = makeSmallEmptyCluster node count inst
870 in case Cluster.genAllocNodes defGroupList nl 2 True >>=
871 Cluster.tryAlloc nl il inst' of
874 case Cluster.asSolution as of
876 Just (xnl, xi, _, cv) ->
877 let il' = Container.add (Instance.idx xi) xi il
878 tbl = Cluster.Table xnl il' cv []
879 in not (canBalance tbl True True False)
881 -- | Checks that on a 2-5 node cluster, we can allocate a random
882 -- instance spec via tiered allocation (whatever the original instance
883 -- spec), on either one or two nodes.
884 prop_ClusterCanTieredAlloc node inst =
885 forAll (choose (2, 5)) $ \count ->
886 forAll (choose (1, 2)) $ \rqnodes ->
887 not (Node.offline node)
888 && not (Node.failN1 node)
891 let nl = makeSmallCluster node count
893 allocnodes = Cluster.genAllocNodes defGroupList nl rqnodes True
894 in case allocnodes >>= \allocnodes' ->
895 Cluster.tieredAlloc nl il (Just 1) inst allocnodes' [] [] of
897 Types.Ok (_, _, il', ixes, cstats) -> not (null ixes) &&
898 IntMap.size il' == length ixes &&
899 length ixes == length cstats
901 -- | Checks that on a 4-8 node cluster, once we allocate an instance,
902 -- we can also evacuate it.
903 prop_ClusterAllocEvac node inst =
904 forAll (choose (4, 8)) $ \count ->
905 not (Node.offline node)
906 && not (Node.failN1 node)
909 let (nl, il, inst') = makeSmallEmptyCluster node count inst
910 in case Cluster.genAllocNodes defGroupList nl 2 True >>=
911 Cluster.tryAlloc nl il inst' of
914 case Cluster.asSolution as of
916 Just (xnl, xi, _, _) ->
917 let sdx = Instance.sNode xi
918 il' = Container.add (Instance.idx xi) xi il
919 in case IAlloc.processRelocate defGroupList xnl il'
920 (Instance.idx xi) 1 [sdx] of
924 -- | Check that allocating multiple instances on a cluster, then
925 -- adding an empty node, results in a valid rebalance.
926 prop_ClusterAllocBalance =
927 forAll (genNode (Just 5) (Just 128)) $ \node ->
928 forAll (choose (3, 5)) $ \count ->
929 not (Node.offline node) && not (Node.failN1 node) ==>
930 let nl = makeSmallCluster node count
931 (hnode, nl') = IntMap.deleteFindMax nl
933 allocnodes = Cluster.genAllocNodes defGroupList nl' 2 True
934 i_templ = createInstance Types.unitMem Types.unitDsk Types.unitCpu
935 in case allocnodes >>= \allocnodes' ->
936 Cluster.iterateAlloc nl' il (Just 5) i_templ allocnodes' [] [] of
938 Types.Ok (_, xnl, il', _, _) ->
939 let ynl = Container.add (Node.idx hnode) hnode xnl
940 cv = Cluster.compCV ynl
941 tbl = Cluster.Table ynl il' cv []
942 in canBalance tbl True True False
944 -- | Checks consistency.
945 prop_ClusterCheckConsistency node inst =
946 let nl = makeSmallCluster node 3
947 [node1, node2, node3] = Container.elems nl
948 node3' = node3 { Node.group = 1 }
949 nl' = Container.add (Node.idx node3') node3' nl
950 inst1 = Instance.setBoth inst (Node.idx node1) (Node.idx node2)
951 inst2 = Instance.setBoth inst (Node.idx node1) Node.noSecondary
952 inst3 = Instance.setBoth inst (Node.idx node1) (Node.idx node3)
953 ccheck = Cluster.findSplitInstances nl' . Container.fromList
954 in null (ccheck [(0, inst1)]) &&
955 null (ccheck [(0, inst2)]) &&
956 (not . null $ ccheck [(0, inst3)])
958 -- | For now, we only test that we don't lose instances during the split.
959 prop_ClusterSplitCluster node inst =
960 forAll (choose (0, 100)) $ \icnt ->
961 let nl = makeSmallCluster node 2
962 (nl', il') = foldl (\(ns, is) _ -> assignInstance ns is inst 0 1)
963 (nl, Container.empty) [1..icnt]
964 gni = Cluster.splitCluster nl' il'
965 in sum (map (Container.size . snd . snd) gni) == icnt &&
966 all (\(guuid, (nl'', _)) -> all ((== guuid) . Node.group)
967 (Container.elems nl'')) gni
972 , 'prop_ClusterAlloc_sane
973 , 'prop_ClusterCanTieredAlloc
974 , 'prop_ClusterAllocEvac
975 , 'prop_ClusterAllocBalance
976 , 'prop_ClusterCheckConsistency
977 , 'prop_ClusterSplitCluster
982 -- | Check that opcode serialization is idempotent.
983 prop_OpCodes_serialization op =
984 case J.readJSON (J.showJSON op) of
985 J.Error e -> printTestCase ("Cannot deserialise: " ++ e) False
986 J.Ok op' -> op ==? op'
987 where _types = op::OpCodes.OpCode
990 [ 'prop_OpCodes_serialization ]
994 -- | Check that (queued) job\/opcode status serialization is idempotent.
995 prop_OpStatus_serialization os =
996 case J.readJSON (J.showJSON os) of
997 J.Error e -> printTestCase ("Cannot deserialise: " ++ e) False
998 J.Ok os' -> os ==? os'
999 where _types = os::Jobs.OpStatus
1001 prop_JobStatus_serialization js =
1002 case J.readJSON (J.showJSON js) of
1003 J.Error e -> printTestCase ("Cannot deserialise: " ++ e) False
1004 J.Ok js' -> js ==? js'
1005 where _types = js::Jobs.JobStatus
1008 [ 'prop_OpStatus_serialization
1009 , 'prop_JobStatus_serialization
1014 prop_Loader_lookupNode ktn inst node =
1015 Loader.lookupNode nl inst node ==? Data.Map.lookup node nl
1016 where nl = Data.Map.fromList ktn
1018 prop_Loader_lookupInstance kti inst =
1019 Loader.lookupInstance il inst ==? Data.Map.lookup inst il
1020 where il = Data.Map.fromList kti
1022 prop_Loader_assignIndices nodes =
1023 Data.Map.size nassoc == length nodes &&
1024 Container.size kt == length nodes &&
1025 (if not (null nodes)
1026 then maximum (IntMap.keys kt) == length nodes - 1
1028 where (nassoc, kt) =
1029 Loader.assignIndices (map (\n -> (Node.name n, n)) nodes)
1031 -- | Checks that the number of primary instances recorded on the nodes
1033 prop_Loader_mergeData ns =
1034 let na = Container.fromList $ map (\n -> (Node.idx n, n)) ns
1035 in case Loader.mergeData [] [] [] []
1036 (Loader.emptyCluster {Loader.cdNodes = na}) of
1037 Types.Bad _ -> False
1038 Types.Ok (Loader.ClusterData _ nl il _) ->
1039 let nodes = Container.elems nl
1040 instances = Container.elems il
1041 in (sum . map (length . Node.pList)) nodes == 0 &&
1044 -- | Check that compareNameComponent on equal strings works.
1045 prop_Loader_compareNameComponent_equal :: String -> Bool
1046 prop_Loader_compareNameComponent_equal s =
1047 Loader.compareNameComponent s s ==
1048 Loader.LookupResult Loader.ExactMatch s
1050 -- | Check that compareNameComponent on prefix strings works.
1051 prop_Loader_compareNameComponent_prefix :: NonEmptyList Char -> String -> Bool
1052 prop_Loader_compareNameComponent_prefix (NonEmpty s1) s2 =
1053 Loader.compareNameComponent (s1 ++ "." ++ s2) s1 ==
1054 Loader.LookupResult Loader.PartialMatch s1
1057 [ 'prop_Loader_lookupNode
1058 , 'prop_Loader_lookupInstance
1059 , 'prop_Loader_assignIndices
1060 , 'prop_Loader_mergeData
1061 , 'prop_Loader_compareNameComponent_equal
1062 , 'prop_Loader_compareNameComponent_prefix
1067 prop_Types_AllocPolicy_serialisation apol =
1068 case J.readJSON (J.showJSON apol) of
1069 J.Ok p -> printTestCase ("invalid deserialisation " ++ show p) $
1071 J.Error s -> printTestCase ("failed to deserialise: " ++ s) False
1072 where _types = apol::Types.AllocPolicy
1074 prop_Types_DiskTemplate_serialisation dt =
1075 case J.readJSON (J.showJSON dt) of
1076 J.Ok p -> printTestCase ("invalid deserialisation " ++ show p) $
1078 J.Error s -> printTestCase ("failed to deserialise: " ++ s)
1080 where _types = dt::Types.DiskTemplate
1082 prop_Types_opToResult op =
1084 Types.OpFail _ -> Types.isBad r
1085 Types.OpGood v -> case r of
1086 Types.Bad _ -> False
1087 Types.Ok v' -> v == v'
1088 where r = Types.opToResult op
1089 _types = op::Types.OpResult Int
1091 prop_Types_eitherToResult ei =
1093 Left _ -> Types.isBad r
1094 Right v -> case r of
1095 Types.Bad _ -> False
1096 Types.Ok v' -> v == v'
1097 where r = Types.eitherToResult ei
1098 _types = ei::Either String Int
1101 [ 'prop_Types_AllocPolicy_serialisation
1102 , 'prop_Types_DiskTemplate_serialisation
1103 , 'prop_Types_opToResult
1104 , 'prop_Types_eitherToResult