1 {-| Unittests for ganeti-htools
7 Copyright (C) 2009 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
35 import Test.QuickCheck
36 import Test.QuickCheck.Batch
38 import qualified Data.Map
39 import qualified Data.IntMap as IntMap
40 import qualified Ganeti.HTools.CLI as CLI
41 import qualified Ganeti.HTools.Cluster as Cluster
42 import qualified Ganeti.HTools.Container as Container
43 import qualified Ganeti.HTools.IAlloc as IAlloc
44 import qualified Ganeti.HTools.Instance as Instance
45 import qualified Ganeti.HTools.Loader as Loader
46 import qualified Ganeti.HTools.Node as Node
47 import qualified Ganeti.HTools.PeerMap as PeerMap
48 import qualified Ganeti.HTools.Text as Text
49 import qualified Ganeti.HTools.Types as Types
50 import qualified Ganeti.HTools.Utils as Utils
54 -- | Maximum memory (1TiB, somewhat random value)
58 -- | Maximum disk (8TiB, somewhat random value)
60 maxDsk = 1024 * 1024 * 8
62 -- | Max CPUs (1024, somewhat random value)
68 -- | Simple checker for whether OpResult is fail or pass
69 isFailure :: Types.OpResult a -> Bool
70 isFailure (Types.OpFail _) = True
73 -- | Simple checker for whether Result is fail or pass
74 isOk :: Types.Result a -> Bool
75 isOk (Types.Ok _ ) = True
78 -- | Update an instance to be smaller than a node
79 setInstanceSmallerThanNode node inst =
80 inst { Instance.mem = (Node.availMem node) `div` 2
81 , Instance.dsk = (Node.availDisk node) `div` 2
82 , Instance.vcpus = (Node.availCpu node) `div` 2
85 -- | Create an instance given its spec
86 createInstance mem dsk vcpus =
87 Instance.create "inst-unnamed" mem dsk vcpus "running" [] (-1) (-1)
89 -- | Create a small cluster by repeating a node spec
90 makeSmallCluster :: Node.Node -> Int -> Node.List
91 makeSmallCluster node count =
92 let fn = Node.buildPeers node Container.empty
93 namelst = map (\n -> (Node.name n, n)) (replicate count fn)
94 (_, nlst) = Loader.assignIndices namelst
95 in Container.fromAssocList nlst
97 -- | Checks if a node is "big" enough
98 isNodeBig :: Node.Node -> Int -> Bool
99 isNodeBig node size = Node.availDisk node > size * Types.unitDsk
100 && Node.availMem node > size * Types.unitMem
101 && Node.availCpu node > size * Types.unitCpu
103 canBalance :: Cluster.Table -> Bool -> Bool -> Bool
104 canBalance tbl dm evac = isJust $ Cluster.tryBalance tbl dm evac
106 -- * Arbitrary instances
108 -- copied from the introduction to quickcheck
109 instance Arbitrary Char where
110 arbitrary = choose ('\32', '\128')
112 -- let's generate a random instance
113 instance Arbitrary Instance.Instance where
116 mem <- choose (0, maxMem)
117 dsk <- choose (0, maxDsk)
118 run_st <- elements ["ERROR_up", "ERROR_down", "ADMIN_down"
119 , "ERROR_nodedown", "ERROR_nodeoffline"
121 , "no_such_status1", "no_such_status2"]
124 vcpus <- choose (0, maxCpu)
125 return $ Instance.create name mem dsk vcpus run_st [] pn sn
128 instance Arbitrary Node.Node where
131 mem_t <- choose (0, maxMem)
132 mem_f <- choose (0, mem_t)
133 mem_n <- choose (0, mem_t - mem_f)
134 dsk_t <- choose (0, maxDsk)
135 dsk_f <- choose (0, dsk_t)
136 cpu_t <- choose (0, maxCpu)
138 let n = Node.create name (fromIntegral mem_t) mem_n mem_f
139 (fromIntegral dsk_t) dsk_f (fromIntegral cpu_t) offl
140 n' = Node.buildPeers n Container.empty
145 -- | Make sure add is idempotent
146 prop_PeerMap_addIdempotent pmap key em =
147 fn puniq == fn (fn puniq)
148 where _types = (pmap::PeerMap.PeerMap,
149 key::PeerMap.Key, em::PeerMap.Elem)
150 fn = PeerMap.add key em
151 puniq = PeerMap.accumArray const pmap
153 -- | Make sure remove is idempotent
154 prop_PeerMap_removeIdempotent pmap key =
155 fn puniq == fn (fn puniq)
156 where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
157 fn = PeerMap.remove key
158 puniq = PeerMap.accumArray const pmap
160 -- | Make sure a missing item returns 0
161 prop_PeerMap_findMissing pmap key =
162 PeerMap.find key (PeerMap.remove key puniq) == 0
163 where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
164 puniq = PeerMap.accumArray const pmap
166 -- | Make sure an added item is found
167 prop_PeerMap_addFind pmap key em =
168 PeerMap.find key (PeerMap.add key em puniq) == em
169 where _types = (pmap::PeerMap.PeerMap,
170 key::PeerMap.Key, em::PeerMap.Elem)
171 puniq = PeerMap.accumArray const pmap
173 -- | Manual check that maxElem returns the maximum indeed, or 0 for null
174 prop_PeerMap_maxElem pmap =
175 PeerMap.maxElem puniq == if null puniq then 0
176 else (maximum . snd . unzip) puniq
177 where _types = pmap::PeerMap.PeerMap
178 puniq = PeerMap.accumArray const pmap
181 [ run prop_PeerMap_addIdempotent
182 , run prop_PeerMap_removeIdempotent
183 , run prop_PeerMap_maxElem
184 , run prop_PeerMap_addFind
185 , run prop_PeerMap_findMissing
190 prop_Container_addTwo cdata i1 i2 =
191 fn i1 i2 cont == fn i2 i1 cont &&
192 fn i1 i2 cont == fn i1 i2 (fn i1 i2 cont)
193 where _types = (cdata::[Int],
195 cont = foldl (\c x -> Container.add x x c) Container.empty cdata
196 fn x1 x2 = Container.addTwo x1 x1 x2 x2
199 [ run prop_Container_addTwo ]
201 -- Simple instance tests, we only have setter/getters
203 prop_Instance_creat inst =
204 Instance.name inst == Instance.alias inst
206 prop_Instance_setIdx inst idx =
207 Instance.idx (Instance.setIdx inst idx) == idx
208 where _types = (inst::Instance.Instance, idx::Types.Idx)
210 prop_Instance_setName inst name =
211 Instance.name newinst == name &&
212 Instance.alias newinst == name
213 where _types = (inst::Instance.Instance, name::String)
214 newinst = Instance.setName inst name
216 prop_Instance_setAlias inst name =
217 Instance.name newinst == Instance.name inst &&
218 Instance.alias newinst == name
219 where _types = (inst::Instance.Instance, name::String)
220 newinst = Instance.setAlias inst name
222 prop_Instance_setPri inst pdx =
223 Instance.pNode (Instance.setPri inst pdx) == pdx
224 where _types = (inst::Instance.Instance, pdx::Types.Ndx)
226 prop_Instance_setSec inst sdx =
227 Instance.sNode (Instance.setSec inst sdx) == sdx
228 where _types = (inst::Instance.Instance, sdx::Types.Ndx)
230 prop_Instance_setBoth inst pdx sdx =
231 Instance.pNode si == pdx && Instance.sNode si == sdx
232 where _types = (inst::Instance.Instance, pdx::Types.Ndx, sdx::Types.Ndx)
233 si = Instance.setBoth inst pdx sdx
235 prop_Instance_runStatus_True inst =
236 let run_st = Instance.running inst
237 run_tx = Instance.runSt inst
239 run_tx `elem` Instance.runningStates ==> run_st
241 prop_Instance_runStatus_False inst =
242 let run_st = Instance.running inst
243 run_tx = Instance.runSt inst
245 run_tx `notElem` Instance.runningStates ==> not run_st
247 prop_Instance_shrinkMG inst =
248 Instance.mem inst >= 2 * Types.unitMem ==>
249 case Instance.shrinkByType inst Types.FailMem of
251 Instance.mem inst' == Instance.mem inst - Types.unitMem
253 where _types = (inst::Instance.Instance)
255 prop_Instance_shrinkMF inst =
256 Instance.mem inst < 2 * Types.unitMem ==>
257 not . isOk $ Instance.shrinkByType inst Types.FailMem
258 where _types = (inst::Instance.Instance)
260 prop_Instance_shrinkCG inst =
261 Instance.vcpus inst >= 2 * Types.unitCpu ==>
262 case Instance.shrinkByType inst Types.FailCPU of
264 Instance.vcpus inst' == Instance.vcpus inst - Types.unitCpu
266 where _types = (inst::Instance.Instance)
268 prop_Instance_shrinkCF inst =
269 Instance.vcpus inst < 2 * Types.unitCpu ==>
270 not . isOk $ Instance.shrinkByType inst Types.FailCPU
271 where _types = (inst::Instance.Instance)
273 prop_Instance_shrinkDG inst =
274 Instance.dsk inst >= 2 * Types.unitDsk ==>
275 case Instance.shrinkByType inst Types.FailDisk of
277 Instance.dsk inst' == Instance.dsk inst - Types.unitDsk
279 where _types = (inst::Instance.Instance)
281 prop_Instance_shrinkDF inst =
282 Instance.dsk inst < 2 * Types.unitDsk ==>
283 not . isOk $ Instance.shrinkByType inst Types.FailDisk
284 where _types = (inst::Instance.Instance)
286 prop_Instance_setMovable inst m =
287 Instance.movable inst' == m
288 where _types = (inst::Instance.Instance, m::Bool)
289 inst' = Instance.setMovable inst m
292 [ run prop_Instance_creat
293 , run prop_Instance_setIdx
294 , run prop_Instance_setName
295 , run prop_Instance_setAlias
296 , run prop_Instance_setPri
297 , run prop_Instance_setSec
298 , run prop_Instance_setBoth
299 , run prop_Instance_runStatus_True
300 , run prop_Instance_runStatus_False
301 , run prop_Instance_shrinkMG
302 , run prop_Instance_shrinkMF
303 , run prop_Instance_shrinkCG
304 , run prop_Instance_shrinkCF
305 , run prop_Instance_shrinkDG
306 , run prop_Instance_shrinkDF
307 , run prop_Instance_setMovable
310 -- Instance text loader tests
312 prop_Text_Load_Instance name mem dsk vcpus status pnode snode pdx sdx =
313 not (null pnode) && pdx >= 0 && sdx >= 0 ==>
314 let vcpus_s = show vcpus
322 else [(pnode, pdx), (snode, rsdx)]
324 inst = Text.loadInst ndx
325 [name, mem_s, dsk_s, vcpus_s, status, pnode, snode, tags]::
326 Maybe (String, Instance.Instance)
327 fail1 = Text.loadInst ndx
328 [name, mem_s, dsk_s, vcpus_s, status, pnode, pnode, tags]::
329 Maybe (String, Instance.Instance)
330 _types = ( name::String, mem::Int, dsk::Int
331 , vcpus::Int, status::String
332 , pnode::String, snode::String
333 , pdx::Types.Ndx, sdx::Types.Ndx)
338 (Instance.name i == name &&
339 Instance.vcpus i == vcpus &&
340 Instance.mem i == mem &&
341 Instance.pNode i == pdx &&
342 Instance.sNode i == (if null snode
343 then Node.noSecondary
347 prop_Text_Load_InstanceFail ktn fields =
348 length fields /= 8 ==> isNothing $ Text.loadInst ktn fields
350 prop_Text_Load_Node name tm nm fm td fd tc fo =
351 let conv v = if v < 0
363 any_broken = any (< 0) [tm, nm, fm, td, fd, tc]
364 in case Text.loadNode [name, tm_s, nm_s, fm_s, td_s, fd_s, tc_s, fo_s] of
366 Just (name', node) ->
368 then Node.offline node
369 else (Node.name node == name' && name' == name &&
370 Node.alias node == name &&
371 Node.tMem node == fromIntegral tm &&
372 Node.nMem node == nm &&
373 Node.fMem node == fm &&
374 Node.tDsk node == fromIntegral td &&
375 Node.fDsk node == fd &&
376 Node.tCpu node == fromIntegral tc)
378 prop_Text_Load_NodeFail fields =
379 length fields /= 8 ==> isNothing $ Text.loadNode fields
382 [ run prop_Text_Load_Instance
383 , run prop_Text_Load_InstanceFail
384 , run prop_Text_Load_Node
385 , run prop_Text_Load_NodeFail
390 prop_Node_setAlias node name =
391 Node.name newnode == Node.name node &&
392 Node.alias newnode == name
393 where _types = (node::Node.Node, name::String)
394 newnode = Node.setAlias node name
396 prop_Node_setOffline node status =
397 Node.offline newnode == status
398 where newnode = Node.setOffline node status
400 prop_Node_setXmem node xm =
401 Node.xMem newnode == xm
402 where newnode = Node.setXmem node xm
404 prop_Node_setMcpu node mc =
405 Node.mCpu newnode == mc
406 where newnode = Node.setMcpu node mc
408 -- | Check that an instance add with too high memory or disk will be rejected
409 prop_Node_addPriFM node inst = Instance.mem inst >= Node.fMem node &&
410 not (Node.failN1 node)
412 case Node.addPri node inst'' of
413 Types.OpFail Types.FailMem -> True
415 where _types = (node::Node.Node, inst::Instance.Instance)
416 inst' = setInstanceSmallerThanNode node inst
417 inst'' = inst' { Instance.mem = Instance.mem inst }
419 prop_Node_addPriFD node inst = Instance.dsk inst >= Node.fDsk node &&
420 not (Node.failN1 node)
422 case Node.addPri node inst'' of
423 Types.OpFail Types.FailDisk -> True
425 where _types = (node::Node.Node, inst::Instance.Instance)
426 inst' = setInstanceSmallerThanNode node inst
427 inst'' = inst' { Instance.dsk = Instance.dsk inst }
429 prop_Node_addPriFC node inst = Instance.vcpus inst > Node.availCpu node &&
430 not (Node.failN1 node)
432 case Node.addPri node inst'' of
433 Types.OpFail Types.FailCPU -> True
435 where _types = (node::Node.Node, inst::Instance.Instance)
436 inst' = setInstanceSmallerThanNode node inst
437 inst'' = inst' { Instance.vcpus = Instance.vcpus inst }
439 -- | Check that an instance add with too high memory or disk will be rejected
440 prop_Node_addSec node inst pdx =
441 (Instance.mem inst >= (Node.fMem node - Node.rMem node) ||
442 Instance.dsk inst >= Node.fDsk node) &&
443 not (Node.failN1 node)
444 ==> isFailure (Node.addSec node inst pdx)
445 where _types = (node::Node.Node, inst::Instance.Instance, pdx::Int)
447 newtype SmallRatio = SmallRatio Double deriving Show
448 instance Arbitrary SmallRatio where
451 return $ SmallRatio v
453 -- | Check mdsk setting
454 prop_Node_setMdsk node mx =
455 Node.loDsk node' >= 0 &&
456 fromIntegral (Node.loDsk node') <= Node.tDsk node &&
457 Node.availDisk node' >= 0 &&
458 Node.availDisk node' <= Node.fDsk node' &&
459 fromIntegral (Node.availDisk node') <= Node.tDsk node' &&
460 Node.mDsk node' == mx'
461 where _types = (node::Node.Node, mx::SmallRatio)
462 node' = Node.setMdsk node mx'
466 prop_Node_tagMaps_idempotent tags =
467 Node.delTags (Node.addTags m tags) tags == m
468 where _types = (tags::[String])
471 prop_Node_tagMaps_reject tags =
473 any (\t -> Node.rejectAddTags m [t]) tags
474 where _types = (tags::[String])
475 m = Node.addTags (Data.Map.empty) tags
477 prop_Node_showField node =
478 forAll (elements Node.defaultFields) $ \ field ->
479 fst (Node.showHeader field) /= Types.unknownField &&
480 Node.showField node field /= Types.unknownField
483 [ run prop_Node_setAlias
484 , run prop_Node_setOffline
485 , run prop_Node_setMcpu
486 , run prop_Node_setXmem
487 , run prop_Node_addPriFM
488 , run prop_Node_addPriFD
489 , run prop_Node_addPriFC
490 , run prop_Node_addSec
491 , run prop_Node_setMdsk
492 , run prop_Node_tagMaps_idempotent
493 , run prop_Node_tagMaps_reject
494 , run prop_Node_showField
500 -- | Check that the cluster score is close to zero for a homogeneous cluster
501 prop_Score_Zero node count =
502 (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
503 (Node.tDsk node > 0) && (Node.tMem node > 0)) ==>
504 let fn = Node.buildPeers node Container.empty
505 nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
506 nl = Container.fromAssocList nlst
507 score = Cluster.compCV nl
508 -- we can't say == 0 here as the floating point errors accumulate;
509 -- this should be much lower than the default score in CLI.hs
512 -- | Check that cluster stats are sane
513 prop_CStats_sane node count =
514 (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
515 (Node.availDisk node > 0) && (Node.availMem node > 0)) ==>
516 let fn = Node.buildPeers node Container.empty
517 nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
518 nl = Container.fromAssocList nlst
519 cstats = Cluster.totalResources nl
520 in Cluster.csAdsk cstats >= 0 &&
521 Cluster.csAdsk cstats <= Cluster.csFdsk cstats
523 -- | Check that one instance is allocated correctly, without
525 prop_ClusterAlloc_sane node inst =
526 forAll (choose (5, 20)) $ \count ->
527 not (Node.offline node)
528 && not (Node.failN1 node)
529 && Node.availDisk node > 0
530 && Node.availMem node > 0
532 let nl = makeSmallCluster node count
535 inst' = setInstanceSmallerThanNode node inst
536 in case Cluster.tryAlloc nl il inst' rqnodes of
538 Types.Ok (errs, _, sols3) ->
541 (_, (xnl, xi, _)):[] ->
542 let cv = Cluster.compCV xnl
543 il' = Container.add (Instance.idx xi) xi il
544 tbl = Cluster.Table xnl il' cv []
545 in not (canBalance tbl True False)
548 -- | Checks that on a 2-5 node cluster, we can allocate a random
549 -- instance spec via tiered allocation (whatever the original instance
550 -- spec), on either one or two nodes
551 prop_ClusterCanTieredAlloc node inst =
552 forAll (choose (2, 5)) $ \count ->
553 forAll (choose (1, 2)) $ \rqnodes ->
554 not (Node.offline node)
555 && not (Node.failN1 node)
558 let nl = makeSmallCluster node count
560 in case Cluster.tieredAlloc nl il inst rqnodes [] of
562 Types.Ok (_, _, ixes) -> not (null ixes)
564 -- | Checks that on a 4-8 node cluster, once we allocate an instance,
565 -- we can also evacuate it
566 prop_ClusterAllocEvac node inst =
567 forAll (choose (4, 8)) $ \count ->
568 not (Node.offline node)
569 && not (Node.failN1 node)
572 let nl = makeSmallCluster node count
575 inst' = setInstanceSmallerThanNode node inst
576 in case Cluster.tryAlloc nl il inst' rqnodes of
578 Types.Ok (errs, _, sols3) ->
581 (_, (xnl, xi, _)):[] ->
582 let sdx = Instance.sNode xi
583 il' = Container.add (Instance.idx xi) xi il
584 in case Cluster.tryEvac xnl il' [sdx] of
589 -- | Check that allocating multiple instances on a cluster, then
590 -- adding an empty node, results in a valid rebalance
591 prop_ClusterAllocBalance node =
592 forAll (choose (3, 5)) $ \count ->
593 not (Node.offline node)
594 && not (Node.failN1 node)
596 && not (isNodeBig node 8)
598 let nl = makeSmallCluster node count
599 (hnode, nl') = IntMap.deleteFindMax nl
602 i_templ = createInstance Types.unitMem Types.unitDsk Types.unitCpu
603 in case Cluster.iterateAlloc nl' il i_templ rqnodes [] of
605 Types.Ok (_, xnl, insts) ->
606 let ynl = Container.add (Node.idx hnode) hnode xnl
607 cv = Cluster.compCV ynl
609 Container.add (Instance.idx i) i l)
611 tbl = Cluster.Table ynl il' cv []
612 in canBalance tbl True False
615 [ run prop_Score_Zero
616 , run prop_CStats_sane
617 , run prop_ClusterAlloc_sane
618 , run prop_ClusterCanTieredAlloc
619 , run prop_ClusterAllocEvac
620 , run prop_ClusterAllocBalance