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.Loader as Loader
59 import qualified Ganeti.HTools.Luxi
60 import qualified Ganeti.HTools.Node as Node
61 import qualified Ganeti.HTools.Group as Group
62 import qualified Ganeti.HTools.PeerMap as PeerMap
63 import qualified Ganeti.HTools.Rapi
64 import qualified Ganeti.HTools.Simu
65 import qualified Ganeti.HTools.Text as Text
66 import qualified Ganeti.HTools.Types as Types
67 import qualified Ganeti.HTools.Utils as Utils
68 import qualified Ganeti.HTools.Version
69 import qualified Ganeti.Constants as C
71 import qualified Ganeti.HTools.Program.Hail
72 import qualified Ganeti.HTools.Program.Hbal
73 import qualified Ganeti.HTools.Program.Hscan
74 import qualified Ganeti.HTools.Program.Hspace
76 import Ganeti.HTools.QCHelper (testSuite)
80 -- | Maximum memory (1TiB, somewhat random value).
84 -- | Maximum disk (8TiB, somewhat random value).
86 maxDsk = 1024 * 1024 * 8
88 -- | Max CPUs (1024, somewhat random value).
92 defGroup :: Group.Group
93 defGroup = flip Group.setIdx 0 $
94 Group.create "default" Utils.defaultGroupID
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 -- | Checks if a node is "big" enough.
138 isNodeBig :: Node.Node -> Int -> Bool
139 isNodeBig node size = Node.availDisk node > size * Types.unitDsk
140 && Node.availMem node > size * Types.unitMem
141 && Node.availCpu node > size * Types.unitCpu
143 canBalance :: Cluster.Table -> Bool -> Bool -> Bool -> Bool
144 canBalance tbl dm im evac = isJust $ Cluster.tryBalance tbl dm im evac 0 0
146 -- | Assigns a new fresh instance to a cluster; this is not
147 -- allocation, so no resource checks are done.
148 assignInstance :: Node.List -> Instance.List -> Instance.Instance ->
149 Types.Idx -> Types.Idx ->
150 (Node.List, Instance.List)
151 assignInstance nl il inst pdx sdx =
152 let pnode = Container.find pdx nl
153 snode = Container.find sdx nl
154 maxiidx = if Container.null il
156 else fst (Container.findMax il) + 1
157 inst' = inst { Instance.idx = maxiidx,
158 Instance.pNode = pdx, Instance.sNode = sdx }
159 pnode' = Node.setPri pnode inst'
160 snode' = Node.setSec snode inst'
161 nl' = Container.addTwo pdx pnode' sdx snode' nl
162 il' = Container.add maxiidx inst' il
165 -- * Arbitrary instances
167 -- | Defines a DNS name.
168 newtype DNSChar = DNSChar { dnsGetChar::Char }
170 instance Arbitrary DNSChar where
172 x <- elements (['a'..'z'] ++ ['0'..'9'] ++ "_-")
175 getName :: Gen String
178 dn <- vector n::Gen [DNSChar]
179 return (map dnsGetChar dn)
182 getFQDN :: Gen String
185 ncomps <- choose (1, 4)
186 frest <- vector ncomps::Gen [[DNSChar]]
187 let frest' = map (map dnsGetChar) frest
188 return (felem ++ "." ++ intercalate "." frest')
190 instance Arbitrary Types.InstanceStatus where
191 arbitrary = elements [ Types.AdminDown
200 -- let's generate a random instance
201 instance Arbitrary Instance.Instance where
204 mem <- choose (0, maxMem)
205 dsk <- choose (0, maxDsk)
209 vcpus <- choose (0, maxCpu)
210 return $ Instance.create name mem dsk vcpus run_st [] True pn sn
213 -- | Generas an arbitrary node based on sizing information.
214 genNode :: Maybe Int -- ^ Minimum node size in terms of units
215 -> Maybe Int -- ^ Maximum node size (when Nothing, bounded
216 -- just by the max... constants)
218 genNode min_multiplier max_multiplier = do
219 let (base_mem, base_dsk, base_cpu) =
220 case min_multiplier of
221 Just mm -> (mm * Types.unitMem,
225 (top_mem, top_dsk, top_cpu) =
226 case max_multiplier of
227 Just mm -> (mm * Types.unitMem,
230 Nothing -> (maxMem, maxDsk, maxCpu)
232 mem_t <- choose (base_mem, top_mem)
233 mem_f <- choose (base_mem, mem_t)
234 mem_n <- choose (0, mem_t - mem_f)
235 dsk_t <- choose (base_dsk, top_dsk)
236 dsk_f <- choose (base_dsk, dsk_t)
237 cpu_t <- choose (base_cpu, top_cpu)
239 let n = Node.create name (fromIntegral mem_t) mem_n mem_f
240 (fromIntegral dsk_t) dsk_f (fromIntegral cpu_t) offl 0
241 return $ Node.buildPeers n Container.empty
244 instance Arbitrary Node.Node where
245 arbitrary = genNode Nothing Nothing
248 instance Arbitrary OpCodes.ReplaceDisksMode where
249 arbitrary = elements [ OpCodes.ReplaceOnPrimary
250 , OpCodes.ReplaceOnSecondary
251 , OpCodes.ReplaceNewSecondary
252 , OpCodes.ReplaceAuto
255 instance Arbitrary OpCodes.OpCode where
257 op_id <- elements [ "OP_TEST_DELAY"
258 , "OP_INSTANCE_REPLACE_DISKS"
259 , "OP_INSTANCE_FAILOVER"
260 , "OP_INSTANCE_MIGRATE"
264 liftM3 OpCodes.OpTestDelay arbitrary arbitrary arbitrary
265 "OP_INSTANCE_REPLACE_DISKS" ->
266 liftM5 OpCodes.OpInstanceReplaceDisks arbitrary arbitrary
267 arbitrary arbitrary arbitrary
268 "OP_INSTANCE_FAILOVER" ->
269 liftM3 OpCodes.OpInstanceFailover arbitrary arbitrary
271 "OP_INSTANCE_MIGRATE" ->
272 liftM5 OpCodes.OpInstanceMigrate arbitrary arbitrary
275 _ -> fail "Wrong opcode")
277 instance Arbitrary Jobs.OpStatus where
278 arbitrary = elements [minBound..maxBound]
280 instance Arbitrary Jobs.JobStatus where
281 arbitrary = elements [minBound..maxBound]
283 newtype SmallRatio = SmallRatio Double deriving Show
284 instance Arbitrary SmallRatio where
287 return $ SmallRatio v
289 instance Arbitrary Types.AllocPolicy where
290 arbitrary = elements [minBound..maxBound]
292 instance Arbitrary Types.DiskTemplate where
293 arbitrary = elements [minBound..maxBound]
295 instance Arbitrary Types.FailMode where
296 arbitrary = elements [minBound..maxBound]
298 instance Arbitrary a => Arbitrary (Types.OpResult a) where
299 arbitrary = arbitrary >>= \c ->
301 False -> liftM Types.OpFail arbitrary
302 True -> liftM Types.OpGood arbitrary
308 -- | If the list is not just an empty element, and if the elements do
309 -- not contain commas, then join+split should be idempotent.
310 prop_Utils_commaJoinSplit =
311 forAll (arbitrary `suchThat`
312 (\l -> l /= [""] && all (not . elem ',') l )) $ \lst ->
313 Utils.sepSplit ',' (Utils.commaJoin lst) ==? lst
315 -- | Split and join should always be idempotent.
316 prop_Utils_commaSplitJoin s =
317 Utils.commaJoin (Utils.sepSplit ',' s) ==? s
319 -- | fromObjWithDefault, we test using the Maybe monad and an integer
321 prop_Utils_fromObjWithDefault def_value random_key =
322 -- a missing key will be returned with the default
323 Utils.fromObjWithDefault [] random_key def_value == Just def_value &&
324 -- a found key will be returned as is, not with default
325 Utils.fromObjWithDefault [(random_key, J.showJSON def_value)]
326 random_key (def_value+1) == Just def_value
327 where _types = def_value :: Integer
329 -- | Test that functional if' behaves like the syntactic sugar if.
330 prop_Utils_if'if :: Bool -> Int -> Int -> Gen Prop
331 prop_Utils_if'if cnd a b =
332 Utils.if' cnd a b ==? if cnd then a else b
334 -- | Test basic select functionality
335 prop_Utils_select :: Int -- ^ Default result
336 -> [Int] -- ^ List of False values
337 -> [Int] -- ^ List of True values
338 -> Gen Prop -- ^ Test result
339 prop_Utils_select def lst1 lst2 =
340 Utils.select def cndlist ==? expectedresult
341 where expectedresult = Utils.if' (null lst2) def (head lst2)
342 flist = map (\e -> (False, e)) lst1
343 tlist = map (\e -> (True, e)) lst2
344 cndlist = flist ++ tlist
346 -- | Test basic select functionality with undefined default
347 prop_Utils_select_undefd :: [Int] -- ^ List of False values
348 -> NonEmptyList Int -- ^ List of True values
349 -> Gen Prop -- ^ Test result
350 prop_Utils_select_undefd lst1 (NonEmpty lst2) =
351 Utils.select undefined cndlist ==? head lst2
352 where flist = map (\e -> (False, e)) lst1
353 tlist = map (\e -> (True, e)) lst2
354 cndlist = flist ++ tlist
356 -- | Test basic select functionality with undefined list values
357 prop_Utils_select_undefv :: [Int] -- ^ List of False values
358 -> NonEmptyList Int -- ^ List of True values
359 -> Gen Prop -- ^ Test result
360 prop_Utils_select_undefv lst1 (NonEmpty lst2) =
361 Utils.select undefined cndlist ==? head lst2
362 where flist = map (\e -> (False, e)) lst1
363 tlist = map (\e -> (True, e)) lst2
364 cndlist = flist ++ tlist ++ [undefined]
366 prop_Utils_parseUnit (NonNegative n) =
367 Utils.parseUnit (show n) == Types.Ok n &&
368 Utils.parseUnit (show n ++ "m") == Types.Ok n &&
369 (case Utils.parseUnit (show n ++ "M") of
370 Types.Ok m -> if n > 0
371 then m < n -- for positive values, X MB is less than X MiB
372 else m == 0 -- but for 0, 0 MB == 0 MiB
373 Types.Bad _ -> False) &&
374 Utils.parseUnit (show n ++ "g") == Types.Ok (n*1024) &&
375 Utils.parseUnit (show n ++ "t") == Types.Ok (n*1048576) &&
376 Types.isBad (Utils.parseUnit (show n ++ "x")::Types.Result Int)
377 where _types = n::Int
379 -- | Test list for the Utils module.
381 [ 'prop_Utils_commaJoinSplit
382 , 'prop_Utils_commaSplitJoin
383 , 'prop_Utils_fromObjWithDefault
386 , 'prop_Utils_select_undefd
387 , 'prop_Utils_select_undefv
388 , 'prop_Utils_parseUnit
393 -- | Make sure add is idempotent.
394 prop_PeerMap_addIdempotent pmap key em =
395 fn puniq ==? fn (fn puniq)
396 where _types = (pmap::PeerMap.PeerMap,
397 key::PeerMap.Key, em::PeerMap.Elem)
398 fn = PeerMap.add key em
399 puniq = PeerMap.accumArray const pmap
401 -- | Make sure remove is idempotent.
402 prop_PeerMap_removeIdempotent pmap key =
403 fn puniq ==? fn (fn puniq)
404 where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
405 fn = PeerMap.remove key
406 puniq = PeerMap.accumArray const pmap
408 -- | Make sure a missing item returns 0.
409 prop_PeerMap_findMissing pmap key =
410 PeerMap.find key (PeerMap.remove key puniq) ==? 0
411 where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
412 puniq = PeerMap.accumArray const pmap
414 -- | Make sure an added item is found.
415 prop_PeerMap_addFind pmap key em =
416 PeerMap.find key (PeerMap.add key em puniq) ==? em
417 where _types = (pmap::PeerMap.PeerMap,
418 key::PeerMap.Key, em::PeerMap.Elem)
419 puniq = PeerMap.accumArray const pmap
421 -- | Manual check that maxElem returns the maximum indeed, or 0 for null.
422 prop_PeerMap_maxElem pmap =
423 PeerMap.maxElem puniq ==? if null puniq then 0
424 else (maximum . snd . unzip) puniq
425 where _types = pmap::PeerMap.PeerMap
426 puniq = PeerMap.accumArray const pmap
428 -- | List of tests for the PeerMap module.
430 [ 'prop_PeerMap_addIdempotent
431 , 'prop_PeerMap_removeIdempotent
432 , 'prop_PeerMap_maxElem
433 , 'prop_PeerMap_addFind
434 , 'prop_PeerMap_findMissing
437 -- ** Container tests
439 prop_Container_addTwo cdata i1 i2 =
440 fn i1 i2 cont == fn i2 i1 cont &&
441 fn i1 i2 cont == fn i1 i2 (fn i1 i2 cont)
442 where _types = (cdata::[Int],
444 cont = foldl (\c x -> Container.add x x c) Container.empty cdata
445 fn x1 x2 = Container.addTwo x1 x1 x2 x2
447 prop_Container_nameOf node =
448 let nl = makeSmallCluster node 1
449 fnode = head (Container.elems nl)
450 in Container.nameOf nl (Node.idx fnode) ==? Node.name fnode
452 -- | We test that in a cluster, given a random node, we can find it by
453 -- its name and alias, as long as all names and aliases are unique,
454 -- and that we fail to find a non-existing name.
455 prop_Container_findByName node othername =
456 forAll (choose (1, 20)) $ \ cnt ->
457 forAll (choose (0, cnt - 1)) $ \ fidx ->
458 forAll (vector cnt) $ \ names ->
459 (length . nub) (map fst names ++ map snd names) ==
461 not (othername `elem` (map fst names ++ map snd names)) ==>
462 let nl = makeSmallCluster node cnt
463 nodes = Container.elems nl
464 nodes' = map (\((name, alias), nn) -> (Node.idx nn,
465 nn { Node.name = name,
466 Node.alias = alias }))
468 nl' = Container.fromList nodes'
469 target = snd (nodes' !! fidx)
470 in Container.findByName nl' (Node.name target) == Just target &&
471 Container.findByName nl' (Node.alias target) == Just target &&
472 Container.findByName nl' othername == Nothing
474 testSuite "Container"
475 [ 'prop_Container_addTwo
476 , 'prop_Container_nameOf
477 , 'prop_Container_findByName
482 -- Simple instance tests, we only have setter/getters
484 prop_Instance_creat inst =
485 Instance.name inst ==? Instance.alias inst
487 prop_Instance_setIdx inst idx =
488 Instance.idx (Instance.setIdx inst idx) ==? idx
489 where _types = (inst::Instance.Instance, idx::Types.Idx)
491 prop_Instance_setName inst name =
492 Instance.name newinst == name &&
493 Instance.alias newinst == name
494 where _types = (inst::Instance.Instance, name::String)
495 newinst = Instance.setName inst name
497 prop_Instance_setAlias inst name =
498 Instance.name newinst == Instance.name inst &&
499 Instance.alias newinst == name
500 where _types = (inst::Instance.Instance, name::String)
501 newinst = Instance.setAlias inst name
503 prop_Instance_setPri inst pdx =
504 Instance.pNode (Instance.setPri inst pdx) ==? pdx
505 where _types = (inst::Instance.Instance, pdx::Types.Ndx)
507 prop_Instance_setSec inst sdx =
508 Instance.sNode (Instance.setSec inst sdx) ==? sdx
509 where _types = (inst::Instance.Instance, sdx::Types.Ndx)
511 prop_Instance_setBoth inst pdx sdx =
512 Instance.pNode si == pdx && Instance.sNode si == sdx
513 where _types = (inst::Instance.Instance, pdx::Types.Ndx, sdx::Types.Ndx)
514 si = Instance.setBoth inst pdx sdx
516 prop_Instance_shrinkMG inst =
517 Instance.mem inst >= 2 * Types.unitMem ==>
518 case Instance.shrinkByType inst Types.FailMem of
520 Instance.mem inst' == Instance.mem inst - Types.unitMem
523 prop_Instance_shrinkMF inst =
524 forAll (choose (0, 2 * Types.unitMem - 1)) $ \mem ->
525 let inst' = inst { Instance.mem = mem}
526 in Types.isBad $ Instance.shrinkByType inst' Types.FailMem
528 prop_Instance_shrinkCG inst =
529 Instance.vcpus inst >= 2 * Types.unitCpu ==>
530 case Instance.shrinkByType inst Types.FailCPU of
532 Instance.vcpus inst' == Instance.vcpus inst - Types.unitCpu
535 prop_Instance_shrinkCF inst =
536 forAll (choose (0, 2 * Types.unitCpu - 1)) $ \vcpus ->
537 let inst' = inst { Instance.vcpus = vcpus }
538 in Types.isBad $ Instance.shrinkByType inst' Types.FailCPU
540 prop_Instance_shrinkDG inst =
541 Instance.dsk inst >= 2 * Types.unitDsk ==>
542 case Instance.shrinkByType inst Types.FailDisk of
544 Instance.dsk inst' == Instance.dsk inst - Types.unitDsk
547 prop_Instance_shrinkDF inst =
548 forAll (choose (0, 2 * Types.unitDsk - 1)) $ \dsk ->
549 let inst' = inst { Instance.dsk = dsk }
550 in Types.isBad $ Instance.shrinkByType inst' Types.FailDisk
552 prop_Instance_setMovable inst m =
553 Instance.movable inst' ==? m
554 where inst' = Instance.setMovable inst m
557 [ 'prop_Instance_creat
558 , 'prop_Instance_setIdx
559 , 'prop_Instance_setName
560 , 'prop_Instance_setAlias
561 , 'prop_Instance_setPri
562 , 'prop_Instance_setSec
563 , 'prop_Instance_setBoth
564 , 'prop_Instance_shrinkMG
565 , 'prop_Instance_shrinkMF
566 , 'prop_Instance_shrinkCG
567 , 'prop_Instance_shrinkCF
568 , 'prop_Instance_shrinkDG
569 , 'prop_Instance_shrinkDF
570 , 'prop_Instance_setMovable
573 -- ** Text backend tests
575 -- Instance text loader tests
577 prop_Text_Load_Instance name mem dsk vcpus status
578 (NonEmpty pnode) snode
579 (NonNegative pdx) (NonNegative sdx) autobal dt =
580 pnode /= snode && pdx /= sdx ==>
581 let vcpus_s = show vcpus
584 status_s = Types.instanceStatusToRaw status
587 else [(pnode, pdx), (snode, sdx)]
588 nl = Data.Map.fromList ndx
590 sbal = if autobal then "Y" else "N"
591 sdt = Types.diskTemplateToRaw dt
592 inst = Text.loadInst nl
593 [name, mem_s, dsk_s, vcpus_s, status_s,
594 sbal, pnode, snode, sdt, tags]
595 fail1 = Text.loadInst nl
596 [name, mem_s, dsk_s, vcpus_s, status_s,
597 sbal, pnode, pnode, tags]
598 _types = ( name::String, mem::Int, dsk::Int
599 , vcpus::Int, status::Types.InstanceStatus
604 Types.Bad msg -> printTestCase ("Failed to load instance: " ++ msg)
606 Types.Ok (_, i) -> printTestCase "Mismatch in some field while\
607 \ loading the instance" $
608 Instance.name i == name &&
609 Instance.vcpus i == vcpus &&
610 Instance.mem i == mem &&
611 Instance.pNode i == pdx &&
612 Instance.sNode i == (if null snode
613 then Node.noSecondary
615 Instance.autoBalance i == autobal &&
618 prop_Text_Load_InstanceFail ktn fields =
619 length fields /= 10 ==>
620 case Text.loadInst nl fields of
621 Types.Ok _ -> printTestCase "Managed to load instance from invalid\
623 Types.Bad msg -> printTestCase ("Unrecognised error message: " ++ msg) $
624 "Invalid/incomplete instance data: '" `isPrefixOf` msg
625 where nl = Data.Map.fromList ktn
627 prop_Text_Load_Node name tm nm fm td fd tc fo =
628 let conv v = if v < 0
640 any_broken = any (< 0) [tm, nm, fm, td, fd, tc]
641 gid = Group.uuid defGroup
642 in case Text.loadNode defGroupAssoc
643 [name, tm_s, nm_s, fm_s, td_s, fd_s, tc_s, fo_s, gid] of
645 Just (name', node) ->
647 then Node.offline node
648 else Node.name node == name' && name' == name &&
649 Node.alias node == name &&
650 Node.tMem node == fromIntegral tm &&
651 Node.nMem node == nm &&
652 Node.fMem node == fm &&
653 Node.tDsk node == fromIntegral td &&
654 Node.fDsk node == fd &&
655 Node.tCpu node == fromIntegral tc
657 prop_Text_Load_NodeFail fields =
658 length fields /= 8 ==> isNothing $ Text.loadNode Data.Map.empty fields
660 prop_Text_NodeLSIdempotent node =
661 (Text.loadNode defGroupAssoc.
662 Utils.sepSplit '|' . Text.serializeNode defGroupList) n ==
663 Just (Node.name n, n)
664 -- override failN1 to what loadNode returns by default
665 where n = node { Node.failN1 = True, Node.offline = False }
668 [ 'prop_Text_Load_Instance
669 , 'prop_Text_Load_InstanceFail
670 , 'prop_Text_Load_Node
671 , 'prop_Text_Load_NodeFail
672 , 'prop_Text_NodeLSIdempotent
677 prop_Node_setAlias node name =
678 Node.name newnode == Node.name node &&
679 Node.alias newnode == name
680 where _types = (node::Node.Node, name::String)
681 newnode = Node.setAlias node name
683 prop_Node_setOffline node status =
684 Node.offline newnode ==? status
685 where newnode = Node.setOffline node status
687 prop_Node_setXmem node xm =
688 Node.xMem newnode ==? xm
689 where newnode = Node.setXmem node xm
691 prop_Node_setMcpu node mc =
692 Node.mCpu newnode ==? mc
693 where newnode = Node.setMcpu node mc
695 -- | Check that an instance add with too high memory or disk will be
697 prop_Node_addPriFM node inst = Instance.mem inst >= Node.fMem node &&
698 not (Node.failN1 node)
700 case Node.addPri node inst'' of
701 Types.OpFail Types.FailMem -> True
703 where _types = (node::Node.Node, inst::Instance.Instance)
704 inst' = setInstanceSmallerThanNode node inst
705 inst'' = inst' { Instance.mem = Instance.mem inst }
707 prop_Node_addPriFD node inst = Instance.dsk inst >= Node.fDsk node &&
708 not (Node.failN1 node)
710 case Node.addPri node inst'' of
711 Types.OpFail Types.FailDisk -> True
713 where _types = (node::Node.Node, inst::Instance.Instance)
714 inst' = setInstanceSmallerThanNode node inst
715 inst'' = inst' { Instance.dsk = Instance.dsk inst }
717 prop_Node_addPriFC node inst (Positive extra) =
718 not (Node.failN1 node) ==>
719 case Node.addPri node inst'' of
720 Types.OpFail Types.FailCPU -> True
722 where _types = (node::Node.Node, inst::Instance.Instance)
723 inst' = setInstanceSmallerThanNode node inst
724 inst'' = inst' { Instance.vcpus = Node.availCpu node + extra }
726 -- | Check that an instance add with too high memory or disk will be
728 prop_Node_addSec node inst pdx =
729 (Instance.mem inst >= (Node.fMem node - Node.rMem node) ||
730 Instance.dsk inst >= Node.fDsk node) &&
731 not (Node.failN1 node)
732 ==> isFailure (Node.addSec node inst pdx)
733 where _types = (node::Node.Node, inst::Instance.Instance, pdx::Int)
735 -- | Checks for memory reservation changes.
736 prop_Node_rMem inst =
737 forAll (arbitrary `suchThat` ((> Types.unitMem) . Node.fMem)) $ \node ->
738 -- ab = auto_balance, nb = non-auto_balance
739 -- we use -1 as the primary node of the instance
740 let inst' = inst { Instance.pNode = -1, Instance.autoBalance = True }
741 inst_ab = setInstanceSmallerThanNode node inst'
742 inst_nb = inst_ab { Instance.autoBalance = False }
743 -- now we have the two instances, identical except the
744 -- autoBalance attribute
745 orig_rmem = Node.rMem node
746 inst_idx = Instance.idx inst_ab
747 node_add_ab = Node.addSec node inst_ab (-1)
748 node_add_nb = Node.addSec node inst_nb (-1)
749 node_del_ab = liftM (`Node.removeSec` inst_ab) node_add_ab
750 node_del_nb = liftM (`Node.removeSec` inst_nb) node_add_nb
751 in case (node_add_ab, node_add_nb, node_del_ab, node_del_nb) of
752 (Types.OpGood a_ab, Types.OpGood a_nb,
753 Types.OpGood d_ab, Types.OpGood d_nb) ->
754 printTestCase "Consistency checks failed" $
755 Node.rMem a_ab > orig_rmem &&
756 Node.rMem a_ab - orig_rmem == Instance.mem inst_ab &&
757 Node.rMem a_nb == orig_rmem &&
758 Node.rMem d_ab == orig_rmem &&
759 Node.rMem d_nb == orig_rmem &&
760 -- this is not related to rMem, but as good a place to
762 inst_idx `elem` Node.sList a_ab &&
763 not (inst_idx `elem` Node.sList d_ab)
764 x -> printTestCase ("Failed to add/remove instances: " ++ show x)
767 -- | Check mdsk setting.
768 prop_Node_setMdsk node mx =
769 Node.loDsk node' >= 0 &&
770 fromIntegral (Node.loDsk node') <= Node.tDsk node &&
771 Node.availDisk node' >= 0 &&
772 Node.availDisk node' <= Node.fDsk node' &&
773 fromIntegral (Node.availDisk node') <= Node.tDsk node' &&
774 Node.mDsk node' == mx'
775 where _types = (node::Node.Node, mx::SmallRatio)
776 node' = Node.setMdsk node mx'
780 prop_Node_tagMaps_idempotent tags =
781 Node.delTags (Node.addTags m tags) tags ==? m
782 where m = Data.Map.empty
784 prop_Node_tagMaps_reject tags =
786 all (\t -> Node.rejectAddTags m [t]) tags
787 where m = Node.addTags Data.Map.empty tags
789 prop_Node_showField node =
790 forAll (elements Node.defaultFields) $ \ field ->
791 fst (Node.showHeader field) /= Types.unknownField &&
792 Node.showField node field /= Types.unknownField
794 prop_Node_computeGroups nodes =
795 let ng = Node.computeGroups nodes
796 onlyuuid = map fst ng
797 in length nodes == sum (map (length . snd) ng) &&
798 all (\(guuid, ns) -> all ((== guuid) . Node.group) ns) ng &&
799 length (nub onlyuuid) == length onlyuuid &&
800 (null nodes || not (null ng))
803 [ 'prop_Node_setAlias
804 , 'prop_Node_setOffline
807 , 'prop_Node_addPriFM
808 , 'prop_Node_addPriFD
809 , 'prop_Node_addPriFC
813 , 'prop_Node_tagMaps_idempotent
814 , 'prop_Node_tagMaps_reject
815 , 'prop_Node_showField
816 , 'prop_Node_computeGroups
821 -- | Check that the cluster score is close to zero for a homogeneous
823 prop_Score_Zero node =
824 forAll (choose (1, 1024)) $ \count ->
825 (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
826 (Node.tDsk node > 0) && (Node.tMem node > 0)) ==>
827 let fn = Node.buildPeers node Container.empty
828 nlst = replicate count fn
829 score = Cluster.compCVNodes nlst
830 -- we can't say == 0 here as the floating point errors accumulate;
831 -- this should be much lower than the default score in CLI.hs
834 -- | Check that cluster stats are sane.
835 prop_CStats_sane node =
836 forAll (choose (1, 1024)) $ \count ->
837 (not (Node.offline node) && not (Node.failN1 node) &&
838 (Node.availDisk node > 0) && (Node.availMem node > 0)) ==>
839 let fn = Node.buildPeers node Container.empty
840 nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
841 nl = Container.fromList nlst
842 cstats = Cluster.totalResources nl
843 in Cluster.csAdsk cstats >= 0 &&
844 Cluster.csAdsk cstats <= Cluster.csFdsk cstats
846 -- | Check that one instance is allocated correctly, without
847 -- rebalances needed.
848 prop_ClusterAlloc_sane node inst =
849 forAll (choose (5, 20)) $ \count ->
850 not (Node.offline node)
851 && not (Node.failN1 node)
852 && Node.availDisk node > 0
853 && Node.availMem node > 0
855 let nl = makeSmallCluster node count
857 inst' = setInstanceSmallerThanNode node inst
858 in case Cluster.genAllocNodes defGroupList nl 2 True >>=
859 Cluster.tryAlloc nl il inst' of
862 case Cluster.asSolution as of
864 Just (xnl, xi, _, cv) ->
865 let il' = Container.add (Instance.idx xi) xi il
866 tbl = Cluster.Table xnl il' cv []
867 in not (canBalance tbl True True False)
869 -- | Checks that on a 2-5 node cluster, we can allocate a random
870 -- instance spec via tiered allocation (whatever the original instance
871 -- spec), on either one or two nodes.
872 prop_ClusterCanTieredAlloc node inst =
873 forAll (choose (2, 5)) $ \count ->
874 forAll (choose (1, 2)) $ \rqnodes ->
875 not (Node.offline node)
876 && not (Node.failN1 node)
879 let nl = makeSmallCluster node count
881 allocnodes = Cluster.genAllocNodes defGroupList nl rqnodes True
882 in case allocnodes >>= \allocnodes' ->
883 Cluster.tieredAlloc nl il (Just 1) inst allocnodes' [] [] of
885 Types.Ok (_, _, il', ixes, cstats) -> not (null ixes) &&
886 IntMap.size il' == length ixes &&
887 length ixes == length cstats
889 -- | Checks that on a 4-8 node cluster, once we allocate an instance,
890 -- we can also evacuate it.
891 prop_ClusterAllocEvac node inst =
892 forAll (choose (4, 8)) $ \count ->
893 not (Node.offline node)
894 && not (Node.failN1 node)
897 let nl = makeSmallCluster node count
899 inst' = setInstanceSmallerThanNode node inst
900 in case Cluster.genAllocNodes defGroupList nl 2 True >>=
901 Cluster.tryAlloc nl il inst' of
904 case Cluster.asSolution as of
906 Just (xnl, xi, _, _) ->
907 let sdx = Instance.sNode xi
908 il' = Container.add (Instance.idx xi) xi il
909 in case IAlloc.processRelocate defGroupList xnl il'
910 (Instance.idx xi) 1 [sdx] of
914 -- | Check that allocating multiple instances on a cluster, then
915 -- adding an empty node, results in a valid rebalance.
916 prop_ClusterAllocBalance =
917 forAll (genNode (Just 5) (Just 128)) $ \node ->
918 forAll (choose (3, 5)) $ \count ->
919 not (Node.offline node) && not (Node.failN1 node) ==>
920 let nl = makeSmallCluster node count
921 (hnode, nl') = IntMap.deleteFindMax nl
923 allocnodes = Cluster.genAllocNodes defGroupList nl' 2 True
924 i_templ = createInstance Types.unitMem Types.unitDsk Types.unitCpu
925 in case allocnodes >>= \allocnodes' ->
926 Cluster.iterateAlloc nl' il (Just 5) i_templ allocnodes' [] [] of
928 Types.Ok (_, xnl, il', _, _) ->
929 let ynl = Container.add (Node.idx hnode) hnode xnl
930 cv = Cluster.compCV ynl
931 tbl = Cluster.Table ynl il' cv []
932 in canBalance tbl True True False
934 -- | Checks consistency.
935 prop_ClusterCheckConsistency node inst =
936 let nl = makeSmallCluster node 3
937 [node1, node2, node3] = Container.elems nl
938 node3' = node3 { Node.group = 1 }
939 nl' = Container.add (Node.idx node3') node3' nl
940 inst1 = Instance.setBoth inst (Node.idx node1) (Node.idx node2)
941 inst2 = Instance.setBoth inst (Node.idx node1) Node.noSecondary
942 inst3 = Instance.setBoth inst (Node.idx node1) (Node.idx node3)
943 ccheck = Cluster.findSplitInstances nl' . Container.fromList
944 in null (ccheck [(0, inst1)]) &&
945 null (ccheck [(0, inst2)]) &&
946 (not . null $ ccheck [(0, inst3)])
948 -- | For now, we only test that we don't lose instances during the split.
949 prop_ClusterSplitCluster node inst =
950 forAll (choose (0, 100)) $ \icnt ->
951 let nl = makeSmallCluster node 2
952 (nl', il') = foldl (\(ns, is) _ -> assignInstance ns is inst 0 1)
953 (nl, Container.empty) [1..icnt]
954 gni = Cluster.splitCluster nl' il'
955 in sum (map (Container.size . snd . snd) gni) == icnt &&
956 all (\(guuid, (nl'', _)) -> all ((== guuid) . Node.group)
957 (Container.elems nl'')) gni
962 , 'prop_ClusterAlloc_sane
963 , 'prop_ClusterCanTieredAlloc
964 , 'prop_ClusterAllocEvac
965 , 'prop_ClusterAllocBalance
966 , 'prop_ClusterCheckConsistency
967 , 'prop_ClusterSplitCluster
972 -- | Check that opcode serialization is idempotent.
973 prop_OpCodes_serialization op =
974 case J.readJSON (J.showJSON op) of
975 J.Error e -> printTestCase ("Cannot deserialise: " ++ e) False
976 J.Ok op' -> op ==? op'
977 where _types = op::OpCodes.OpCode
980 [ 'prop_OpCodes_serialization ]
984 -- | Check that (queued) job\/opcode status serialization is idempotent.
985 prop_OpStatus_serialization os =
986 case J.readJSON (J.showJSON os) of
987 J.Error e -> printTestCase ("Cannot deserialise: " ++ e) False
988 J.Ok os' -> os ==? os'
989 where _types = os::Jobs.OpStatus
991 prop_JobStatus_serialization js =
992 case J.readJSON (J.showJSON js) of
993 J.Error e -> printTestCase ("Cannot deserialise: " ++ e) False
994 J.Ok js' -> js ==? js'
995 where _types = js::Jobs.JobStatus
998 [ 'prop_OpStatus_serialization
999 , 'prop_JobStatus_serialization
1004 prop_Loader_lookupNode ktn inst node =
1005 Loader.lookupNode nl inst node ==? Data.Map.lookup node nl
1006 where nl = Data.Map.fromList ktn
1008 prop_Loader_lookupInstance kti inst =
1009 Loader.lookupInstance il inst ==? Data.Map.lookup inst il
1010 where il = Data.Map.fromList kti
1012 prop_Loader_assignIndices nodes =
1013 Data.Map.size nassoc == length nodes &&
1014 Container.size kt == length nodes &&
1015 (if not (null nodes)
1016 then maximum (IntMap.keys kt) == length nodes - 1
1018 where (nassoc, kt) = Loader.assignIndices (map (\n -> (Node.name n, n)) nodes)
1020 -- | Checks that the number of primary instances recorded on the nodes
1022 prop_Loader_mergeData ns =
1023 let na = Container.fromList $ map (\n -> (Node.idx n, n)) ns
1024 in case Loader.mergeData [] [] [] []
1025 (Loader.emptyCluster {Loader.cdNodes = na}) of
1026 Types.Bad _ -> False
1027 Types.Ok (Loader.ClusterData _ nl il _) ->
1028 let nodes = Container.elems nl
1029 instances = Container.elems il
1030 in (sum . map (length . Node.pList)) nodes == 0 &&
1033 -- | Check that compareNameComponent on equal strings works.
1034 prop_Loader_compareNameComponent_equal :: String -> Bool
1035 prop_Loader_compareNameComponent_equal s =
1036 Loader.compareNameComponent s s ==
1037 Loader.LookupResult Loader.ExactMatch s
1039 -- | Check that compareNameComponent on prefix strings works.
1040 prop_Loader_compareNameComponent_prefix :: NonEmptyList Char -> String -> Bool
1041 prop_Loader_compareNameComponent_prefix (NonEmpty s1) s2 =
1042 Loader.compareNameComponent (s1 ++ "." ++ s2) s1 ==
1043 Loader.LookupResult Loader.PartialMatch s1
1046 [ 'prop_Loader_lookupNode
1047 , 'prop_Loader_lookupInstance
1048 , 'prop_Loader_assignIndices
1049 , 'prop_Loader_mergeData
1050 , 'prop_Loader_compareNameComponent_equal
1051 , 'prop_Loader_compareNameComponent_prefix
1056 prop_Types_AllocPolicy_serialisation apol =
1057 case J.readJSON (J.showJSON apol) of
1058 J.Ok p -> printTestCase ("invalid deserialisation " ++ show p) $
1060 J.Error s -> printTestCase ("failed to deserialise: " ++ s) False
1061 where _types = apol::Types.AllocPolicy
1063 prop_Types_DiskTemplate_serialisation dt =
1064 case J.readJSON (J.showJSON dt) of
1065 J.Ok p -> printTestCase ("invalid deserialisation " ++ show p) $
1067 J.Error s -> printTestCase ("failed to deserialise: " ++ s)
1069 where _types = dt::Types.DiskTemplate
1071 prop_Types_opToResult op =
1073 Types.OpFail _ -> Types.isBad r
1074 Types.OpGood v -> case r of
1075 Types.Bad _ -> False
1076 Types.Ok v' -> v == v'
1077 where r = Types.opToResult op
1078 _types = op::Types.OpResult Int
1080 prop_Types_eitherToResult ei =
1082 Left _ -> Types.isBad r
1083 Right v -> case r of
1084 Types.Bad _ -> False
1085 Types.Ok v' -> v == v'
1086 where r = Types.eitherToResult ei
1087 _types = ei::Either String Int
1090 [ 'prop_Types_AllocPolicy_serialisation
1091 , 'prop_Types_DiskTemplate_serialisation
1092 , 'prop_Types_opToResult
1093 , 'prop_Types_eitherToResult