Statistics
| Branch: | Tag: | Revision:

root / Ganeti / HTools / QC.hs @ 3fea6959

History | View | Annotate | Download (18.4 kB)

1
{-| Unittests for ganeti-htools
2

    
3
-}
4

    
5
{-
6

    
7
Copyright (C) 2009 Google Inc.
8

    
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.
13

    
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.
18

    
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
22
02110-1301, USA.
23

    
24
-}
25

    
26
module Ganeti.HTools.QC
27
    ( testPeerMap
28
    , testContainer
29
    , testInstance
30
    , testNode
31
    , testText
32
    , testCluster
33
    ) where
34

    
35
import Test.QuickCheck
36
import Test.QuickCheck.Batch
37
import Data.Maybe
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
51

    
52
-- * Constants
53

    
54
-- | Maximum memory (1TiB, somewhat random value)
55
maxMem :: Int
56
maxMem = 1024 * 1024
57

    
58
-- | Maximum disk (8TiB, somewhat random value)
59
maxDsk :: Int
60
maxDsk = 1024 * 1024 * 8
61

    
62
-- | Max CPUs (1024, somewhat random value)
63
maxCpu :: Int
64
maxCpu = 1024
65

    
66
-- * Helper functions
67

    
68
-- | Simple checker for whether OpResult is fail or pass
69
isFailure :: Types.OpResult a -> Bool
70
isFailure (Types.OpFail _) = True
71
isFailure _ = False
72

    
73
-- | Simple checker for whether Result is fail or pass
74
isOk :: Types.Result a -> Bool
75
isOk (Types.Ok _ ) = True
76
isOk _ = False
77

    
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
83
         }
84

    
85
-- | Create an instance given its spec
86
createInstance mem dsk vcpus =
87
    Instance.create "inst-unnamed" mem dsk vcpus "running" [] (-1) (-1)
88

    
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
96

    
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
102

    
103
canBalance :: Cluster.Table -> Bool -> Bool -> Bool
104
canBalance tbl dm evac = isJust $ Cluster.tryBalance tbl dm evac
105

    
106
-- * Arbitrary instances
107

    
108
-- copied from the introduction to quickcheck
109
instance Arbitrary Char where
110
    arbitrary = choose ('\32', '\128')
111

    
112
-- let's generate a random instance
113
instance Arbitrary Instance.Instance where
114
    arbitrary = do
115
      name <- arbitrary
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"
120
                         , "running"
121
                         , "no_such_status1", "no_such_status2"]
122
      pn <- arbitrary
123
      sn <- arbitrary
124
      vcpus <- choose (0, maxCpu)
125
      return $ Instance.create name mem dsk vcpus run_st [] pn sn
126

    
127
-- and a random node
128
instance Arbitrary Node.Node where
129
    arbitrary = do
130
      name <- arbitrary
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)
137
      offl <- arbitrary
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
141
      return n'
142

    
143
-- * Actual tests
144

    
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
152

    
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
159

    
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
165

    
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
172

    
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
179

    
180
testPeerMap =
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
186
    ]
187

    
188
-- Container tests
189

    
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],
194
                    i1::Int, i2::Int)
195
          cont = foldl (\c x -> Container.add x x c) Container.empty cdata
196
          fn x1 x2 = Container.addTwo x1 x1 x2 x2
197

    
198
testContainer =
199
    [ run prop_Container_addTwo ]
200

    
201
-- Simple instance tests, we only have setter/getters
202

    
203
prop_Instance_setIdx inst idx =
204
    Instance.idx (Instance.setIdx inst idx) == idx
205
    where _types = (inst::Instance.Instance, idx::Types.Idx)
206

    
207
prop_Instance_setName inst name =
208
    Instance.name (Instance.setName inst name) == name
209
    where _types = (inst::Instance.Instance, name::String)
210

    
211
prop_Instance_setPri inst pdx =
212
    Instance.pNode (Instance.setPri inst pdx) == pdx
213
    where _types = (inst::Instance.Instance, pdx::Types.Ndx)
214

    
215
prop_Instance_setSec inst sdx =
216
    Instance.sNode (Instance.setSec inst sdx) == sdx
217
    where _types = (inst::Instance.Instance, sdx::Types.Ndx)
218

    
219
prop_Instance_setBoth inst pdx sdx =
220
    Instance.pNode si == pdx && Instance.sNode si == sdx
221
    where _types = (inst::Instance.Instance, pdx::Types.Ndx, sdx::Types.Ndx)
222
          si = Instance.setBoth inst pdx sdx
223

    
224
prop_Instance_runStatus_True inst =
225
    let run_st = Instance.running inst
226
        run_tx = Instance.runSt inst
227
    in
228
      run_tx `elem` Instance.runningStates ==> run_st
229

    
230
prop_Instance_runStatus_False inst =
231
    let run_st = Instance.running inst
232
        run_tx = Instance.runSt inst
233
    in
234
      run_tx `notElem` Instance.runningStates ==> not run_st
235

    
236
prop_Instance_shrinkMG inst =
237
    Instance.mem inst >= 2 * Types.unitMem ==>
238
        case Instance.shrinkByType inst Types.FailMem of
239
          Types.Ok inst' ->
240
              Instance.mem inst' == Instance.mem inst - Types.unitMem
241
          _ -> False
242
    where _types = (inst::Instance.Instance)
243

    
244
prop_Instance_shrinkMF inst =
245
    Instance.mem inst < 2 * Types.unitMem ==>
246
        not . isOk $ Instance.shrinkByType inst Types.FailMem
247
    where _types = (inst::Instance.Instance)
248

    
249
prop_Instance_shrinkCG inst =
250
    Instance.vcpus inst >= 2 * Types.unitCpu ==>
251
        case Instance.shrinkByType inst Types.FailCPU of
252
          Types.Ok inst' ->
253
              Instance.vcpus inst' == Instance.vcpus inst - Types.unitCpu
254
          _ -> False
255
    where _types = (inst::Instance.Instance)
256

    
257
prop_Instance_shrinkCF inst =
258
    Instance.vcpus inst < 2 * Types.unitCpu ==>
259
        not . isOk $ Instance.shrinkByType inst Types.FailCPU
260
    where _types = (inst::Instance.Instance)
261

    
262
prop_Instance_shrinkDG inst =
263
    Instance.dsk inst >= 2 * Types.unitDsk ==>
264
        case Instance.shrinkByType inst Types.FailDisk of
265
          Types.Ok inst' ->
266
              Instance.dsk inst' == Instance.dsk inst - Types.unitDsk
267
          _ -> False
268
    where _types = (inst::Instance.Instance)
269

    
270
prop_Instance_shrinkDF inst =
271
    Instance.dsk inst < 2 * Types.unitDsk ==>
272
        not . isOk $ Instance.shrinkByType inst Types.FailDisk
273
    where _types = (inst::Instance.Instance)
274

    
275
prop_Instance_setMovable inst m =
276
    Instance.movable inst' == m
277
    where _types = (inst::Instance.Instance, m::Bool)
278
          inst' = Instance.setMovable inst m
279

    
280
testInstance =
281
    [ run prop_Instance_setIdx
282
    , run prop_Instance_setName
283
    , run prop_Instance_setPri
284
    , run prop_Instance_setSec
285
    , run prop_Instance_setBoth
286
    , run prop_Instance_runStatus_True
287
    , run prop_Instance_runStatus_False
288
    , run prop_Instance_shrinkMG
289
    , run prop_Instance_shrinkMF
290
    , run prop_Instance_shrinkCG
291
    , run prop_Instance_shrinkCF
292
    , run prop_Instance_shrinkDG
293
    , run prop_Instance_shrinkDF
294
    , run prop_Instance_setMovable
295
    ]
296

    
297
-- Instance text loader tests
298

    
299
prop_Text_Load_Instance name mem dsk vcpus status pnode snode pdx sdx =
300
    let vcpus_s = show vcpus
301
        dsk_s = show dsk
302
        mem_s = show mem
303
        rsnode = snode ++ "a" -- non-empty secondary node
304
        rsdx = if pdx == sdx
305
               then sdx + 1
306
               else sdx
307
        ndx = [(pnode, pdx), (rsnode, rsdx)]
308
        tags = ""
309
        inst = Text.loadInst ndx
310
               [name, mem_s, dsk_s, vcpus_s, status, pnode, rsnode, tags]::
311
               Maybe (String, Instance.Instance)
312
        _types = ( name::String, mem::Int, dsk::Int
313
                 , vcpus::Int, status::String
314
                 , pnode::String, snode::String
315
                 , pdx::Types.Ndx, sdx::Types.Ndx)
316
    in
317
      case inst of
318
        Nothing -> False
319
        Just (_, i) ->
320
            (Instance.name i == name &&
321
             Instance.vcpus i == vcpus &&
322
             Instance.mem i == mem &&
323
             Instance.pNode i == pdx &&
324
             Instance.sNode i == rsdx)
325

    
326
testText =
327
    [ run prop_Text_Load_Instance
328
    ]
329

    
330
-- Node tests
331

    
332
-- | Check that an instance add with too high memory or disk will be rejected
333
prop_Node_addPriFM node inst = Instance.mem inst >= Node.fMem node &&
334
                               not (Node.failN1 node)
335
                               ==>
336
                               case Node.addPri node inst'' of
337
                                 Types.OpFail Types.FailMem -> True
338
                                 _ -> False
339
    where _types = (node::Node.Node, inst::Instance.Instance)
340
          inst' = setInstanceSmallerThanNode node inst
341
          inst'' = inst' { Instance.mem = Instance.mem inst }
342

    
343
prop_Node_addPriFD node inst = Instance.dsk inst >= Node.fDsk node &&
344
                               not (Node.failN1 node)
345
                               ==>
346
                               case Node.addPri node inst'' of
347
                                 Types.OpFail Types.FailDisk -> True
348
                                 _ -> False
349
    where _types = (node::Node.Node, inst::Instance.Instance)
350
          inst' = setInstanceSmallerThanNode node inst
351
          inst'' = inst' { Instance.dsk = Instance.dsk inst }
352

    
353
prop_Node_addPriFC node inst = Instance.vcpus inst > Node.availCpu node &&
354
                               not (Node.failN1 node)
355
                               ==>
356
                               case Node.addPri node inst'' of
357
                                 Types.OpFail Types.FailCPU -> True
358
                                 _ -> False
359
    where _types = (node::Node.Node, inst::Instance.Instance)
360
          inst' = setInstanceSmallerThanNode node inst
361
          inst'' = inst' { Instance.vcpus = Instance.vcpus inst }
362

    
363
-- | Check that an instance add with too high memory or disk will be rejected
364
prop_Node_addSec node inst pdx =
365
    (Instance.mem inst >= (Node.fMem node - Node.rMem node) ||
366
     Instance.dsk inst >= Node.fDsk node) &&
367
    not (Node.failN1 node)
368
    ==> isFailure (Node.addSec node inst pdx)
369
        where _types = (node::Node.Node, inst::Instance.Instance, pdx::Int)
370

    
371
newtype SmallRatio = SmallRatio Double deriving Show
372
instance Arbitrary SmallRatio where
373
    arbitrary = do
374
      v <- choose (0, 1)
375
      return $ SmallRatio v
376

    
377
-- | Check mdsk setting
378
prop_Node_setMdsk node mx =
379
    Node.loDsk node' >= 0 &&
380
    fromIntegral (Node.loDsk node') <= Node.tDsk node &&
381
    Node.availDisk node' >= 0 &&
382
    Node.availDisk node' <= Node.fDsk node' &&
383
    fromIntegral (Node.availDisk node') <= Node.tDsk node'
384
    where _types = (node::Node.Node, mx::SmallRatio)
385
          node' = Node.setMdsk node mx'
386
          SmallRatio mx' = mx
387

    
388
-- Check tag maps
389
prop_Node_tagMaps_idempotent tags =
390
    Node.delTags (Node.addTags m tags) tags == m
391
    where _types = (tags::[String])
392
          m = Data.Map.empty
393

    
394
prop_Node_tagMaps_reject tags =
395
    not (null tags) ==>
396
    any (\t -> Node.rejectAddTags m [t]) tags
397
    where _types = (tags::[String])
398
          m = Node.addTags (Data.Map.empty) tags
399

    
400
testNode =
401
    [ run prop_Node_addPriFM
402
    , run prop_Node_addPriFD
403
    , run prop_Node_addPriFC
404
    , run prop_Node_addSec
405
    , run prop_Node_setMdsk
406
    , run prop_Node_tagMaps_idempotent
407
    , run prop_Node_tagMaps_reject
408
    ]
409

    
410

    
411
-- Cluster tests
412

    
413
-- | Check that the cluster score is close to zero for a homogeneous cluster
414
prop_Score_Zero node count =
415
    (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
416
     (Node.tDsk node > 0) && (Node.tMem node > 0)) ==>
417
    let fn = Node.buildPeers node Container.empty
418
        nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
419
        nl = Container.fromAssocList nlst
420
        score = Cluster.compCV nl
421
    -- we can't say == 0 here as the floating point errors accumulate;
422
    -- this should be much lower than the default score in CLI.hs
423
    in score <= 1e-15
424

    
425
-- | Check that cluster stats are sane
426
prop_CStats_sane node count =
427
    (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
428
     (Node.availDisk node > 0) && (Node.availMem node > 0)) ==>
429
    let fn = Node.buildPeers node Container.empty
430
        nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
431
        nl = Container.fromAssocList nlst
432
        cstats = Cluster.totalResources nl
433
    in Cluster.csAdsk cstats >= 0 &&
434
       Cluster.csAdsk cstats <= Cluster.csFdsk cstats
435

    
436
-- | Check that one instance is allocated correctly, without
437
-- rebalances needed
438
prop_ClusterAlloc_sane node inst =
439
    forAll (choose (5, 20)) $ \count ->
440
    not (Node.offline node)
441
            && not (Node.failN1 node)
442
            && Node.availDisk node > 0
443
            && Node.availMem node > 0
444
            ==>
445
    let nl = makeSmallCluster node count
446
        il = Container.empty
447
        rqnodes = 2
448
        inst' = setInstanceSmallerThanNode node inst
449
    in case Cluster.tryAlloc nl il inst' rqnodes of
450
         Types.Bad _ -> False
451
         Types.Ok (errs, _, sols3) ->
452
             case sols3 of
453
               [] -> False
454
               (_, (xnl, xi, _)):[] ->
455
                   let cv = Cluster.compCV xnl
456
                       il' = Container.add (Instance.idx xi) xi il
457
                       tbl = Cluster.Table xnl il' cv []
458
                   in not (canBalance tbl True False)
459
               _ -> False
460

    
461
-- | Checks that on a 2-5 node cluster, we can allocate a random
462
-- instance spec via tiered allocation (whatever the original instance
463
-- spec), on either one or two nodes
464
prop_ClusterCanTieredAlloc node inst =
465
    forAll (choose (2, 5)) $ \count ->
466
    forAll (choose (1, 2)) $ \rqnodes ->
467
    not (Node.offline node)
468
            && not (Node.failN1 node)
469
            && isNodeBig node 4
470
            ==>
471
    let nl = makeSmallCluster node count
472
        il = Container.empty
473
    in case Cluster.tieredAlloc nl il inst rqnodes [] of
474
         Types.Bad _ -> False
475
         Types.Ok (_, _, ixes) -> not (null ixes)
476

    
477
-- | Checks that on a 4-8 node cluster, once we allocate an instance,
478
-- we can also evacuate it
479
prop_ClusterAllocEvac node inst =
480
    forAll (choose (4, 8)) $ \count ->
481
    not (Node.offline node)
482
            && not (Node.failN1 node)
483
            && isNodeBig node 4
484
            ==>
485
    let nl = makeSmallCluster node count
486
        il = Container.empty
487
        rqnodes = 2
488
        inst' = setInstanceSmallerThanNode node inst
489
    in case Cluster.tryAlloc nl il inst' rqnodes of
490
         Types.Bad _ -> False
491
         Types.Ok (errs, _, sols3) ->
492
             case sols3 of
493
               [] -> False
494
               (_, (xnl, xi, _)):[] ->
495
                   let sdx = Instance.sNode xi
496
                       il' = Container.add (Instance.idx xi) xi il
497
                   in case Cluster.tryEvac xnl il' [sdx] of
498
                        Just _ -> True
499
                        _ -> False
500
               _ -> False
501

    
502
-- | Check that allocating multiple instances on a cluster, then
503
-- adding an empty node, results in a valid rebalance
504
prop_ClusterAllocBalance node =
505
    forAll (choose (3, 5)) $ \count ->
506
    not (Node.offline node)
507
            && not (Node.failN1 node)
508
            && isNodeBig node 4
509
            && not (isNodeBig node 8)
510
            ==>
511
    let nl = makeSmallCluster node count
512
        (hnode, nl') = IntMap.deleteFindMax nl
513
        il = Container.empty
514
        rqnodes = 2
515
        i_templ = createInstance Types.unitMem Types.unitDsk Types.unitCpu
516
    in case Cluster.iterateAlloc nl' il i_templ rqnodes [] of
517
         Types.Bad _ -> False
518
         Types.Ok (_, xnl, insts) ->
519
                   let ynl = Container.add (Node.idx hnode) hnode xnl
520
                       cv = Cluster.compCV ynl
521
                       il' = foldl (\l i ->
522
                                        Container.add (Instance.idx i) i l)
523
                             il insts
524
                       tbl = Cluster.Table ynl il' cv []
525
                   in canBalance tbl True False
526

    
527
testCluster =
528
    [ run prop_Score_Zero
529
    , run prop_CStats_sane
530
    , run prop_ClusterAlloc_sane
531
    , run prop_ClusterCanTieredAlloc
532
    , run prop_ClusterAllocEvac
533
    , run prop_ClusterAllocBalance
534
    ]