Statistics
| Branch: | Tag: | Revision:

root / htools / Ganeti / HTools / QC.hs @ 0047d4e2

History | View | Annotate | Download (38.8 kB)

1
{-| Unittests for ganeti-htools.
2

    
3
-}
4

    
5
{-
6

    
7
Copyright (C) 2009, 2010, 2011 Google Inc.
8

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

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

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

    
24
-}
25

    
26
module Ganeti.HTools.QC
27
    ( testUtils
28
    , testPeerMap
29
    , testContainer
30
    , testInstance
31
    , testNode
32
    , testText
33
    , testOpCodes
34
    , testJobs
35
    , testCluster
36
    , testLoader
37
    , 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
          liftM2 OpCodes.OpInstanceFailover arbitrary arbitrary
259
        "OP_INSTANCE_MIGRATE" ->
260
          liftM4 OpCodes.OpInstanceMigrate arbitrary arbitrary arbitrary
261
          arbitrary
262
        _ -> fail "Wrong opcode")
263

    
264
instance Arbitrary Jobs.OpStatus where
265
  arbitrary = elements [minBound..maxBound]
266

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

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

    
276
instance Arbitrary Types.AllocPolicy where
277
  arbitrary = elements [minBound..maxBound]
278

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

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

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

    
291
-- * Actual tests
292

    
293
-- ** Utils tests
294

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

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

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

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

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

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

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

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

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

    
376
-- ** PeerMap tests
377

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

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

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

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

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

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

    
422
-- ** Container tests
423

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

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

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

    
459
testContainer =
460
    [ run prop_Container_addTwo
461
    , run prop_Container_nameOf
462
    , run prop_Container_findByName
463
    ]
464

    
465
-- ** Instance tests
466

    
467
-- Simple instance tests, we only have setter/getters
468

    
469
prop_Instance_creat inst =
470
    Instance.name inst == Instance.alias inst
471

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
548
prop_Instance_setMovable inst m =
549
    Instance.movable inst' == m
550
    where inst' = Instance.setMovable inst m
551

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

    
571
-- ** Text backend tests
572

    
573
-- Instance text loader tests
574

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

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

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

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

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

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

    
672
-- ** Node tests
673

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

    
680
prop_Node_setOffline node status =
681
    Node.offline newnode == status
682
    where newnode = Node.setOffline node status
683

    
684
prop_Node_setXmem node xm =
685
    Node.xMem newnode == xm
686
    where newnode = Node.setXmem node xm
687

    
688
prop_Node_setMcpu node mc =
689
    Node.mCpu newnode == mc
690
    where newnode = Node.setMcpu node mc
691

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

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

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

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

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

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

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

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

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

    
791

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

    
800
testNode =
801
    [ run prop_Node_setAlias
802
    , run prop_Node_setOffline
803
    , run prop_Node_setMcpu
804
    , run prop_Node_setXmem
805
    , run prop_Node_addPriFM
806
    , run prop_Node_addPriFD
807
    , run prop_Node_addPriFC
808
    , run prop_Node_addSec
809
    , run prop_Node_rMem
810
    , run prop_Node_setMdsk
811
    , run prop_Node_tagMaps_idempotent
812
    , run prop_Node_tagMaps_reject
813
    , run prop_Node_showField
814
    , run prop_Node_computeGroups
815
    ]
816

    
817

    
818
-- ** Cluster tests
819

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

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

    
845
-- | Check that one instance is allocated correctly, without
846
-- rebalances needed.
847
prop_ClusterAlloc_sane node inst =
848
    forAll (choose (5, 20)) $ \count ->
849
    not (Node.offline node)
850
            && not (Node.failN1 node)
851
            && Node.availDisk node > 0
852
            && Node.availMem node > 0
853
            ==>
854
    let nl = makeSmallCluster node count
855
        il = Container.empty
856
        inst' = setInstanceSmallerThanNode node inst
857
    in case Cluster.genAllocNodes defGroupList nl 2 True >>=
858
       Cluster.tryAlloc nl il inst' of
859
         Types.Bad _ -> False
860
         Types.Ok as ->
861
             case Cluster.asSolutions as of
862
               [] -> False
863
               (xnl, xi, _, cv):[] ->
864
                   let il' = Container.add (Instance.idx xi) xi il
865
                       tbl = Cluster.Table xnl il' cv []
866
                   in not (canBalance tbl True True False)
867
               _ -> 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.asSolutions as of
905
               [] -> False
906
               (xnl, xi, _, _):[] ->
907
                   let sdx = Instance.sNode xi
908
                       il' = Container.add (Instance.idx xi) xi il
909
                   in case Cluster.tryEvac xnl il' [Instance.idx xi] [sdx] of
910
                        Just _ -> True
911
                        _ -> False
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
testCluster =
960
    [ run prop_Score_Zero
961
    , run prop_CStats_sane
962
    , run prop_ClusterAlloc_sane
963
    , run prop_ClusterCanTieredAlloc
964
    , run prop_ClusterAllocEvac
965
    , run prop_ClusterAllocBalance
966
    , run prop_ClusterCheckConsistency
967
    , run 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
testOpCodes =
980
  [ run prop_OpCodes_serialization
981
  ]
982

    
983
-- ** Jobs tests
984

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

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

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

    
1003
-- ** Loader tests
1004

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

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

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

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

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

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

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

    
1055
-- ** Types tests
1056

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

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

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

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

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