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.Node
32 , setInstanceSmallerThanNode
38 import Test.QuickCheck
42 import qualified Data.Map as Map
43 import qualified Data.Graph as Graph
46 import Test.Ganeti.TestHelper
47 import Test.Ganeti.TestCommon
48 import Test.Ganeti.TestHTools
49 import Test.Ganeti.HTools.Instance ( genInstanceSmallerThanNode
51 , genInstanceOnNodeList)
53 import Ganeti.BasicTypes
54 import qualified Ganeti.HTools.Loader as Loader
55 import qualified Ganeti.HTools.Container as Container
56 import qualified Ganeti.HTools.Instance as Instance
57 import qualified Ganeti.HTools.Node as Node
58 import qualified Ganeti.HTools.Types as Types
59 import qualified Ganeti.HTools.Graph as HGraph
61 {-# ANN module "HLint: ignore Use camelCase" #-}
63 -- * Arbitrary instances
65 -- | Generates an arbitrary node based on sizing information.
66 genNode :: Maybe Int -- ^ Minimum node size in terms of units
67 -> Maybe Int -- ^ Maximum node size (when Nothing, bounded
68 -- just by the max... constants)
70 genNode min_multiplier max_multiplier = do
71 let (base_mem, base_dsk, base_cpu) =
72 case min_multiplier of
73 Just mm -> (mm * Types.unitMem,
77 (top_mem, top_dsk, top_cpu) =
78 case max_multiplier of
79 Just mm -> (mm * Types.unitMem,
82 Nothing -> (maxMem, maxDsk, maxCpu)
84 mem_t <- choose (base_mem, top_mem)
85 mem_f <- choose (base_mem, mem_t)
86 mem_n <- choose (0, mem_t - mem_f)
87 dsk_t <- choose (base_dsk, top_dsk)
88 dsk_f <- choose (base_dsk, dsk_t)
89 cpu_t <- choose (base_cpu, top_cpu)
91 let n = Node.create name (fromIntegral mem_t) mem_n mem_f
92 (fromIntegral dsk_t) dsk_f (fromIntegral cpu_t) offl 1 0
93 n' = Node.setPolicy nullIPolicy n
94 return $ Node.buildPeers n' Container.empty
96 -- | Helper function to generate a sane node.
97 genOnlineNode :: Gen Node.Node
99 arbitrary `suchThat` (\n -> not (Node.offline n) &&
100 not (Node.failN1 n) &&
101 Node.availDisk n > 0 &&
102 Node.availMem n > 0 &&
106 instance Arbitrary Node.Node where
107 arbitrary = genNode Nothing Nothing
109 -- | Node list generator.
110 -- Given a node generator, create a random length node list. Note that "real"
111 -- clusters always have at least one node, so we don't generate empty node
113 genNodeList :: Gen Node.Node -> Gen Node.List
114 genNodeList ngen = fmap (snd . Loader.assignIndices) names_nodes
115 where names_nodes = (fmap . map) (\n -> (Node.name n, n)) $ listOf1 ngen
117 -- | Generate a node list, an instance list, and a node graph.
118 -- We choose instances with nodes contained in the node list.
119 genNodeGraph :: Gen (Maybe Graph.Graph, Node.List, Instance.List)
121 nl <- genNodeList genOnlineNode `suchThat` ((2<=).Container.size)
122 il <- genInstanceList (genInstanceOnNodeList nl)
123 return (Node.mkNodeGraph nl il, nl, il)
127 prop_setAlias :: Node.Node -> String -> Bool
128 prop_setAlias node name =
129 Node.name newnode == Node.name node &&
130 Node.alias newnode == name
131 where newnode = Node.setAlias node name
133 prop_setOffline :: Node.Node -> Bool -> Property
134 prop_setOffline node status =
135 Node.offline newnode ==? status
136 where newnode = Node.setOffline node status
138 prop_setXmem :: Node.Node -> Int -> Property
139 prop_setXmem node xm =
140 Node.xMem newnode ==? xm
141 where newnode = Node.setXmem node xm
143 prop_setMcpu :: Node.Node -> Double -> Property
144 prop_setMcpu node mc =
145 Types.iPolicyVcpuRatio (Node.iPolicy newnode) ==? mc
146 where newnode = Node.setMcpu node mc
148 prop_setFmemGreater :: Node.Node -> Int -> Property
149 prop_setFmemGreater node new_mem =
150 not (Node.failN1 node) && (Node.rMem node >= 0) &&
151 (new_mem > Node.rMem node) ==>
152 not (Node.failN1 (Node.setFmem node new_mem))
154 prop_setFmemExact :: Node.Node -> Property
155 prop_setFmemExact node =
156 not (Node.failN1 node) && (Node.rMem node >= 0) ==>
157 not (Node.failN1 (Node.setFmem node (Node.rMem node)))
159 -- Check if adding an instance that consumes exactly all reserved
160 -- memory does not raise an N+1 error
161 prop_addPri_NoN1Fail :: Property
162 prop_addPri_NoN1Fail =
163 forAll genOnlineNode $ \node ->
164 forAll (genInstanceSmallerThanNode node) $ \inst ->
165 let inst' = inst { Instance.mem = Node.fMem node - Node.rMem node }
166 in (Node.addPri node inst' /=? Bad Types.FailN1)
168 -- | Check that an instance add with too high memory or disk will be
170 prop_addPriFM :: Node.Node -> Instance.Instance -> Property
171 prop_addPriFM node inst =
172 Instance.mem inst >= Node.fMem node && not (Node.failN1 node) &&
173 not (Instance.isOffline inst) ==>
174 (Node.addPri node inst'' ==? Bad Types.FailMem)
175 where inst' = setInstanceSmallerThanNode node inst
176 inst'' = inst' { Instance.mem = Instance.mem inst }
178 -- | Check that adding a primary instance with too much disk fails
179 -- with type FailDisk.
180 prop_addPriFD :: Node.Node -> Instance.Instance -> Property
181 prop_addPriFD node inst =
182 forAll (elements Instance.localStorageTemplates) $ \dt ->
183 Instance.dsk inst >= Node.fDsk node && not (Node.failN1 node) ==>
184 let inst' = setInstanceSmallerThanNode node inst
185 inst'' = inst' { Instance.dsk = Instance.dsk inst
186 , Instance.diskTemplate = dt }
187 in (Node.addPri node inst'' ==? Bad Types.FailDisk)
189 -- | Check that adding a primary instance with too many VCPUs fails
190 -- with type FailCPU.
191 prop_addPriFC :: Property
193 forAll (choose (1, maxCpu)) $ \extra ->
194 forAll genOnlineNode $ \node ->
195 forAll (arbitrary `suchThat` Instance.notOffline) $ \inst ->
196 let inst' = setInstanceSmallerThanNode node inst
197 inst'' = inst' { Instance.vcpus = Node.availCpu node + extra }
198 in case Node.addPri node inst'' of
199 Bad Types.FailCPU -> passTest
200 v -> failTest $ "Expected OpFail FailCPU, but got " ++ show v
202 -- | Check that an instance add with too high memory or disk will be
204 prop_addSec :: Node.Node -> Instance.Instance -> Int -> Property
205 prop_addSec node inst pdx =
206 ((Instance.mem inst >= (Node.fMem node - Node.rMem node) &&
207 not (Instance.isOffline inst)) ||
208 Instance.dsk inst >= Node.fDsk node) &&
209 not (Node.failN1 node) ==>
210 isBad (Node.addSec node inst pdx)
212 -- | Check that an offline instance with reasonable disk size but
213 -- extra mem/cpu can always be added.
214 prop_addOfflinePri :: NonNegative Int -> NonNegative Int -> Property
215 prop_addOfflinePri (NonNegative extra_mem) (NonNegative extra_cpu) =
216 forAll genOnlineNode $ \node ->
217 forAll (genInstanceSmallerThanNode node) $ \inst ->
218 let inst' = inst { Instance.runSt = Types.StatusOffline
219 , Instance.mem = Node.availMem node + extra_mem
220 , Instance.vcpus = Node.availCpu node + extra_cpu }
221 in case Node.addPri node inst' of
223 v -> failTest $ "Expected OpGood, but got: " ++ show v
225 -- | Check that an offline instance with reasonable disk size but
226 -- extra mem/cpu can always be added.
227 prop_addOfflineSec :: NonNegative Int -> NonNegative Int
228 -> Types.Ndx -> Property
229 prop_addOfflineSec (NonNegative extra_mem) (NonNegative extra_cpu) pdx =
230 forAll genOnlineNode $ \node ->
231 forAll (genInstanceSmallerThanNode node) $ \inst ->
232 let inst' = inst { Instance.runSt = Types.StatusOffline
233 , Instance.mem = Node.availMem node + extra_mem
234 , Instance.vcpus = Node.availCpu node + extra_cpu
235 , Instance.diskTemplate = Types.DTDrbd8 }
236 in case Node.addSec node inst' pdx of
238 v -> failTest $ "Expected OpGood/OpGood, but got: " ++ show v
240 -- | Checks for memory reservation changes.
241 prop_rMem :: Instance.Instance -> Property
243 not (Instance.isOffline inst) ==>
244 forAll (genOnlineNode `suchThat` ((> Types.unitMem) . Node.fMem)) $ \node ->
245 -- ab = auto_balance, nb = non-auto_balance
246 -- we use -1 as the primary node of the instance
247 let inst' = inst { Instance.pNode = -1, Instance.autoBalance = True
248 , Instance.diskTemplate = Types.DTDrbd8 }
249 inst_ab = setInstanceSmallerThanNode node inst'
250 inst_nb = inst_ab { Instance.autoBalance = False }
251 -- now we have the two instances, identical except the
252 -- autoBalance attribute
253 orig_rmem = Node.rMem node
254 inst_idx = Instance.idx inst_ab
255 node_add_ab = Node.addSec node inst_ab (-1)
256 node_add_nb = Node.addSec node inst_nb (-1)
257 node_del_ab = liftM (`Node.removeSec` inst_ab) node_add_ab
258 node_del_nb = liftM (`Node.removeSec` inst_nb) node_add_nb
259 in case (node_add_ab, node_add_nb, node_del_ab, node_del_nb) of
262 printTestCase "Consistency checks failed" $
263 Node.rMem a_ab > orig_rmem &&
264 Node.rMem a_ab - orig_rmem == Instance.mem inst_ab &&
265 Node.rMem a_nb == orig_rmem &&
266 Node.rMem d_ab == orig_rmem &&
267 Node.rMem d_nb == orig_rmem &&
268 -- this is not related to rMem, but as good a place to
270 inst_idx `elem` Node.sList a_ab &&
271 inst_idx `notElem` Node.sList d_ab
272 x -> failTest $ "Failed to add/remove instances: " ++ show x
274 -- | Check mdsk setting.
275 prop_setMdsk :: Node.Node -> SmallRatio -> Bool
276 prop_setMdsk node mx =
277 Node.loDsk node' >= 0 &&
278 fromIntegral (Node.loDsk node') <= Node.tDsk node &&
279 Node.availDisk node' >= 0 &&
280 Node.availDisk node' <= Node.fDsk node' &&
281 fromIntegral (Node.availDisk node') <= Node.tDsk node' &&
282 Node.mDsk node' == mx'
283 where node' = Node.setMdsk node mx'
287 prop_tagMaps_idempotent :: Property
288 prop_tagMaps_idempotent =
289 forAll genTags $ \tags ->
290 Node.delTags (Node.addTags m tags) tags ==? m
293 prop_tagMaps_reject :: Property
294 prop_tagMaps_reject =
295 forAll (genTags `suchThat` (not . null)) $ \tags ->
296 let m = Node.addTags Map.empty tags
297 in all (\t -> Node.rejectAddTags m [t]) tags
299 prop_showField :: Node.Node -> Property
300 prop_showField node =
301 forAll (elements Node.defaultFields) $ \ field ->
302 fst (Node.showHeader field) /= Types.unknownField &&
303 Node.showField node field /= Types.unknownField
305 prop_computeGroups :: [Node.Node] -> Bool
306 prop_computeGroups nodes =
307 let ng = Node.computeGroups nodes
308 onlyuuid = map fst ng
309 in length nodes == sum (map (length . snd) ng) &&
310 all (\(guuid, ns) -> all ((== guuid) . Node.group) ns) ng &&
311 length (nub onlyuuid) == length onlyuuid &&
312 (null nodes || not (null ng))
314 -- Check idempotence of add/remove operations
315 prop_addPri_idempotent :: Property
316 prop_addPri_idempotent =
317 forAll genOnlineNode $ \node ->
318 forAll (genInstanceSmallerThanNode node) $ \inst ->
319 case Node.addPri node inst of
320 Ok node' -> Node.removePri node' inst ==? node
321 _ -> failTest "Can't add instance"
323 prop_addSec_idempotent :: Property
324 prop_addSec_idempotent =
325 forAll genOnlineNode $ \node ->
326 forAll (genInstanceSmallerThanNode node) $ \inst ->
327 let pdx = Node.idx node + 1
328 inst' = Instance.setPri inst pdx
329 inst'' = inst' { Instance.diskTemplate = Types.DTDrbd8 }
330 in case Node.addSec node inst'' pdx of
331 Ok node' -> Node.removeSec node' inst'' ==? node
332 _ -> failTest "Can't add instance"
334 -- | Check that no graph is created on an empty node list.
335 case_emptyNodeList :: Assertion
337 assertEqual "" Nothing $ Node.mkNodeGraph emptynodes emptyinstances
338 where emptynodes = Container.empty :: Node.List
339 emptyinstances = Container.empty :: Instance.List
341 -- | Check that the number of vertices of a nodegraph is equal to the number of
342 -- nodes in the original node list.
343 prop_numVertices :: Property
345 forAll genNodeGraph $ \(graph, nl, _) ->
346 (fmap numvertices graph ==? Just (Container.size nl))
347 where numvertices = length . Graph.vertices
349 -- | Check that the number of edges of a nodegraph is equal to twice the number
350 -- of instances with secondary nodes in the original instance list.
351 prop_numEdges :: Property
353 forAll genNodeGraph $ \(graph, _, il) ->
354 (fmap numedges graph ==? Just (numwithsec il * 2))
355 where numedges = length . Graph.edges
356 numwithsec = length . filter Instance.hasSecondary . Container.elems
358 -- | Check that a node graph is colorable.
359 prop_nodeGraphIsColorable :: Property
360 prop_nodeGraphIsColorable =
361 forAll genNodeGraph $ \(graph, _, _) ->
362 fmap HGraph.isColorable graph ==? Just True
364 -- | Check that each edge in a nodegraph is an instance.
365 prop_instanceIsEdge :: Property
366 prop_instanceIsEdge =
367 forAll genNodeGraph $ \(graph, _, il) ->
368 fmap (\g -> all (`isEdgeOn` g) (iwithsec il)) graph ==? Just True
369 where i `isEdgeOn` g = iEdges i `intersect` Graph.edges g == iEdges i
370 iEdges i = [ (Instance.pNode i, Instance.sNode i)
371 , (Instance.sNode i, Instance.pNode i)]
372 iwithsec = filter Instance.hasSecondary . Container.elems
374 -- | Check that each instance in an edge in the resulting nodegraph.
375 prop_edgeIsInstance :: Property
376 prop_edgeIsInstance =
377 forAll genNodeGraph $ \(graph, _, il) ->
378 fmap (all (`isInstanceIn` il).Graph.edges) graph ==? Just True
379 where e `isInstanceIn` il = any (`hasNodes` e) (Container.elems il)
380 i `hasNodes` (v1,v2) =
381 Instance.allNodes i `elem` permutations [v1,v2]
383 -- | List of tests for the Node module.
384 testSuite "HTools/Node"
388 , 'prop_setFmemGreater
394 , 'prop_addPri_NoN1Fail
396 , 'prop_addOfflinePri
397 , 'prop_addOfflineSec
400 , 'prop_tagMaps_idempotent
401 , 'prop_tagMaps_reject
403 , 'prop_computeGroups
404 , 'prop_addPri_idempotent
405 , 'prop_addSec_idempotent
406 , 'case_emptyNodeList
409 , 'prop_nodeGraphIsColorable
410 , 'prop_edgeIsInstance
411 , 'prop_instanceIsEdge