Statistics
| Branch: | Tag: | Revision:

root / htools / Ganeti / HTools / QC.hs @ 129734d3

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.asSolution as of
864
               Nothing -> False
865
               Just (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

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

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

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

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

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

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

    
971
-- ** OpCodes tests
972

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

    
980
testOpCodes =
981
  [ run prop_OpCodes_serialization
982
  ]
983

    
984
-- ** Jobs tests
985

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

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

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

    
1004
-- ** Loader tests
1005

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

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

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

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

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

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

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

    
1056
-- ** Types tests
1057

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

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

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

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

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