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 -- | Check that an instance add with too high memory or disk will be rejected
391 prop_Node_addPriFM node inst = Instance.mem inst >= Node.fMem node &&
392 not (Node.failN1 node)
394 case Node.addPri node inst'' of
395 Types.OpFail Types.FailMem -> True
397 where _types = (node::Node.Node, inst::Instance.Instance)
398 inst' = setInstanceSmallerThanNode node inst
399 inst'' = inst' { Instance.mem = Instance.mem inst }
401 prop_Node_addPriFD node inst = Instance.dsk inst >= Node.fDsk node &&
402 not (Node.failN1 node)
404 case Node.addPri node inst'' of
405 Types.OpFail Types.FailDisk -> True
407 where _types = (node::Node.Node, inst::Instance.Instance)
408 inst' = setInstanceSmallerThanNode node inst
409 inst'' = inst' { Instance.dsk = Instance.dsk inst }
411 prop_Node_addPriFC node inst = Instance.vcpus inst > Node.availCpu node &&
412 not (Node.failN1 node)
414 case Node.addPri node inst'' of
415 Types.OpFail Types.FailCPU -> True
417 where _types = (node::Node.Node, inst::Instance.Instance)
418 inst' = setInstanceSmallerThanNode node inst
419 inst'' = inst' { Instance.vcpus = Instance.vcpus inst }
421 -- | Check that an instance add with too high memory or disk will be rejected
422 prop_Node_addSec node inst pdx =
423 (Instance.mem inst >= (Node.fMem node - Node.rMem node) ||
424 Instance.dsk inst >= Node.fDsk node) &&
425 not (Node.failN1 node)
426 ==> isFailure (Node.addSec node inst pdx)
427 where _types = (node::Node.Node, inst::Instance.Instance, pdx::Int)
429 newtype SmallRatio = SmallRatio Double deriving Show
430 instance Arbitrary SmallRatio where
433 return $ SmallRatio v
435 -- | Check mdsk setting
436 prop_Node_setMdsk node mx =
437 Node.loDsk node' >= 0 &&
438 fromIntegral (Node.loDsk node') <= Node.tDsk node &&
439 Node.availDisk node' >= 0 &&
440 Node.availDisk node' <= Node.fDsk node' &&
441 fromIntegral (Node.availDisk node') <= Node.tDsk node'
442 where _types = (node::Node.Node, mx::SmallRatio)
443 node' = Node.setMdsk node mx'
447 prop_Node_tagMaps_idempotent tags =
448 Node.delTags (Node.addTags m tags) tags == m
449 where _types = (tags::[String])
452 prop_Node_tagMaps_reject tags =
454 any (\t -> Node.rejectAddTags m [t]) tags
455 where _types = (tags::[String])
456 m = Node.addTags (Data.Map.empty) tags
459 [ run prop_Node_addPriFM
460 , run prop_Node_addPriFD
461 , run prop_Node_addPriFC
462 , run prop_Node_addSec
463 , run prop_Node_setMdsk
464 , run prop_Node_tagMaps_idempotent
465 , run prop_Node_tagMaps_reject
471 -- | Check that the cluster score is close to zero for a homogeneous cluster
472 prop_Score_Zero node count =
473 (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
474 (Node.tDsk node > 0) && (Node.tMem node > 0)) ==>
475 let fn = Node.buildPeers node Container.empty
476 nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
477 nl = Container.fromAssocList nlst
478 score = Cluster.compCV nl
479 -- we can't say == 0 here as the floating point errors accumulate;
480 -- this should be much lower than the default score in CLI.hs
483 -- | Check that cluster stats are sane
484 prop_CStats_sane node count =
485 (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
486 (Node.availDisk node > 0) && (Node.availMem node > 0)) ==>
487 let fn = Node.buildPeers node Container.empty
488 nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
489 nl = Container.fromAssocList nlst
490 cstats = Cluster.totalResources nl
491 in Cluster.csAdsk cstats >= 0 &&
492 Cluster.csAdsk cstats <= Cluster.csFdsk cstats
494 -- | Check that one instance is allocated correctly, without
496 prop_ClusterAlloc_sane node inst =
497 forAll (choose (5, 20)) $ \count ->
498 not (Node.offline node)
499 && not (Node.failN1 node)
500 && Node.availDisk node > 0
501 && Node.availMem node > 0
503 let nl = makeSmallCluster node count
506 inst' = setInstanceSmallerThanNode node inst
507 in case Cluster.tryAlloc nl il inst' rqnodes of
509 Types.Ok (errs, _, sols3) ->
512 (_, (xnl, xi, _)):[] ->
513 let cv = Cluster.compCV xnl
514 il' = Container.add (Instance.idx xi) xi il
515 tbl = Cluster.Table xnl il' cv []
516 in not (canBalance tbl True False)
519 -- | Checks that on a 2-5 node cluster, we can allocate a random
520 -- instance spec via tiered allocation (whatever the original instance
521 -- spec), on either one or two nodes
522 prop_ClusterCanTieredAlloc node inst =
523 forAll (choose (2, 5)) $ \count ->
524 forAll (choose (1, 2)) $ \rqnodes ->
525 not (Node.offline node)
526 && not (Node.failN1 node)
529 let nl = makeSmallCluster node count
531 in case Cluster.tieredAlloc nl il inst rqnodes [] of
533 Types.Ok (_, _, ixes) -> not (null ixes)
535 -- | Checks that on a 4-8 node cluster, once we allocate an instance,
536 -- we can also evacuate it
537 prop_ClusterAllocEvac node inst =
538 forAll (choose (4, 8)) $ \count ->
539 not (Node.offline node)
540 && not (Node.failN1 node)
543 let nl = makeSmallCluster node count
546 inst' = setInstanceSmallerThanNode node inst
547 in case Cluster.tryAlloc nl il inst' rqnodes of
549 Types.Ok (errs, _, sols3) ->
552 (_, (xnl, xi, _)):[] ->
553 let sdx = Instance.sNode xi
554 il' = Container.add (Instance.idx xi) xi il
555 in case Cluster.tryEvac xnl il' [sdx] of
560 -- | Check that allocating multiple instances on a cluster, then
561 -- adding an empty node, results in a valid rebalance
562 prop_ClusterAllocBalance node =
563 forAll (choose (3, 5)) $ \count ->
564 not (Node.offline node)
565 && not (Node.failN1 node)
567 && not (isNodeBig node 8)
569 let nl = makeSmallCluster node count
570 (hnode, nl') = IntMap.deleteFindMax nl
573 i_templ = createInstance Types.unitMem Types.unitDsk Types.unitCpu
574 in case Cluster.iterateAlloc nl' il i_templ rqnodes [] of
576 Types.Ok (_, xnl, insts) ->
577 let ynl = Container.add (Node.idx hnode) hnode xnl
578 cv = Cluster.compCV ynl
580 Container.add (Instance.idx i) i l)
582 tbl = Cluster.Table ynl il' cv []
583 in canBalance tbl True False
586 [ run prop_Score_Zero
587 , run prop_CStats_sane
588 , run prop_ClusterAlloc_sane
589 , run prop_ClusterCanTieredAlloc
590 , run prop_ClusterAllocEvac
591 , run prop_ClusterAllocBalance