Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (30.6 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" [] True (-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 [] True 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.OpInstanceReplaceDisks arbitrary arbitrary
226
          arbitrary arbitrary arbitrary
227
        "OP_INSTANCE_FAILOVER" ->
228
          liftM2 OpCodes.OpInstanceFailover arbitrary arbitrary
229
        "OP_INSTANCE_MIGRATE" ->
230
          liftM4 OpCodes.OpInstanceMigrate arbitrary arbitrary arbitrary
231
          arbitrary
232
        _ -> fail "Wrong opcode")
233

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

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

    
240
-- * Actual tests
241

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

    
250
-- | fromObjWithDefault, we test using the Maybe monad and an integer
251
-- value
252
prop_Utils_fromObjWithDefault def_value random_key =
253
    -- a missing key will be returned with the default
254
    Utils.fromObjWithDefault [] random_key def_value == Just def_value &&
255
    -- a found key will be returned as is, not with default
256
    Utils.fromObjWithDefault [(random_key, J.showJSON def_value)]
257
         random_key (def_value+1) == Just def_value
258
        where _types = (def_value :: Integer)
259

    
260
testUtils =
261
  [ run prop_Utils_commaJoinSplit
262
  , run prop_Utils_commaSplitJoin
263
  , run prop_Utils_fromObjWithDefault
264
  ]
265

    
266
-- | Make sure add is idempotent
267
prop_PeerMap_addIdempotent pmap key em =
268
    fn puniq == fn (fn puniq)
269
    where _types = (pmap::PeerMap.PeerMap,
270
                    key::PeerMap.Key, em::PeerMap.Elem)
271
          fn = PeerMap.add key em
272
          puniq = PeerMap.accumArray const pmap
273

    
274
-- | Make sure remove is idempotent
275
prop_PeerMap_removeIdempotent pmap key =
276
    fn puniq == fn (fn puniq)
277
    where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
278
          fn = PeerMap.remove key
279
          puniq = PeerMap.accumArray const pmap
280

    
281
-- | Make sure a missing item returns 0
282
prop_PeerMap_findMissing pmap key =
283
    PeerMap.find key (PeerMap.remove key puniq) == 0
284
    where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
285
          puniq = PeerMap.accumArray const pmap
286

    
287
-- | Make sure an added item is found
288
prop_PeerMap_addFind pmap key em =
289
    PeerMap.find key (PeerMap.add key em puniq) == em
290
    where _types = (pmap::PeerMap.PeerMap,
291
                    key::PeerMap.Key, em::PeerMap.Elem)
292
          puniq = PeerMap.accumArray const pmap
293

    
294
-- | Manual check that maxElem returns the maximum indeed, or 0 for null
295
prop_PeerMap_maxElem pmap =
296
    PeerMap.maxElem puniq == if null puniq then 0
297
                             else (maximum . snd . unzip) puniq
298
    where _types = pmap::PeerMap.PeerMap
299
          puniq = PeerMap.accumArray const pmap
300

    
301
testPeerMap =
302
    [ run prop_PeerMap_addIdempotent
303
    , run prop_PeerMap_removeIdempotent
304
    , run prop_PeerMap_maxElem
305
    , run prop_PeerMap_addFind
306
    , run prop_PeerMap_findMissing
307
    ]
308

    
309
-- Container tests
310

    
311
prop_Container_addTwo cdata i1 i2 =
312
    fn i1 i2 cont == fn i2 i1 cont &&
313
       fn i1 i2 cont == fn i1 i2 (fn i1 i2 cont)
314
    where _types = (cdata::[Int],
315
                    i1::Int, i2::Int)
316
          cont = foldl (\c x -> Container.add x x c) Container.empty cdata
317
          fn x1 x2 = Container.addTwo x1 x1 x2 x2
318

    
319
prop_Container_nameOf node =
320
  let nl = makeSmallCluster node 1
321
      fnode = head (Container.elems nl)
322
  in Container.nameOf nl (Node.idx fnode) == Node.name fnode
323

    
324
-- We test that in a cluster, given a random node, we can find it by
325
-- its name and alias, as long as all names and aliases are unique,
326
-- and that we fail to find a non-existing name
327
prop_Container_findByName node othername =
328
  forAll (choose (1, 20)) $ \ cnt ->
329
  forAll (choose (0, cnt - 1)) $ \ fidx ->
330
  forAll (vector cnt) $ \ names ->
331
  (length . nub) (map fst names ++ map snd names) ==
332
  length names * 2 &&
333
  not (othername `elem` (map fst names ++ map snd names)) ==>
334
  let nl = makeSmallCluster node cnt
335
      nodes = Container.elems nl
336
      nodes' = map (\((name, alias), nn) -> (Node.idx nn,
337
                                             nn { Node.name = name,
338
                                                  Node.alias = alias }))
339
               $ zip names nodes
340
      nl' = Container.fromList nodes'
341
      target = snd (nodes' !! fidx)
342
  in Container.findByName nl' (Node.name target) == Just target &&
343
     Container.findByName nl' (Node.alias target) == Just target &&
344
     Container.findByName nl' othername == Nothing
345

    
346
testContainer =
347
    [ run prop_Container_addTwo
348
    , run prop_Container_nameOf
349
    , run prop_Container_findByName
350
    ]
351

    
352
-- Simple instance tests, we only have setter/getters
353

    
354
prop_Instance_creat inst =
355
    Instance.name inst == Instance.alias inst
356

    
357
prop_Instance_setIdx inst idx =
358
    Instance.idx (Instance.setIdx inst idx) == idx
359
    where _types = (inst::Instance.Instance, idx::Types.Idx)
360

    
361
prop_Instance_setName inst name =
362
    Instance.name newinst == name &&
363
    Instance.alias newinst == name
364
    where _types = (inst::Instance.Instance, name::String)
365
          newinst = Instance.setName inst name
366

    
367
prop_Instance_setAlias inst name =
368
    Instance.name newinst == Instance.name inst &&
369
    Instance.alias newinst == name
370
    where _types = (inst::Instance.Instance, name::String)
371
          newinst = Instance.setAlias inst name
372

    
373
prop_Instance_setPri inst pdx =
374
    Instance.pNode (Instance.setPri inst pdx) == pdx
375
    where _types = (inst::Instance.Instance, pdx::Types.Ndx)
376

    
377
prop_Instance_setSec inst sdx =
378
    Instance.sNode (Instance.setSec inst sdx) == sdx
379
    where _types = (inst::Instance.Instance, sdx::Types.Ndx)
380

    
381
prop_Instance_setBoth inst pdx sdx =
382
    Instance.pNode si == pdx && Instance.sNode si == sdx
383
    where _types = (inst::Instance.Instance, pdx::Types.Ndx, sdx::Types.Ndx)
384
          si = Instance.setBoth inst pdx sdx
385

    
386
prop_Instance_runStatus_True inst =
387
    let run_st = Instance.running inst
388
        run_tx = Instance.runSt inst
389
    in
390
      run_tx `elem` Instance.runningStates ==> run_st
391

    
392
prop_Instance_runStatus_False inst =
393
    let run_st = Instance.running inst
394
        run_tx = Instance.runSt inst
395
    in
396
      run_tx `notElem` Instance.runningStates ==> not run_st
397

    
398
prop_Instance_shrinkMG inst =
399
    Instance.mem inst >= 2 * Types.unitMem ==>
400
        case Instance.shrinkByType inst Types.FailMem of
401
          Types.Ok inst' ->
402
              Instance.mem inst' == Instance.mem inst - Types.unitMem
403
          _ -> False
404

    
405
prop_Instance_shrinkMF inst =
406
    Instance.mem inst < 2 * Types.unitMem ==>
407
        Types.isBad $ Instance.shrinkByType inst Types.FailMem
408

    
409
prop_Instance_shrinkCG inst =
410
    Instance.vcpus inst >= 2 * Types.unitCpu ==>
411
        case Instance.shrinkByType inst Types.FailCPU of
412
          Types.Ok inst' ->
413
              Instance.vcpus inst' == Instance.vcpus inst - Types.unitCpu
414
          _ -> False
415

    
416
prop_Instance_shrinkCF inst =
417
    Instance.vcpus inst < 2 * Types.unitCpu ==>
418
        Types.isBad $ Instance.shrinkByType inst Types.FailCPU
419

    
420
prop_Instance_shrinkDG inst =
421
    Instance.dsk inst >= 2 * Types.unitDsk ==>
422
        case Instance.shrinkByType inst Types.FailDisk of
423
          Types.Ok inst' ->
424
              Instance.dsk inst' == Instance.dsk inst - Types.unitDsk
425
          _ -> False
426

    
427
prop_Instance_shrinkDF inst =
428
    Instance.dsk inst < 2 * Types.unitDsk ==>
429
        Types.isBad $ Instance.shrinkByType inst Types.FailDisk
430

    
431
prop_Instance_setMovable inst m =
432
    Instance.movable inst' == m
433
    where inst' = Instance.setMovable inst m
434

    
435
testInstance =
436
    [ run prop_Instance_creat
437
    , run prop_Instance_setIdx
438
    , run prop_Instance_setName
439
    , run prop_Instance_setAlias
440
    , run prop_Instance_setPri
441
    , run prop_Instance_setSec
442
    , run prop_Instance_setBoth
443
    , run prop_Instance_runStatus_True
444
    , run prop_Instance_runStatus_False
445
    , run prop_Instance_shrinkMG
446
    , run prop_Instance_shrinkMF
447
    , run prop_Instance_shrinkCG
448
    , run prop_Instance_shrinkCF
449
    , run prop_Instance_shrinkDG
450
    , run prop_Instance_shrinkDF
451
    , run prop_Instance_setMovable
452
    ]
453

    
454
-- Instance text loader tests
455

    
456
prop_Text_Load_Instance name mem dsk vcpus status pnode snode pdx sdx =
457
    not (null pnode) && pdx >= 0 && sdx >= 0 ==>
458
    let vcpus_s = show vcpus
459
        dsk_s = show dsk
460
        mem_s = show mem
461
        rsdx = if pdx == sdx
462
               then sdx + 1
463
               else sdx
464
        ndx = if null snode
465
              then [(pnode, pdx)]
466
              else [(pnode, pdx), (snode, rsdx)]
467
        nl = Data.Map.fromList ndx
468
        tags = ""
469
        inst = Text.loadInst nl
470
               [name, mem_s, dsk_s, vcpus_s, status, pnode, snode, tags]::
471
               Maybe (String, Instance.Instance)
472
        fail1 = Text.loadInst nl
473
               [name, mem_s, dsk_s, vcpus_s, status, pnode, pnode, tags]::
474
               Maybe (String, Instance.Instance)
475
        _types = ( name::String, mem::Int, dsk::Int
476
                 , vcpus::Int, status::String
477
                 , pnode::String, snode::String
478
                 , pdx::Types.Ndx, sdx::Types.Ndx)
479
    in
480
      case inst of
481
        Nothing -> False
482
        Just (_, i) ->
483
            (Instance.name i == name &&
484
             Instance.vcpus i == vcpus &&
485
             Instance.mem i == mem &&
486
             Instance.pNode i == pdx &&
487
             Instance.sNode i == (if null snode
488
                                  then Node.noSecondary
489
                                  else rsdx) &&
490
             isNothing fail1)
491

    
492
prop_Text_Load_InstanceFail ktn fields =
493
    length fields /= 8 ==> isNothing $ Text.loadInst nl fields
494
    where nl = Data.Map.fromList ktn
495

    
496
prop_Text_Load_Node name tm nm fm td fd tc fo =
497
    let conv v = if v < 0
498
                    then "?"
499
                    else show v
500
        tm_s = conv tm
501
        nm_s = conv nm
502
        fm_s = conv fm
503
        td_s = conv td
504
        fd_s = conv fd
505
        tc_s = conv tc
506
        fo_s = if fo
507
               then "Y"
508
               else "N"
509
        any_broken = any (< 0) [tm, nm, fm, td, fd, tc]
510
        gid = Group.uuid defGroup
511
    in case Text.loadNode defGroupAssoc
512
           [name, tm_s, nm_s, fm_s, td_s, fd_s, tc_s, fo_s, gid] of
513
         Nothing -> False
514
         Just (name', node) ->
515
             if fo || any_broken
516
             then Node.offline node
517
             else Node.name node == name' && name' == name &&
518
                  Node.alias node == name &&
519
                  Node.tMem node == fromIntegral tm &&
520
                  Node.nMem node == nm &&
521
                  Node.fMem node == fm &&
522
                  Node.tDsk node == fromIntegral td &&
523
                  Node.fDsk node == fd &&
524
                  Node.tCpu node == fromIntegral tc
525

    
526
prop_Text_Load_NodeFail fields =
527
    length fields /= 8 ==> isNothing $ Text.loadNode Data.Map.empty fields
528

    
529
prop_Text_NodeLSIdempotent node =
530
    (Text.loadNode defGroupAssoc.
531
         Utils.sepSplit '|' . Text.serializeNode defGroupList) n ==
532
    Just (Node.name n, n)
533
    -- override failN1 to what loadNode returns by default
534
    where n = node { Node.failN1 = True, Node.offline = False }
535

    
536
testText =
537
    [ run prop_Text_Load_Instance
538
    , run prop_Text_Load_InstanceFail
539
    , run prop_Text_Load_Node
540
    , run prop_Text_Load_NodeFail
541
    , run prop_Text_NodeLSIdempotent
542
    ]
543

    
544
-- Node tests
545

    
546
prop_Node_setAlias node name =
547
    Node.name newnode == Node.name node &&
548
    Node.alias newnode == name
549
    where _types = (node::Node.Node, name::String)
550
          newnode = Node.setAlias node name
551

    
552
prop_Node_setOffline node status =
553
    Node.offline newnode == status
554
    where newnode = Node.setOffline node status
555

    
556
prop_Node_setXmem node xm =
557
    Node.xMem newnode == xm
558
    where newnode = Node.setXmem node xm
559

    
560
prop_Node_setMcpu node mc =
561
    Node.mCpu newnode == mc
562
    where newnode = Node.setMcpu node mc
563

    
564
-- | Check that an instance add with too high memory or disk will be rejected
565
prop_Node_addPriFM node inst = Instance.mem inst >= Node.fMem node &&
566
                               not (Node.failN1 node)
567
                               ==>
568
                               case Node.addPri node inst'' of
569
                                 Types.OpFail Types.FailMem -> True
570
                                 _ -> False
571
    where _types = (node::Node.Node, inst::Instance.Instance)
572
          inst' = setInstanceSmallerThanNode node inst
573
          inst'' = inst' { Instance.mem = Instance.mem inst }
574

    
575
prop_Node_addPriFD node inst = Instance.dsk inst >= Node.fDsk node &&
576
                               not (Node.failN1 node)
577
                               ==>
578
                               case Node.addPri node inst'' of
579
                                 Types.OpFail Types.FailDisk -> True
580
                                 _ -> False
581
    where _types = (node::Node.Node, inst::Instance.Instance)
582
          inst' = setInstanceSmallerThanNode node inst
583
          inst'' = inst' { Instance.dsk = Instance.dsk inst }
584

    
585
prop_Node_addPriFC node inst = Instance.vcpus inst > Node.availCpu node &&
586
                               not (Node.failN1 node)
587
                               ==>
588
                               case Node.addPri node inst'' of
589
                                 Types.OpFail Types.FailCPU -> True
590
                                 _ -> False
591
    where _types = (node::Node.Node, inst::Instance.Instance)
592
          inst' = setInstanceSmallerThanNode node inst
593
          inst'' = inst' { Instance.vcpus = Instance.vcpus inst }
594

    
595
-- | Check that an instance add with too high memory or disk will be rejected
596
prop_Node_addSec node inst pdx =
597
    (Instance.mem inst >= (Node.fMem node - Node.rMem node) ||
598
     Instance.dsk inst >= Node.fDsk node) &&
599
    not (Node.failN1 node)
600
    ==> isFailure (Node.addSec node inst pdx)
601
        where _types = (node::Node.Node, inst::Instance.Instance, pdx::Int)
602

    
603
newtype SmallRatio = SmallRatio Double deriving Show
604
instance Arbitrary SmallRatio where
605
    arbitrary = do
606
      v <- choose (0, 1)
607
      return $ SmallRatio v
608

    
609
-- | Check mdsk setting
610
prop_Node_setMdsk node mx =
611
    Node.loDsk node' >= 0 &&
612
    fromIntegral (Node.loDsk node') <= Node.tDsk node &&
613
    Node.availDisk node' >= 0 &&
614
    Node.availDisk node' <= Node.fDsk node' &&
615
    fromIntegral (Node.availDisk node') <= Node.tDsk node' &&
616
    Node.mDsk node' == mx'
617
    where _types = (node::Node.Node, mx::SmallRatio)
618
          node' = Node.setMdsk node mx'
619
          SmallRatio mx' = mx
620

    
621
-- Check tag maps
622
prop_Node_tagMaps_idempotent tags =
623
    Node.delTags (Node.addTags m tags) tags == m
624
    where m = Data.Map.empty
625

    
626
prop_Node_tagMaps_reject tags =
627
    not (null tags) ==>
628
    any (\t -> Node.rejectAddTags m [t]) tags
629
    where m = Node.addTags Data.Map.empty tags
630

    
631
prop_Node_showField node =
632
  forAll (elements Node.defaultFields) $ \ field ->
633
  fst (Node.showHeader field) /= Types.unknownField &&
634
  Node.showField node field /= Types.unknownField
635

    
636

    
637
prop_Node_computeGroups nodes =
638
  let ng = Node.computeGroups nodes
639
      onlyuuid = map fst ng
640
  in length nodes == sum (map (length . snd) ng) &&
641
     all (\(guuid, ns) -> all ((== guuid) . Node.group) ns) ng &&
642
     length (nub onlyuuid) == length onlyuuid &&
643
     if null nodes then True else not (null ng)
644

    
645
testNode =
646
    [ run prop_Node_setAlias
647
    , run prop_Node_setOffline
648
    , run prop_Node_setMcpu
649
    , run prop_Node_setXmem
650
    , run prop_Node_addPriFM
651
    , run prop_Node_addPriFD
652
    , run prop_Node_addPriFC
653
    , run prop_Node_addSec
654
    , run prop_Node_setMdsk
655
    , run prop_Node_tagMaps_idempotent
656
    , run prop_Node_tagMaps_reject
657
    , run prop_Node_showField
658
    , run prop_Node_computeGroups
659
    ]
660

    
661

    
662
-- Cluster tests
663

    
664
-- | Check that the cluster score is close to zero for a homogeneous cluster
665
prop_Score_Zero node count =
666
    (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
667
     (Node.tDsk node > 0) && (Node.tMem 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
        score = Cluster.compCV nl
672
    -- we can't say == 0 here as the floating point errors accumulate;
673
    -- this should be much lower than the default score in CLI.hs
674
    in score <= 1e-15
675

    
676
-- | Check that cluster stats are sane
677
prop_CStats_sane node count =
678
    (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
679
     (Node.availDisk node > 0) && (Node.availMem node > 0)) ==>
680
    let fn = Node.buildPeers node Container.empty
681
        nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
682
        nl = Container.fromList nlst
683
        cstats = Cluster.totalResources nl
684
    in Cluster.csAdsk cstats >= 0 &&
685
       Cluster.csAdsk cstats <= Cluster.csFdsk cstats
686

    
687
-- | Check that one instance is allocated correctly, without
688
-- rebalances needed
689
prop_ClusterAlloc_sane node inst =
690
    forAll (choose (5, 20)) $ \count ->
691
    not (Node.offline node)
692
            && not (Node.failN1 node)
693
            && Node.availDisk node > 0
694
            && Node.availMem node > 0
695
            ==>
696
    let nl = makeSmallCluster node count
697
        il = Container.empty
698
        inst' = setInstanceSmallerThanNode node inst
699
    in case Cluster.genAllocNodes defGroupList nl 2 True >>=
700
       Cluster.tryAlloc nl il inst' of
701
         Types.Bad _ -> False
702
         Types.Ok as ->
703
             case Cluster.asSolutions as of
704
               [] -> False
705
               (xnl, xi, _, cv):[] ->
706
                   let il' = Container.add (Instance.idx xi) xi il
707
                       tbl = Cluster.Table xnl il' cv []
708
                   in not (canBalance tbl True False)
709
               _ -> False
710

    
711
-- | Checks that on a 2-5 node cluster, we can allocate a random
712
-- instance spec via tiered allocation (whatever the original instance
713
-- spec), on either one or two nodes
714
prop_ClusterCanTieredAlloc node inst =
715
    forAll (choose (2, 5)) $ \count ->
716
    forAll (choose (1, 2)) $ \rqnodes ->
717
    not (Node.offline node)
718
            && not (Node.failN1 node)
719
            && isNodeBig node 4
720
            ==>
721
    let nl = makeSmallCluster node count
722
        il = Container.empty
723
        allocnodes = Cluster.genAllocNodes defGroupList nl rqnodes True
724
    in case allocnodes >>= \allocnodes' ->
725
        Cluster.tieredAlloc nl il inst allocnodes' [] [] of
726
         Types.Bad _ -> False
727
         Types.Ok (_, _, il', ixes, cstats) -> not (null ixes) &&
728
                                      IntMap.size il' == length ixes &&
729
                                      length ixes == length cstats
730

    
731
-- | Checks that on a 4-8 node cluster, once we allocate an instance,
732
-- we can also evacuate it
733
prop_ClusterAllocEvac node inst =
734
    forAll (choose (4, 8)) $ \count ->
735
    not (Node.offline node)
736
            && not (Node.failN1 node)
737
            && isNodeBig node 4
738
            ==>
739
    let nl = makeSmallCluster node count
740
        il = Container.empty
741
        inst' = setInstanceSmallerThanNode node inst
742
    in case Cluster.genAllocNodes defGroupList nl 2 True >>=
743
       Cluster.tryAlloc nl il inst' of
744
         Types.Bad _ -> False
745
         Types.Ok as ->
746
             case Cluster.asSolutions as of
747
               [] -> False
748
               (xnl, xi, _, _):[] ->
749
                   let sdx = Instance.sNode xi
750
                       il' = Container.add (Instance.idx xi) xi il
751
                   in case Cluster.tryEvac xnl il' [Instance.idx xi] [sdx] of
752
                        Just _ -> True
753
                        _ -> False
754
               _ -> False
755

    
756
-- | Check that allocating multiple instances on a cluster, then
757
-- adding an empty node, results in a valid rebalance
758
prop_ClusterAllocBalance node =
759
    forAll (choose (3, 5)) $ \count ->
760
    not (Node.offline node)
761
            && not (Node.failN1 node)
762
            && isNodeBig node 4
763
            && not (isNodeBig node 8)
764
            ==>
765
    let nl = makeSmallCluster node count
766
        (hnode, nl') = IntMap.deleteFindMax nl
767
        il = Container.empty
768
        allocnodes = Cluster.genAllocNodes defGroupList nl' 2 True
769
        i_templ = createInstance Types.unitMem Types.unitDsk Types.unitCpu
770
    in case allocnodes >>= \allocnodes' ->
771
        Cluster.iterateAlloc nl' il i_templ allocnodes' [] [] of
772
         Types.Bad _ -> False
773
         Types.Ok (_, xnl, il', _, _) ->
774
                   let ynl = Container.add (Node.idx hnode) hnode xnl
775
                       cv = Cluster.compCV ynl
776
                       tbl = Cluster.Table ynl il' cv []
777
                   in canBalance tbl True False
778

    
779
-- | Checks consistency
780
prop_ClusterCheckConsistency node inst =
781
  let nl = makeSmallCluster node 3
782
      [node1, node2, node3] = Container.elems nl
783
      node3' = node3 { Node.group = 1 }
784
      nl' = Container.add (Node.idx node3') node3' nl
785
      inst1 = Instance.setBoth inst (Node.idx node1) (Node.idx node2)
786
      inst2 = Instance.setBoth inst (Node.idx node1) Node.noSecondary
787
      inst3 = Instance.setBoth inst (Node.idx node1) (Node.idx node3)
788
      ccheck = Cluster.findSplitInstances nl' . Container.fromList
789
  in null (ccheck [(0, inst1)]) &&
790
     null (ccheck [(0, inst2)]) &&
791
     (not . null $ ccheck [(0, inst3)])
792

    
793
-- For now, we only test that we don't lose instances during the split
794
prop_ClusterSplitCluster node inst =
795
  forAll (choose (0, 100)) $ \icnt ->
796
  let nl = makeSmallCluster node 2
797
      (nl', il') = foldl (\(ns, is) _ -> assignInstance ns is inst 0 1)
798
                   (nl, Container.empty) [1..icnt]
799
      gni = Cluster.splitCluster nl' il'
800
  in sum (map (Container.size . snd . snd) gni) == icnt &&
801
     all (\(guuid, (nl'', _)) -> all ((== guuid) . Node.group)
802
                                 (Container.elems nl'')) gni
803

    
804
testCluster =
805
    [ run prop_Score_Zero
806
    , run prop_CStats_sane
807
    , run prop_ClusterAlloc_sane
808
    , run prop_ClusterCanTieredAlloc
809
    , run prop_ClusterAllocEvac
810
    , run prop_ClusterAllocBalance
811
    , run prop_ClusterCheckConsistency
812
    , run prop_ClusterSplitCluster
813
    ]
814

    
815
-- | Check that opcode serialization is idempotent
816

    
817
prop_OpCodes_serialization op =
818
  case J.readJSON (J.showJSON op) of
819
    J.Error _ -> False
820
    J.Ok op' -> op == op'
821
  where _types = op::OpCodes.OpCode
822

    
823
testOpCodes =
824
  [ run prop_OpCodes_serialization
825
  ]
826

    
827
-- | Check that (queued) job\/opcode status serialization is idempotent
828
prop_OpStatus_serialization os =
829
  case J.readJSON (J.showJSON os) of
830
    J.Error _ -> False
831
    J.Ok os' -> os == os'
832
  where _types = os::Jobs.OpStatus
833

    
834
prop_JobStatus_serialization js =
835
  case J.readJSON (J.showJSON js) of
836
    J.Error _ -> False
837
    J.Ok js' -> js == js'
838
  where _types = js::Jobs.JobStatus
839

    
840
testJobs =
841
  [ run prop_OpStatus_serialization
842
  , run prop_JobStatus_serialization
843
  ]
844

    
845
-- | Loader tests
846

    
847
prop_Loader_lookupNode ktn inst node =
848
  Loader.lookupNode nl inst node == Data.Map.lookup node nl
849
  where nl = Data.Map.fromList ktn
850

    
851
prop_Loader_lookupInstance kti inst =
852
  Loader.lookupInstance il inst == Data.Map.lookup inst il
853
  where il = Data.Map.fromList kti
854

    
855
prop_Loader_assignIndices nodes =
856
  Data.Map.size nassoc == length nodes &&
857
  Container.size kt == length nodes &&
858
  (if not (null nodes)
859
   then maximum (IntMap.keys kt) == length nodes - 1
860
   else True)
861
  where (nassoc, kt) = Loader.assignIndices (map (\n -> (Node.name n, n)) nodes)
862

    
863

    
864
-- | Checks that the number of primary instances recorded on the nodes
865
-- is zero
866
prop_Loader_mergeData ns =
867
  let na = Container.fromList $ map (\n -> (Node.idx n, n)) ns
868
  in case Loader.mergeData [] [] []
869
         (Loader.emptyCluster {Loader.cdNodes = na}) of
870
    Types.Bad _ -> False
871
    Types.Ok (Loader.ClusterData _ nl il _) ->
872
      let nodes = Container.elems nl
873
          instances = Container.elems il
874
      in (sum . map (length . Node.pList)) nodes == 0 &&
875
         null instances
876

    
877
testLoader =
878
  [ run prop_Loader_lookupNode
879
  , run prop_Loader_lookupInstance
880
  , run prop_Loader_assignIndices
881
  , run prop_Loader_mergeData
882
  ]