Statistics
| Branch: | Tag: | Revision:

root / htools / Ganeti / HTools / QC.hs @ c478f837

History | View | Annotate | Download (30 kB)

1
{-| Unittests for ganeti-htools
2

    
3
-}
4

    
5
{-
6

    
7
Copyright (C) 2009, 2010, 2011 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
    ( testUtils
28
    , testPeerMap
29
    , testContainer
30
    , testInstance
31
    , testNode
32
    , testText
33
    , testOpCodes
34
    , testJobs
35
    , testCluster
36
    , testLoader
37
    ) where
38

    
39
import Test.QuickCheck
40
import Test.QuickCheck.Batch
41
import Data.List (findIndex, intercalate, nub)
42
import Data.Maybe
43
import Control.Monad
44
import qualified Text.JSON as J
45
import qualified Data.Map
46
import qualified Data.IntMap as IntMap
47
import qualified Ganeti.OpCodes as OpCodes
48
import qualified Ganeti.Jobs as Jobs
49
import qualified Ganeti.Luxi
50
import qualified Ganeti.HTools.CLI as CLI
51
import qualified Ganeti.HTools.Cluster as Cluster
52
import qualified Ganeti.HTools.Container as Container
53
import qualified Ganeti.HTools.ExtLoader
54
import qualified Ganeti.HTools.IAlloc as IAlloc
55
import qualified Ganeti.HTools.Instance as Instance
56
import qualified Ganeti.HTools.Loader as Loader
57
import qualified Ganeti.HTools.Luxi
58
import qualified Ganeti.HTools.Node as Node
59
import qualified Ganeti.HTools.Group as Group
60
import qualified Ganeti.HTools.PeerMap as PeerMap
61
import qualified Ganeti.HTools.Rapi
62
import qualified Ganeti.HTools.Simu
63
import qualified Ganeti.HTools.Text as Text
64
import qualified Ganeti.HTools.Types as Types
65
import qualified Ganeti.HTools.Utils as Utils
66
import qualified Ganeti.HTools.Version
67

    
68
-- * Constants
69

    
70
-- | Maximum memory (1TiB, somewhat random value)
71
maxMem :: Int
72
maxMem = 1024 * 1024
73

    
74
-- | Maximum disk (8TiB, somewhat random value)
75
maxDsk :: Int
76
maxDsk = 1024 * 1024 * 8
77

    
78
-- | Max CPUs (1024, somewhat random value)
79
maxCpu :: Int
80
maxCpu = 1024
81

    
82
defGroup :: Group.Group
83
defGroup = flip Group.setIdx 0 $
84
               Group.create "default" Utils.defaultGroupID
85
                    Types.AllocPreferred
86

    
87
defGroupList :: Group.List
88
defGroupList = Container.fromList [(Group.idx defGroup, defGroup)]
89

    
90
defGroupAssoc :: Data.Map.Map String Types.Gdx
91
defGroupAssoc = Data.Map.singleton (Group.uuid defGroup) (Group.idx defGroup)
92

    
93
-- * Helper functions
94

    
95
-- | Simple checker for whether OpResult is fail or pass
96
isFailure :: Types.OpResult a -> Bool
97
isFailure (Types.OpFail _) = True
98
isFailure _ = False
99

    
100
-- | Update an instance to be smaller than a node
101
setInstanceSmallerThanNode node inst =
102
    inst { Instance.mem = Node.availMem node `div` 2
103
         , Instance.dsk = Node.availDisk node `div` 2
104
         , Instance.vcpus = Node.availCpu node `div` 2
105
         }
106

    
107
-- | Create an instance given its spec
108
createInstance mem dsk vcpus =
109
    Instance.create "inst-unnamed" mem dsk vcpus "running" [] (-1) (-1)
110

    
111
-- | Create a small cluster by repeating a node spec
112
makeSmallCluster :: Node.Node -> Int -> Node.List
113
makeSmallCluster node count =
114
    let fn = Node.buildPeers node Container.empty
115
        namelst = map (\n -> (Node.name n, n)) (replicate count fn)
116
        (_, nlst) = Loader.assignIndices namelst
117
    in nlst
118

    
119
-- | Checks if a node is "big" enough
120
isNodeBig :: Node.Node -> Int -> Bool
121
isNodeBig node size = Node.availDisk node > size * Types.unitDsk
122
                      && Node.availMem node > size * Types.unitMem
123
                      && Node.availCpu node > size * Types.unitCpu
124

    
125
canBalance :: Cluster.Table -> Bool -> Bool -> Bool
126
canBalance tbl dm evac = isJust $ Cluster.tryBalance tbl dm evac 0 0
127

    
128
-- | Assigns a new fresh instance to a cluster; this is not
129
-- allocation, so no resource checks are done
130
assignInstance :: Node.List -> Instance.List -> Instance.Instance ->
131
                  Types.Idx -> Types.Idx ->
132
                  (Node.List, Instance.List)
133
assignInstance nl il inst pdx sdx =
134
  let pnode = Container.find pdx nl
135
      snode = Container.find sdx nl
136
      maxiidx = if Container.null il
137
                then 0
138
                else fst (Container.findMax il) + 1
139
      inst' = inst { Instance.idx = maxiidx,
140
                     Instance.pNode = pdx, Instance.sNode = sdx }
141
      pnode' = Node.setPri pnode inst'
142
      snode' = Node.setSec snode inst'
143
      nl' = Container.addTwo pdx pnode' sdx snode' nl
144
      il' = Container.add maxiidx inst' il
145
  in (nl', il')
146

    
147
-- * Arbitrary instances
148

    
149
-- copied from the introduction to quickcheck
150
instance Arbitrary Char where
151
    arbitrary = choose ('\32', '\128')
152

    
153
newtype DNSChar = DNSChar { dnsGetChar::Char }
154
instance Arbitrary DNSChar where
155
    arbitrary = do
156
      x <- elements (['a'..'z'] ++ ['0'..'9'] ++ "_-")
157
      return (DNSChar x)
158

    
159
getName :: Gen String
160
getName = do
161
  n <- choose (1, 64)
162
  dn <- vector n::Gen [DNSChar]
163
  return (map dnsGetChar dn)
164

    
165

    
166
getFQDN :: Gen String
167
getFQDN = do
168
  felem <- getName
169
  ncomps <- choose (1, 4)
170
  frest <- vector ncomps::Gen [[DNSChar]]
171
  let frest' = map (map dnsGetChar) frest
172
  return (felem ++ "." ++ intercalate "." frest')
173

    
174
-- let's generate a random instance
175
instance Arbitrary Instance.Instance where
176
    arbitrary = do
177
      name <- getFQDN
178
      mem <- choose (0, maxMem)
179
      dsk <- choose (0, maxDsk)
180
      run_st <- elements ["ERROR_up", "ERROR_down", "ADMIN_down"
181
                         , "ERROR_nodedown", "ERROR_nodeoffline"
182
                         , "running"
183
                         , "no_such_status1", "no_such_status2"]
184
      pn <- arbitrary
185
      sn <- arbitrary
186
      vcpus <- choose (0, maxCpu)
187
      return $ Instance.create name mem dsk vcpus run_st [] pn sn
188

    
189
-- and a random node
190
instance Arbitrary Node.Node where
191
    arbitrary = do
192
      name <- getFQDN
193
      mem_t <- choose (0, maxMem)
194
      mem_f <- choose (0, mem_t)
195
      mem_n <- choose (0, mem_t - mem_f)
196
      dsk_t <- choose (0, maxDsk)
197
      dsk_f <- choose (0, dsk_t)
198
      cpu_t <- choose (0, maxCpu)
199
      offl <- arbitrary
200
      let n = Node.create name (fromIntegral mem_t) mem_n mem_f
201
              (fromIntegral dsk_t) dsk_f (fromIntegral cpu_t) offl
202
              0
203
          n' = Node.buildPeers n Container.empty
204
      return n'
205

    
206
-- replace disks
207
instance Arbitrary OpCodes.ReplaceDisksMode where
208
  arbitrary = elements [ OpCodes.ReplaceOnPrimary
209
                       , OpCodes.ReplaceOnSecondary
210
                       , OpCodes.ReplaceNewSecondary
211
                       , OpCodes.ReplaceAuto
212
                       ]
213

    
214
instance Arbitrary OpCodes.OpCode where
215
  arbitrary = do
216
    op_id <- elements [ "OP_TEST_DELAY"
217
                      , "OP_INSTANCE_REPLACE_DISKS"
218
                      , "OP_INSTANCE_FAILOVER"
219
                      , "OP_INSTANCE_MIGRATE"
220
                      ]
221
    (case op_id of
222
        "OP_TEST_DELAY" ->
223
          liftM3 OpCodes.OpTestDelay arbitrary arbitrary arbitrary
224
        "OP_INSTANCE_REPLACE_DISKS" ->
225
          liftM5 OpCodes.OpReplaceDisks arbitrary arbitrary
226
          arbitrary arbitrary arbitrary
227
        "OP_INSTANCE_FAILOVER" ->
228
          liftM2 OpCodes.OpFailoverInstance arbitrary arbitrary
229
        "OP_INSTANCE_MIGRATE" ->
230
          liftM3 OpCodes.OpMigrateInstance arbitrary arbitrary arbitrary
231
        _ -> fail "Wrong opcode")
232

    
233
instance Arbitrary Jobs.OpStatus where
234
  arbitrary = elements [minBound..maxBound]
235

    
236
instance Arbitrary Jobs.JobStatus where
237
  arbitrary = elements [minBound..maxBound]
238

    
239
-- * Actual tests
240

    
241
-- If the list is not just an empty element, and if the elements do
242
-- not contain commas, then join+split should be idepotent
243
prop_Utils_commaJoinSplit lst = lst /= [""] &&
244
                                all (not . elem ',') lst ==>
245
                                Utils.sepSplit ',' (Utils.commaJoin lst) == lst
246
-- Split and join should always be idempotent
247
prop_Utils_commaSplitJoin s = Utils.commaJoin (Utils.sepSplit ',' s) == s
248

    
249
testUtils =
250
  [ run prop_Utils_commaJoinSplit
251
  , run prop_Utils_commaSplitJoin
252
  ]
253

    
254
-- | Make sure add is idempotent
255
prop_PeerMap_addIdempotent pmap key em =
256
    fn puniq == fn (fn puniq)
257
    where _types = (pmap::PeerMap.PeerMap,
258
                    key::PeerMap.Key, em::PeerMap.Elem)
259
          fn = PeerMap.add key em
260
          puniq = PeerMap.accumArray const pmap
261

    
262
-- | Make sure remove is idempotent
263
prop_PeerMap_removeIdempotent pmap key =
264
    fn puniq == fn (fn puniq)
265
    where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
266
          fn = PeerMap.remove key
267
          puniq = PeerMap.accumArray const pmap
268

    
269
-- | Make sure a missing item returns 0
270
prop_PeerMap_findMissing pmap key =
271
    PeerMap.find key (PeerMap.remove key puniq) == 0
272
    where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
273
          puniq = PeerMap.accumArray const pmap
274

    
275
-- | Make sure an added item is found
276
prop_PeerMap_addFind pmap key em =
277
    PeerMap.find key (PeerMap.add key em puniq) == em
278
    where _types = (pmap::PeerMap.PeerMap,
279
                    key::PeerMap.Key, em::PeerMap.Elem)
280
          puniq = PeerMap.accumArray const pmap
281

    
282
-- | Manual check that maxElem returns the maximum indeed, or 0 for null
283
prop_PeerMap_maxElem pmap =
284
    PeerMap.maxElem puniq == if null puniq then 0
285
                             else (maximum . snd . unzip) puniq
286
    where _types = pmap::PeerMap.PeerMap
287
          puniq = PeerMap.accumArray const pmap
288

    
289
testPeerMap =
290
    [ run prop_PeerMap_addIdempotent
291
    , run prop_PeerMap_removeIdempotent
292
    , run prop_PeerMap_maxElem
293
    , run prop_PeerMap_addFind
294
    , run prop_PeerMap_findMissing
295
    ]
296

    
297
-- Container tests
298

    
299
prop_Container_addTwo cdata i1 i2 =
300
    fn i1 i2 cont == fn i2 i1 cont &&
301
       fn i1 i2 cont == fn i1 i2 (fn i1 i2 cont)
302
    where _types = (cdata::[Int],
303
                    i1::Int, i2::Int)
304
          cont = foldl (\c x -> Container.add x x c) Container.empty cdata
305
          fn x1 x2 = Container.addTwo x1 x1 x2 x2
306

    
307
prop_Container_nameOf node =
308
  let nl = makeSmallCluster node 1
309
      fnode = head (Container.elems nl)
310
  in Container.nameOf nl (Node.idx fnode) == Node.name fnode
311

    
312
-- We test that in a cluster, given a random node, we can find it by
313
-- its name and alias, as long as all names and aliases are unique,
314
-- and that we fail to find a non-existing name
315
prop_Container_findByName node othername =
316
  forAll (choose (1, 20)) $ \ cnt ->
317
  forAll (choose (0, cnt - 1)) $ \ fidx ->
318
  forAll (vector cnt) $ \ names ->
319
  (length . nub) (map fst names ++ map snd names) ==
320
  length names * 2 &&
321
  not (othername `elem` (map fst names ++ map snd names)) ==>
322
  let nl = makeSmallCluster node cnt
323
      nodes = Container.elems nl
324
      nodes' = map (\((name, alias), nn) -> (Node.idx nn,
325
                                             nn { Node.name = name,
326
                                                  Node.alias = alias }))
327
               $ zip names nodes
328
      nl' = Container.fromList nodes'
329
      target = snd (nodes' !! fidx)
330
  in Container.findByName nl' (Node.name target) == Just target &&
331
     Container.findByName nl' (Node.alias target) == Just target &&
332
     Container.findByName nl' othername == Nothing
333

    
334
testContainer =
335
    [ run prop_Container_addTwo
336
    , run prop_Container_nameOf
337
    , run prop_Container_findByName
338
    ]
339

    
340
-- Simple instance tests, we only have setter/getters
341

    
342
prop_Instance_creat inst =
343
    Instance.name inst == Instance.alias inst
344

    
345
prop_Instance_setIdx inst idx =
346
    Instance.idx (Instance.setIdx inst idx) == idx
347
    where _types = (inst::Instance.Instance, idx::Types.Idx)
348

    
349
prop_Instance_setName inst name =
350
    Instance.name newinst == name &&
351
    Instance.alias newinst == name
352
    where _types = (inst::Instance.Instance, name::String)
353
          newinst = Instance.setName inst name
354

    
355
prop_Instance_setAlias inst name =
356
    Instance.name newinst == Instance.name inst &&
357
    Instance.alias newinst == name
358
    where _types = (inst::Instance.Instance, name::String)
359
          newinst = Instance.setAlias inst name
360

    
361
prop_Instance_setPri inst pdx =
362
    Instance.pNode (Instance.setPri inst pdx) == pdx
363
    where _types = (inst::Instance.Instance, pdx::Types.Ndx)
364

    
365
prop_Instance_setSec inst sdx =
366
    Instance.sNode (Instance.setSec inst sdx) == sdx
367
    where _types = (inst::Instance.Instance, sdx::Types.Ndx)
368

    
369
prop_Instance_setBoth inst pdx sdx =
370
    Instance.pNode si == pdx && Instance.sNode si == sdx
371
    where _types = (inst::Instance.Instance, pdx::Types.Ndx, sdx::Types.Ndx)
372
          si = Instance.setBoth inst pdx sdx
373

    
374
prop_Instance_runStatus_True inst =
375
    let run_st = Instance.running inst
376
        run_tx = Instance.runSt inst
377
    in
378
      run_tx `elem` Instance.runningStates ==> run_st
379

    
380
prop_Instance_runStatus_False inst =
381
    let run_st = Instance.running inst
382
        run_tx = Instance.runSt inst
383
    in
384
      run_tx `notElem` Instance.runningStates ==> not run_st
385

    
386
prop_Instance_shrinkMG inst =
387
    Instance.mem inst >= 2 * Types.unitMem ==>
388
        case Instance.shrinkByType inst Types.FailMem of
389
          Types.Ok inst' ->
390
              Instance.mem inst' == Instance.mem inst - Types.unitMem
391
          _ -> False
392

    
393
prop_Instance_shrinkMF inst =
394
    Instance.mem inst < 2 * Types.unitMem ==>
395
        Types.isBad $ Instance.shrinkByType inst Types.FailMem
396

    
397
prop_Instance_shrinkCG inst =
398
    Instance.vcpus inst >= 2 * Types.unitCpu ==>
399
        case Instance.shrinkByType inst Types.FailCPU of
400
          Types.Ok inst' ->
401
              Instance.vcpus inst' == Instance.vcpus inst - Types.unitCpu
402
          _ -> False
403

    
404
prop_Instance_shrinkCF inst =
405
    Instance.vcpus inst < 2 * Types.unitCpu ==>
406
        Types.isBad $ Instance.shrinkByType inst Types.FailCPU
407

    
408
prop_Instance_shrinkDG inst =
409
    Instance.dsk inst >= 2 * Types.unitDsk ==>
410
        case Instance.shrinkByType inst Types.FailDisk of
411
          Types.Ok inst' ->
412
              Instance.dsk inst' == Instance.dsk inst - Types.unitDsk
413
          _ -> False
414

    
415
prop_Instance_shrinkDF inst =
416
    Instance.dsk inst < 2 * Types.unitDsk ==>
417
        Types.isBad $ Instance.shrinkByType inst Types.FailDisk
418

    
419
prop_Instance_setMovable inst m =
420
    Instance.movable inst' == m
421
    where inst' = Instance.setMovable inst m
422

    
423
testInstance =
424
    [ run prop_Instance_creat
425
    , run prop_Instance_setIdx
426
    , run prop_Instance_setName
427
    , run prop_Instance_setAlias
428
    , run prop_Instance_setPri
429
    , run prop_Instance_setSec
430
    , run prop_Instance_setBoth
431
    , run prop_Instance_runStatus_True
432
    , run prop_Instance_runStatus_False
433
    , run prop_Instance_shrinkMG
434
    , run prop_Instance_shrinkMF
435
    , run prop_Instance_shrinkCG
436
    , run prop_Instance_shrinkCF
437
    , run prop_Instance_shrinkDG
438
    , run prop_Instance_shrinkDF
439
    , run prop_Instance_setMovable
440
    ]
441

    
442
-- Instance text loader tests
443

    
444
prop_Text_Load_Instance name mem dsk vcpus status pnode snode pdx sdx =
445
    not (null pnode) && pdx >= 0 && sdx >= 0 ==>
446
    let vcpus_s = show vcpus
447
        dsk_s = show dsk
448
        mem_s = show mem
449
        rsdx = if pdx == sdx
450
               then sdx + 1
451
               else sdx
452
        ndx = if null snode
453
              then [(pnode, pdx)]
454
              else [(pnode, pdx), (snode, rsdx)]
455
        nl = Data.Map.fromList ndx
456
        tags = ""
457
        inst = Text.loadInst nl
458
               [name, mem_s, dsk_s, vcpus_s, status, pnode, snode, tags]::
459
               Maybe (String, Instance.Instance)
460
        fail1 = Text.loadInst nl
461
               [name, mem_s, dsk_s, vcpus_s, status, pnode, pnode, tags]::
462
               Maybe (String, Instance.Instance)
463
        _types = ( name::String, mem::Int, dsk::Int
464
                 , vcpus::Int, status::String
465
                 , pnode::String, snode::String
466
                 , pdx::Types.Ndx, sdx::Types.Ndx)
467
    in
468
      case inst of
469
        Nothing -> False
470
        Just (_, i) ->
471
            (Instance.name i == name &&
472
             Instance.vcpus i == vcpus &&
473
             Instance.mem i == mem &&
474
             Instance.pNode i == pdx &&
475
             Instance.sNode i == (if null snode
476
                                  then Node.noSecondary
477
                                  else rsdx) &&
478
             isNothing fail1)
479

    
480
prop_Text_Load_InstanceFail ktn fields =
481
    length fields /= 8 ==> isNothing $ Text.loadInst nl fields
482
    where nl = Data.Map.fromList ktn
483

    
484
prop_Text_Load_Node name tm nm fm td fd tc fo =
485
    let conv v = if v < 0
486
                    then "?"
487
                    else show v
488
        tm_s = conv tm
489
        nm_s = conv nm
490
        fm_s = conv fm
491
        td_s = conv td
492
        fd_s = conv fd
493
        tc_s = conv tc
494
        fo_s = if fo
495
               then "Y"
496
               else "N"
497
        any_broken = any (< 0) [tm, nm, fm, td, fd, tc]
498
        gid = Group.uuid defGroup
499
    in case Text.loadNode defGroupAssoc
500
           [name, tm_s, nm_s, fm_s, td_s, fd_s, tc_s, fo_s, gid] of
501
         Nothing -> False
502
         Just (name', node) ->
503
             if fo || any_broken
504
             then Node.offline node
505
             else Node.name node == name' && name' == name &&
506
                  Node.alias node == name &&
507
                  Node.tMem node == fromIntegral tm &&
508
                  Node.nMem node == nm &&
509
                  Node.fMem node == fm &&
510
                  Node.tDsk node == fromIntegral td &&
511
                  Node.fDsk node == fd &&
512
                  Node.tCpu node == fromIntegral tc
513

    
514
prop_Text_Load_NodeFail fields =
515
    length fields /= 8 ==> isNothing $ Text.loadNode Data.Map.empty fields
516

    
517
prop_Text_NodeLSIdempotent node =
518
    (Text.loadNode defGroupAssoc.
519
         Utils.sepSplit '|' . Text.serializeNode defGroupList) n ==
520
    Just (Node.name n, n)
521
    -- override failN1 to what loadNode returns by default
522
    where n = node { Node.failN1 = True, Node.offline = False }
523

    
524
testText =
525
    [ run prop_Text_Load_Instance
526
    , run prop_Text_Load_InstanceFail
527
    , run prop_Text_Load_Node
528
    , run prop_Text_Load_NodeFail
529
    , run prop_Text_NodeLSIdempotent
530
    ]
531

    
532
-- Node tests
533

    
534
prop_Node_setAlias node name =
535
    Node.name newnode == Node.name node &&
536
    Node.alias newnode == name
537
    where _types = (node::Node.Node, name::String)
538
          newnode = Node.setAlias node name
539

    
540
prop_Node_setOffline node status =
541
    Node.offline newnode == status
542
    where newnode = Node.setOffline node status
543

    
544
prop_Node_setXmem node xm =
545
    Node.xMem newnode == xm
546
    where newnode = Node.setXmem node xm
547

    
548
prop_Node_setMcpu node mc =
549
    Node.mCpu newnode == mc
550
    where newnode = Node.setMcpu node mc
551

    
552
-- | Check that an instance add with too high memory or disk will be rejected
553
prop_Node_addPriFM node inst = Instance.mem inst >= Node.fMem node &&
554
                               not (Node.failN1 node)
555
                               ==>
556
                               case Node.addPri node inst'' of
557
                                 Types.OpFail Types.FailMem -> True
558
                                 _ -> False
559
    where _types = (node::Node.Node, inst::Instance.Instance)
560
          inst' = setInstanceSmallerThanNode node inst
561
          inst'' = inst' { Instance.mem = Instance.mem inst }
562

    
563
prop_Node_addPriFD node inst = Instance.dsk inst >= Node.fDsk node &&
564
                               not (Node.failN1 node)
565
                               ==>
566
                               case Node.addPri node inst'' of
567
                                 Types.OpFail Types.FailDisk -> True
568
                                 _ -> False
569
    where _types = (node::Node.Node, inst::Instance.Instance)
570
          inst' = setInstanceSmallerThanNode node inst
571
          inst'' = inst' { Instance.dsk = Instance.dsk inst }
572

    
573
prop_Node_addPriFC node inst = Instance.vcpus inst > Node.availCpu node &&
574
                               not (Node.failN1 node)
575
                               ==>
576
                               case Node.addPri node inst'' of
577
                                 Types.OpFail Types.FailCPU -> True
578
                                 _ -> False
579
    where _types = (node::Node.Node, inst::Instance.Instance)
580
          inst' = setInstanceSmallerThanNode node inst
581
          inst'' = inst' { Instance.vcpus = Instance.vcpus inst }
582

    
583
-- | Check that an instance add with too high memory or disk will be rejected
584
prop_Node_addSec node inst pdx =
585
    (Instance.mem inst >= (Node.fMem node - Node.rMem node) ||
586
     Instance.dsk inst >= Node.fDsk node) &&
587
    not (Node.failN1 node)
588
    ==> isFailure (Node.addSec node inst pdx)
589
        where _types = (node::Node.Node, inst::Instance.Instance, pdx::Int)
590

    
591
newtype SmallRatio = SmallRatio Double deriving Show
592
instance Arbitrary SmallRatio where
593
    arbitrary = do
594
      v <- choose (0, 1)
595
      return $ SmallRatio v
596

    
597
-- | Check mdsk setting
598
prop_Node_setMdsk node mx =
599
    Node.loDsk node' >= 0 &&
600
    fromIntegral (Node.loDsk node') <= Node.tDsk node &&
601
    Node.availDisk node' >= 0 &&
602
    Node.availDisk node' <= Node.fDsk node' &&
603
    fromIntegral (Node.availDisk node') <= Node.tDsk node' &&
604
    Node.mDsk node' == mx'
605
    where _types = (node::Node.Node, mx::SmallRatio)
606
          node' = Node.setMdsk node mx'
607
          SmallRatio mx' = mx
608

    
609
-- Check tag maps
610
prop_Node_tagMaps_idempotent tags =
611
    Node.delTags (Node.addTags m tags) tags == m
612
    where m = Data.Map.empty
613

    
614
prop_Node_tagMaps_reject tags =
615
    not (null tags) ==>
616
    any (\t -> Node.rejectAddTags m [t]) tags
617
    where m = Node.addTags Data.Map.empty tags
618

    
619
prop_Node_showField node =
620
  forAll (elements Node.defaultFields) $ \ field ->
621
  fst (Node.showHeader field) /= Types.unknownField &&
622
  Node.showField node field /= Types.unknownField
623

    
624

    
625
prop_Node_computeGroups nodes =
626
  let ng = Node.computeGroups nodes
627
      onlyuuid = map fst ng
628
  in length nodes == sum (map (length . snd) ng) &&
629
     all (\(guuid, ns) -> all ((== guuid) . Node.group) ns) ng &&
630
     length (nub onlyuuid) == length onlyuuid &&
631
     if null nodes then True else not (null ng)
632

    
633
testNode =
634
    [ run prop_Node_setAlias
635
    , run prop_Node_setOffline
636
    , run prop_Node_setMcpu
637
    , run prop_Node_setXmem
638
    , run prop_Node_addPriFM
639
    , run prop_Node_addPriFD
640
    , run prop_Node_addPriFC
641
    , run prop_Node_addSec
642
    , run prop_Node_setMdsk
643
    , run prop_Node_tagMaps_idempotent
644
    , run prop_Node_tagMaps_reject
645
    , run prop_Node_showField
646
    , run prop_Node_computeGroups
647
    ]
648

    
649

    
650
-- Cluster tests
651

    
652
-- | Check that the cluster score is close to zero for a homogeneous cluster
653
prop_Score_Zero node count =
654
    (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
655
     (Node.tDsk node > 0) && (Node.tMem node > 0)) ==>
656
    let fn = Node.buildPeers node Container.empty
657
        nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
658
        nl = Container.fromList nlst
659
        score = Cluster.compCV nl
660
    -- we can't say == 0 here as the floating point errors accumulate;
661
    -- this should be much lower than the default score in CLI.hs
662
    in score <= 1e-15
663

    
664
-- | Check that cluster stats are sane
665
prop_CStats_sane node count =
666
    (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
667
     (Node.availDisk node > 0) && (Node.availMem node > 0)) ==>
668
    let fn = Node.buildPeers node Container.empty
669
        nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
670
        nl = Container.fromList nlst
671
        cstats = Cluster.totalResources nl
672
    in Cluster.csAdsk cstats >= 0 &&
673
       Cluster.csAdsk cstats <= Cluster.csFdsk cstats
674

    
675
-- | Check that one instance is allocated correctly, without
676
-- rebalances needed
677
prop_ClusterAlloc_sane node inst =
678
    forAll (choose (5, 20)) $ \count ->
679
    not (Node.offline node)
680
            && not (Node.failN1 node)
681
            && Node.availDisk node > 0
682
            && Node.availMem node > 0
683
            ==>
684
    let nl = makeSmallCluster node count
685
        il = Container.empty
686
        inst' = setInstanceSmallerThanNode node inst
687
    in case Cluster.genAllocNodes defGroupList nl 2 True >>=
688
       Cluster.tryAlloc nl il inst' of
689
         Types.Bad _ -> False
690
         Types.Ok as ->
691
             case Cluster.asSolutions as of
692
               [] -> False
693
               (xnl, xi, _, cv):[] ->
694
                   let il' = Container.add (Instance.idx xi) xi il
695
                       tbl = Cluster.Table xnl il' cv []
696
                   in not (canBalance tbl True False)
697
               _ -> False
698

    
699
-- | Checks that on a 2-5 node cluster, we can allocate a random
700
-- instance spec via tiered allocation (whatever the original instance
701
-- spec), on either one or two nodes
702
prop_ClusterCanTieredAlloc node inst =
703
    forAll (choose (2, 5)) $ \count ->
704
    forAll (choose (1, 2)) $ \rqnodes ->
705
    not (Node.offline node)
706
            && not (Node.failN1 node)
707
            && isNodeBig node 4
708
            ==>
709
    let nl = makeSmallCluster node count
710
        il = Container.empty
711
        allocnodes = Cluster.genAllocNodes defGroupList nl rqnodes True
712
    in case allocnodes >>= \allocnodes' ->
713
        Cluster.tieredAlloc nl il inst allocnodes' [] [] of
714
         Types.Bad _ -> False
715
         Types.Ok (_, _, il', ixes, cstats) -> not (null ixes) &&
716
                                      IntMap.size il' == length ixes &&
717
                                      length ixes == length cstats
718

    
719
-- | Checks that on a 4-8 node cluster, once we allocate an instance,
720
-- we can also evacuate it
721
prop_ClusterAllocEvac node inst =
722
    forAll (choose (4, 8)) $ \count ->
723
    not (Node.offline node)
724
            && not (Node.failN1 node)
725
            && isNodeBig node 4
726
            ==>
727
    let nl = makeSmallCluster node count
728
        il = Container.empty
729
        inst' = setInstanceSmallerThanNode node inst
730
    in case Cluster.genAllocNodes defGroupList nl 2 True >>=
731
       Cluster.tryAlloc nl il inst' of
732
         Types.Bad _ -> False
733
         Types.Ok as ->
734
             case Cluster.asSolutions as of
735
               [] -> False
736
               (xnl, xi, _, _):[] ->
737
                   let sdx = Instance.sNode xi
738
                       il' = Container.add (Instance.idx xi) xi il
739
                   in case Cluster.tryEvac xnl il' [Instance.idx xi] [sdx] of
740
                        Just _ -> True
741
                        _ -> False
742
               _ -> False
743

    
744
-- | Check that allocating multiple instances on a cluster, then
745
-- adding an empty node, results in a valid rebalance
746
prop_ClusterAllocBalance node =
747
    forAll (choose (3, 5)) $ \count ->
748
    not (Node.offline node)
749
            && not (Node.failN1 node)
750
            && isNodeBig node 4
751
            && not (isNodeBig node 8)
752
            ==>
753
    let nl = makeSmallCluster node count
754
        (hnode, nl') = IntMap.deleteFindMax nl
755
        il = Container.empty
756
        allocnodes = Cluster.genAllocNodes defGroupList nl' 2 True
757
        i_templ = createInstance Types.unitMem Types.unitDsk Types.unitCpu
758
    in case allocnodes >>= \allocnodes' ->
759
        Cluster.iterateAlloc nl' il i_templ allocnodes' [] [] of
760
         Types.Bad _ -> False
761
         Types.Ok (_, xnl, il', _, _) ->
762
                   let ynl = Container.add (Node.idx hnode) hnode xnl
763
                       cv = Cluster.compCV ynl
764
                       tbl = Cluster.Table ynl il' cv []
765
                   in canBalance tbl True False
766

    
767
-- | Checks consistency
768
prop_ClusterCheckConsistency node inst =
769
  let nl = makeSmallCluster node 3
770
      [node1, node2, node3] = Container.elems nl
771
      node3' = node3 { Node.group = 1 }
772
      nl' = Container.add (Node.idx node3') node3' nl
773
      inst1 = Instance.setBoth inst (Node.idx node1) (Node.idx node2)
774
      inst2 = Instance.setBoth inst (Node.idx node1) Node.noSecondary
775
      inst3 = Instance.setBoth inst (Node.idx node1) (Node.idx node3)
776
      ccheck = Cluster.findSplitInstances nl' . Container.fromList
777
  in null (ccheck [(0, inst1)]) &&
778
     null (ccheck [(0, inst2)]) &&
779
     (not . null $ ccheck [(0, inst3)])
780

    
781
-- For now, we only test that we don't lose instances during the split
782
prop_ClusterSplitCluster node inst =
783
  forAll (choose (0, 100)) $ \icnt ->
784
  let nl = makeSmallCluster node 2
785
      (nl', il') = foldl (\(ns, is) _ -> assignInstance ns is inst 0 1)
786
                   (nl, Container.empty) [1..icnt]
787
      gni = Cluster.splitCluster nl' il'
788
  in sum (map (Container.size . snd . snd) gni) == icnt &&
789
     all (\(guuid, (nl'', _)) -> all ((== guuid) . Node.group)
790
                                 (Container.elems nl'')) gni
791

    
792
testCluster =
793
    [ run prop_Score_Zero
794
    , run prop_CStats_sane
795
    , run prop_ClusterAlloc_sane
796
    , run prop_ClusterCanTieredAlloc
797
    , run prop_ClusterAllocEvac
798
    , run prop_ClusterAllocBalance
799
    , run prop_ClusterCheckConsistency
800
    , run prop_ClusterSplitCluster
801
    ]
802

    
803
-- | Check that opcode serialization is idempotent
804

    
805
prop_OpCodes_serialization op =
806
  case J.readJSON (J.showJSON op) of
807
    J.Error _ -> False
808
    J.Ok op' -> op == op'
809
  where _types = op::OpCodes.OpCode
810

    
811
testOpCodes =
812
  [ run prop_OpCodes_serialization
813
  ]
814

    
815
-- | Check that (queued) job\/opcode status serialization is idempotent
816
prop_OpStatus_serialization os =
817
  case J.readJSON (J.showJSON os) of
818
    J.Error _ -> False
819
    J.Ok os' -> os == os'
820
  where _types = os::Jobs.OpStatus
821

    
822
prop_JobStatus_serialization js =
823
  case J.readJSON (J.showJSON js) of
824
    J.Error _ -> False
825
    J.Ok js' -> js == js'
826
  where _types = js::Jobs.JobStatus
827

    
828
testJobs =
829
  [ run prop_OpStatus_serialization
830
  , run prop_JobStatus_serialization
831
  ]
832

    
833
-- | Loader tests
834

    
835
prop_Loader_lookupNode ktn inst node =
836
  Loader.lookupNode nl inst node == Data.Map.lookup node nl
837
  where nl = Data.Map.fromList ktn
838

    
839
prop_Loader_lookupInstance kti inst =
840
  Loader.lookupInstance il inst == Data.Map.lookup inst il
841
  where il = Data.Map.fromList kti
842

    
843
prop_Loader_assignIndices nodes =
844
  Data.Map.size nassoc == length nodes &&
845
  Container.size kt == length nodes &&
846
  (if not (null nodes)
847
   then maximum (IntMap.keys kt) == length nodes - 1
848
   else True)
849
  where (nassoc, kt) = Loader.assignIndices (map (\n -> (Node.name n, n)) nodes)
850

    
851

    
852
-- | Checks that the number of primary instances recorded on the nodes
853
-- is zero
854
prop_Loader_mergeData ns =
855
  let na = Container.fromList $ map (\n -> (Node.idx n, n)) ns
856
  in case Loader.mergeData [] [] []
857
         (Loader.emptyCluster {Loader.cdNodes = na}) of
858
    Types.Bad _ -> False
859
    Types.Ok (Loader.ClusterData _ nl il _) ->
860
      let nodes = Container.elems nl
861
          instances = Container.elems il
862
      in (sum . map (length . Node.pList)) nodes == 0 &&
863
         null instances
864

    
865
testLoader =
866
  [ run prop_Loader_lookupNode
867
  , run prop_Loader_lookupInstance
868
  , run prop_Loader_assignIndices
869
  , run prop_Loader_mergeData
870
  ]