Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (37.7 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
    , testTypes
38
    ) where
39

    
40
import Test.QuickCheck
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
import qualified Ganeti.Constants as C
68

    
69
run :: Testable prop => prop -> Args -> IO Result
70
run = flip quickCheckWithResult
71

    
72
-- * Constants
73

    
74
-- | Maximum memory (1TiB, somewhat random value).
75
maxMem :: Int
76
maxMem = 1024 * 1024
77

    
78
-- | Maximum disk (8TiB, somewhat random value).
79
maxDsk :: Int
80
maxDsk = 1024 * 1024 * 8
81

    
82
-- | Max CPUs (1024, somewhat random value).
83
maxCpu :: Int
84
maxCpu = 1024
85

    
86
defGroup :: Group.Group
87
defGroup = flip Group.setIdx 0 $
88
               Group.create "default" Utils.defaultGroupID
89
                    Types.AllocPreferred
90

    
91
defGroupList :: Group.List
92
defGroupList = Container.fromList [(Group.idx defGroup, defGroup)]
93

    
94
defGroupAssoc :: Data.Map.Map String Types.Gdx
95
defGroupAssoc = Data.Map.singleton (Group.uuid defGroup) (Group.idx defGroup)
96

    
97
-- * Helper functions
98

    
99
-- | Simple checker for whether OpResult is fail or pass.
100
isFailure :: Types.OpResult a -> Bool
101
isFailure (Types.OpFail _) = True
102
isFailure _ = False
103

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

    
111
-- | Create an instance given its spec.
112
createInstance mem dsk vcpus =
113
    Instance.create "inst-unnamed" mem dsk vcpus "running" [] True (-1) (-1)
114
                    Types.DTDrbd8
115

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

    
124
-- | Checks if a node is "big" enough.
125
isNodeBig :: Node.Node -> Int -> Bool
126
isNodeBig node size = Node.availDisk node > size * Types.unitDsk
127
                      && Node.availMem node > size * Types.unitMem
128
                      && Node.availCpu node > size * Types.unitCpu
129

    
130
canBalance :: Cluster.Table -> Bool -> Bool -> Bool -> Bool
131
canBalance tbl dm im evac = isJust $ Cluster.tryBalance tbl dm im evac 0 0
132

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

    
152
-- * Arbitrary instances
153

    
154
-- | Defines a DNS name.
155
newtype DNSChar = DNSChar { dnsGetChar::Char }
156

    
157
instance Arbitrary DNSChar where
158
    arbitrary = do
159
      x <- elements (['a'..'z'] ++ ['0'..'9'] ++ "_-")
160
      return (DNSChar x)
161

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

    
168

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

    
177
-- let's generate a random instance
178
instance Arbitrary Instance.Instance where
179
    arbitrary = do
180
      name <- getFQDN
181
      mem <- choose (0, maxMem)
182
      dsk <- choose (0, maxDsk)
183
      run_st <- elements [ C.inststErrorup
184
                         , C.inststErrordown
185
                         , C.inststAdmindown
186
                         , C.inststNodedown
187
                         , C.inststNodeoffline
188
                         , C.inststRunning
189
                         , "no_such_status1"
190
                         , "no_such_status2"]
191
      pn <- arbitrary
192
      sn <- arbitrary
193
      vcpus <- choose (0, maxCpu)
194
      return $ Instance.create name mem dsk vcpus run_st [] True pn sn
195
                               Types.DTDrbd8
196

    
197
-- | Generas an arbitrary node based on sizing information.
198
genNode :: Maybe Int -- ^ Minimum node size in terms of units
199
        -> Maybe Int -- ^ Maximum node size (when Nothing, bounded
200
                     -- just by the max... constants)
201
        -> Gen Node.Node
202
genNode min_multiplier max_multiplier = do
203
  let (base_mem, base_dsk, base_cpu) =
204
          case min_multiplier of
205
            Just mm -> (mm * Types.unitMem,
206
                        mm * Types.unitDsk,
207
                        mm * Types.unitCpu)
208
            Nothing -> (0, 0, 0)
209
      (top_mem, top_dsk, top_cpu)  =
210
          case max_multiplier of
211
            Just mm -> (mm * Types.unitMem,
212
                        mm * Types.unitDsk,
213
                        mm * Types.unitCpu)
214
            Nothing -> (maxMem, maxDsk, maxCpu)
215
  name  <- getFQDN
216
  mem_t <- choose (base_mem, top_mem)
217
  mem_f <- choose (base_mem, mem_t)
218
  mem_n <- choose (0, mem_t - mem_f)
219
  dsk_t <- choose (base_dsk, top_dsk)
220
  dsk_f <- choose (base_dsk, dsk_t)
221
  cpu_t <- choose (base_cpu, top_cpu)
222
  offl  <- arbitrary
223
  let n = Node.create name (fromIntegral mem_t) mem_n mem_f
224
          (fromIntegral dsk_t) dsk_f (fromIntegral cpu_t) offl 0
225
  return $ Node.buildPeers n Container.empty
226

    
227
-- and a random node
228
instance Arbitrary Node.Node where
229
    arbitrary = genNode Nothing Nothing
230

    
231
-- replace disks
232
instance Arbitrary OpCodes.ReplaceDisksMode where
233
  arbitrary = elements [ OpCodes.ReplaceOnPrimary
234
                       , OpCodes.ReplaceOnSecondary
235
                       , OpCodes.ReplaceNewSecondary
236
                       , OpCodes.ReplaceAuto
237
                       ]
238

    
239
instance Arbitrary OpCodes.OpCode where
240
  arbitrary = do
241
    op_id <- elements [ "OP_TEST_DELAY"
242
                      , "OP_INSTANCE_REPLACE_DISKS"
243
                      , "OP_INSTANCE_FAILOVER"
244
                      , "OP_INSTANCE_MIGRATE"
245
                      ]
246
    (case op_id of
247
        "OP_TEST_DELAY" ->
248
          liftM3 OpCodes.OpTestDelay arbitrary arbitrary arbitrary
249
        "OP_INSTANCE_REPLACE_DISKS" ->
250
          liftM5 OpCodes.OpInstanceReplaceDisks arbitrary arbitrary
251
          arbitrary arbitrary arbitrary
252
        "OP_INSTANCE_FAILOVER" ->
253
          liftM2 OpCodes.OpInstanceFailover arbitrary arbitrary
254
        "OP_INSTANCE_MIGRATE" ->
255
          liftM4 OpCodes.OpInstanceMigrate arbitrary arbitrary arbitrary
256
          arbitrary
257
        _ -> fail "Wrong opcode")
258

    
259
instance Arbitrary Jobs.OpStatus where
260
  arbitrary = elements [minBound..maxBound]
261

    
262
instance Arbitrary Jobs.JobStatus where
263
  arbitrary = elements [minBound..maxBound]
264

    
265
newtype SmallRatio = SmallRatio Double deriving Show
266
instance Arbitrary SmallRatio where
267
    arbitrary = do
268
      v <- choose (0, 1)
269
      return $ SmallRatio v
270

    
271
instance Arbitrary Types.AllocPolicy where
272
  arbitrary = elements [minBound..maxBound]
273

    
274
instance Arbitrary Types.DiskTemplate where
275
  arbitrary = elements [minBound..maxBound]
276

    
277
-- * Actual tests
278

    
279
-- ** Utils tests
280

    
281
-- | If the list is not just an empty element, and if the elements do
282
-- not contain commas, then join+split should be idempotent.
283
prop_Utils_commaJoinSplit =
284
    forAll (arbitrary `suchThat`
285
            (\l -> l /= [""] && all (not . elem ',') l )) $ \lst ->
286
    Utils.sepSplit ',' (Utils.commaJoin lst) == lst
287

    
288
-- | Split and join should always be idempotent.
289
prop_Utils_commaSplitJoin s = Utils.commaJoin (Utils.sepSplit ',' s) == s
290

    
291
-- | fromObjWithDefault, we test using the Maybe monad and an integer
292
-- value.
293
prop_Utils_fromObjWithDefault def_value random_key =
294
    -- a missing key will be returned with the default
295
    Utils.fromObjWithDefault [] random_key def_value == Just def_value &&
296
    -- a found key will be returned as is, not with default
297
    Utils.fromObjWithDefault [(random_key, J.showJSON def_value)]
298
         random_key (def_value+1) == Just def_value
299
        where _types = def_value :: Integer
300

    
301
-- | Test that functional if' behaves like the syntactic sugar if.
302
prop_Utils_if'if :: Bool -> Int -> Int -> Bool
303
prop_Utils_if'if cnd a b = Utils.if' cnd a b == if cnd then a else b
304

    
305
-- | Test basic select functionality
306
prop_Utils_select :: Int   -- ^ Default result
307
                  -> [Int] -- ^ List of False values
308
                  -> [Int] -- ^ List of True values
309
                  -> Bool  -- ^ Test result
310
prop_Utils_select def lst1 lst2 =
311
  Utils.select def cndlist == expectedresult
312
  where expectedresult = Utils.if' (null lst2) def (head lst2)
313
        flist = map (\e -> (False, e)) lst1
314
        tlist = map (\e -> (True, e)) lst2
315
        cndlist = flist ++ tlist
316

    
317
-- | Test basic select functionality with undefined default
318
prop_Utils_select_undefd :: [Int] -- ^ List of False values
319
                         -> NonEmptyList Int -- ^ List of True values
320
                         -> Bool  -- ^ Test result
321
prop_Utils_select_undefd lst1 (NonEmpty lst2) =
322
  Utils.select undefined cndlist == head lst2
323
  where flist = map (\e -> (False, e)) lst1
324
        tlist = map (\e -> (True, e)) lst2
325
        cndlist = flist ++ tlist
326

    
327
-- | Test basic select functionality with undefined list values
328
prop_Utils_select_undefv :: [Int] -- ^ List of False values
329
                         -> NonEmptyList Int -- ^ List of True values
330
                         -> Bool  -- ^ Test result
331
prop_Utils_select_undefv lst1 (NonEmpty lst2) =
332
  Utils.select undefined cndlist == head lst2
333
  where flist = map (\e -> (False, e)) lst1
334
        tlist = map (\e -> (True, e)) lst2
335
        cndlist = flist ++ tlist ++ [undefined]
336

    
337
prop_Utils_parseUnit (NonNegative n) =
338
    Utils.parseUnit (show n) == Types.Ok n &&
339
    Utils.parseUnit (show n ++ "m") == Types.Ok n &&
340
    (case Utils.parseUnit (show n ++ "M") of
341
      Types.Ok m -> if n > 0
342
                    then m < n  -- for positive values, X MB is less than X MiB
343
                    else m == 0 -- but for 0, 0 MB == 0 MiB
344
      Types.Bad _ -> False) &&
345
    Utils.parseUnit (show n ++ "g") == Types.Ok (n*1024) &&
346
    Utils.parseUnit (show n ++ "t") == Types.Ok (n*1048576) &&
347
    Types.isBad (Utils.parseUnit (show n ++ "x")::Types.Result Int)
348
    where _types = (n::Int)
349

    
350
-- | Test list for the Utils module.
351
testUtils =
352
  [ run prop_Utils_commaJoinSplit
353
  , run prop_Utils_commaSplitJoin
354
  , run prop_Utils_fromObjWithDefault
355
  , run prop_Utils_if'if
356
  , run prop_Utils_select
357
  , run prop_Utils_select_undefd
358
  , run prop_Utils_select_undefv
359
  , run prop_Utils_parseUnit
360
  ]
361

    
362
-- ** PeerMap tests
363

    
364
-- | Make sure add is idempotent.
365
prop_PeerMap_addIdempotent pmap key em =
366
    fn puniq == fn (fn puniq)
367
    where _types = (pmap::PeerMap.PeerMap,
368
                    key::PeerMap.Key, em::PeerMap.Elem)
369
          fn = PeerMap.add key em
370
          puniq = PeerMap.accumArray const pmap
371

    
372
-- | Make sure remove is idempotent.
373
prop_PeerMap_removeIdempotent pmap key =
374
    fn puniq == fn (fn puniq)
375
    where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
376
          fn = PeerMap.remove key
377
          puniq = PeerMap.accumArray const pmap
378

    
379
-- | Make sure a missing item returns 0.
380
prop_PeerMap_findMissing pmap key =
381
    PeerMap.find key (PeerMap.remove key puniq) == 0
382
    where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
383
          puniq = PeerMap.accumArray const pmap
384

    
385
-- | Make sure an added item is found.
386
prop_PeerMap_addFind pmap key em =
387
    PeerMap.find key (PeerMap.add key em puniq) == em
388
    where _types = (pmap::PeerMap.PeerMap,
389
                    key::PeerMap.Key, em::PeerMap.Elem)
390
          puniq = PeerMap.accumArray const pmap
391

    
392
-- | Manual check that maxElem returns the maximum indeed, or 0 for null.
393
prop_PeerMap_maxElem pmap =
394
    PeerMap.maxElem puniq == if null puniq then 0
395
                             else (maximum . snd . unzip) puniq
396
    where _types = pmap::PeerMap.PeerMap
397
          puniq = PeerMap.accumArray const pmap
398

    
399
-- | List of tests for the PeerMap module.
400
testPeerMap =
401
    [ run prop_PeerMap_addIdempotent
402
    , run prop_PeerMap_removeIdempotent
403
    , run prop_PeerMap_maxElem
404
    , run prop_PeerMap_addFind
405
    , run prop_PeerMap_findMissing
406
    ]
407

    
408
-- ** Container tests
409

    
410
prop_Container_addTwo cdata i1 i2 =
411
    fn i1 i2 cont == fn i2 i1 cont &&
412
       fn i1 i2 cont == fn i1 i2 (fn i1 i2 cont)
413
    where _types = (cdata::[Int],
414
                    i1::Int, i2::Int)
415
          cont = foldl (\c x -> Container.add x x c) Container.empty cdata
416
          fn x1 x2 = Container.addTwo x1 x1 x2 x2
417

    
418
prop_Container_nameOf node =
419
  let nl = makeSmallCluster node 1
420
      fnode = head (Container.elems nl)
421
  in Container.nameOf nl (Node.idx fnode) == Node.name fnode
422

    
423
-- | We test that in a cluster, given a random node, we can find it by
424
-- its name and alias, as long as all names and aliases are unique,
425
-- and that we fail to find a non-existing name.
426
prop_Container_findByName node othername =
427
  forAll (choose (1, 20)) $ \ cnt ->
428
  forAll (choose (0, cnt - 1)) $ \ fidx ->
429
  forAll (vector cnt) $ \ names ->
430
  (length . nub) (map fst names ++ map snd names) ==
431
  length names * 2 &&
432
  not (othername `elem` (map fst names ++ map snd names)) ==>
433
  let nl = makeSmallCluster node cnt
434
      nodes = Container.elems nl
435
      nodes' = map (\((name, alias), nn) -> (Node.idx nn,
436
                                             nn { Node.name = name,
437
                                                  Node.alias = alias }))
438
               $ zip names nodes
439
      nl' = Container.fromList nodes'
440
      target = snd (nodes' !! fidx)
441
  in Container.findByName nl' (Node.name target) == Just target &&
442
     Container.findByName nl' (Node.alias target) == Just target &&
443
     Container.findByName nl' othername == Nothing
444

    
445
testContainer =
446
    [ run prop_Container_addTwo
447
    , run prop_Container_nameOf
448
    , run prop_Container_findByName
449
    ]
450

    
451
-- ** Instance tests
452

    
453
-- Simple instance tests, we only have setter/getters
454

    
455
prop_Instance_creat inst =
456
    Instance.name inst == Instance.alias inst
457

    
458
prop_Instance_setIdx inst idx =
459
    Instance.idx (Instance.setIdx inst idx) == idx
460
    where _types = (inst::Instance.Instance, idx::Types.Idx)
461

    
462
prop_Instance_setName inst name =
463
    Instance.name newinst == name &&
464
    Instance.alias newinst == name
465
    where _types = (inst::Instance.Instance, name::String)
466
          newinst = Instance.setName inst name
467

    
468
prop_Instance_setAlias inst name =
469
    Instance.name newinst == Instance.name inst &&
470
    Instance.alias newinst == name
471
    where _types = (inst::Instance.Instance, name::String)
472
          newinst = Instance.setAlias inst name
473

    
474
prop_Instance_setPri inst pdx =
475
    Instance.pNode (Instance.setPri inst pdx) == pdx
476
    where _types = (inst::Instance.Instance, pdx::Types.Ndx)
477

    
478
prop_Instance_setSec inst sdx =
479
    Instance.sNode (Instance.setSec inst sdx) == sdx
480
    where _types = (inst::Instance.Instance, sdx::Types.Ndx)
481

    
482
prop_Instance_setBoth inst pdx sdx =
483
    Instance.pNode si == pdx && Instance.sNode si == sdx
484
    where _types = (inst::Instance.Instance, pdx::Types.Ndx, sdx::Types.Ndx)
485
          si = Instance.setBoth inst pdx sdx
486

    
487
prop_Instance_runStatus_True =
488
    forAll (arbitrary `suchThat`
489
            ((`elem` Instance.runningStates) . Instance.runSt))
490
    Instance.running
491

    
492
prop_Instance_runStatus_False inst =
493
    let run_st = Instance.running inst
494
        run_tx = Instance.runSt inst
495
    in
496
      run_tx `notElem` Instance.runningStates ==> not run_st
497

    
498
prop_Instance_shrinkMG inst =
499
    Instance.mem inst >= 2 * Types.unitMem ==>
500
        case Instance.shrinkByType inst Types.FailMem of
501
          Types.Ok inst' ->
502
              Instance.mem inst' == Instance.mem inst - Types.unitMem
503
          _ -> False
504

    
505
prop_Instance_shrinkMF inst =
506
    forAll (choose (0, 2 * Types.unitMem - 1)) $ \mem ->
507
    let inst' = inst { Instance.mem = mem}
508
    in Types.isBad $ Instance.shrinkByType inst' Types.FailMem
509

    
510
prop_Instance_shrinkCG inst =
511
    Instance.vcpus inst >= 2 * Types.unitCpu ==>
512
        case Instance.shrinkByType inst Types.FailCPU of
513
          Types.Ok inst' ->
514
              Instance.vcpus inst' == Instance.vcpus inst - Types.unitCpu
515
          _ -> False
516

    
517
prop_Instance_shrinkCF inst =
518
    forAll (choose (0, 2 * Types.unitCpu - 1)) $ \vcpus ->
519
    let inst' = inst { Instance.vcpus = vcpus }
520
    in Types.isBad $ Instance.shrinkByType inst' Types.FailCPU
521

    
522
prop_Instance_shrinkDG inst =
523
    Instance.dsk inst >= 2 * Types.unitDsk ==>
524
        case Instance.shrinkByType inst Types.FailDisk of
525
          Types.Ok inst' ->
526
              Instance.dsk inst' == Instance.dsk inst - Types.unitDsk
527
          _ -> False
528

    
529
prop_Instance_shrinkDF inst =
530
    forAll (choose (0, 2 * Types.unitDsk - 1)) $ \dsk ->
531
    let inst' = inst { Instance.dsk = dsk }
532
    in Types.isBad $ Instance.shrinkByType inst' Types.FailDisk
533

    
534
prop_Instance_setMovable inst m =
535
    Instance.movable inst' == m
536
    where inst' = Instance.setMovable inst m
537

    
538
testInstance =
539
    [ run prop_Instance_creat
540
    , run prop_Instance_setIdx
541
    , run prop_Instance_setName
542
    , run prop_Instance_setAlias
543
    , run prop_Instance_setPri
544
    , run prop_Instance_setSec
545
    , run prop_Instance_setBoth
546
    , run prop_Instance_runStatus_True
547
    , run prop_Instance_runStatus_False
548
    , run prop_Instance_shrinkMG
549
    , run prop_Instance_shrinkMF
550
    , run prop_Instance_shrinkCG
551
    , run prop_Instance_shrinkCF
552
    , run prop_Instance_shrinkDG
553
    , run prop_Instance_shrinkDF
554
    , run prop_Instance_setMovable
555
    ]
556

    
557
-- ** Text backend tests
558

    
559
-- Instance text loader tests
560

    
561
prop_Text_Load_Instance name mem dsk vcpus status
562
                        (NonEmpty pnode) snode
563
                        (NonNegative pdx) (NonNegative sdx) autobal dt =
564
    pnode /= snode && pdx /= sdx ==>
565
    let vcpus_s = show vcpus
566
        dsk_s = show dsk
567
        mem_s = show mem
568
        ndx = if null snode
569
              then [(pnode, pdx)]
570
              else [(pnode, pdx), (snode, sdx)]
571
        nl = Data.Map.fromList ndx
572
        tags = ""
573
        sbal = if autobal then "Y" else "N"
574
        sdt = Types.dtToString dt
575
        inst = Text.loadInst nl
576
               [name, mem_s, dsk_s, vcpus_s, status,
577
                sbal, pnode, snode, sdt, tags]
578
        fail1 = Text.loadInst nl
579
               [name, mem_s, dsk_s, vcpus_s, status,
580
                sbal, pnode, pnode, tags]
581
        _types = ( name::String, mem::Int, dsk::Int
582
                 , vcpus::Int, status::String
583
                 , snode::String
584
                 , autobal::Bool)
585
    in
586
      case inst of
587
        Types.Bad msg -> printTestCase ("Failed to load instance: " ++ msg)
588
                         False
589
        Types.Ok (_, i) -> printTestCase ("Mismatch in some field while\
590
                                          \ loading the instance") $
591
            Instance.name i == name &&
592
            Instance.vcpus i == vcpus &&
593
            Instance.mem i == mem &&
594
            Instance.pNode i == pdx &&
595
            Instance.sNode i == (if null snode
596
                                 then Node.noSecondary
597
                                 else sdx) &&
598
            Instance.autoBalance i == autobal &&
599
            Types.isBad fail1
600

    
601
prop_Text_Load_InstanceFail ktn fields =
602
    length fields /= 10 ==>
603
    case Text.loadInst nl fields of
604
      Types.Ok _ -> printTestCase "Managed to load instance from invalid\
605
                                  \ data" False
606
      Types.Bad msg -> printTestCase ("Unrecognised error message: " ++ msg) $
607
                       "Invalid/incomplete instance data: '" `isPrefixOf` msg
608
    where nl = Data.Map.fromList ktn
609

    
610
prop_Text_Load_Node name tm nm fm td fd tc fo =
611
    let conv v = if v < 0
612
                    then "?"
613
                    else show v
614
        tm_s = conv tm
615
        nm_s = conv nm
616
        fm_s = conv fm
617
        td_s = conv td
618
        fd_s = conv fd
619
        tc_s = conv tc
620
        fo_s = if fo
621
               then "Y"
622
               else "N"
623
        any_broken = any (< 0) [tm, nm, fm, td, fd, tc]
624
        gid = Group.uuid defGroup
625
    in case Text.loadNode defGroupAssoc
626
           [name, tm_s, nm_s, fm_s, td_s, fd_s, tc_s, fo_s, gid] of
627
         Nothing -> False
628
         Just (name', node) ->
629
             if fo || any_broken
630
             then Node.offline node
631
             else Node.name node == name' && name' == name &&
632
                  Node.alias node == name &&
633
                  Node.tMem node == fromIntegral tm &&
634
                  Node.nMem node == nm &&
635
                  Node.fMem node == fm &&
636
                  Node.tDsk node == fromIntegral td &&
637
                  Node.fDsk node == fd &&
638
                  Node.tCpu node == fromIntegral tc
639

    
640
prop_Text_Load_NodeFail fields =
641
    length fields /= 8 ==> isNothing $ Text.loadNode Data.Map.empty fields
642

    
643
prop_Text_NodeLSIdempotent node =
644
    (Text.loadNode defGroupAssoc.
645
         Utils.sepSplit '|' . Text.serializeNode defGroupList) n ==
646
    Just (Node.name n, n)
647
    -- override failN1 to what loadNode returns by default
648
    where n = node { Node.failN1 = True, Node.offline = False }
649

    
650
testText =
651
    [ run prop_Text_Load_Instance
652
    , run prop_Text_Load_InstanceFail
653
    , run prop_Text_Load_Node
654
    , run prop_Text_Load_NodeFail
655
    , run prop_Text_NodeLSIdempotent
656
    ]
657

    
658
-- ** Node tests
659

    
660
prop_Node_setAlias node name =
661
    Node.name newnode == Node.name node &&
662
    Node.alias newnode == name
663
    where _types = (node::Node.Node, name::String)
664
          newnode = Node.setAlias node name
665

    
666
prop_Node_setOffline node status =
667
    Node.offline newnode == status
668
    where newnode = Node.setOffline node status
669

    
670
prop_Node_setXmem node xm =
671
    Node.xMem newnode == xm
672
    where newnode = Node.setXmem node xm
673

    
674
prop_Node_setMcpu node mc =
675
    Node.mCpu newnode == mc
676
    where newnode = Node.setMcpu node mc
677

    
678
-- | Check that an instance add with too high memory or disk will be
679
-- rejected.
680
prop_Node_addPriFM node inst = Instance.mem inst >= Node.fMem node &&
681
                               not (Node.failN1 node)
682
                               ==>
683
                               case Node.addPri node inst'' of
684
                                 Types.OpFail Types.FailMem -> True
685
                                 _ -> False
686
    where _types = (node::Node.Node, inst::Instance.Instance)
687
          inst' = setInstanceSmallerThanNode node inst
688
          inst'' = inst' { Instance.mem = Instance.mem inst }
689

    
690
prop_Node_addPriFD node inst = Instance.dsk inst >= Node.fDsk node &&
691
                               not (Node.failN1 node)
692
                               ==>
693
                               case Node.addPri node inst'' of
694
                                 Types.OpFail Types.FailDisk -> True
695
                                 _ -> False
696
    where _types = (node::Node.Node, inst::Instance.Instance)
697
          inst' = setInstanceSmallerThanNode node inst
698
          inst'' = inst' { Instance.dsk = Instance.dsk inst }
699

    
700
prop_Node_addPriFC node inst (Positive extra) =
701
    not (Node.failN1 node) ==>
702
        case Node.addPri node inst'' of
703
          Types.OpFail Types.FailCPU -> True
704
          _ -> False
705
    where _types = (node::Node.Node, inst::Instance.Instance)
706
          inst' = setInstanceSmallerThanNode node inst
707
          inst'' = inst' { Instance.vcpus = Node.availCpu node + extra }
708

    
709
-- | Check that an instance add with too high memory or disk will be
710
-- rejected.
711
prop_Node_addSec node inst pdx =
712
    (Instance.mem inst >= (Node.fMem node - Node.rMem node) ||
713
     Instance.dsk inst >= Node.fDsk node) &&
714
    not (Node.failN1 node)
715
    ==> isFailure (Node.addSec node inst pdx)
716
        where _types = (node::Node.Node, inst::Instance.Instance, pdx::Int)
717

    
718
-- | Checks for memory reservation changes.
719
prop_Node_rMem inst =
720
    forAll (arbitrary `suchThat` ((> Types.unitMem) . Node.fMem)) $ \node ->
721
    -- ab = auto_balance, nb = non-auto_balance
722
    -- we use -1 as the primary node of the instance
723
    let inst' = inst { Instance.pNode = -1, Instance.autoBalance = True }
724
        inst_ab = setInstanceSmallerThanNode node inst'
725
        inst_nb = inst_ab { Instance.autoBalance = False }
726
        -- now we have the two instances, identical except the
727
        -- autoBalance attribute
728
        orig_rmem = Node.rMem node
729
        inst_idx = Instance.idx inst_ab
730
        node_add_ab = Node.addSec node inst_ab (-1)
731
        node_add_nb = Node.addSec node inst_nb (-1)
732
        node_del_ab = liftM (flip Node.removeSec inst_ab) node_add_ab
733
        node_del_nb = liftM (flip Node.removeSec inst_nb) node_add_nb
734
    in case (node_add_ab, node_add_nb, node_del_ab, node_del_nb) of
735
         (Types.OpGood a_ab, Types.OpGood a_nb,
736
          Types.OpGood d_ab, Types.OpGood d_nb) ->
737
             printTestCase "Consistency checks failed" $
738
             Node.rMem a_ab >  orig_rmem &&
739
             Node.rMem a_ab - orig_rmem == Instance.mem inst_ab &&
740
             Node.rMem a_nb == orig_rmem &&
741
             Node.rMem d_ab == orig_rmem &&
742
             Node.rMem d_nb == orig_rmem &&
743
             -- this is not related to rMem, but as good a place to
744
             -- test as any
745
             inst_idx `elem` Node.sList a_ab &&
746
             not (inst_idx `elem` Node.sList d_ab)
747
         x -> printTestCase ("Failed to add/remove instances: " ++ show x)
748
              False
749

    
750
-- | Check mdsk setting.
751
prop_Node_setMdsk node mx =
752
    Node.loDsk node' >= 0 &&
753
    fromIntegral (Node.loDsk node') <= Node.tDsk node &&
754
    Node.availDisk node' >= 0 &&
755
    Node.availDisk node' <= Node.fDsk node' &&
756
    fromIntegral (Node.availDisk node') <= Node.tDsk node' &&
757
    Node.mDsk node' == mx'
758
    where _types = (node::Node.Node, mx::SmallRatio)
759
          node' = Node.setMdsk node mx'
760
          SmallRatio mx' = mx
761

    
762
-- Check tag maps
763
prop_Node_tagMaps_idempotent tags =
764
    Node.delTags (Node.addTags m tags) tags == m
765
    where m = Data.Map.empty
766

    
767
prop_Node_tagMaps_reject tags =
768
    not (null tags) ==>
769
    any (\t -> Node.rejectAddTags m [t]) tags
770
    where m = Node.addTags Data.Map.empty tags
771

    
772
prop_Node_showField node =
773
  forAll (elements Node.defaultFields) $ \ field ->
774
  fst (Node.showHeader field) /= Types.unknownField &&
775
  Node.showField node field /= Types.unknownField
776

    
777

    
778
prop_Node_computeGroups nodes =
779
  let ng = Node.computeGroups nodes
780
      onlyuuid = map fst ng
781
  in length nodes == sum (map (length . snd) ng) &&
782
     all (\(guuid, ns) -> all ((== guuid) . Node.group) ns) ng &&
783
     length (nub onlyuuid) == length onlyuuid &&
784
     (null nodes || not (null ng))
785

    
786
testNode =
787
    [ run prop_Node_setAlias
788
    , run prop_Node_setOffline
789
    , run prop_Node_setMcpu
790
    , run prop_Node_setXmem
791
    , run prop_Node_addPriFM
792
    , run prop_Node_addPriFD
793
    , run prop_Node_addPriFC
794
    , run prop_Node_addSec
795
    , run prop_Node_rMem
796
    , run prop_Node_setMdsk
797
    , run prop_Node_tagMaps_idempotent
798
    , run prop_Node_tagMaps_reject
799
    , run prop_Node_showField
800
    , run prop_Node_computeGroups
801
    ]
802

    
803

    
804
-- ** Cluster tests
805

    
806
-- | Check that the cluster score is close to zero for a homogeneous
807
-- cluster.
808
prop_Score_Zero node =
809
    forAll (choose (1, 1024)) $ \count ->
810
    (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
811
     (Node.tDsk node > 0) && (Node.tMem node > 0)) ==>
812
    let fn = Node.buildPeers node Container.empty
813
        nlst = replicate count fn
814
        score = Cluster.compCVNodes nlst
815
    -- we can't say == 0 here as the floating point errors accumulate;
816
    -- this should be much lower than the default score in CLI.hs
817
    in score <= 1e-12
818

    
819
-- | Check that cluster stats are sane.
820
prop_CStats_sane node =
821
    forAll (choose (1, 1024)) $ \count ->
822
    (not (Node.offline node) && not (Node.failN1 node) &&
823
     (Node.availDisk node > 0) && (Node.availMem node > 0)) ==>
824
    let fn = Node.buildPeers node Container.empty
825
        nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
826
        nl = Container.fromList nlst
827
        cstats = Cluster.totalResources nl
828
    in Cluster.csAdsk cstats >= 0 &&
829
       Cluster.csAdsk cstats <= Cluster.csFdsk cstats
830

    
831
-- | Check that one instance is allocated correctly, without
832
-- rebalances needed.
833
prop_ClusterAlloc_sane node inst =
834
    forAll (choose (5, 20)) $ \count ->
835
    not (Node.offline node)
836
            && not (Node.failN1 node)
837
            && Node.availDisk node > 0
838
            && Node.availMem node > 0
839
            ==>
840
    let nl = makeSmallCluster node count
841
        il = Container.empty
842
        inst' = setInstanceSmallerThanNode node inst
843
    in case Cluster.genAllocNodes defGroupList nl 2 True >>=
844
       Cluster.tryAlloc nl il inst' of
845
         Types.Bad _ -> False
846
         Types.Ok as ->
847
             case Cluster.asSolutions as of
848
               [] -> False
849
               (xnl, xi, _, cv):[] ->
850
                   let il' = Container.add (Instance.idx xi) xi il
851
                       tbl = Cluster.Table xnl il' cv []
852
                   in not (canBalance tbl True True False)
853
               _ -> False
854

    
855
-- | Checks that on a 2-5 node cluster, we can allocate a random
856
-- instance spec via tiered allocation (whatever the original instance
857
-- spec), on either one or two nodes.
858
prop_ClusterCanTieredAlloc node inst =
859
    forAll (choose (2, 5)) $ \count ->
860
    forAll (choose (1, 2)) $ \rqnodes ->
861
    not (Node.offline node)
862
            && not (Node.failN1 node)
863
            && isNodeBig node 4
864
            ==>
865
    let nl = makeSmallCluster node count
866
        il = Container.empty
867
        allocnodes = Cluster.genAllocNodes defGroupList nl rqnodes True
868
    in case allocnodes >>= \allocnodes' ->
869
        Cluster.tieredAlloc nl il (Just 1) inst allocnodes' [] [] of
870
         Types.Bad _ -> False
871
         Types.Ok (_, _, il', ixes, cstats) -> not (null ixes) &&
872
                                      IntMap.size il' == length ixes &&
873
                                      length ixes == length cstats
874

    
875
-- | Checks that on a 4-8 node cluster, once we allocate an instance,
876
-- we can also evacuate it.
877
prop_ClusterAllocEvac node inst =
878
    forAll (choose (4, 8)) $ \count ->
879
    not (Node.offline node)
880
            && not (Node.failN1 node)
881
            && isNodeBig node 4
882
            ==>
883
    let nl = makeSmallCluster node count
884
        il = Container.empty
885
        inst' = setInstanceSmallerThanNode node inst
886
    in case Cluster.genAllocNodes defGroupList nl 2 True >>=
887
       Cluster.tryAlloc nl il inst' of
888
         Types.Bad _ -> False
889
         Types.Ok as ->
890
             case Cluster.asSolutions as of
891
               [] -> False
892
               (xnl, xi, _, _):[] ->
893
                   let sdx = Instance.sNode xi
894
                       il' = Container.add (Instance.idx xi) xi il
895
                   in case Cluster.tryEvac xnl il' [Instance.idx xi] [sdx] of
896
                        Just _ -> True
897
                        _ -> False
898
               _ -> False
899

    
900
-- | Check that allocating multiple instances on a cluster, then
901
-- adding an empty node, results in a valid rebalance.
902
prop_ClusterAllocBalance =
903
    forAll (genNode (Just 5) (Just 128)) $ \node ->
904
    forAll (choose (3, 5)) $ \count ->
905
    not (Node.offline node) && not (Node.failN1 node) ==>
906
    let nl = makeSmallCluster node count
907
        (hnode, nl') = IntMap.deleteFindMax nl
908
        il = Container.empty
909
        allocnodes = Cluster.genAllocNodes defGroupList nl' 2 True
910
        i_templ = createInstance Types.unitMem Types.unitDsk Types.unitCpu
911
    in case allocnodes >>= \allocnodes' ->
912
        Cluster.iterateAlloc nl' il (Just 5) i_templ allocnodes' [] [] of
913
         Types.Bad _ -> False
914
         Types.Ok (_, xnl, il', _, _) ->
915
                   let ynl = Container.add (Node.idx hnode) hnode xnl
916
                       cv = Cluster.compCV ynl
917
                       tbl = Cluster.Table ynl il' cv []
918
                   in canBalance tbl True True False
919

    
920
-- | Checks consistency.
921
prop_ClusterCheckConsistency node inst =
922
  let nl = makeSmallCluster node 3
923
      [node1, node2, node3] = Container.elems nl
924
      node3' = node3 { Node.group = 1 }
925
      nl' = Container.add (Node.idx node3') node3' nl
926
      inst1 = Instance.setBoth inst (Node.idx node1) (Node.idx node2)
927
      inst2 = Instance.setBoth inst (Node.idx node1) Node.noSecondary
928
      inst3 = Instance.setBoth inst (Node.idx node1) (Node.idx node3)
929
      ccheck = Cluster.findSplitInstances nl' . Container.fromList
930
  in null (ccheck [(0, inst1)]) &&
931
     null (ccheck [(0, inst2)]) &&
932
     (not . null $ ccheck [(0, inst3)])
933

    
934
-- | For now, we only test that we don't lose instances during the split.
935
prop_ClusterSplitCluster node inst =
936
  forAll (choose (0, 100)) $ \icnt ->
937
  let nl = makeSmallCluster node 2
938
      (nl', il') = foldl (\(ns, is) _ -> assignInstance ns is inst 0 1)
939
                   (nl, Container.empty) [1..icnt]
940
      gni = Cluster.splitCluster nl' il'
941
  in sum (map (Container.size . snd . snd) gni) == icnt &&
942
     all (\(guuid, (nl'', _)) -> all ((== guuid) . Node.group)
943
                                 (Container.elems nl'')) gni
944

    
945
testCluster =
946
    [ run prop_Score_Zero
947
    , run prop_CStats_sane
948
    , run prop_ClusterAlloc_sane
949
    , run prop_ClusterCanTieredAlloc
950
    , run prop_ClusterAllocEvac
951
    , run prop_ClusterAllocBalance
952
    , run prop_ClusterCheckConsistency
953
    , run prop_ClusterSplitCluster
954
    ]
955

    
956
-- ** OpCodes tests
957

    
958
-- | Check that opcode serialization is idempotent.
959
prop_OpCodes_serialization op =
960
  case J.readJSON (J.showJSON op) of
961
    J.Error _ -> False
962
    J.Ok op' -> op == op'
963
  where _types = op::OpCodes.OpCode
964

    
965
testOpCodes =
966
  [ run prop_OpCodes_serialization
967
  ]
968

    
969
-- ** Jobs tests
970

    
971
-- | Check that (queued) job\/opcode status serialization is idempotent.
972
prop_OpStatus_serialization os =
973
  case J.readJSON (J.showJSON os) of
974
    J.Error _ -> False
975
    J.Ok os' -> os == os'
976
  where _types = os::Jobs.OpStatus
977

    
978
prop_JobStatus_serialization js =
979
  case J.readJSON (J.showJSON js) of
980
    J.Error _ -> False
981
    J.Ok js' -> js == js'
982
  where _types = js::Jobs.JobStatus
983

    
984
testJobs =
985
  [ run prop_OpStatus_serialization
986
  , run prop_JobStatus_serialization
987
  ]
988

    
989
-- ** Loader tests
990

    
991
prop_Loader_lookupNode ktn inst node =
992
  Loader.lookupNode nl inst node == Data.Map.lookup node nl
993
  where nl = Data.Map.fromList ktn
994

    
995
prop_Loader_lookupInstance kti inst =
996
  Loader.lookupInstance il inst == Data.Map.lookup inst il
997
  where il = Data.Map.fromList kti
998

    
999
prop_Loader_assignIndices nodes =
1000
  Data.Map.size nassoc == length nodes &&
1001
  Container.size kt == length nodes &&
1002
  (if not (null nodes)
1003
   then maximum (IntMap.keys kt) == length nodes - 1
1004
   else True)
1005
  where (nassoc, kt) = Loader.assignIndices (map (\n -> (Node.name n, n)) nodes)
1006

    
1007
-- | Checks that the number of primary instances recorded on the nodes
1008
-- is zero.
1009
prop_Loader_mergeData ns =
1010
  let na = Container.fromList $ map (\n -> (Node.idx n, n)) ns
1011
  in case Loader.mergeData [] [] [] []
1012
         (Loader.emptyCluster {Loader.cdNodes = na}) of
1013
    Types.Bad _ -> False
1014
    Types.Ok (Loader.ClusterData _ nl il _) ->
1015
      let nodes = Container.elems nl
1016
          instances = Container.elems il
1017
      in (sum . map (length . Node.pList)) nodes == 0 &&
1018
         null instances
1019

    
1020
-- | Check that compareNameComponent on equal strings works.
1021
prop_Loader_compareNameComponent_equal :: String -> Bool
1022
prop_Loader_compareNameComponent_equal s =
1023
  Loader.compareNameComponent s s ==
1024
    Loader.LookupResult Loader.ExactMatch s
1025

    
1026
-- | Check that compareNameComponent on prefix strings works.
1027
prop_Loader_compareNameComponent_prefix :: NonEmptyList Char -> String -> Bool
1028
prop_Loader_compareNameComponent_prefix (NonEmpty s1) s2 =
1029
  Loader.compareNameComponent (s1 ++ "." ++ s2) s1 ==
1030
    Loader.LookupResult Loader.PartialMatch s1
1031

    
1032
testLoader =
1033
  [ run prop_Loader_lookupNode
1034
  , run prop_Loader_lookupInstance
1035
  , run prop_Loader_assignIndices
1036
  , run prop_Loader_mergeData
1037
  , run prop_Loader_compareNameComponent_equal
1038
  , run prop_Loader_compareNameComponent_prefix
1039
  ]
1040

    
1041
-- ** Types tests
1042

    
1043
prop_AllocPolicy_serialisation apol =
1044
    case Types.apolFromString (Types.apolToString apol) of
1045
      Types.Ok p -> printTestCase ("invalid deserialisation " ++ show p) $
1046
                    p == apol
1047
      Types.Bad s -> printTestCase ("failed to deserialise: " ++ s) False
1048

    
1049
prop_DiskTemplate_serialisation dt =
1050
    case Types.dtFromString (Types.dtToString dt) of
1051
      Types.Ok p -> printTestCase ("invalid deserialisation " ++ show p) $
1052
                    p == dt
1053
      Types.Bad s -> printTestCase ("failed to deserialise: " ++ s) False
1054

    
1055
testTypes =
1056
    [ run prop_AllocPolicy_serialisation
1057
    , run prop_DiskTemplate_serialisation
1058
    ]