root / test / hs / Test / Ganeti / HTools / Node.hs @ 09d8b0fc
History | View | Annotate | Download (15.4 kB)
1 |
{-# LANGUAGE TemplateHaskell #-} |
---|---|
2 |
{-# OPTIONS_GHC -fno-warn-orphans #-} |
3 |
|
4 |
{-| Unittests for ganeti-htools. |
5 |
|
6 |
-} |
7 |
|
8 |
{- |
9 |
|
10 |
Copyright (C) 2009, 2010, 2011, 2012 Google Inc. |
11 |
|
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. |
16 |
|
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. |
21 |
|
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 |
25 |
02110-1301, USA. |
26 |
|
27 |
-} |
28 |
|
29 |
module Test.Ganeti.HTools.Node |
30 |
( testHTools_Node |
31 |
, Node.Node(..) |
32 |
, setInstanceSmallerThanNode |
33 |
, genNode |
34 |
, genOnlineNode |
35 |
, genNodeList |
36 |
) where |
37 |
|
38 |
import Test.QuickCheck |
39 |
import Test.HUnit |
40 |
|
41 |
import Control.Monad |
42 |
import qualified Data.Map as Map |
43 |
import qualified Data.Graph as Graph |
44 |
import Data.List |
45 |
|
46 |
import Test.Ganeti.TestHelper |
47 |
import Test.Ganeti.TestCommon |
48 |
import Test.Ganeti.TestHTools |
49 |
import Test.Ganeti.HTools.Instance ( genInstanceSmallerThanNode |
50 |
, genInstanceList |
51 |
, genInstanceOnNodeList) |
52 |
|
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 |
60 |
|
61 |
{-# ANN module "HLint: ignore Use camelCase" #-} |
62 |
|
63 |
-- * Arbitrary instances |
64 |
|
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) |
69 |
-> Gen Node.Node |
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, |
74 |
mm * Types.unitDsk, |
75 |
mm * Types.unitCpu) |
76 |
Nothing -> (0, 0, 0) |
77 |
(top_mem, top_dsk, top_cpu) = |
78 |
case max_multiplier of |
79 |
Just mm -> (mm * Types.unitMem, |
80 |
mm * Types.unitDsk, |
81 |
mm * Types.unitCpu) |
82 |
Nothing -> (maxMem, maxDsk, maxCpu) |
83 |
name <- genFQDN |
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) |
90 |
offl <- arbitrary |
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 |
95 |
|
96 |
-- | Helper function to generate a sane node. |
97 |
genOnlineNode :: Gen Node.Node |
98 |
genOnlineNode = |
99 |
arbitrary `suchThat` (\n -> not (Node.offline n) && |
100 |
not (Node.failN1 n) && |
101 |
Node.availDisk n > 0 && |
102 |
Node.availMem n > 0 && |
103 |
Node.availCpu n > 0) |
104 |
|
105 |
-- and a random node |
106 |
instance Arbitrary Node.Node where |
107 |
arbitrary = genNode Nothing Nothing |
108 |
|
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 |
112 |
-- lists here. |
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 |
116 |
|
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) |
120 |
genNodeGraph = do |
121 |
nl <- genNodeList genOnlineNode `suchThat` ((2<=).Container.size) |
122 |
il <- genInstanceList (genInstanceOnNodeList nl) |
123 |
return (Node.mkNodeGraph nl il, nl, il) |
124 |
|
125 |
-- * Test cases |
126 |
|
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 |
132 |
|
133 |
prop_setOffline :: Node.Node -> Bool -> Property |
134 |
prop_setOffline node status = |
135 |
Node.offline newnode ==? status |
136 |
where newnode = Node.setOffline node status |
137 |
|
138 |
prop_setXmem :: Node.Node -> Int -> Property |
139 |
prop_setXmem node xm = |
140 |
Node.xMem newnode ==? xm |
141 |
where newnode = Node.setXmem node xm |
142 |
|
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 |
147 |
|
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)) |
153 |
|
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))) |
158 |
|
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) |
167 |
|
168 |
-- | Check that an instance add with too high memory or disk will be |
169 |
-- rejected. |
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 } |
177 |
|
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) |
188 |
|
189 |
-- | Check that adding a primary instance with too many VCPUs fails |
190 |
-- with type FailCPU. |
191 |
prop_addPriFC :: Property |
192 |
prop_addPriFC = |
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 |
201 |
|
202 |
-- | Check that an instance add with too high memory or disk will be |
203 |
-- rejected. |
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) |
211 |
|
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 |
222 |
Ok _ -> passTest |
223 |
v -> failTest $ "Expected OpGood, but got: " ++ show v |
224 |
|
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 |
237 |
Ok _ -> passTest |
238 |
v -> failTest $ "Expected OpGood/OpGood, but got: " ++ show v |
239 |
|
240 |
-- | Checks for memory reservation changes. |
241 |
prop_rMem :: Instance.Instance -> Property |
242 |
prop_rMem inst = |
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 |
260 |
(Ok a_ab, Ok a_nb, |
261 |
Ok d_ab, Ok d_nb) -> |
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 |
269 |
-- test as any |
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 |
273 |
|
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' |
284 |
SmallRatio mx' = mx |
285 |
|
286 |
-- Check tag maps |
287 |
prop_tagMaps_idempotent :: Property |
288 |
prop_tagMaps_idempotent = |
289 |
forAll genTags $ \tags -> |
290 |
Node.delTags (Node.addTags m tags) tags ==? m |
291 |
where m = Map.empty |
292 |
|
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 |
298 |
|
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 |
304 |
|
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)) |
313 |
|
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" |
322 |
|
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" |
333 |
|
334 |
-- | Check that no graph is created on an empty node list. |
335 |
case_emptyNodeList :: Assertion |
336 |
case_emptyNodeList = |
337 |
assertEqual "" Nothing $ Node.mkNodeGraph emptynodes emptyinstances |
338 |
where emptynodes = Container.empty :: Node.List |
339 |
emptyinstances = Container.empty :: Instance.List |
340 |
|
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 |
344 |
prop_numVertices = |
345 |
forAll genNodeGraph $ \(graph, nl, _) -> |
346 |
(fmap numvertices graph ==? Just (Container.size nl)) |
347 |
where numvertices = length . Graph.vertices |
348 |
|
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 |
352 |
prop_numEdges = |
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 |
357 |
|
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 |
363 |
|
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 |
373 |
|
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] |
382 |
|
383 |
-- | List of tests for the Node module. |
384 |
testSuite "HTools/Node" |
385 |
[ 'prop_setAlias |
386 |
, 'prop_setOffline |
387 |
, 'prop_setMcpu |
388 |
, 'prop_setFmemGreater |
389 |
, 'prop_setFmemExact |
390 |
, 'prop_setXmem |
391 |
, 'prop_addPriFM |
392 |
, 'prop_addPriFD |
393 |
, 'prop_addPriFC |
394 |
, 'prop_addPri_NoN1Fail |
395 |
, 'prop_addSec |
396 |
, 'prop_addOfflinePri |
397 |
, 'prop_addOfflineSec |
398 |
, 'prop_rMem |
399 |
, 'prop_setMdsk |
400 |
, 'prop_tagMaps_idempotent |
401 |
, 'prop_tagMaps_reject |
402 |
, 'prop_showField |
403 |
, 'prop_computeGroups |
404 |
, 'prop_addPri_idempotent |
405 |
, 'prop_addSec_idempotent |
406 |
, 'case_emptyNodeList |
407 |
, 'prop_numVertices |
408 |
, 'prop_numEdges |
409 |
, 'prop_nodeGraphIsColorable |
410 |
, 'prop_edgeIsInstance |
411 |
, 'prop_instanceIsEdge |
412 |
] |