Statistics
| Branch: | Tag: | Revision:

root / htools / Ganeti / HTools / QC.hs @ 97da6b71

History | View | Annotate | Download (38.9 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
import qualified Ganeti.HTools.Program.Hail
70
import qualified Ganeti.HTools.Program.Hbal
71
import qualified Ganeti.HTools.Program.Hscan
72
import qualified Ganeti.HTools.Program.Hspace
73

    
74
run :: Testable prop => prop -> Args -> IO Result
75
run = flip quickCheckWithResult
76

    
77
-- * Constants
78

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

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

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

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

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

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

    
102
-- * Helper functions
103

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

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

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

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

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

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

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

    
157
-- * Arbitrary instances
158

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

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

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

    
173

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

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

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

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

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

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

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

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

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

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

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

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

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

    
293
-- * Actual tests
294

    
295
-- ** Utils tests
296

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

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

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

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

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

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

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

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

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

    
378
-- ** PeerMap tests
379

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

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

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

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

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

    
415
-- | List of tests for the PeerMap module.
416
testPeerMap =
417
    [ run prop_PeerMap_addIdempotent
418
    , run prop_PeerMap_removeIdempotent
419
    , run prop_PeerMap_maxElem
420
    , run prop_PeerMap_addFind
421
    , run prop_PeerMap_findMissing
422
    ]
423

    
424
-- ** Container tests
425

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

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

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

    
461
testContainer =
462
    [ run prop_Container_addTwo
463
    , run prop_Container_nameOf
464
    , run prop_Container_findByName
465
    ]
466

    
467
-- ** Instance tests
468

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
573
-- ** Text backend tests
574

    
575
-- Instance text loader tests
576

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

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

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

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

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

    
666
testText =
667
    [ run prop_Text_Load_Instance
668
    , run prop_Text_Load_InstanceFail
669
    , run prop_Text_Load_Node
670
    , run prop_Text_Load_NodeFail
671
    , run prop_Text_NodeLSIdempotent
672
    ]
673

    
674
-- ** Node tests
675

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

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

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

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

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

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

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

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

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

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

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

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

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

    
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
testNode =
803
    [ run prop_Node_setAlias
804
    , run prop_Node_setOffline
805
    , run prop_Node_setMcpu
806
    , run prop_Node_setXmem
807
    , run prop_Node_addPriFM
808
    , run prop_Node_addPriFD
809
    , run prop_Node_addPriFC
810
    , run prop_Node_addSec
811
    , run prop_Node_rMem
812
    , run prop_Node_setMdsk
813
    , run prop_Node_tagMaps_idempotent
814
    , run prop_Node_tagMaps_reject
815
    , run prop_Node_showField
816
    , run prop_Node_computeGroups
817
    ]
818

    
819

    
820
-- ** Cluster tests
821

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

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

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

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

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

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

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

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

    
961
testCluster =
962
    [ run prop_Score_Zero
963
    , run prop_CStats_sane
964
    , run prop_ClusterAlloc_sane
965
    , run prop_ClusterCanTieredAlloc
966
    , run prop_ClusterAllocEvac
967
    , run prop_ClusterAllocBalance
968
    , run prop_ClusterCheckConsistency
969
    , run prop_ClusterSplitCluster
970
    ]
971

    
972
-- ** OpCodes tests
973

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

    
981
testOpCodes =
982
  [ run prop_OpCodes_serialization
983
  ]
984

    
985
-- ** Jobs tests
986

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

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

    
1000
testJobs =
1001
  [ run prop_OpStatus_serialization
1002
  , run prop_JobStatus_serialization
1003
  ]
1004

    
1005
-- ** Loader tests
1006

    
1007
prop_Loader_lookupNode ktn inst node =
1008
  Loader.lookupNode nl inst node == Data.Map.lookup node nl
1009
  where nl = Data.Map.fromList ktn
1010

    
1011
prop_Loader_lookupInstance kti inst =
1012
  Loader.lookupInstance il inst == Data.Map.lookup inst il
1013
  where il = Data.Map.fromList kti
1014

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

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

    
1036
-- | Check that compareNameComponent on equal strings works.
1037
prop_Loader_compareNameComponent_equal :: String -> Bool
1038
prop_Loader_compareNameComponent_equal s =
1039
  Loader.compareNameComponent s s ==
1040
    Loader.LookupResult Loader.ExactMatch s
1041

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

    
1048
testLoader =
1049
  [ run prop_Loader_lookupNode
1050
  , run prop_Loader_lookupInstance
1051
  , run prop_Loader_assignIndices
1052
  , run prop_Loader_mergeData
1053
  , run prop_Loader_compareNameComponent_equal
1054
  , run prop_Loader_compareNameComponent_prefix
1055
  ]
1056

    
1057
-- ** Types tests
1058

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

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

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

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

    
1092
testTypes =
1093
    [ run prop_Types_AllocPolicy_serialisation
1094
    , run prop_Types_DiskTemplate_serialisation
1095
    , run prop_Types_opToResult
1096
    , run prop_Types_eitherToResult
1097
    ]