Statistics
| Branch: | Tag: | Revision:

root / htools / Ganeti / HTools / QC.hs @ 23fe06c2

History | View | Annotate | Download (39.6 kB)

1
{-# LANGUAGE TemplateHaskell #-}
2

    
3
{-| Unittests for ganeti-htools.
4

    
5
-}
6

    
7
{-
8

    
9
Copyright (C) 2009, 2010, 2011 Google Inc.
10

    
11
This program is free software; you can redistribute it and/or modify
12
it under the terms of the GNU General Public License as published by
13
the Free Software Foundation; either version 2 of the License, or
14
(at your option) any later version.
15

    
16
This program is distributed in the hope that it will be useful, but
17
WITHOUT ANY WARRANTY; without even the implied warranty of
18
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
19
General Public License for more details.
20

    
21
You should have received a copy of the GNU General Public License
22
along with this program; if not, write to the Free Software
23
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
24
02110-1301, USA.
25

    
26
-}
27

    
28
module Ganeti.HTools.QC
29
    ( testUtils
30
    , testPeerMap
31
    , testContainer
32
    , testInstance
33
    , testNode
34
    , testText
35
    , testOpCodes
36
    , testJobs
37
    , testCluster
38
    , testLoader
39
    , testTypes
40
    ) where
41

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

    
71
import qualified Ganeti.HTools.Program.Hail
72
import qualified Ganeti.HTools.Program.Hbal
73
import qualified Ganeti.HTools.Program.Hscan
74
import qualified Ganeti.HTools.Program.Hspace
75

    
76
import Ganeti.HTools.QCHelper (testSuite)
77

    
78
-- * Constants
79

    
80
-- | Maximum memory (1TiB, somewhat random value).
81
maxMem :: Int
82
maxMem = 1024 * 1024
83

    
84
-- | Maximum disk (8TiB, somewhat random value).
85
maxDsk :: Int
86
maxDsk = 1024 * 1024 * 8
87

    
88
-- | Max CPUs (1024, somewhat random value).
89
maxCpu :: Int
90
maxCpu = 1024
91

    
92
defGroup :: Group.Group
93
defGroup = flip Group.setIdx 0 $
94
               Group.create "default" Utils.defaultGroupID
95
                    Types.AllocPreferred
96

    
97
defGroupList :: Group.List
98
defGroupList = Container.fromList [(Group.idx defGroup, defGroup)]
99

    
100
defGroupAssoc :: Data.Map.Map String Types.Gdx
101
defGroupAssoc = Data.Map.singleton (Group.uuid defGroup) (Group.idx defGroup)
102

    
103
-- * Helper functions
104

    
105
-- | Simple checker for whether OpResult is fail or pass.
106
isFailure :: Types.OpResult a -> Bool
107
isFailure (Types.OpFail _) = True
108
isFailure _ = False
109

    
110
-- | Update an instance to be smaller than a node.
111
setInstanceSmallerThanNode node inst =
112
    inst { Instance.mem = Node.availMem node `div` 2
113
         , Instance.dsk = Node.availDisk node `div` 2
114
         , Instance.vcpus = Node.availCpu node `div` 2
115
         }
116

    
117
-- | Create an instance given its spec.
118
createInstance mem dsk vcpus =
119
    Instance.create "inst-unnamed" mem dsk vcpus "running" [] True (-1) (-1)
120
                    Types.DTDrbd8
121

    
122
-- | Create a small cluster by repeating a node spec.
123
makeSmallCluster :: Node.Node -> Int -> Node.List
124
makeSmallCluster node count =
125
    let fn = Node.buildPeers node Container.empty
126
        namelst = map (\n -> (Node.name n, n)) (replicate count fn)
127
        (_, nlst) = Loader.assignIndices namelst
128
    in nlst
129

    
130
-- | Checks if a node is "big" enough.
131
isNodeBig :: Node.Node -> Int -> Bool
132
isNodeBig node size = Node.availDisk node > size * Types.unitDsk
133
                      && Node.availMem node > size * Types.unitMem
134
                      && Node.availCpu node > size * Types.unitCpu
135

    
136
canBalance :: Cluster.Table -> Bool -> Bool -> Bool -> Bool
137
canBalance tbl dm im evac = isJust $ Cluster.tryBalance tbl dm im evac 0 0
138

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

    
158
-- * Arbitrary instances
159

    
160
-- | Defines a DNS name.
161
newtype DNSChar = DNSChar { dnsGetChar::Char }
162

    
163
instance Arbitrary DNSChar where
164
    arbitrary = do
165
      x <- elements (['a'..'z'] ++ ['0'..'9'] ++ "_-")
166
      return (DNSChar x)
167

    
168
getName :: Gen String
169
getName = do
170
  n <- choose (1, 64)
171
  dn <- vector n::Gen [DNSChar]
172
  return (map dnsGetChar dn)
173

    
174

    
175
getFQDN :: Gen String
176
getFQDN = do
177
  felem <- getName
178
  ncomps <- choose (1, 4)
179
  frest <- vector ncomps::Gen [[DNSChar]]
180
  let frest' = map (map dnsGetChar) frest
181
  return (felem ++ "." ++ intercalate "." frest')
182

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

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

    
233
-- and a random node
234
instance Arbitrary Node.Node where
235
    arbitrary = genNode Nothing Nothing
236

    
237
-- replace disks
238
instance Arbitrary OpCodes.ReplaceDisksMode where
239
  arbitrary = elements [ OpCodes.ReplaceOnPrimary
240
                       , OpCodes.ReplaceOnSecondary
241
                       , OpCodes.ReplaceNewSecondary
242
                       , OpCodes.ReplaceAuto
243
                       ]
244

    
245
instance Arbitrary OpCodes.OpCode where
246
  arbitrary = do
247
    op_id <- elements [ "OP_TEST_DELAY"
248
                      , "OP_INSTANCE_REPLACE_DISKS"
249
                      , "OP_INSTANCE_FAILOVER"
250
                      , "OP_INSTANCE_MIGRATE"
251
                      ]
252
    (case op_id of
253
        "OP_TEST_DELAY" ->
254
          liftM3 OpCodes.OpTestDelay arbitrary arbitrary arbitrary
255
        "OP_INSTANCE_REPLACE_DISKS" ->
256
          liftM5 OpCodes.OpInstanceReplaceDisks arbitrary arbitrary
257
          arbitrary arbitrary arbitrary
258
        "OP_INSTANCE_FAILOVER" ->
259
          liftM3 OpCodes.OpInstanceFailover arbitrary arbitrary
260
                 arbitrary
261
        "OP_INSTANCE_MIGRATE" ->
262
          liftM5 OpCodes.OpInstanceMigrate arbitrary arbitrary
263
                 arbitrary arbitrary
264
          arbitrary
265
        _ -> fail "Wrong opcode")
266

    
267
instance Arbitrary Jobs.OpStatus where
268
  arbitrary = elements [minBound..maxBound]
269

    
270
instance Arbitrary Jobs.JobStatus where
271
  arbitrary = elements [minBound..maxBound]
272

    
273
newtype SmallRatio = SmallRatio Double deriving Show
274
instance Arbitrary SmallRatio where
275
    arbitrary = do
276
      v <- choose (0, 1)
277
      return $ SmallRatio v
278

    
279
instance Arbitrary Types.AllocPolicy where
280
  arbitrary = elements [minBound..maxBound]
281

    
282
instance Arbitrary Types.DiskTemplate where
283
  arbitrary = elements [minBound..maxBound]
284

    
285
instance Arbitrary Types.FailMode where
286
    arbitrary = elements [minBound..maxBound]
287

    
288
instance Arbitrary a => Arbitrary (Types.OpResult a) where
289
    arbitrary = arbitrary >>= \c ->
290
                case c of
291
                  False -> liftM Types.OpFail arbitrary
292
                  True -> liftM Types.OpGood arbitrary
293

    
294
-- * Actual tests
295

    
296
-- ** Utils tests
297

    
298
-- | If the list is not just an empty element, and if the elements do
299
-- not contain commas, then join+split should be idempotent.
300
prop_Utils_commaJoinSplit =
301
    forAll (arbitrary `suchThat`
302
            (\l -> l /= [""] && all (not . elem ',') l )) $ \lst ->
303
    Utils.sepSplit ',' (Utils.commaJoin lst) == lst
304

    
305
-- | Split and join should always be idempotent.
306
prop_Utils_commaSplitJoin s = Utils.commaJoin (Utils.sepSplit ',' s) == s
307

    
308
-- | fromObjWithDefault, we test using the Maybe monad and an integer
309
-- value.
310
prop_Utils_fromObjWithDefault def_value random_key =
311
    -- a missing key will be returned with the default
312
    Utils.fromObjWithDefault [] random_key def_value == Just def_value &&
313
    -- a found key will be returned as is, not with default
314
    Utils.fromObjWithDefault [(random_key, J.showJSON def_value)]
315
         random_key (def_value+1) == Just def_value
316
        where _types = def_value :: Integer
317

    
318
-- | Test that functional if' behaves like the syntactic sugar if.
319
prop_Utils_if'if :: Bool -> Int -> Int -> Bool
320
prop_Utils_if'if cnd a b = Utils.if' cnd a b == if cnd then a else b
321

    
322
-- | Test basic select functionality
323
prop_Utils_select :: Int   -- ^ Default result
324
                  -> [Int] -- ^ List of False values
325
                  -> [Int] -- ^ List of True values
326
                  -> Bool  -- ^ Test result
327
prop_Utils_select def lst1 lst2 =
328
  Utils.select def cndlist == expectedresult
329
  where expectedresult = Utils.if' (null lst2) def (head lst2)
330
        flist = map (\e -> (False, e)) lst1
331
        tlist = map (\e -> (True, e)) lst2
332
        cndlist = flist ++ tlist
333

    
334
-- | Test basic select functionality with undefined default
335
prop_Utils_select_undefd :: [Int] -- ^ List of False values
336
                         -> NonEmptyList Int -- ^ List of True values
337
                         -> Bool  -- ^ Test result
338
prop_Utils_select_undefd lst1 (NonEmpty lst2) =
339
  Utils.select undefined cndlist == head lst2
340
  where flist = map (\e -> (False, e)) lst1
341
        tlist = map (\e -> (True, e)) lst2
342
        cndlist = flist ++ tlist
343

    
344
-- | Test basic select functionality with undefined list values
345
prop_Utils_select_undefv :: [Int] -- ^ List of False values
346
                         -> NonEmptyList Int -- ^ List of True values
347
                         -> Bool  -- ^ Test result
348
prop_Utils_select_undefv lst1 (NonEmpty lst2) =
349
  Utils.select undefined cndlist == head lst2
350
  where flist = map (\e -> (False, e)) lst1
351
        tlist = map (\e -> (True, e)) lst2
352
        cndlist = flist ++ tlist ++ [undefined]
353

    
354
prop_Utils_parseUnit (NonNegative n) =
355
    Utils.parseUnit (show n) == Types.Ok n &&
356
    Utils.parseUnit (show n ++ "m") == Types.Ok n &&
357
    (case Utils.parseUnit (show n ++ "M") of
358
      Types.Ok m -> if n > 0
359
                    then m < n  -- for positive values, X MB is less than X MiB
360
                    else m == 0 -- but for 0, 0 MB == 0 MiB
361
      Types.Bad _ -> False) &&
362
    Utils.parseUnit (show n ++ "g") == Types.Ok (n*1024) &&
363
    Utils.parseUnit (show n ++ "t") == Types.Ok (n*1048576) &&
364
    Types.isBad (Utils.parseUnit (show n ++ "x")::Types.Result Int)
365
    where _types = n::Int
366

    
367
-- | Test list for the Utils module.
368
testSuite "Utils"
369
              [ 'prop_Utils_commaJoinSplit
370
              , 'prop_Utils_commaSplitJoin
371
              , 'prop_Utils_fromObjWithDefault
372
              , 'prop_Utils_if'if
373
              , 'prop_Utils_select
374
              , 'prop_Utils_select_undefd
375
              , 'prop_Utils_select_undefv
376
              , 'prop_Utils_parseUnit
377
              ]
378

    
379
-- ** PeerMap tests
380

    
381
-- | Make sure add is idempotent.
382
prop_PeerMap_addIdempotent pmap key em =
383
    fn puniq == fn (fn puniq)
384
    where _types = (pmap::PeerMap.PeerMap,
385
                    key::PeerMap.Key, em::PeerMap.Elem)
386
          fn = PeerMap.add key em
387
          puniq = PeerMap.accumArray const pmap
388

    
389
-- | Make sure remove is idempotent.
390
prop_PeerMap_removeIdempotent pmap key =
391
    fn puniq == fn (fn puniq)
392
    where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
393
          fn = PeerMap.remove key
394
          puniq = PeerMap.accumArray const pmap
395

    
396
-- | Make sure a missing item returns 0.
397
prop_PeerMap_findMissing pmap key =
398
    PeerMap.find key (PeerMap.remove key puniq) == 0
399
    where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
400
          puniq = PeerMap.accumArray const pmap
401

    
402
-- | Make sure an added item is found.
403
prop_PeerMap_addFind pmap key em =
404
    PeerMap.find key (PeerMap.add key em puniq) == em
405
    where _types = (pmap::PeerMap.PeerMap,
406
                    key::PeerMap.Key, em::PeerMap.Elem)
407
          puniq = PeerMap.accumArray const pmap
408

    
409
-- | Manual check that maxElem returns the maximum indeed, or 0 for null.
410
prop_PeerMap_maxElem pmap =
411
    PeerMap.maxElem puniq == if null puniq then 0
412
                             else (maximum . snd . unzip) puniq
413
    where _types = pmap::PeerMap.PeerMap
414
          puniq = PeerMap.accumArray const pmap
415

    
416
-- | List of tests for the PeerMap module.
417
testSuite "PeerMap"
418
              [ 'prop_PeerMap_addIdempotent
419
              , 'prop_PeerMap_removeIdempotent
420
              , 'prop_PeerMap_maxElem
421
              , 'prop_PeerMap_addFind
422
              , 'prop_PeerMap_findMissing
423
              ]
424

    
425
-- ** Container tests
426

    
427
prop_Container_addTwo cdata i1 i2 =
428
    fn i1 i2 cont == fn i2 i1 cont &&
429
       fn i1 i2 cont == fn i1 i2 (fn i1 i2 cont)
430
    where _types = (cdata::[Int],
431
                    i1::Int, i2::Int)
432
          cont = foldl (\c x -> Container.add x x c) Container.empty cdata
433
          fn x1 x2 = Container.addTwo x1 x1 x2 x2
434

    
435
prop_Container_nameOf node =
436
  let nl = makeSmallCluster node 1
437
      fnode = head (Container.elems nl)
438
  in Container.nameOf nl (Node.idx fnode) == Node.name fnode
439

    
440
-- | We test that in a cluster, given a random node, we can find it by
441
-- its name and alias, as long as all names and aliases are unique,
442
-- and that we fail to find a non-existing name.
443
prop_Container_findByName node othername =
444
  forAll (choose (1, 20)) $ \ cnt ->
445
  forAll (choose (0, cnt - 1)) $ \ fidx ->
446
  forAll (vector cnt) $ \ names ->
447
  (length . nub) (map fst names ++ map snd names) ==
448
  length names * 2 &&
449
  not (othername `elem` (map fst names ++ map snd names)) ==>
450
  let nl = makeSmallCluster node cnt
451
      nodes = Container.elems nl
452
      nodes' = map (\((name, alias), nn) -> (Node.idx nn,
453
                                             nn { Node.name = name,
454
                                                  Node.alias = alias }))
455
               $ zip names nodes
456
      nl' = Container.fromList nodes'
457
      target = snd (nodes' !! fidx)
458
  in Container.findByName nl' (Node.name target) == Just target &&
459
     Container.findByName nl' (Node.alias target) == Just target &&
460
     Container.findByName nl' othername == Nothing
461

    
462
testSuite "Container"
463
              [ 'prop_Container_addTwo
464
              , 'prop_Container_nameOf
465
              , 'prop_Container_findByName
466
              ]
467

    
468
-- ** Instance tests
469

    
470
-- Simple instance tests, we only have setter/getters
471

    
472
prop_Instance_creat inst =
473
    Instance.name inst == Instance.alias inst
474

    
475
prop_Instance_setIdx inst idx =
476
    Instance.idx (Instance.setIdx inst idx) == idx
477
    where _types = (inst::Instance.Instance, idx::Types.Idx)
478

    
479
prop_Instance_setName inst name =
480
    Instance.name newinst == name &&
481
    Instance.alias newinst == name
482
    where _types = (inst::Instance.Instance, name::String)
483
          newinst = Instance.setName inst name
484

    
485
prop_Instance_setAlias inst name =
486
    Instance.name newinst == Instance.name inst &&
487
    Instance.alias newinst == name
488
    where _types = (inst::Instance.Instance, name::String)
489
          newinst = Instance.setAlias inst name
490

    
491
prop_Instance_setPri inst pdx =
492
    Instance.pNode (Instance.setPri inst pdx) == pdx
493
    where _types = (inst::Instance.Instance, pdx::Types.Ndx)
494

    
495
prop_Instance_setSec inst sdx =
496
    Instance.sNode (Instance.setSec inst sdx) == sdx
497
    where _types = (inst::Instance.Instance, sdx::Types.Ndx)
498

    
499
prop_Instance_setBoth inst pdx sdx =
500
    Instance.pNode si == pdx && Instance.sNode si == sdx
501
    where _types = (inst::Instance.Instance, pdx::Types.Ndx, sdx::Types.Ndx)
502
          si = Instance.setBoth inst pdx sdx
503

    
504
prop_Instance_runStatus_True =
505
    forAll (arbitrary `suchThat`
506
            ((`elem` Instance.runningStates) . Instance.runSt))
507
    Instance.running
508

    
509
prop_Instance_runStatus_False inst =
510
    let run_st = Instance.running inst
511
        run_tx = Instance.runSt inst
512
    in
513
      run_tx `notElem` Instance.runningStates ==> not run_st
514

    
515
prop_Instance_shrinkMG inst =
516
    Instance.mem inst >= 2 * Types.unitMem ==>
517
        case Instance.shrinkByType inst Types.FailMem of
518
          Types.Ok inst' ->
519
              Instance.mem inst' == Instance.mem inst - Types.unitMem
520
          _ -> False
521

    
522
prop_Instance_shrinkMF inst =
523
    forAll (choose (0, 2 * Types.unitMem - 1)) $ \mem ->
524
    let inst' = inst { Instance.mem = mem}
525
    in Types.isBad $ Instance.shrinkByType inst' Types.FailMem
526

    
527
prop_Instance_shrinkCG inst =
528
    Instance.vcpus inst >= 2 * Types.unitCpu ==>
529
        case Instance.shrinkByType inst Types.FailCPU of
530
          Types.Ok inst' ->
531
              Instance.vcpus inst' == Instance.vcpus inst - Types.unitCpu
532
          _ -> False
533

    
534
prop_Instance_shrinkCF inst =
535
    forAll (choose (0, 2 * Types.unitCpu - 1)) $ \vcpus ->
536
    let inst' = inst { Instance.vcpus = vcpus }
537
    in Types.isBad $ Instance.shrinkByType inst' Types.FailCPU
538

    
539
prop_Instance_shrinkDG inst =
540
    Instance.dsk inst >= 2 * Types.unitDsk ==>
541
        case Instance.shrinkByType inst Types.FailDisk of
542
          Types.Ok inst' ->
543
              Instance.dsk inst' == Instance.dsk inst - Types.unitDsk
544
          _ -> False
545

    
546
prop_Instance_shrinkDF inst =
547
    forAll (choose (0, 2 * Types.unitDsk - 1)) $ \dsk ->
548
    let inst' = inst { Instance.dsk = dsk }
549
    in Types.isBad $ Instance.shrinkByType inst' Types.FailDisk
550

    
551
prop_Instance_setMovable inst m =
552
    Instance.movable inst' == m
553
    where inst' = Instance.setMovable inst m
554

    
555
testSuite "Instance"
556
              [ 'prop_Instance_creat
557
              , 'prop_Instance_setIdx
558
              , 'prop_Instance_setName
559
              , 'prop_Instance_setAlias
560
              , 'prop_Instance_setPri
561
              , 'prop_Instance_setSec
562
              , 'prop_Instance_setBoth
563
              , 'prop_Instance_runStatus_True
564
              , 'prop_Instance_runStatus_False
565
              , 'prop_Instance_shrinkMG
566
              , 'prop_Instance_shrinkMF
567
              , 'prop_Instance_shrinkCG
568
              , 'prop_Instance_shrinkCF
569
              , 'prop_Instance_shrinkDG
570
              , 'prop_Instance_shrinkDF
571
              , 'prop_Instance_setMovable
572
              ]
573

    
574
-- ** Text backend tests
575

    
576
-- Instance text loader tests
577

    
578
prop_Text_Load_Instance name mem dsk vcpus status
579
                        (NonEmpty pnode) snode
580
                        (NonNegative pdx) (NonNegative sdx) autobal dt =
581
    pnode /= snode && pdx /= sdx ==>
582
    let vcpus_s = show vcpus
583
        dsk_s = show dsk
584
        mem_s = show mem
585
        ndx = if null snode
586
              then [(pnode, pdx)]
587
              else [(pnode, pdx), (snode, sdx)]
588
        nl = Data.Map.fromList ndx
589
        tags = ""
590
        sbal = if autobal then "Y" else "N"
591
        sdt = Types.diskTemplateToString dt
592
        inst = Text.loadInst nl
593
               [name, mem_s, dsk_s, vcpus_s, status,
594
                sbal, pnode, snode, sdt, tags]
595
        fail1 = Text.loadInst nl
596
               [name, mem_s, dsk_s, vcpus_s, status,
597
                sbal, pnode, pnode, tags]
598
        _types = ( name::String, mem::Int, dsk::Int
599
                 , vcpus::Int, status::String
600
                 , snode::String
601
                 , autobal::Bool)
602
    in
603
      case inst of
604
        Types.Bad msg -> printTestCase ("Failed to load instance: " ++ msg)
605
                         False
606
        Types.Ok (_, i) -> printTestCase "Mismatch in some field while\
607
                                         \ loading the instance" $
608
            Instance.name i == name &&
609
            Instance.vcpus i == vcpus &&
610
            Instance.mem i == mem &&
611
            Instance.pNode i == pdx &&
612
            Instance.sNode i == (if null snode
613
                                 then Node.noSecondary
614
                                 else sdx) &&
615
            Instance.autoBalance i == autobal &&
616
            Types.isBad fail1
617

    
618
prop_Text_Load_InstanceFail ktn fields =
619
    length fields /= 10 ==>
620
    case Text.loadInst nl fields of
621
      Types.Ok _ -> printTestCase "Managed to load instance from invalid\
622
                                  \ data" False
623
      Types.Bad msg -> printTestCase ("Unrecognised error message: " ++ msg) $
624
                       "Invalid/incomplete instance data: '" `isPrefixOf` msg
625
    where nl = Data.Map.fromList ktn
626

    
627
prop_Text_Load_Node name tm nm fm td fd tc fo =
628
    let conv v = if v < 0
629
                    then "?"
630
                    else show v
631
        tm_s = conv tm
632
        nm_s = conv nm
633
        fm_s = conv fm
634
        td_s = conv td
635
        fd_s = conv fd
636
        tc_s = conv tc
637
        fo_s = if fo
638
               then "Y"
639
               else "N"
640
        any_broken = any (< 0) [tm, nm, fm, td, fd, tc]
641
        gid = Group.uuid defGroup
642
    in case Text.loadNode defGroupAssoc
643
           [name, tm_s, nm_s, fm_s, td_s, fd_s, tc_s, fo_s, gid] of
644
         Nothing -> False
645
         Just (name', node) ->
646
             if fo || any_broken
647
             then Node.offline node
648
             else Node.name node == name' && name' == name &&
649
                  Node.alias node == name &&
650
                  Node.tMem node == fromIntegral tm &&
651
                  Node.nMem node == nm &&
652
                  Node.fMem node == fm &&
653
                  Node.tDsk node == fromIntegral td &&
654
                  Node.fDsk node == fd &&
655
                  Node.tCpu node == fromIntegral tc
656

    
657
prop_Text_Load_NodeFail fields =
658
    length fields /= 8 ==> isNothing $ Text.loadNode Data.Map.empty fields
659

    
660
prop_Text_NodeLSIdempotent node =
661
    (Text.loadNode defGroupAssoc.
662
         Utils.sepSplit '|' . Text.serializeNode defGroupList) n ==
663
    Just (Node.name n, n)
664
    -- override failN1 to what loadNode returns by default
665
    where n = node { Node.failN1 = True, Node.offline = False }
666

    
667
testSuite "Text"
668
              [ 'prop_Text_Load_Instance
669
              , 'prop_Text_Load_InstanceFail
670
              , 'prop_Text_Load_Node
671
              , 'prop_Text_Load_NodeFail
672
              , 'prop_Text_NodeLSIdempotent
673
              ]
674

    
675
-- ** Node tests
676

    
677
prop_Node_setAlias node name =
678
    Node.name newnode == Node.name node &&
679
    Node.alias newnode == name
680
    where _types = (node::Node.Node, name::String)
681
          newnode = Node.setAlias node name
682

    
683
prop_Node_setOffline node status =
684
    Node.offline newnode == status
685
    where newnode = Node.setOffline node status
686

    
687
prop_Node_setXmem node xm =
688
    Node.xMem newnode == xm
689
    where newnode = Node.setXmem node xm
690

    
691
prop_Node_setMcpu node mc =
692
    Node.mCpu newnode == mc
693
    where newnode = Node.setMcpu node mc
694

    
695
-- | Check that an instance add with too high memory or disk will be
696
-- rejected.
697
prop_Node_addPriFM node inst = Instance.mem inst >= Node.fMem node &&
698
                               not (Node.failN1 node)
699
                               ==>
700
                               case Node.addPri node inst'' of
701
                                 Types.OpFail Types.FailMem -> True
702
                                 _ -> False
703
    where _types = (node::Node.Node, inst::Instance.Instance)
704
          inst' = setInstanceSmallerThanNode node inst
705
          inst'' = inst' { Instance.mem = Instance.mem inst }
706

    
707
prop_Node_addPriFD node inst = Instance.dsk inst >= Node.fDsk node &&
708
                               not (Node.failN1 node)
709
                               ==>
710
                               case Node.addPri node inst'' of
711
                                 Types.OpFail Types.FailDisk -> True
712
                                 _ -> False
713
    where _types = (node::Node.Node, inst::Instance.Instance)
714
          inst' = setInstanceSmallerThanNode node inst
715
          inst'' = inst' { Instance.dsk = Instance.dsk inst }
716

    
717
prop_Node_addPriFC node inst (Positive extra) =
718
    not (Node.failN1 node) ==>
719
        case Node.addPri node inst'' of
720
          Types.OpFail Types.FailCPU -> True
721
          _ -> False
722
    where _types = (node::Node.Node, inst::Instance.Instance)
723
          inst' = setInstanceSmallerThanNode node inst
724
          inst'' = inst' { Instance.vcpus = Node.availCpu node + extra }
725

    
726
-- | Check that an instance add with too high memory or disk will be
727
-- rejected.
728
prop_Node_addSec node inst pdx =
729
    (Instance.mem inst >= (Node.fMem node - Node.rMem node) ||
730
     Instance.dsk inst >= Node.fDsk node) &&
731
    not (Node.failN1 node)
732
    ==> isFailure (Node.addSec node inst pdx)
733
        where _types = (node::Node.Node, inst::Instance.Instance, pdx::Int)
734

    
735
-- | Checks for memory reservation changes.
736
prop_Node_rMem inst =
737
    forAll (arbitrary `suchThat` ((> Types.unitMem) . Node.fMem)) $ \node ->
738
    -- ab = auto_balance, nb = non-auto_balance
739
    -- we use -1 as the primary node of the instance
740
    let inst' = inst { Instance.pNode = -1, Instance.autoBalance = True }
741
        inst_ab = setInstanceSmallerThanNode node inst'
742
        inst_nb = inst_ab { Instance.autoBalance = False }
743
        -- now we have the two instances, identical except the
744
        -- autoBalance attribute
745
        orig_rmem = Node.rMem node
746
        inst_idx = Instance.idx inst_ab
747
        node_add_ab = Node.addSec node inst_ab (-1)
748
        node_add_nb = Node.addSec node inst_nb (-1)
749
        node_del_ab = liftM (`Node.removeSec` inst_ab) node_add_ab
750
        node_del_nb = liftM (`Node.removeSec` inst_nb) node_add_nb
751
    in case (node_add_ab, node_add_nb, node_del_ab, node_del_nb) of
752
         (Types.OpGood a_ab, Types.OpGood a_nb,
753
          Types.OpGood d_ab, Types.OpGood d_nb) ->
754
             printTestCase "Consistency checks failed" $
755
             Node.rMem a_ab >  orig_rmem &&
756
             Node.rMem a_ab - orig_rmem == Instance.mem inst_ab &&
757
             Node.rMem a_nb == orig_rmem &&
758
             Node.rMem d_ab == orig_rmem &&
759
             Node.rMem d_nb == orig_rmem &&
760
             -- this is not related to rMem, but as good a place to
761
             -- test as any
762
             inst_idx `elem` Node.sList a_ab &&
763
             not (inst_idx `elem` Node.sList d_ab)
764
         x -> printTestCase ("Failed to add/remove instances: " ++ show x)
765
              False
766

    
767
-- | Check mdsk setting.
768
prop_Node_setMdsk node mx =
769
    Node.loDsk node' >= 0 &&
770
    fromIntegral (Node.loDsk node') <= Node.tDsk node &&
771
    Node.availDisk node' >= 0 &&
772
    Node.availDisk node' <= Node.fDsk node' &&
773
    fromIntegral (Node.availDisk node') <= Node.tDsk node' &&
774
    Node.mDsk node' == mx'
775
    where _types = (node::Node.Node, mx::SmallRatio)
776
          node' = Node.setMdsk node mx'
777
          SmallRatio mx' = mx
778

    
779
-- Check tag maps
780
prop_Node_tagMaps_idempotent tags =
781
    Node.delTags (Node.addTags m tags) tags == m
782
    where m = Data.Map.empty
783

    
784
prop_Node_tagMaps_reject tags =
785
    not (null tags) ==>
786
    any (\t -> Node.rejectAddTags m [t]) tags
787
    where m = Node.addTags Data.Map.empty tags
788

    
789
prop_Node_showField node =
790
  forAll (elements Node.defaultFields) $ \ field ->
791
  fst (Node.showHeader field) /= Types.unknownField &&
792
  Node.showField node field /= Types.unknownField
793

    
794
prop_Node_computeGroups nodes =
795
  let ng = Node.computeGroups nodes
796
      onlyuuid = map fst ng
797
  in length nodes == sum (map (length . snd) ng) &&
798
     all (\(guuid, ns) -> all ((== guuid) . Node.group) ns) ng &&
799
     length (nub onlyuuid) == length onlyuuid &&
800
     (null nodes || not (null ng))
801

    
802
testSuite "Node"
803
              [ 'prop_Node_setAlias
804
              , 'prop_Node_setOffline
805
              , 'prop_Node_setMcpu
806
              , 'prop_Node_setXmem
807
              , 'prop_Node_addPriFM
808
              , 'prop_Node_addPriFD
809
              , 'prop_Node_addPriFC
810
              , 'prop_Node_addSec
811
              , 'prop_Node_rMem
812
              , 'prop_Node_setMdsk
813
              , 'prop_Node_tagMaps_idempotent
814
              , 'prop_Node_tagMaps_reject
815
              , 'prop_Node_showField
816
              , 'prop_Node_computeGroups
817
              ]
818

    
819
-- ** Cluster tests
820

    
821
-- | Check that the cluster score is close to zero for a homogeneous
822
-- cluster.
823
prop_Score_Zero node =
824
    forAll (choose (1, 1024)) $ \count ->
825
    (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
826
     (Node.tDsk node > 0) && (Node.tMem node > 0)) ==>
827
    let fn = Node.buildPeers node Container.empty
828
        nlst = replicate count fn
829
        score = Cluster.compCVNodes nlst
830
    -- we can't say == 0 here as the floating point errors accumulate;
831
    -- this should be much lower than the default score in CLI.hs
832
    in score <= 1e-12
833

    
834
-- | Check that cluster stats are sane.
835
prop_CStats_sane node =
836
    forAll (choose (1, 1024)) $ \count ->
837
    (not (Node.offline node) && not (Node.failN1 node) &&
838
     (Node.availDisk node > 0) && (Node.availMem node > 0)) ==>
839
    let fn = Node.buildPeers node Container.empty
840
        nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
841
        nl = Container.fromList nlst
842
        cstats = Cluster.totalResources nl
843
    in Cluster.csAdsk cstats >= 0 &&
844
       Cluster.csAdsk cstats <= Cluster.csFdsk cstats
845

    
846
-- | Check that one instance is allocated correctly, without
847
-- rebalances needed.
848
prop_ClusterAlloc_sane node inst =
849
    forAll (choose (5, 20)) $ \count ->
850
    not (Node.offline node)
851
            && not (Node.failN1 node)
852
            && Node.availDisk node > 0
853
            && Node.availMem node > 0
854
            ==>
855
    let nl = makeSmallCluster node count
856
        il = Container.empty
857
        inst' = setInstanceSmallerThanNode node inst
858
    in case Cluster.genAllocNodes defGroupList nl 2 True >>=
859
       Cluster.tryAlloc nl il inst' of
860
         Types.Bad _ -> False
861
         Types.Ok as ->
862
             case Cluster.asSolution as of
863
               Nothing -> False
864
               Just (xnl, xi, _, cv) ->
865
                   let il' = Container.add (Instance.idx xi) xi il
866
                       tbl = Cluster.Table xnl il' cv []
867
                   in not (canBalance tbl True True False)
868

    
869
-- | Checks that on a 2-5 node cluster, we can allocate a random
870
-- instance spec via tiered allocation (whatever the original instance
871
-- spec), on either one or two nodes.
872
prop_ClusterCanTieredAlloc node inst =
873
    forAll (choose (2, 5)) $ \count ->
874
    forAll (choose (1, 2)) $ \rqnodes ->
875
    not (Node.offline node)
876
            && not (Node.failN1 node)
877
            && isNodeBig node 4
878
            ==>
879
    let nl = makeSmallCluster node count
880
        il = Container.empty
881
        allocnodes = Cluster.genAllocNodes defGroupList nl rqnodes True
882
    in case allocnodes >>= \allocnodes' ->
883
        Cluster.tieredAlloc nl il (Just 1) inst allocnodes' [] [] of
884
         Types.Bad _ -> False
885
         Types.Ok (_, _, il', ixes, cstats) -> not (null ixes) &&
886
                                      IntMap.size il' == length ixes &&
887
                                      length ixes == length cstats
888

    
889
-- | Checks that on a 4-8 node cluster, once we allocate an instance,
890
-- we can also evacuate it.
891
prop_ClusterAllocEvac node inst =
892
    forAll (choose (4, 8)) $ \count ->
893
    not (Node.offline node)
894
            && not (Node.failN1 node)
895
            && isNodeBig node 4
896
            ==>
897
    let nl = makeSmallCluster node count
898
        il = Container.empty
899
        inst' = setInstanceSmallerThanNode node inst
900
    in case Cluster.genAllocNodes defGroupList nl 2 True >>=
901
       Cluster.tryAlloc nl il inst' of
902
         Types.Bad _ -> False
903
         Types.Ok as ->
904
             case Cluster.asSolution as of
905
               Nothing -> False
906
               Just (xnl, xi, _, _) ->
907
                   let sdx = Instance.sNode xi
908
                       il' = Container.add (Instance.idx xi) xi il
909
                   in case IAlloc.processRelocate defGroupList xnl il'
910
                          (Instance.idx xi) 1 [sdx] of
911
                        Types.Ok _ -> True
912
                        _ -> False
913

    
914
-- | Check that allocating multiple instances on a cluster, then
915
-- adding an empty node, results in a valid rebalance.
916
prop_ClusterAllocBalance =
917
    forAll (genNode (Just 5) (Just 128)) $ \node ->
918
    forAll (choose (3, 5)) $ \count ->
919
    not (Node.offline node) && not (Node.failN1 node) ==>
920
    let nl = makeSmallCluster node count
921
        (hnode, nl') = IntMap.deleteFindMax nl
922
        il = Container.empty
923
        allocnodes = Cluster.genAllocNodes defGroupList nl' 2 True
924
        i_templ = createInstance Types.unitMem Types.unitDsk Types.unitCpu
925
    in case allocnodes >>= \allocnodes' ->
926
        Cluster.iterateAlloc nl' il (Just 5) i_templ allocnodes' [] [] of
927
         Types.Bad _ -> False
928
         Types.Ok (_, xnl, il', _, _) ->
929
                   let ynl = Container.add (Node.idx hnode) hnode xnl
930
                       cv = Cluster.compCV ynl
931
                       tbl = Cluster.Table ynl il' cv []
932
                   in canBalance tbl True True False
933

    
934
-- | Checks consistency.
935
prop_ClusterCheckConsistency node inst =
936
  let nl = makeSmallCluster node 3
937
      [node1, node2, node3] = Container.elems nl
938
      node3' = node3 { Node.group = 1 }
939
      nl' = Container.add (Node.idx node3') node3' nl
940
      inst1 = Instance.setBoth inst (Node.idx node1) (Node.idx node2)
941
      inst2 = Instance.setBoth inst (Node.idx node1) Node.noSecondary
942
      inst3 = Instance.setBoth inst (Node.idx node1) (Node.idx node3)
943
      ccheck = Cluster.findSplitInstances nl' . Container.fromList
944
  in null (ccheck [(0, inst1)]) &&
945
     null (ccheck [(0, inst2)]) &&
946
     (not . null $ ccheck [(0, inst3)])
947

    
948
-- | For now, we only test that we don't lose instances during the split.
949
prop_ClusterSplitCluster node inst =
950
  forAll (choose (0, 100)) $ \icnt ->
951
  let nl = makeSmallCluster node 2
952
      (nl', il') = foldl (\(ns, is) _ -> assignInstance ns is inst 0 1)
953
                   (nl, Container.empty) [1..icnt]
954
      gni = Cluster.splitCluster nl' il'
955
  in sum (map (Container.size . snd . snd) gni) == icnt &&
956
     all (\(guuid, (nl'', _)) -> all ((== guuid) . Node.group)
957
                                 (Container.elems nl'')) gni
958

    
959
testSuite "Cluster"
960
              [ 'prop_Score_Zero
961
              , 'prop_CStats_sane
962
              , 'prop_ClusterAlloc_sane
963
              , 'prop_ClusterCanTieredAlloc
964
              , 'prop_ClusterAllocEvac
965
              , 'prop_ClusterAllocBalance
966
              , 'prop_ClusterCheckConsistency
967
              , 'prop_ClusterSplitCluster
968
              ]
969

    
970
-- ** OpCodes tests
971

    
972
-- | Check that opcode serialization is idempotent.
973
prop_OpCodes_serialization op =
974
  case J.readJSON (J.showJSON op) of
975
    J.Error _ -> False
976
    J.Ok op' -> op == op'
977
  where _types = op::OpCodes.OpCode
978

    
979
testSuite "OpCodes"
980
              [ 'prop_OpCodes_serialization ]
981

    
982
-- ** Jobs tests
983

    
984
-- | Check that (queued) job\/opcode status serialization is idempotent.
985
prop_OpStatus_serialization os =
986
  case J.readJSON (J.showJSON os) of
987
    J.Error _ -> False
988
    J.Ok os' -> os == os'
989
  where _types = os::Jobs.OpStatus
990

    
991
prop_JobStatus_serialization js =
992
  case J.readJSON (J.showJSON js) of
993
    J.Error _ -> False
994
    J.Ok js' -> js == js'
995
  where _types = js::Jobs.JobStatus
996

    
997
testSuite "Jobs"
998
              [ 'prop_OpStatus_serialization
999
              , 'prop_JobStatus_serialization
1000
              ]
1001

    
1002
-- ** Loader tests
1003

    
1004
prop_Loader_lookupNode ktn inst node =
1005
  Loader.lookupNode nl inst node == Data.Map.lookup node nl
1006
  where nl = Data.Map.fromList ktn
1007

    
1008
prop_Loader_lookupInstance kti inst =
1009
  Loader.lookupInstance il inst == Data.Map.lookup inst il
1010
  where il = Data.Map.fromList kti
1011

    
1012
prop_Loader_assignIndices nodes =
1013
  Data.Map.size nassoc == length nodes &&
1014
  Container.size kt == length nodes &&
1015
  (if not (null nodes)
1016
   then maximum (IntMap.keys kt) == length nodes - 1
1017
   else True)
1018
  where (nassoc, kt) = Loader.assignIndices (map (\n -> (Node.name n, n)) nodes)
1019

    
1020
-- | Checks that the number of primary instances recorded on the nodes
1021
-- is zero.
1022
prop_Loader_mergeData ns =
1023
  let na = Container.fromList $ map (\n -> (Node.idx n, n)) ns
1024
  in case Loader.mergeData [] [] [] []
1025
         (Loader.emptyCluster {Loader.cdNodes = na}) of
1026
    Types.Bad _ -> False
1027
    Types.Ok (Loader.ClusterData _ nl il _) ->
1028
      let nodes = Container.elems nl
1029
          instances = Container.elems il
1030
      in (sum . map (length . Node.pList)) nodes == 0 &&
1031
         null instances
1032

    
1033
-- | Check that compareNameComponent on equal strings works.
1034
prop_Loader_compareNameComponent_equal :: String -> Bool
1035
prop_Loader_compareNameComponent_equal s =
1036
  Loader.compareNameComponent s s ==
1037
    Loader.LookupResult Loader.ExactMatch s
1038

    
1039
-- | Check that compareNameComponent on prefix strings works.
1040
prop_Loader_compareNameComponent_prefix :: NonEmptyList Char -> String -> Bool
1041
prop_Loader_compareNameComponent_prefix (NonEmpty s1) s2 =
1042
  Loader.compareNameComponent (s1 ++ "." ++ s2) s1 ==
1043
    Loader.LookupResult Loader.PartialMatch s1
1044

    
1045
testSuite "Loader"
1046
              [ 'prop_Loader_lookupNode
1047
              , 'prop_Loader_lookupInstance
1048
              , 'prop_Loader_assignIndices
1049
              , 'prop_Loader_mergeData
1050
              , 'prop_Loader_compareNameComponent_equal
1051
              , 'prop_Loader_compareNameComponent_prefix
1052
              ]
1053

    
1054
-- ** Types tests
1055

    
1056
prop_Types_AllocPolicy_serialisation apol =
1057
    case J.readJSON (J.showJSON apol) of
1058
      J.Ok p -> printTestCase ("invalid deserialisation " ++ show p) $
1059
                p == apol
1060
      J.Error s -> printTestCase ("failed to deserialise: " ++ s) False
1061
    where _types = apol::Types.AllocPolicy
1062

    
1063
prop_Types_DiskTemplate_serialisation dt =
1064
    case J.readJSON (J.showJSON dt) of
1065
      J.Ok p -> printTestCase ("invalid deserialisation " ++ show p) $
1066
                p == dt
1067
      J.Error s -> printTestCase ("failed to deserialise: " ++ s)
1068
                   False
1069
    where _types = dt::Types.DiskTemplate
1070

    
1071
prop_Types_opToResult op =
1072
    case op of
1073
      Types.OpFail _ -> Types.isBad r
1074
      Types.OpGood v -> case r of
1075
                          Types.Bad _ -> False
1076
                          Types.Ok v' -> v == v'
1077
    where r = Types.opToResult op
1078
          _types = op::Types.OpResult Int
1079

    
1080
prop_Types_eitherToResult ei =
1081
    case ei of
1082
      Left _ -> Types.isBad r
1083
      Right v -> case r of
1084
                   Types.Bad _ -> False
1085
                   Types.Ok v' -> v == v'
1086
    where r = Types.eitherToResult ei
1087
          _types = ei::Either String Int
1088

    
1089
testSuite "Types"
1090
              [ 'prop_Types_AllocPolicy_serialisation
1091
              , 'prop_Types_DiskTemplate_serialisation
1092
              , 'prop_Types_opToResult
1093
              , 'prop_Types_eitherToResult
1094
              ]