1 {-# LANGUAGE TemplateHaskell #-}
2 {-# OPTIONS_GHC -fno-warn-orphans #-}
4 {-| Unittests for ganeti-htools.
10 Copyright (C) 2009, 2010, 2011, 2012 Google Inc.
12 This program is free software; you can redistribute it and/or modify
13 it under the terms of the GNU General Public License as published by
14 the Free Software Foundation; either version 2 of the License, or
15 (at your option) any later version.
17 This program is distributed in the hope that it will be useful, but
18 WITHOUT ANY WARRANTY; without even the implied warranty of
19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 General Public License for more details.
22 You should have received a copy of the GNU General Public License
23 along with this program; if not, write to the Free Software
24 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
29 module Test.Ganeti.HTools.Cluster (testHTools_Cluster) where
31 import Test.QuickCheck hiding (Result)
33 import qualified Data.IntMap as IntMap
36 import Test.Ganeti.TestHelper
37 import Test.Ganeti.TestCommon
38 import Test.Ganeti.TestHTools
39 import Test.Ganeti.HTools.Instance ( genInstanceSmallerThanNode
40 , genInstanceMaybeBiggerThanNode )
41 import Test.Ganeti.HTools.Node (genOnlineNode, genNode)
43 import Ganeti.BasicTypes
44 import qualified Ganeti.HTools.Backend.IAlloc as IAlloc
45 import qualified Ganeti.HTools.Cluster as Cluster
46 import qualified Ganeti.HTools.Container as Container
47 import qualified Ganeti.HTools.Group as Group
48 import qualified Ganeti.HTools.Instance as Instance
49 import qualified Ganeti.HTools.Node as Node
50 import qualified Ganeti.HTools.Types as Types
51 import qualified Ganeti.Types as Types (EvacMode(..))
53 {-# ANN module "HLint: ignore Use camelCase" #-}
57 -- | Make a small cluster, both nodes and instances.
58 makeSmallEmptyCluster :: Node.Node -> Int -> Instance.Instance
59 -> (Node.List, Instance.List, Instance.Instance)
60 makeSmallEmptyCluster node count inst =
61 (makeSmallCluster node count, Container.empty,
62 setInstanceSmallerThanNode node inst)
64 -- | Checks if a node is "big" enough.
65 isNodeBig :: Int -> Node.Node -> Bool
66 isNodeBig size node = Node.availDisk node > size * Types.unitDsk
67 && Node.availMem node > size * Types.unitMem
68 && Node.availCpu node > size * Types.unitCpu
70 canBalance :: Cluster.Table -> Bool -> Bool -> Bool -> Bool
71 canBalance tbl dm im evac = isJust $ Cluster.tryBalance tbl dm im evac 0 0
73 -- | Assigns a new fresh instance to a cluster; this is not
74 -- allocation, so no resource checks are done.
75 assignInstance :: Node.List -> Instance.List -> Instance.Instance ->
76 Types.Idx -> Types.Idx ->
77 (Node.List, Instance.List)
78 assignInstance nl il inst pdx sdx =
79 let pnode = Container.find pdx nl
80 snode = Container.find sdx nl
81 maxiidx = if Container.null il
83 else fst (Container.findMax il) + 1
84 inst' = inst { Instance.idx = maxiidx,
85 Instance.pNode = pdx, Instance.sNode = sdx }
86 pnode' = Node.setPri pnode inst'
87 snode' = Node.setSec snode inst'
88 nl' = Container.addTwo pdx pnode' sdx snode' nl
89 il' = Container.add maxiidx inst' il
92 -- | Checks if an instance is mirrored.
93 isMirrored :: Instance.Instance -> Bool
94 isMirrored = (/= Types.MirrorNone) . Instance.mirrorType
96 -- | Returns the possible change node types for a disk template.
97 evacModeOptions :: Types.MirrorType -> [Types.EvacMode]
98 evacModeOptions Types.MirrorNone = []
99 evacModeOptions Types.MirrorInternal = [minBound..maxBound] -- DRBD can do all
100 evacModeOptions Types.MirrorExternal = [Types.ChangePrimary, Types.ChangeAll]
104 -- | Check that the cluster score is close to zero for a homogeneous
106 prop_Score_Zero :: Node.Node -> Property
107 prop_Score_Zero node =
108 forAll (choose (1, 1024)) $ \count ->
109 (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
110 (Node.tDsk node > 0) && (Node.tMem node > 0) &&
111 (Node.tSpindles node > 0) && (Node.tCpu node > 0)) ==>
112 let fn = Node.buildPeers node Container.empty
113 nlst = replicate count fn
114 score = Cluster.compCVNodes nlst
115 -- we can't say == 0 here as the floating point errors accumulate;
116 -- this should be much lower than the default score in CLI.hs
119 -- | Check that cluster stats are sane.
120 prop_CStats_sane :: Property
122 forAll (choose (1, 1024)) $ \count ->
123 forAll genOnlineNode $ \node ->
124 let fn = Node.buildPeers node Container.empty
125 nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
126 nl = Container.fromList nlst
127 cstats = Cluster.totalResources nl
128 in Cluster.csAdsk cstats >= 0 &&
129 Cluster.csAdsk cstats <= Cluster.csFdsk cstats
131 -- | Check that one instance is allocated correctly on an empty cluster,
132 -- without rebalances needed.
133 prop_Alloc_sane :: Instance.Instance -> Property
134 prop_Alloc_sane inst =
135 forAll (choose (5, 20)) $ \count ->
136 forAll genOnlineNode $ \node ->
137 let (nl, il, inst') = makeSmallEmptyCluster node count inst
138 reqnodes = Instance.requiredNodes $ Instance.diskTemplate inst
139 in case Cluster.genAllocNodes defGroupList nl reqnodes True >>=
140 Cluster.tryAlloc nl il inst' of
141 Bad msg -> failTest msg
143 case Cluster.asSolution as of
144 Nothing -> failTest "Failed to allocate, empty solution"
145 Just (xnl, xi, _, cv) ->
146 let il' = Container.add (Instance.idx xi) xi il
147 tbl = Cluster.Table xnl il' cv []
148 in printTestCase "Cluster can be balanced after allocation"
149 (not (canBalance tbl True True False)) .&&.
150 printTestCase "Solution score differs from actual node list:"
151 (Cluster.compCV xnl ==? cv)
153 -- | Checks that on a 2-5 node cluster, we can allocate a random
154 -- instance spec via tiered allocation (whatever the original instance
155 -- spec), on either one or two nodes. Furthermore, we test that
156 -- computed allocation statistics are correct.
157 prop_CanTieredAlloc :: Property
158 prop_CanTieredAlloc =
159 forAll (choose (2, 5)) $ \count ->
160 forAll (genOnlineNode `suchThat` isNodeBig 4) $ \node ->
161 forAll (genInstanceMaybeBiggerThanNode node) $ \inst ->
162 let nl = makeSmallCluster node count
164 rqnodes = Instance.requiredNodes $ Instance.diskTemplate inst
165 allocnodes = Cluster.genAllocNodes defGroupList nl rqnodes True
166 in case allocnodes >>= \allocnodes' ->
167 Cluster.tieredAlloc nl il (Just 5) inst allocnodes' [] [] of
168 Bad msg -> failTest $ "Failed to tiered alloc: " ++ msg
169 Ok (_, nl', il', ixes, cstats) ->
170 let (ai_alloc, ai_pool, ai_unav) =
171 Cluster.computeAllocationDelta
172 (Cluster.totalResources nl)
173 (Cluster.totalResources nl')
174 all_nodes fn = sum $ map fn (Container.elems nl)
175 all_res fn = sum $ map fn [ai_alloc, ai_pool, ai_unav]
177 [ printTestCase "No instances allocated" $ not (null ixes)
178 , IntMap.size il' ==? length ixes
179 , length ixes ==? length cstats
180 , all_res Types.allocInfoVCpus ==? all_nodes Node.hiCpu
181 , all_res Types.allocInfoNCpus ==? all_nodes Node.tCpu
182 , all_res Types.allocInfoMem ==? truncate (all_nodes Node.tMem)
183 , all_res Types.allocInfoDisk ==? truncate (all_nodes Node.tDsk)
186 -- | Helper function to create a cluster with the given range of nodes
187 -- and allocate an instance on it.
188 genClusterAlloc :: Int -> Node.Node -> Instance.Instance
189 -> Result (Node.List, Instance.List, Instance.Instance)
190 genClusterAlloc count node inst =
191 let nl = makeSmallCluster node count
192 reqnodes = Instance.requiredNodes $ Instance.diskTemplate inst
193 in case Cluster.genAllocNodes defGroupList nl reqnodes True >>=
194 Cluster.tryAlloc nl Container.empty inst of
195 Bad msg -> Bad $ "Can't allocate: " ++ msg
197 case Cluster.asSolution as of
198 Nothing -> Bad "Empty solution?"
199 Just (xnl, xi, _, _) ->
200 let xil = Container.add (Instance.idx xi) xi Container.empty
203 -- | Checks that on a 4-8 node cluster, once we allocate an instance,
204 -- we can also relocate it.
205 prop_AllocRelocate :: Property
207 forAll (choose (4, 8)) $ \count ->
208 forAll (genOnlineNode `suchThat` isNodeBig 4) $ \node ->
209 forAll (genInstanceSmallerThanNode node `suchThat` isMirrored) $ \inst ->
210 case genClusterAlloc count node inst of
211 Bad msg -> failTest msg
212 Ok (nl, il, inst') ->
213 case IAlloc.processRelocate defGroupList nl il
214 (Instance.idx inst) 1
215 [(if Instance.diskTemplate inst' == Types.DTDrbd8
217 else Instance.pNode) inst'] of
219 Bad msg -> failTest $ "Failed to relocate: " ++ msg
221 -- | Helper property checker for the result of a nodeEvac or
222 -- changeGroup operation.
223 check_EvacMode :: Group.Group -> Instance.Instance
224 -> Result (Node.List, Instance.List, Cluster.EvacSolution)
226 check_EvacMode grp inst result =
228 Bad msg -> failTest $ "Couldn't evacuate/change group:" ++ msg
230 let moved = Cluster.esMoved es
231 failed = Cluster.esFailed es
232 opcodes = not . null $ Cluster.esOpCodes es
234 [ failmsg ("'failed' not empty: " ++ show failed) (null failed)
235 , failmsg "'opcodes' is null" opcodes
238 failmsg "invalid instance moved" (idx == idx') .&&.
239 failmsg "wrong target group" (gdx == Group.idx grp)
240 v -> failmsg ("invalid solution: " ++ show v) False
242 where failmsg :: String -> Bool -> Property
243 failmsg msg = printTestCase ("Failed to evacuate: " ++ msg)
244 idx = Instance.idx inst
246 -- | Checks that on a 4-8 node cluster, once we allocate an instance,
247 -- we can also node-evacuate it.
248 prop_AllocEvacuate :: Property
250 forAll (choose (4, 8)) $ \count ->
251 forAll (genOnlineNode `suchThat` isNodeBig 4) $ \node ->
252 forAll (genInstanceSmallerThanNode node `suchThat` isMirrored) $ \inst ->
253 case genClusterAlloc count node inst of
254 Bad msg -> failTest msg
255 Ok (nl, il, inst') ->
256 conjoin . map (\mode -> check_EvacMode defGroup inst' $
257 Cluster.tryNodeEvac defGroupList nl il mode
258 [Instance.idx inst']) .
260 Instance.mirrorType $ inst'
262 -- | Checks that on a 4-8 node cluster with two node groups, once we
263 -- allocate an instance on the first node group, we can also change
265 prop_AllocChangeGroup :: Property
266 prop_AllocChangeGroup =
267 forAll (choose (4, 8)) $ \count ->
268 forAll (genOnlineNode `suchThat` isNodeBig 4) $ \node ->
269 forAll (genInstanceSmallerThanNode node `suchThat` isMirrored) $ \inst ->
270 case genClusterAlloc count node inst of
271 Bad msg -> failTest msg
272 Ok (nl, il, inst') ->
273 -- we need to add a second node group and nodes to the cluster
274 let nl2 = Container.elems $ makeSmallCluster node count
275 grp2 = Group.setIdx defGroup (Group.idx defGroup + 1)
276 maxndx = maximum . map Node.idx $ nl2
277 nl3 = map (\n -> n { Node.group = Group.idx grp2
278 , Node.idx = Node.idx n + maxndx }) nl2
279 nl4 = Container.fromList . map (\n -> (Node.idx n, n)) $ nl3
280 gl' = Container.add (Group.idx grp2) grp2 defGroupList
281 nl' = IntMap.union nl nl4
282 in check_EvacMode grp2 inst' $
283 Cluster.tryChangeGroup gl' nl' il [] [Instance.idx inst']
285 -- | Check that allocating multiple instances on a cluster, then
286 -- adding an empty node, results in a valid rebalance.
287 prop_AllocBalance :: Property
289 forAll (genNode (Just 5) (Just 128)) $ \node ->
290 forAll (choose (3, 5)) $ \count ->
291 not (Node.offline node) && not (Node.failN1 node) ==>
292 let nl = makeSmallCluster node count
293 hnode = snd $ IntMap.findMax nl
294 nl' = IntMap.deleteMax nl
296 allocnodes = Cluster.genAllocNodes defGroupList nl' 2 True
297 i_templ = createInstance Types.unitMem Types.unitDsk Types.unitCpu
298 in case allocnodes >>= \allocnodes' ->
299 Cluster.iterateAlloc nl' il (Just 5) i_templ allocnodes' [] [] of
300 Bad msg -> failTest $ "Failed to allocate: " ++ msg
301 Ok (_, _, _, [], _) -> failTest "Failed to allocate: no instances"
302 Ok (_, xnl, il', _, _) ->
303 let ynl = Container.add (Node.idx hnode) hnode xnl
304 cv = Cluster.compCV ynl
305 tbl = Cluster.Table ynl il' cv []
306 in printTestCase "Failed to rebalance" $
307 canBalance tbl True True False
309 -- | Checks consistency.
310 prop_CheckConsistency :: Node.Node -> Instance.Instance -> Bool
311 prop_CheckConsistency node inst =
312 let nl = makeSmallCluster node 3
313 (node1, node2, node3) =
314 case Container.elems nl of
315 [a, b, c] -> (a, b, c)
316 l -> error $ "Invalid node list out of makeSmallCluster/3: " ++
318 node3' = node3 { Node.group = 1 }
319 nl' = Container.add (Node.idx node3') node3' nl
320 inst1 = Instance.setBoth inst (Node.idx node1) (Node.idx node2)
321 inst2 = Instance.setBoth inst (Node.idx node1) Node.noSecondary
322 inst3 = Instance.setBoth inst (Node.idx node1) (Node.idx node3)
323 ccheck = Cluster.findSplitInstances nl' . Container.fromList
324 in null (ccheck [(0, inst1)]) &&
325 null (ccheck [(0, inst2)]) &&
326 (not . null $ ccheck [(0, inst3)])
328 -- | For now, we only test that we don't lose instances during the split.
329 prop_SplitCluster :: Node.Node -> Instance.Instance -> Property
330 prop_SplitCluster node inst =
331 forAll (choose (0, 100)) $ \icnt ->
332 let nl = makeSmallCluster node 2
333 (nl', il') = foldl (\(ns, is) _ -> assignInstance ns is inst 0 1)
334 (nl, Container.empty) [1..icnt]
335 gni = Cluster.splitCluster nl' il'
336 in sum (map (Container.size . snd . snd) gni) == icnt &&
337 all (\(guuid, (nl'', _)) -> all ((== guuid) . Node.group)
338 (Container.elems nl'')) gni
340 -- | Helper function to check if we can allocate an instance on a
341 -- given node list. Successful allocation is denoted by 'Nothing',
342 -- otherwise the 'Just' value will contain the error message.
343 canAllocOn :: Node.List -> Int -> Instance.Instance -> Maybe String
344 canAllocOn nl reqnodes inst =
345 case Cluster.genAllocNodes defGroupList nl reqnodes True >>=
346 Cluster.tryAlloc nl Container.empty inst of
347 Bad msg -> Just $ "Can't allocate: " ++ msg
349 case Cluster.asSolution as of
350 Nothing -> Just $ "No allocation solution; failures: " ++
351 show (Cluster.collapseFailures $ Cluster.asFailures as)
354 -- | Checks that allocation obeys minimum and maximum instance
355 -- policies. The unittest generates a random node, duplicates it /count/
356 -- times, and generates a random instance that can be allocated on
357 -- this mini-cluster; it then checks that after applying a policy that
358 -- the instance doesn't fits, the allocation fails.
359 prop_AllocPolicy :: Property
361 forAll genOnlineNode $ \node ->
362 forAll (choose (5, 20)) $ \count ->
363 forAll (genInstanceSmallerThanNode node) $ \inst ->
364 forAll (arbitrary `suchThat`
365 (isBad . flip (Instance.instMatchesPolicy inst)
366 (Node.exclStorage node))) $ \ipol ->
367 let rqn = Instance.requiredNodes $ Instance.diskTemplate inst
368 node' = Node.setPolicy ipol node
369 nl = makeSmallCluster node' count
370 in printTestCase "Allocation check:"
371 (isNothing (canAllocOn (makeSmallCluster node count) rqn inst)) .&&.
372 printTestCase "Policy failure check:" (isJust $ canAllocOn nl rqn inst)
374 testSuite "HTools/Cluster"
378 , 'prop_CanTieredAlloc
379 , 'prop_AllocRelocate
380 , 'prop_AllocEvacuate
381 , 'prop_AllocChangeGroup
383 , 'prop_CheckConsistency