Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (30.8 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, isPrefixOf)
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 autobal =
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
        sbal = if autobal then "Y" else "N"
470
        inst = Text.loadInst nl
471
               [name, mem_s, dsk_s, vcpus_s, status,
472
                sbal, pnode, snode, tags]:: Maybe (String, Instance.Instance)
473
        fail1 = Text.loadInst nl
474
               [name, mem_s, dsk_s, vcpus_s, status,
475
                sbal, pnode, pnode, tags]:: Maybe (String, Instance.Instance)
476
        _types = ( name::String, mem::Int, dsk::Int
477
                 , vcpus::Int, status::String
478
                 , pnode::String, snode::String
479
                 , pdx::Types.Ndx, sdx::Types.Ndx
480
                 , autobal::Bool)
481
    in
482
      case inst of
483
        Nothing -> False
484
        Just (_, i) ->
485
            (Instance.name i == name &&
486
             Instance.vcpus i == vcpus &&
487
             Instance.mem i == mem &&
488
             Instance.pNode i == pdx &&
489
             Instance.sNode i == (if null snode
490
                                  then Node.noSecondary
491
                                  else rsdx) &&
492
             Instance.auto_balance i == autobal &&
493
             isNothing fail1)
494

    
495
prop_Text_Load_InstanceFail ktn fields =
496
    length fields /= 9 ==>
497
    case Text.loadInst nl fields of
498
      Right _ -> False
499
      Left msg -> isPrefixOf "Invalid/incomplete instance data: '" msg
500
    where nl = Data.Map.fromList ktn
501

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

    
532
prop_Text_Load_NodeFail fields =
533
    length fields /= 8 ==> isNothing $ Text.loadNode Data.Map.empty fields
534

    
535
prop_Text_NodeLSIdempotent node =
536
    (Text.loadNode defGroupAssoc.
537
         Utils.sepSplit '|' . Text.serializeNode defGroupList) n ==
538
    Just (Node.name n, n)
539
    -- override failN1 to what loadNode returns by default
540
    where n = node { Node.failN1 = True, Node.offline = False }
541

    
542
testText =
543
    [ run prop_Text_Load_Instance
544
    , run prop_Text_Load_InstanceFail
545
    , run prop_Text_Load_Node
546
    , run prop_Text_Load_NodeFail
547
    , run prop_Text_NodeLSIdempotent
548
    ]
549

    
550
-- Node tests
551

    
552
prop_Node_setAlias node name =
553
    Node.name newnode == Node.name node &&
554
    Node.alias newnode == name
555
    where _types = (node::Node.Node, name::String)
556
          newnode = Node.setAlias node name
557

    
558
prop_Node_setOffline node status =
559
    Node.offline newnode == status
560
    where newnode = Node.setOffline node status
561

    
562
prop_Node_setXmem node xm =
563
    Node.xMem newnode == xm
564
    where newnode = Node.setXmem node xm
565

    
566
prop_Node_setMcpu node mc =
567
    Node.mCpu newnode == mc
568
    where newnode = Node.setMcpu node mc
569

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

    
581
prop_Node_addPriFD node inst = Instance.dsk inst >= Node.fDsk node &&
582
                               not (Node.failN1 node)
583
                               ==>
584
                               case Node.addPri node inst'' of
585
                                 Types.OpFail Types.FailDisk -> True
586
                                 _ -> False
587
    where _types = (node::Node.Node, inst::Instance.Instance)
588
          inst' = setInstanceSmallerThanNode node inst
589
          inst'' = inst' { Instance.dsk = Instance.dsk inst }
590

    
591
prop_Node_addPriFC node inst = Instance.vcpus inst > Node.availCpu node &&
592
                               not (Node.failN1 node)
593
                               ==>
594
                               case Node.addPri node inst'' of
595
                                 Types.OpFail Types.FailCPU -> True
596
                                 _ -> False
597
    where _types = (node::Node.Node, inst::Instance.Instance)
598
          inst' = setInstanceSmallerThanNode node inst
599
          inst'' = inst' { Instance.vcpus = Instance.vcpus inst }
600

    
601
-- | Check that an instance add with too high memory or disk will be rejected
602
prop_Node_addSec node inst pdx =
603
    (Instance.mem inst >= (Node.fMem node - Node.rMem node) ||
604
     Instance.dsk inst >= Node.fDsk node) &&
605
    not (Node.failN1 node)
606
    ==> isFailure (Node.addSec node inst pdx)
607
        where _types = (node::Node.Node, inst::Instance.Instance, pdx::Int)
608

    
609
newtype SmallRatio = SmallRatio Double deriving Show
610
instance Arbitrary SmallRatio where
611
    arbitrary = do
612
      v <- choose (0, 1)
613
      return $ SmallRatio v
614

    
615
-- | Check mdsk setting
616
prop_Node_setMdsk node mx =
617
    Node.loDsk node' >= 0 &&
618
    fromIntegral (Node.loDsk node') <= Node.tDsk node &&
619
    Node.availDisk node' >= 0 &&
620
    Node.availDisk node' <= Node.fDsk node' &&
621
    fromIntegral (Node.availDisk node') <= Node.tDsk node' &&
622
    Node.mDsk node' == mx'
623
    where _types = (node::Node.Node, mx::SmallRatio)
624
          node' = Node.setMdsk node mx'
625
          SmallRatio mx' = mx
626

    
627
-- Check tag maps
628
prop_Node_tagMaps_idempotent tags =
629
    Node.delTags (Node.addTags m tags) tags == m
630
    where m = Data.Map.empty
631

    
632
prop_Node_tagMaps_reject tags =
633
    not (null tags) ==>
634
    any (\t -> Node.rejectAddTags m [t]) tags
635
    where m = Node.addTags Data.Map.empty tags
636

    
637
prop_Node_showField node =
638
  forAll (elements Node.defaultFields) $ \ field ->
639
  fst (Node.showHeader field) /= Types.unknownField &&
640
  Node.showField node field /= Types.unknownField
641

    
642

    
643
prop_Node_computeGroups nodes =
644
  let ng = Node.computeGroups nodes
645
      onlyuuid = map fst ng
646
  in length nodes == sum (map (length . snd) ng) &&
647
     all (\(guuid, ns) -> all ((== guuid) . Node.group) ns) ng &&
648
     length (nub onlyuuid) == length onlyuuid &&
649
     if null nodes then True else not (null ng)
650

    
651
testNode =
652
    [ run prop_Node_setAlias
653
    , run prop_Node_setOffline
654
    , run prop_Node_setMcpu
655
    , run prop_Node_setXmem
656
    , run prop_Node_addPriFM
657
    , run prop_Node_addPriFD
658
    , run prop_Node_addPriFC
659
    , run prop_Node_addSec
660
    , run prop_Node_setMdsk
661
    , run prop_Node_tagMaps_idempotent
662
    , run prop_Node_tagMaps_reject
663
    , run prop_Node_showField
664
    , run prop_Node_computeGroups
665
    ]
666

    
667

    
668
-- Cluster tests
669

    
670
-- | Check that the cluster score is close to zero for a homogeneous cluster
671
prop_Score_Zero node count =
672
    (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
673
     (Node.tDsk node > 0) && (Node.tMem node > 0)) ==>
674
    let fn = Node.buildPeers node Container.empty
675
        nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
676
        nl = Container.fromList nlst
677
        score = Cluster.compCV nl
678
    -- we can't say == 0 here as the floating point errors accumulate;
679
    -- this should be much lower than the default score in CLI.hs
680
    in score <= 1e-15
681

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

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

    
717
-- | Checks that on a 2-5 node cluster, we can allocate a random
718
-- instance spec via tiered allocation (whatever the original instance
719
-- spec), on either one or two nodes
720
prop_ClusterCanTieredAlloc node inst =
721
    forAll (choose (2, 5)) $ \count ->
722
    forAll (choose (1, 2)) $ \rqnodes ->
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
        allocnodes = Cluster.genAllocNodes defGroupList nl rqnodes True
730
    in case allocnodes >>= \allocnodes' ->
731
        Cluster.tieredAlloc nl il inst allocnodes' [] [] of
732
         Types.Bad _ -> False
733
         Types.Ok (_, _, il', ixes, cstats) -> not (null ixes) &&
734
                                      IntMap.size il' == length ixes &&
735
                                      length ixes == length cstats
736

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

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

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

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

    
810
testCluster =
811
    [ run prop_Score_Zero
812
    , run prop_CStats_sane
813
    , run prop_ClusterAlloc_sane
814
    , run prop_ClusterCanTieredAlloc
815
    , run prop_ClusterAllocEvac
816
    , run prop_ClusterAllocBalance
817
    , run prop_ClusterCheckConsistency
818
    , run prop_ClusterSplitCluster
819
    ]
820

    
821
-- | Check that opcode serialization is idempotent
822

    
823
prop_OpCodes_serialization op =
824
  case J.readJSON (J.showJSON op) of
825
    J.Error _ -> False
826
    J.Ok op' -> op == op'
827
  where _types = op::OpCodes.OpCode
828

    
829
testOpCodes =
830
  [ run prop_OpCodes_serialization
831
  ]
832

    
833
-- | Check that (queued) job\/opcode status serialization is idempotent
834
prop_OpStatus_serialization os =
835
  case J.readJSON (J.showJSON os) of
836
    J.Error _ -> False
837
    J.Ok os' -> os == os'
838
  where _types = os::Jobs.OpStatus
839

    
840
prop_JobStatus_serialization js =
841
  case J.readJSON (J.showJSON js) of
842
    J.Error _ -> False
843
    J.Ok js' -> js == js'
844
  where _types = js::Jobs.JobStatus
845

    
846
testJobs =
847
  [ run prop_OpStatus_serialization
848
  , run prop_JobStatus_serialization
849
  ]
850

    
851
-- | Loader tests
852

    
853
prop_Loader_lookupNode ktn inst node =
854
  Loader.lookupNode nl inst node == Data.Map.lookup node nl
855
  where nl = Data.Map.fromList ktn
856

    
857
prop_Loader_lookupInstance kti inst =
858
  Loader.lookupInstance il inst == Data.Map.lookup inst il
859
  where il = Data.Map.fromList kti
860

    
861
prop_Loader_assignIndices nodes =
862
  Data.Map.size nassoc == length nodes &&
863
  Container.size kt == length nodes &&
864
  (if not (null nodes)
865
   then maximum (IntMap.keys kt) == length nodes - 1
866
   else True)
867
  where (nassoc, kt) = Loader.assignIndices (map (\n -> (Node.name n, n)) nodes)
868

    
869

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

    
883
testLoader =
884
  [ run prop_Loader_lookupNode
885
  , run prop_Loader_lookupInstance
886
  , run prop_Loader_assignIndices
887
  , run prop_Loader_mergeData
888
  ]