Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (54.5 kB)

1
{-# LANGUAGE TemplateHaskell #-}
2

    
3
{-| Unittests for ganeti-htools.
4

    
5
-}
6

    
7
{-
8

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

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

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

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

    
26
-}
27

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

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

    
75
import qualified Ganeti.HTools.Program.Hail
76
import qualified Ganeti.HTools.Program.Hbal
77
import qualified Ganeti.HTools.Program.Hscan
78
import qualified Ganeti.HTools.Program.Hspace
79

    
80
import Ganeti.HTools.QCHelper (testSuite)
81

    
82
-- * Constants
83

    
84
-- | Maximum memory (1TiB, somewhat random value).
85
maxMem :: Int
86
maxMem = 1024 * 1024
87

    
88
-- | Maximum disk (8TiB, somewhat random value).
89
maxDsk :: Int
90
maxDsk = 1024 * 1024 * 8
91

    
92
-- | Max CPUs (1024, somewhat random value).
93
maxCpu :: Int
94
maxCpu = 1024
95

    
96
-- | All disk templates (used later)
97
allDiskTemplates :: [Types.DiskTemplate]
98
allDiskTemplates = [minBound..maxBound]
99

    
100
-- | Null iPolicy, and by null we mean very liberal.
101
nullIPolicy = Types.IPolicy
102
  { Types.iPolicyMinSpec = Types.ISpec { Types.iSpecMemorySize = 0
103
                                       , Types.iSpecCpuCount   = 0
104
                                       , Types.iSpecDiskSize   = 0
105
                                       , Types.iSpecDiskCount  = 0
106
                                       , Types.iSpecNicCount   = 0
107
                                       }
108
  , Types.iPolicyMaxSpec = Types.ISpec { Types.iSpecMemorySize = maxBound
109
                                       , Types.iSpecCpuCount   = maxBound
110
                                       , Types.iSpecDiskSize   = maxBound
111
                                       , Types.iSpecDiskCount  = C.maxDisks
112
                                       , Types.iSpecNicCount   = C.maxNics
113
                                       }
114
  , Types.iPolicyStdSpec = Types.ISpec { Types.iSpecMemorySize = Types.unitMem
115
                                       , Types.iSpecCpuCount   = Types.unitCpu
116
                                       , Types.iSpecDiskSize   = Types.unitDsk
117
                                       , Types.iSpecDiskCount  = 1
118
                                       , Types.iSpecNicCount   = 1
119
                                       }
120
  , Types.iPolicyDiskTemplates = [Types.DTDrbd8, Types.DTPlain]
121
  }
122

    
123

    
124
defGroup :: Group.Group
125
defGroup = flip Group.setIdx 0 $
126
             Group.create "default" Types.defaultGroupID Types.AllocPreferred
127
                  nullIPolicy
128

    
129
defGroupList :: Group.List
130
defGroupList = Container.fromList [(Group.idx defGroup, defGroup)]
131

    
132
defGroupAssoc :: Data.Map.Map String Types.Gdx
133
defGroupAssoc = Data.Map.singleton (Group.uuid defGroup) (Group.idx defGroup)
134

    
135
-- * Helper functions
136

    
137
-- | Simple checker for whether OpResult is fail or pass.
138
isFailure :: Types.OpResult a -> Bool
139
isFailure (Types.OpFail _) = True
140
isFailure _ = False
141

    
142
-- | Checks for equality with proper annotation.
143
(==?) :: (Show a, Eq a) => a -> a -> Property
144
(==?) x y = printTestCase
145
            ("Expected equality, but '" ++
146
             show x ++ "' /= '" ++ show y ++ "'") (x == y)
147
infix 3 ==?
148

    
149
-- | Show a message and fail the test.
150
failTest :: String -> Property
151
failTest msg = printTestCase msg False
152

    
153
-- | Update an instance to be smaller than a node.
154
setInstanceSmallerThanNode node inst =
155
  inst { Instance.mem = Node.availMem node `div` 2
156
       , Instance.dsk = Node.availDisk node `div` 2
157
       , Instance.vcpus = Node.availCpu node `div` 2
158
       }
159

    
160
-- | Create an instance given its spec.
161
createInstance mem dsk vcpus =
162
  Instance.create "inst-unnamed" mem dsk vcpus Types.Running [] True (-1) (-1)
163
    Types.DTDrbd8
164

    
165
-- | Create a small cluster by repeating a node spec.
166
makeSmallCluster :: Node.Node -> Int -> Node.List
167
makeSmallCluster node count =
168
  let origname = Node.name node
169
      origalias = Node.alias node
170
      nodes = map (\idx -> node { Node.name = origname ++ "-" ++ show idx
171
                                , Node.alias = origalias ++ "-" ++ show idx })
172
              [1..count]
173
      fn = flip Node.buildPeers Container.empty
174
      namelst = map (\n -> (Node.name n, fn n)) nodes
175
      (_, nlst) = Loader.assignIndices namelst
176
  in nlst
177

    
178
-- | Make a small cluster, both nodes and instances.
179
makeSmallEmptyCluster :: Node.Node -> Int -> Instance.Instance
180
                      -> (Node.List, Instance.List, Instance.Instance)
181
makeSmallEmptyCluster node count inst =
182
  (makeSmallCluster node count, Container.empty,
183
   setInstanceSmallerThanNode node inst)
184

    
185
-- | Checks if a node is "big" enough.
186
isNodeBig :: Int -> Node.Node -> Bool
187
isNodeBig size node = Node.availDisk node > size * Types.unitDsk
188
                      && Node.availMem node > size * Types.unitMem
189
                      && Node.availCpu node > size * Types.unitCpu
190

    
191
canBalance :: Cluster.Table -> Bool -> Bool -> Bool -> Bool
192
canBalance tbl dm im evac = isJust $ Cluster.tryBalance tbl dm im evac 0 0
193

    
194
-- | Assigns a new fresh instance to a cluster; this is not
195
-- allocation, so no resource checks are done.
196
assignInstance :: Node.List -> Instance.List -> Instance.Instance ->
197
                  Types.Idx -> Types.Idx ->
198
                  (Node.List, Instance.List)
199
assignInstance nl il inst pdx sdx =
200
  let pnode = Container.find pdx nl
201
      snode = Container.find sdx nl
202
      maxiidx = if Container.null il
203
                  then 0
204
                  else fst (Container.findMax il) + 1
205
      inst' = inst { Instance.idx = maxiidx,
206
                     Instance.pNode = pdx, Instance.sNode = sdx }
207
      pnode' = Node.setPri pnode inst'
208
      snode' = Node.setSec snode inst'
209
      nl' = Container.addTwo pdx pnode' sdx snode' nl
210
      il' = Container.add maxiidx inst' il
211
  in (nl', il')
212

    
213
-- | Generates a list of a given size with non-duplicate elements.
214
genUniquesList :: (Eq a, Arbitrary a) => Int -> Gen [a]
215
genUniquesList cnt =
216
  foldM (\lst _ -> do
217
           newelem <- arbitrary `suchThat` (`notElem` lst)
218
           return (newelem:lst)) [] [1..cnt]
219

    
220
-- * Arbitrary instances
221

    
222
-- | Defines a DNS name.
223
newtype DNSChar = DNSChar { dnsGetChar::Char }
224

    
225
instance Arbitrary DNSChar where
226
  arbitrary = do
227
    x <- elements (['a'..'z'] ++ ['0'..'9'] ++ "_-")
228
    return (DNSChar x)
229

    
230
-- | Generates a single name component.
231
getName :: Gen String
232
getName = do
233
  n <- choose (1, 64)
234
  dn <- vector n::Gen [DNSChar]
235
  return (map dnsGetChar dn)
236

    
237
-- | Generates an entire FQDN.
238
getFQDN :: Gen String
239
getFQDN = do
240
  ncomps <- choose (1, 4)
241
  names <- mapM (const getName) [1..ncomps::Int]
242
  return $ intercalate "." names
243

    
244
-- | Defines a tag type.
245
newtype TagChar = TagChar { tagGetChar :: Char }
246

    
247
-- | All valid tag chars. This doesn't need to match _exactly_
248
-- Ganeti's own tag regex, just enough for it to be close.
249
tagChar :: [Char]
250
tagChar = ['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9'] ++ ".+*/:@-"
251

    
252
instance Arbitrary TagChar where
253
  arbitrary = do
254
    c <- elements tagChar
255
    return (TagChar c)
256

    
257
-- | Generates a tag
258
genTag :: Gen [TagChar]
259
genTag = do
260
  -- the correct value would be C.maxTagLen, but that's way too
261
  -- verbose in unittests, and at the moment I don't see any possible
262
  -- bugs with longer tags and the way we use tags in htools
263
  n <- choose (1, 10)
264
  vector n
265

    
266
-- | Generates a list of tags (correctly upper bounded).
267
genTags :: Gen [String]
268
genTags = do
269
  -- the correct value would be C.maxTagsPerObj, but per the comment
270
  -- in genTag, we don't use tags enough in htools to warrant testing
271
  -- such big values
272
  n <- choose (0, 10::Int)
273
  tags <- mapM (const genTag) [1..n]
274
  return $ map (map tagGetChar) tags
275

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

    
279
-- | Generates a random instance with maximum disk/mem/cpu values.
280
genInstanceSmallerThan :: Int -> Int -> Int -> Gen Instance.Instance
281
genInstanceSmallerThan lim_mem lim_dsk lim_cpu = do
282
  name <- getFQDN
283
  mem <- choose (0, lim_mem)
284
  dsk <- choose (0, lim_dsk)
285
  run_st <- arbitrary
286
  pn <- arbitrary
287
  sn <- arbitrary
288
  vcpus <- choose (0, lim_cpu)
289
  return $ Instance.create name mem dsk vcpus run_st [] True pn sn
290
         Types.DTDrbd8
291

    
292
-- | Generates an instance smaller than a node.
293
genInstanceSmallerThanNode :: Node.Node -> Gen Instance.Instance
294
genInstanceSmallerThanNode node =
295
  genInstanceSmallerThan (Node.availMem node `div` 2)
296
                         (Node.availDisk node `div` 2)
297
                         (Node.availCpu node `div` 2)
298

    
299
-- let's generate a random instance
300
instance Arbitrary Instance.Instance where
301
  arbitrary = genInstanceSmallerThan maxMem maxDsk maxCpu
302

    
303
-- | Generas an arbitrary node based on sizing information.
304
genNode :: Maybe Int -- ^ Minimum node size in terms of units
305
        -> Maybe Int -- ^ Maximum node size (when Nothing, bounded
306
                     -- just by the max... constants)
307
        -> Gen Node.Node
308
genNode min_multiplier max_multiplier = do
309
  let (base_mem, base_dsk, base_cpu) =
310
        case min_multiplier of
311
          Just mm -> (mm * Types.unitMem,
312
                      mm * Types.unitDsk,
313
                      mm * Types.unitCpu)
314
          Nothing -> (0, 0, 0)
315
      (top_mem, top_dsk, top_cpu)  =
316
        case max_multiplier of
317
          Just mm -> (mm * Types.unitMem,
318
                      mm * Types.unitDsk,
319
                      mm * Types.unitCpu)
320
          Nothing -> (maxMem, maxDsk, maxCpu)
321
  name  <- getFQDN
322
  mem_t <- choose (base_mem, top_mem)
323
  mem_f <- choose (base_mem, mem_t)
324
  mem_n <- choose (0, mem_t - mem_f)
325
  dsk_t <- choose (base_dsk, top_dsk)
326
  dsk_f <- choose (base_dsk, dsk_t)
327
  cpu_t <- choose (base_cpu, top_cpu)
328
  offl  <- arbitrary
329
  let n = Node.create name (fromIntegral mem_t) mem_n mem_f
330
          (fromIntegral dsk_t) dsk_f (fromIntegral cpu_t) offl 0
331
      n' = Node.setPolicy nullIPolicy n
332
  return $ Node.buildPeers n' Container.empty
333

    
334
-- | Helper function to generate a sane node.
335
genOnlineNode :: Gen Node.Node
336
genOnlineNode = do
337
  arbitrary `suchThat` (\n -> not (Node.offline n) &&
338
                              not (Node.failN1 n) &&
339
                              Node.availDisk n > 0 &&
340
                              Node.availMem n > 0 &&
341
                              Node.availCpu n > 0)
342

    
343
-- and a random node
344
instance Arbitrary Node.Node where
345
  arbitrary = genNode Nothing Nothing
346

    
347
-- replace disks
348
instance Arbitrary OpCodes.ReplaceDisksMode where
349
  arbitrary = elements [minBound..maxBound]
350

    
351
instance Arbitrary OpCodes.OpCode where
352
  arbitrary = do
353
    op_id <- elements [ "OP_TEST_DELAY"
354
                      , "OP_INSTANCE_REPLACE_DISKS"
355
                      , "OP_INSTANCE_FAILOVER"
356
                      , "OP_INSTANCE_MIGRATE"
357
                      ]
358
    case op_id of
359
      "OP_TEST_DELAY" ->
360
        liftM3 OpCodes.OpTestDelay arbitrary arbitrary arbitrary
361
      "OP_INSTANCE_REPLACE_DISKS" ->
362
        liftM5 OpCodes.OpInstanceReplaceDisks arbitrary arbitrary
363
          arbitrary arbitrary arbitrary
364
      "OP_INSTANCE_FAILOVER" ->
365
        liftM3 OpCodes.OpInstanceFailover arbitrary arbitrary
366
          arbitrary
367
      "OP_INSTANCE_MIGRATE" ->
368
        liftM5 OpCodes.OpInstanceMigrate arbitrary arbitrary
369
          arbitrary arbitrary arbitrary
370
      _ -> fail "Wrong opcode"
371

    
372
instance Arbitrary Jobs.OpStatus where
373
  arbitrary = elements [minBound..maxBound]
374

    
375
instance Arbitrary Jobs.JobStatus where
376
  arbitrary = elements [minBound..maxBound]
377

    
378
newtype SmallRatio = SmallRatio Double deriving Show
379
instance Arbitrary SmallRatio where
380
  arbitrary = do
381
    v <- choose (0, 1)
382
    return $ SmallRatio v
383

    
384
instance Arbitrary Types.AllocPolicy where
385
  arbitrary = elements [minBound..maxBound]
386

    
387
instance Arbitrary Types.DiskTemplate where
388
  arbitrary = elements [minBound..maxBound]
389

    
390
instance Arbitrary Types.FailMode where
391
  arbitrary = elements [minBound..maxBound]
392

    
393
instance Arbitrary Types.EvacMode where
394
  arbitrary = elements [minBound..maxBound]
395

    
396
instance Arbitrary a => Arbitrary (Types.OpResult a) where
397
  arbitrary = arbitrary >>= \c ->
398
              if c
399
                then liftM Types.OpGood arbitrary
400
                else liftM Types.OpFail arbitrary
401

    
402
instance Arbitrary Types.ISpec where
403
  arbitrary = do
404
    mem_s <- arbitrary::Gen (NonNegative Int)
405
    dsk_c <- arbitrary::Gen (NonNegative Int)
406
    dsk_s <- arbitrary::Gen (NonNegative Int)
407
    cpu_c <- arbitrary::Gen (NonNegative Int)
408
    nic_c <- arbitrary::Gen (NonNegative Int)
409
    return Types.ISpec { Types.iSpecMemorySize = fromIntegral mem_s
410
                       , Types.iSpecCpuCount   = fromIntegral cpu_c
411
                       , Types.iSpecDiskSize   = fromIntegral dsk_s
412
                       , Types.iSpecDiskCount  = fromIntegral dsk_c
413
                       , Types.iSpecNicCount   = fromIntegral nic_c
414
                       }
415

    
416
-- | Generates an ispec bigger than the given one.
417
genBiggerISpec :: Types.ISpec -> Gen Types.ISpec
418
genBiggerISpec imin = do
419
  mem_s <- choose (Types.iSpecMemorySize imin, maxBound)
420
  dsk_c <- choose (Types.iSpecDiskCount imin, maxBound)
421
  dsk_s <- choose (Types.iSpecDiskSize imin, maxBound)
422
  cpu_c <- choose (Types.iSpecCpuCount imin, maxBound)
423
  nic_c <- choose (Types.iSpecNicCount imin, maxBound)
424
  return Types.ISpec { Types.iSpecMemorySize = fromIntegral mem_s
425
                     , Types.iSpecCpuCount   = fromIntegral cpu_c
426
                     , Types.iSpecDiskSize   = fromIntegral dsk_s
427
                     , Types.iSpecDiskCount  = fromIntegral dsk_c
428
                     , Types.iSpecNicCount   = fromIntegral nic_c
429
                     }
430

    
431
instance Arbitrary Types.IPolicy where
432
  arbitrary = do
433
    imin <- arbitrary
434
    istd <- genBiggerISpec imin
435
    imax <- genBiggerISpec istd
436
    num_tmpl <- choose (0, length allDiskTemplates)
437
    dts  <- genUniquesList num_tmpl
438
    return Types.IPolicy { Types.iPolicyMinSpec = imin
439
                         , Types.iPolicyStdSpec = istd
440
                         , Types.iPolicyMaxSpec = imax
441
                         , Types.iPolicyDiskTemplates = dts
442
                         }
443

    
444
-- * Actual tests
445

    
446
-- ** Utils tests
447

    
448
-- | If the list is not just an empty element, and if the elements do
449
-- not contain commas, then join+split should be idempotent.
450
prop_Utils_commaJoinSplit =
451
  forAll (arbitrary `suchThat`
452
          (\l -> l /= [""] && all (notElem ',') l )) $ \lst ->
453
  Utils.sepSplit ',' (Utils.commaJoin lst) ==? lst
454

    
455
-- | Split and join should always be idempotent.
456
prop_Utils_commaSplitJoin s =
457
  Utils.commaJoin (Utils.sepSplit ',' s) ==? s
458

    
459
-- | fromObjWithDefault, we test using the Maybe monad and an integer
460
-- value.
461
prop_Utils_fromObjWithDefault def_value random_key =
462
  -- a missing key will be returned with the default
463
  JSON.fromObjWithDefault [] random_key def_value == Just def_value &&
464
  -- a found key will be returned as is, not with default
465
  JSON.fromObjWithDefault [(random_key, J.showJSON def_value)]
466
       random_key (def_value+1) == Just def_value
467
    where _types = def_value :: Integer
468

    
469
-- | Test that functional if' behaves like the syntactic sugar if.
470
prop_Utils_if'if :: Bool -> Int -> Int -> Gen Prop
471
prop_Utils_if'if cnd a b =
472
  Utils.if' cnd a b ==? if cnd then a else b
473

    
474
-- | Test basic select functionality
475
prop_Utils_select :: Int      -- ^ Default result
476
                  -> [Int]    -- ^ List of False values
477
                  -> [Int]    -- ^ List of True values
478
                  -> Gen Prop -- ^ Test result
479
prop_Utils_select def lst1 lst2 =
480
  Utils.select def (flist ++ tlist) ==? expectedresult
481
    where expectedresult = Utils.if' (null lst2) def (head lst2)
482
          flist = zip (repeat False) lst1
483
          tlist = zip (repeat True)  lst2
484

    
485
-- | Test basic select functionality with undefined default
486
prop_Utils_select_undefd :: [Int]            -- ^ List of False values
487
                         -> NonEmptyList Int -- ^ List of True values
488
                         -> Gen Prop         -- ^ Test result
489
prop_Utils_select_undefd lst1 (NonEmpty lst2) =
490
  Utils.select undefined (flist ++ tlist) ==? head lst2
491
    where flist = zip (repeat False) lst1
492
          tlist = zip (repeat True)  lst2
493

    
494
-- | Test basic select functionality with undefined list values
495
prop_Utils_select_undefv :: [Int]            -- ^ List of False values
496
                         -> NonEmptyList Int -- ^ List of True values
497
                         -> Gen Prop         -- ^ Test result
498
prop_Utils_select_undefv lst1 (NonEmpty lst2) =
499
  Utils.select undefined cndlist ==? head lst2
500
    where flist = zip (repeat False) lst1
501
          tlist = zip (repeat True)  lst2
502
          cndlist = flist ++ tlist ++ [undefined]
503

    
504
prop_Utils_parseUnit (NonNegative n) =
505
  Utils.parseUnit (show n) == Types.Ok n &&
506
  Utils.parseUnit (show n ++ "m") == Types.Ok n &&
507
  (case Utils.parseUnit (show n ++ "M") of
508
     Types.Ok m -> if n > 0
509
                     then m < n  -- for positive values, X MB is < than X MiB
510
                     else m == 0 -- but for 0, 0 MB == 0 MiB
511
     Types.Bad _ -> False) &&
512
  Utils.parseUnit (show n ++ "g") == Types.Ok (n*1024) &&
513
  Utils.parseUnit (show n ++ "t") == Types.Ok (n*1048576) &&
514
  Types.isBad (Utils.parseUnit (show n ++ "x")::Types.Result Int)
515
    where _types = n::Int
516

    
517
-- | Test list for the Utils module.
518
testSuite "Utils"
519
            [ 'prop_Utils_commaJoinSplit
520
            , 'prop_Utils_commaSplitJoin
521
            , 'prop_Utils_fromObjWithDefault
522
            , 'prop_Utils_if'if
523
            , 'prop_Utils_select
524
            , 'prop_Utils_select_undefd
525
            , 'prop_Utils_select_undefv
526
            , 'prop_Utils_parseUnit
527
            ]
528

    
529
-- ** PeerMap tests
530

    
531
-- | Make sure add is idempotent.
532
prop_PeerMap_addIdempotent pmap key em =
533
  fn puniq ==? fn (fn puniq)
534
    where _types = (pmap::PeerMap.PeerMap,
535
                    key::PeerMap.Key, em::PeerMap.Elem)
536
          fn = PeerMap.add key em
537
          puniq = PeerMap.accumArray const pmap
538

    
539
-- | Make sure remove is idempotent.
540
prop_PeerMap_removeIdempotent pmap key =
541
  fn puniq ==? fn (fn puniq)
542
    where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
543
          fn = PeerMap.remove key
544
          puniq = PeerMap.accumArray const pmap
545

    
546
-- | Make sure a missing item returns 0.
547
prop_PeerMap_findMissing pmap key =
548
  PeerMap.find key (PeerMap.remove key puniq) ==? 0
549
    where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
550
          puniq = PeerMap.accumArray const pmap
551

    
552
-- | Make sure an added item is found.
553
prop_PeerMap_addFind pmap key em =
554
  PeerMap.find key (PeerMap.add key em puniq) ==? em
555
    where _types = (pmap::PeerMap.PeerMap,
556
                    key::PeerMap.Key, em::PeerMap.Elem)
557
          puniq = PeerMap.accumArray const pmap
558

    
559
-- | Manual check that maxElem returns the maximum indeed, or 0 for null.
560
prop_PeerMap_maxElem pmap =
561
  PeerMap.maxElem puniq ==? if null puniq then 0
562
                              else (maximum . snd . unzip) puniq
563
    where _types = pmap::PeerMap.PeerMap
564
          puniq = PeerMap.accumArray const pmap
565

    
566
-- | List of tests for the PeerMap module.
567
testSuite "PeerMap"
568
            [ 'prop_PeerMap_addIdempotent
569
            , 'prop_PeerMap_removeIdempotent
570
            , 'prop_PeerMap_maxElem
571
            , 'prop_PeerMap_addFind
572
            , 'prop_PeerMap_findMissing
573
            ]
574

    
575
-- ** Container tests
576

    
577
-- we silence the following due to hlint bug fixed in later versions
578
{-# ANN prop_Container_addTwo "HLint: ignore Avoid lambda" #-}
579
prop_Container_addTwo cdata i1 i2 =
580
  fn i1 i2 cont == fn i2 i1 cont &&
581
  fn i1 i2 cont == fn i1 i2 (fn i1 i2 cont)
582
    where _types = (cdata::[Int],
583
                    i1::Int, i2::Int)
584
          cont = foldl (\c x -> Container.add x x c) Container.empty cdata
585
          fn x1 x2 = Container.addTwo x1 x1 x2 x2
586

    
587
prop_Container_nameOf node =
588
  let nl = makeSmallCluster node 1
589
      fnode = head (Container.elems nl)
590
  in Container.nameOf nl (Node.idx fnode) ==? Node.name fnode
591

    
592
-- | We test that in a cluster, given a random node, we can find it by
593
-- its name and alias, as long as all names and aliases are unique,
594
-- and that we fail to find a non-existing name.
595
prop_Container_findByName node =
596
  forAll (choose (1, 20)) $ \ cnt ->
597
  forAll (choose (0, cnt - 1)) $ \ fidx ->
598
  forAll (genUniquesList (cnt * 2)) $ \ allnames ->
599
  forAll (arbitrary `suchThat` (`notElem` allnames)) $ \ othername ->
600
  let names = zip (take cnt allnames) (drop cnt allnames)
601
      nl = makeSmallCluster node cnt
602
      nodes = Container.elems nl
603
      nodes' = map (\((name, alias), nn) -> (Node.idx nn,
604
                                             nn { Node.name = name,
605
                                                  Node.alias = alias }))
606
               $ zip names nodes
607
      nl' = Container.fromList nodes'
608
      target = snd (nodes' !! fidx)
609
  in Container.findByName nl' (Node.name target) == Just target &&
610
     Container.findByName nl' (Node.alias target) == Just target &&
611
     isNothing (Container.findByName nl' othername)
612

    
613
testSuite "Container"
614
            [ 'prop_Container_addTwo
615
            , 'prop_Container_nameOf
616
            , 'prop_Container_findByName
617
            ]
618

    
619
-- ** Instance tests
620

    
621
-- Simple instance tests, we only have setter/getters
622

    
623
prop_Instance_creat inst =
624
  Instance.name inst ==? Instance.alias inst
625

    
626
prop_Instance_setIdx inst idx =
627
  Instance.idx (Instance.setIdx inst idx) ==? idx
628
    where _types = (inst::Instance.Instance, idx::Types.Idx)
629

    
630
prop_Instance_setName inst name =
631
  Instance.name newinst == name &&
632
  Instance.alias newinst == name
633
    where _types = (inst::Instance.Instance, name::String)
634
          newinst = Instance.setName inst name
635

    
636
prop_Instance_setAlias inst name =
637
  Instance.name newinst == Instance.name inst &&
638
  Instance.alias newinst == name
639
    where _types = (inst::Instance.Instance, name::String)
640
          newinst = Instance.setAlias inst name
641

    
642
prop_Instance_setPri inst pdx =
643
  Instance.pNode (Instance.setPri inst pdx) ==? pdx
644
    where _types = (inst::Instance.Instance, pdx::Types.Ndx)
645

    
646
prop_Instance_setSec inst sdx =
647
  Instance.sNode (Instance.setSec inst sdx) ==? sdx
648
    where _types = (inst::Instance.Instance, sdx::Types.Ndx)
649

    
650
prop_Instance_setBoth inst pdx sdx =
651
  Instance.pNode si == pdx && Instance.sNode si == sdx
652
    where _types = (inst::Instance.Instance, pdx::Types.Ndx, sdx::Types.Ndx)
653
          si = Instance.setBoth inst pdx sdx
654

    
655
prop_Instance_shrinkMG inst =
656
  Instance.mem inst >= 2 * Types.unitMem ==>
657
    case Instance.shrinkByType inst Types.FailMem of
658
      Types.Ok inst' -> Instance.mem inst' == Instance.mem inst - Types.unitMem
659
      _ -> False
660

    
661
prop_Instance_shrinkMF inst =
662
  forAll (choose (0, 2 * Types.unitMem - 1)) $ \mem ->
663
    let inst' = inst { Instance.mem = mem}
664
    in Types.isBad $ Instance.shrinkByType inst' Types.FailMem
665

    
666
prop_Instance_shrinkCG inst =
667
  Instance.vcpus inst >= 2 * Types.unitCpu ==>
668
    case Instance.shrinkByType inst Types.FailCPU of
669
      Types.Ok inst' ->
670
        Instance.vcpus inst' == Instance.vcpus inst - Types.unitCpu
671
      _ -> False
672

    
673
prop_Instance_shrinkCF inst =
674
  forAll (choose (0, 2 * Types.unitCpu - 1)) $ \vcpus ->
675
    let inst' = inst { Instance.vcpus = vcpus }
676
    in Types.isBad $ Instance.shrinkByType inst' Types.FailCPU
677

    
678
prop_Instance_shrinkDG inst =
679
  Instance.dsk inst >= 2 * Types.unitDsk ==>
680
    case Instance.shrinkByType inst Types.FailDisk of
681
      Types.Ok inst' ->
682
        Instance.dsk inst' == Instance.dsk inst - Types.unitDsk
683
      _ -> False
684

    
685
prop_Instance_shrinkDF inst =
686
  forAll (choose (0, 2 * Types.unitDsk - 1)) $ \dsk ->
687
    let inst' = inst { Instance.dsk = dsk }
688
    in Types.isBad $ Instance.shrinkByType inst' Types.FailDisk
689

    
690
prop_Instance_setMovable inst m =
691
  Instance.movable inst' ==? m
692
    where inst' = Instance.setMovable inst m
693

    
694
testSuite "Instance"
695
            [ 'prop_Instance_creat
696
            , 'prop_Instance_setIdx
697
            , 'prop_Instance_setName
698
            , 'prop_Instance_setAlias
699
            , 'prop_Instance_setPri
700
            , 'prop_Instance_setSec
701
            , 'prop_Instance_setBoth
702
            , 'prop_Instance_shrinkMG
703
            , 'prop_Instance_shrinkMF
704
            , 'prop_Instance_shrinkCG
705
            , 'prop_Instance_shrinkCF
706
            , 'prop_Instance_shrinkDG
707
            , 'prop_Instance_shrinkDF
708
            , 'prop_Instance_setMovable
709
            ]
710

    
711
-- ** Backends
712

    
713
-- *** Text backend tests
714

    
715
-- Instance text loader tests
716

    
717
prop_Text_Load_Instance name mem dsk vcpus status
718
                        (NonEmpty pnode) snode
719
                        (NonNegative pdx) (NonNegative sdx) autobal dt =
720
  pnode /= snode && pdx /= sdx ==>
721
  let vcpus_s = show vcpus
722
      dsk_s = show dsk
723
      mem_s = show mem
724
      status_s = Types.instanceStatusToRaw status
725
      ndx = if null snode
726
              then [(pnode, pdx)]
727
              else [(pnode, pdx), (snode, sdx)]
728
      nl = Data.Map.fromList ndx
729
      tags = ""
730
      sbal = if autobal then "Y" else "N"
731
      sdt = Types.diskTemplateToRaw dt
732
      inst = Text.loadInst nl
733
             [name, mem_s, dsk_s, vcpus_s, status_s,
734
              sbal, pnode, snode, sdt, tags]
735
      fail1 = Text.loadInst nl
736
              [name, mem_s, dsk_s, vcpus_s, status_s,
737
               sbal, pnode, pnode, tags]
738
      _types = ( name::String, mem::Int, dsk::Int
739
               , vcpus::Int, status::Types.InstanceStatus
740
               , snode::String
741
               , autobal::Bool)
742
  in case inst of
743
       Types.Bad msg -> failTest $ "Failed to load instance: " ++ msg
744
       Types.Ok (_, i) -> printTestCase "Mismatch in some field while\
745
                                        \ loading the instance" $
746
               Instance.name i == name &&
747
               Instance.vcpus i == vcpus &&
748
               Instance.mem i == mem &&
749
               Instance.pNode i == pdx &&
750
               Instance.sNode i == (if null snode
751
                                      then Node.noSecondary
752
                                      else sdx) &&
753
               Instance.autoBalance i == autobal &&
754
               Types.isBad fail1
755

    
756
prop_Text_Load_InstanceFail ktn fields =
757
  length fields /= 10 ==>
758
    case Text.loadInst nl fields of
759
      Types.Ok _ -> failTest "Managed to load instance from invalid data"
760
      Types.Bad msg -> printTestCase ("Unrecognised error message: " ++ msg) $
761
                       "Invalid/incomplete instance data: '" `isPrefixOf` msg
762
    where nl = Data.Map.fromList ktn
763

    
764
prop_Text_Load_Node name tm nm fm td fd tc fo =
765
  let conv v = if v < 0
766
                 then "?"
767
                 else show v
768
      tm_s = conv tm
769
      nm_s = conv nm
770
      fm_s = conv fm
771
      td_s = conv td
772
      fd_s = conv fd
773
      tc_s = conv tc
774
      fo_s = if fo
775
               then "Y"
776
               else "N"
777
      any_broken = any (< 0) [tm, nm, fm, td, fd, tc]
778
      gid = Group.uuid defGroup
779
  in case Text.loadNode defGroupAssoc
780
       [name, tm_s, nm_s, fm_s, td_s, fd_s, tc_s, fo_s, gid] of
781
       Nothing -> False
782
       Just (name', node) ->
783
         if fo || any_broken
784
           then Node.offline node
785
           else Node.name node == name' && name' == name &&
786
                Node.alias node == name &&
787
                Node.tMem node == fromIntegral tm &&
788
                Node.nMem node == nm &&
789
                Node.fMem node == fm &&
790
                Node.tDsk node == fromIntegral td &&
791
                Node.fDsk node == fd &&
792
                Node.tCpu node == fromIntegral tc
793

    
794
prop_Text_Load_NodeFail fields =
795
  length fields /= 8 ==> isNothing $ Text.loadNode Data.Map.empty fields
796

    
797
prop_Text_NodeLSIdempotent node =
798
  (Text.loadNode defGroupAssoc.
799
       Utils.sepSplit '|' . Text.serializeNode defGroupList) n ==
800
  Just (Node.name n, n)
801
    -- override failN1 to what loadNode returns by default
802
    where n = node { Node.failN1 = True, Node.offline = False
803
                   , Node.iPolicy = Types.defIPolicy }
804

    
805
prop_Text_ISpecIdempotent ispec =
806
  case Text.loadISpec "dummy" . Utils.sepSplit ',' .
807
       Text.serializeISpec $ ispec of
808
    Types.Bad msg -> failTest $ "Failed to load ispec: " ++ msg
809
    Types.Ok ispec' -> ispec ==? ispec'
810

    
811
prop_Text_IPolicyIdempotent ipol =
812
  case Text.loadIPolicy . Utils.sepSplit '|' $
813
       Text.serializeIPolicy owner ipol of
814
    Types.Bad msg -> failTest $ "Failed to load ispec: " ++ msg
815
    Types.Ok res -> (owner, ipol) ==? res
816
  where owner = "dummy"
817

    
818
-- | This property, while being in the text tests, does more than just
819
-- test end-to-end the serialisation and loading back workflow; it
820
-- also tests the Loader.mergeData and the actuall
821
-- Cluster.iterateAlloc (for well-behaving w.r.t. instance
822
-- allocations, not for the business logic). As such, it's a quite
823
-- complex and slow test, and that's the reason we restrict it to
824
-- small cluster sizes.
825
prop_Text_CreateSerialise =
826
  forAll genTags $ \ctags ->
827
  forAll (choose (1, 2)) $ \reqnodes ->
828
  forAll (choose (1, 20)) $ \maxiter ->
829
  forAll (choose (2, 10)) $ \count ->
830
  forAll genOnlineNode $ \node ->
831
  forAll (genInstanceSmallerThanNode node) $ \inst ->
832
  let inst' = Instance.setMovable inst (reqnodes == 2)
833
      nl = makeSmallCluster node count
834
  in case Cluster.genAllocNodes defGroupList nl reqnodes True >>= \allocn ->
835
     Cluster.iterateAlloc nl Container.empty (Just maxiter) inst' allocn [] []
836
     of
837
       Types.Bad msg -> failTest $ "Failed to allocate: " ++ msg
838
       Types.Ok (_, _, _, [], _) -> printTestCase
839
                                    "Failed to allocate: no allocations" False
840
       Types.Ok (_, nl', il', _, _) ->
841
         let cdata = Loader.ClusterData defGroupList nl' il' ctags
842
                     Types.defIPolicy
843
             saved = Text.serializeCluster cdata
844
         in case Text.parseData saved >>= Loader.mergeData [] [] [] [] of
845
              Types.Bad msg -> failTest $ "Failed to load/merge: " ++ msg
846
              Types.Ok (Loader.ClusterData gl2 nl2 il2 ctags2 cpol2) ->
847
                ctags ==? ctags2 .&&.
848
                Types.defIPolicy ==? cpol2 .&&.
849
                il' ==? il2 .&&.
850
                defGroupList ==? gl2 .&&.
851
                nl' ==? nl2
852

    
853
testSuite "Text"
854
            [ 'prop_Text_Load_Instance
855
            , 'prop_Text_Load_InstanceFail
856
            , 'prop_Text_Load_Node
857
            , 'prop_Text_Load_NodeFail
858
            , 'prop_Text_NodeLSIdempotent
859
            , 'prop_Text_ISpecIdempotent
860
            , 'prop_Text_IPolicyIdempotent
861
            , 'prop_Text_CreateSerialise
862
            ]
863

    
864
-- *** Simu backend
865

    
866
-- | Generates a tuple of specs for simulation.
867
genSimuSpec :: Gen (String, Int, Int, Int, Int)
868
genSimuSpec = do
869
  pol <- elements [C.allocPolicyPreferred,
870
                   C.allocPolicyLastResort, C.allocPolicyUnallocable,
871
                  "p", "a", "u"]
872
 -- should be reasonable (nodes/group), bigger values only complicate
873
 -- the display of failed tests, and we don't care (in this particular
874
 -- test) about big node groups
875
  nodes <- choose (0, 20)
876
  dsk <- choose (0, maxDsk)
877
  mem <- choose (0, maxMem)
878
  cpu <- choose (0, maxCpu)
879
  return (pol, nodes, dsk, mem, cpu)
880

    
881
-- | Checks that given a set of corrects specs, we can load them
882
-- successfully, and that at high-level the values look right.
883
prop_SimuLoad =
884
  forAll (choose (0, 10)) $ \ngroups ->
885
  forAll (replicateM ngroups genSimuSpec) $ \specs ->
886
  let strspecs = map (\(p, n, d, m, c) -> printf "%s,%d,%d,%d,%d"
887
                                          p n d m c::String) specs
888
      totnodes = sum $ map (\(_, n, _, _, _) -> n) specs
889
      mdc_in = concatMap (\(_, n, d, m, c) ->
890
                            replicate n (fromIntegral m, fromIntegral d,
891
                                         fromIntegral c,
892
                                         fromIntegral m, fromIntegral d)) specs
893
  in case Simu.parseData strspecs of
894
       Types.Bad msg -> failTest $ "Failed to load specs: " ++ msg
895
       Types.Ok (Loader.ClusterData gl nl il tags ipol) ->
896
         let nodes = map snd $ IntMap.toAscList nl
897
             nidx = map Node.idx nodes
898
             mdc_out = map (\n -> (Node.tMem n, Node.tDsk n, Node.tCpu n,
899
                                   Node.fMem n, Node.fDsk n)) nodes
900
         in
901
         Container.size gl ==? ngroups .&&.
902
         Container.size nl ==? totnodes .&&.
903
         Container.size il ==? 0 .&&.
904
         length tags ==? 0 .&&.
905
         ipol ==? Types.defIPolicy .&&.
906
         nidx ==? [1..totnodes] .&&.
907
         mdc_in ==? mdc_out .&&.
908
         map Group.iPolicy (Container.elems gl) ==?
909
             replicate ngroups Types.defIPolicy
910

    
911
testSuite "Simu"
912
            [ 'prop_SimuLoad
913
            ]
914

    
915
-- ** Node tests
916

    
917
prop_Node_setAlias node name =
918
  Node.name newnode == Node.name node &&
919
  Node.alias newnode == name
920
    where _types = (node::Node.Node, name::String)
921
          newnode = Node.setAlias node name
922

    
923
prop_Node_setOffline node status =
924
  Node.offline newnode ==? status
925
    where newnode = Node.setOffline node status
926

    
927
prop_Node_setXmem node xm =
928
  Node.xMem newnode ==? xm
929
    where newnode = Node.setXmem node xm
930

    
931
prop_Node_setMcpu node mc =
932
  Node.mCpu newnode ==? mc
933
    where newnode = Node.setMcpu node mc
934

    
935
-- | Check that an instance add with too high memory or disk will be
936
-- rejected.
937
prop_Node_addPriFM node inst =
938
  Instance.mem inst >= Node.fMem node && not (Node.failN1 node) &&
939
  not (Instance.instanceOffline inst) ==>
940
  case Node.addPri node inst'' of
941
    Types.OpFail Types.FailMem -> True
942
    _ -> False
943
  where _types = (node::Node.Node, inst::Instance.Instance)
944
        inst' = setInstanceSmallerThanNode node inst
945
        inst'' = inst' { Instance.mem = Instance.mem inst }
946

    
947
prop_Node_addPriFD node inst =
948
  Instance.dsk inst >= Node.fDsk node && not (Node.failN1 node) ==>
949
    case Node.addPri node inst'' of
950
      Types.OpFail Types.FailDisk -> True
951
      _ -> False
952
    where _types = (node::Node.Node, inst::Instance.Instance)
953
          inst' = setInstanceSmallerThanNode node inst
954
          inst'' = inst' { Instance.dsk = Instance.dsk inst }
955

    
956
prop_Node_addPriFC (Positive extra) =
957
  forAll genOnlineNode $ \node ->
958
  forAll (arbitrary `suchThat` Instance.instanceNotOffline) $ \inst ->
959
  let inst' = setInstanceSmallerThanNode node inst
960
      inst'' = inst' { Instance.vcpus = Node.availCpu node + extra }
961
  in case Node.addPri node inst'' of
962
       Types.OpFail Types.FailCPU -> property True
963
       v -> failTest $ "Expected OpFail FailCPU, but got " ++ show v
964

    
965
-- | Check that an instance add with too high memory or disk will be
966
-- rejected.
967
prop_Node_addSec node inst pdx =
968
  ((Instance.mem inst >= (Node.fMem node - Node.rMem node) &&
969
    not (Instance.instanceOffline inst)) ||
970
   Instance.dsk inst >= Node.fDsk node) &&
971
  not (Node.failN1 node) ==>
972
      isFailure (Node.addSec node inst pdx)
973
        where _types = (node::Node.Node, inst::Instance.Instance, pdx::Int)
974

    
975
-- | Check that an offline instance with reasonable disk size can always
976
-- be added.
977
prop_Node_addPriOffline =
978
  forAll (arbitrary `suchThat` ((> 0) . Node.fMem)) $ \node ->
979
  forAll (arbitrary `suchThat`
980
          (\ x ->  (Instance.dsk x  < Node.fDsk node) &&
981
                   Instance.instanceOffline x)) $ \inst ->
982
  case Node.addPri node inst of
983
    Types.OpGood _ -> True
984
    _ -> False
985

    
986
prop_Node_addSecOffline pdx =
987
  forAll genOnlineNode $ \node ->
988
  forAll (arbitrary `suchThat`
989
          (\ inst -> Instance.dsk inst  < Node.availDisk node)) $ \inst ->
990
  case Node.addSec node (inst { Instance.runSt = Types.AdminOffline }) pdx of
991
    Types.OpGood _ -> True
992
    _ -> False
993

    
994
-- | Checks for memory reservation changes.
995
prop_Node_rMem inst =
996
  not (Instance.instanceOffline inst) ==>
997
  forAll (arbitrary `suchThat` ((> Types.unitMem) . Node.fMem)) $ \node ->
998
  -- ab = auto_balance, nb = non-auto_balance
999
  -- we use -1 as the primary node of the instance
1000
  let inst' = inst { Instance.pNode = -1, Instance.autoBalance = True }
1001
      inst_ab = setInstanceSmallerThanNode node inst'
1002
      inst_nb = inst_ab { Instance.autoBalance = False }
1003
      -- now we have the two instances, identical except the
1004
      -- autoBalance attribute
1005
      orig_rmem = Node.rMem node
1006
      inst_idx = Instance.idx inst_ab
1007
      node_add_ab = Node.addSec node inst_ab (-1)
1008
      node_add_nb = Node.addSec node inst_nb (-1)
1009
      node_del_ab = liftM (`Node.removeSec` inst_ab) node_add_ab
1010
      node_del_nb = liftM (`Node.removeSec` inst_nb) node_add_nb
1011
  in case (node_add_ab, node_add_nb, node_del_ab, node_del_nb) of
1012
       (Types.OpGood a_ab, Types.OpGood a_nb,
1013
        Types.OpGood d_ab, Types.OpGood d_nb) ->
1014
         printTestCase "Consistency checks failed" $
1015
           Node.rMem a_ab >  orig_rmem &&
1016
           Node.rMem a_ab - orig_rmem == Instance.mem inst_ab &&
1017
           Node.rMem a_nb == orig_rmem &&
1018
           Node.rMem d_ab == orig_rmem &&
1019
           Node.rMem d_nb == orig_rmem &&
1020
           -- this is not related to rMem, but as good a place to
1021
           -- test as any
1022
           inst_idx `elem` Node.sList a_ab &&
1023
           inst_idx `notElem` Node.sList d_ab
1024
       x -> failTest $ "Failed to add/remove instances: " ++ show x
1025

    
1026
-- | Check mdsk setting.
1027
prop_Node_setMdsk node mx =
1028
  Node.loDsk node' >= 0 &&
1029
  fromIntegral (Node.loDsk node') <= Node.tDsk node &&
1030
  Node.availDisk node' >= 0 &&
1031
  Node.availDisk node' <= Node.fDsk node' &&
1032
  fromIntegral (Node.availDisk node') <= Node.tDsk node' &&
1033
  Node.mDsk node' == mx'
1034
    where _types = (node::Node.Node, mx::SmallRatio)
1035
          node' = Node.setMdsk node mx'
1036
          SmallRatio mx' = mx
1037

    
1038
-- Check tag maps
1039
prop_Node_tagMaps_idempotent tags =
1040
  Node.delTags (Node.addTags m tags) tags ==? m
1041
    where m = Data.Map.empty
1042

    
1043
prop_Node_tagMaps_reject tags =
1044
  not (null tags) ==>
1045
  all (\t -> Node.rejectAddTags m [t]) tags
1046
    where m = Node.addTags Data.Map.empty tags
1047

    
1048
prop_Node_showField node =
1049
  forAll (elements Node.defaultFields) $ \ field ->
1050
  fst (Node.showHeader field) /= Types.unknownField &&
1051
  Node.showField node field /= Types.unknownField
1052

    
1053
prop_Node_computeGroups nodes =
1054
  let ng = Node.computeGroups nodes
1055
      onlyuuid = map fst ng
1056
  in length nodes == sum (map (length . snd) ng) &&
1057
     all (\(guuid, ns) -> all ((== guuid) . Node.group) ns) ng &&
1058
     length (nub onlyuuid) == length onlyuuid &&
1059
     (null nodes || not (null ng))
1060

    
1061
testSuite "Node"
1062
            [ 'prop_Node_setAlias
1063
            , 'prop_Node_setOffline
1064
            , 'prop_Node_setMcpu
1065
            , 'prop_Node_setXmem
1066
            , 'prop_Node_addPriFM
1067
            , 'prop_Node_addPriFD
1068
            , 'prop_Node_addPriFC
1069
            , 'prop_Node_addSec
1070
            , 'prop_Node_addPriOffline
1071
            , 'prop_Node_addSecOffline
1072
            , 'prop_Node_rMem
1073
            , 'prop_Node_setMdsk
1074
            , 'prop_Node_tagMaps_idempotent
1075
            , 'prop_Node_tagMaps_reject
1076
            , 'prop_Node_showField
1077
            , 'prop_Node_computeGroups
1078
            ]
1079

    
1080
-- ** Cluster tests
1081

    
1082
-- | Check that the cluster score is close to zero for a homogeneous
1083
-- cluster.
1084
prop_Score_Zero node =
1085
  forAll (choose (1, 1024)) $ \count ->
1086
    (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
1087
     (Node.tDsk node > 0) && (Node.tMem node > 0)) ==>
1088
  let fn = Node.buildPeers node Container.empty
1089
      nlst = replicate count fn
1090
      score = Cluster.compCVNodes nlst
1091
  -- we can't say == 0 here as the floating point errors accumulate;
1092
  -- this should be much lower than the default score in CLI.hs
1093
  in score <= 1e-12
1094

    
1095
-- | Check that cluster stats are sane.
1096
prop_CStats_sane =
1097
  forAll (choose (1, 1024)) $ \count ->
1098
  forAll genOnlineNode $ \node ->
1099
  let fn = Node.buildPeers node Container.empty
1100
      nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
1101
      nl = Container.fromList nlst
1102
      cstats = Cluster.totalResources nl
1103
  in Cluster.csAdsk cstats >= 0 &&
1104
     Cluster.csAdsk cstats <= Cluster.csFdsk cstats
1105

    
1106
-- | Check that one instance is allocated correctly, without
1107
-- rebalances needed.
1108
prop_ClusterAlloc_sane inst =
1109
  forAll (choose (5, 20)) $ \count ->
1110
  forAll genOnlineNode $ \node ->
1111
  let (nl, il, inst') = makeSmallEmptyCluster node count inst
1112
  in case Cluster.genAllocNodes defGroupList nl 2 True >>=
1113
     Cluster.tryAlloc nl il inst' of
1114
       Types.Bad _ -> False
1115
       Types.Ok as ->
1116
         case Cluster.asSolution as of
1117
           Nothing -> False
1118
           Just (xnl, xi, _, cv) ->
1119
             let il' = Container.add (Instance.idx xi) xi il
1120
                 tbl = Cluster.Table xnl il' cv []
1121
             in not (canBalance tbl True True False)
1122

    
1123
-- | Checks that on a 2-5 node cluster, we can allocate a random
1124
-- instance spec via tiered allocation (whatever the original instance
1125
-- spec), on either one or two nodes.
1126
prop_ClusterCanTieredAlloc inst =
1127
  forAll (choose (2, 5)) $ \count ->
1128
  forAll (choose (1, 2)) $ \rqnodes ->
1129
  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1130
  let nl = makeSmallCluster node count
1131
      il = Container.empty
1132
      allocnodes = Cluster.genAllocNodes defGroupList nl rqnodes True
1133
  in case allocnodes >>= \allocnodes' ->
1134
    Cluster.tieredAlloc nl il (Just 1) inst allocnodes' [] [] of
1135
       Types.Bad _ -> False
1136
       Types.Ok (_, _, il', ixes, cstats) -> not (null ixes) &&
1137
                                             IntMap.size il' == length ixes &&
1138
                                             length ixes == length cstats
1139

    
1140
-- | Helper function to create a cluster with the given range of nodes
1141
-- and allocate an instance on it.
1142
genClusterAlloc count node inst =
1143
  let nl = makeSmallCluster node count
1144
  in case Cluster.genAllocNodes defGroupList nl 2 True >>=
1145
     Cluster.tryAlloc nl Container.empty inst of
1146
       Types.Bad _ -> Types.Bad "Can't allocate"
1147
       Types.Ok as ->
1148
         case Cluster.asSolution as of
1149
           Nothing -> Types.Bad "Empty solution?"
1150
           Just (xnl, xi, _, _) ->
1151
             let xil = Container.add (Instance.idx xi) xi Container.empty
1152
             in Types.Ok (xnl, xil, xi)
1153

    
1154
-- | Checks that on a 4-8 node cluster, once we allocate an instance,
1155
-- we can also relocate it.
1156
prop_ClusterAllocRelocate =
1157
  forAll (choose (4, 8)) $ \count ->
1158
  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1159
  forAll (genInstanceSmallerThanNode node) $ \inst ->
1160
  case genClusterAlloc count node inst of
1161
    Types.Bad msg -> failTest msg
1162
    Types.Ok (nl, il, inst') ->
1163
      case IAlloc.processRelocate defGroupList nl il
1164
             (Instance.idx inst) 1 [Instance.sNode inst'] of
1165
        Types.Ok _ -> printTestCase "??" True  -- huh, how to make
1166
                                               -- this nicer...
1167
        Types.Bad msg -> failTest $ "Failed to relocate: " ++ msg
1168

    
1169
-- | Helper property checker for the result of a nodeEvac or
1170
-- changeGroup operation.
1171
check_EvacMode grp inst result =
1172
  case result of
1173
    Types.Bad msg -> failTest $ "Couldn't evacuate/change group:" ++ msg
1174
    Types.Ok (_, _, es) ->
1175
      let moved = Cluster.esMoved es
1176
          failed = Cluster.esFailed es
1177
          opcodes = not . null $ Cluster.esOpCodes es
1178
      in failmsg ("'failed' not empty: " ++ show failed) (null failed) .&&.
1179
         failmsg "'opcodes' is null" opcodes .&&.
1180
         case moved of
1181
           [(idx', gdx, _)] -> failmsg "invalid instance moved" (idx == idx')
1182
                               .&&.
1183
                               failmsg "wrong target group"
1184
                                         (gdx == Group.idx grp)
1185
           v -> failmsg  ("invalid solution: " ++ show v) False
1186
  where failmsg = \msg -> printTestCase ("Failed to evacuate: " ++ msg)
1187
        idx = Instance.idx inst
1188

    
1189
-- | Checks that on a 4-8 node cluster, once we allocate an instance,
1190
-- we can also node-evacuate it.
1191
prop_ClusterAllocEvacuate =
1192
  forAll (choose (4, 8)) $ \count ->
1193
  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1194
  forAll (genInstanceSmallerThanNode node) $ \inst ->
1195
  case genClusterAlloc count node inst of
1196
    Types.Bad msg -> failTest msg
1197
    Types.Ok (nl, il, inst') ->
1198
      conjoin $ map (\mode -> check_EvacMode defGroup inst' $
1199
                              Cluster.tryNodeEvac defGroupList nl il mode
1200
                                [Instance.idx inst']) [minBound..maxBound]
1201

    
1202
-- | Checks that on a 4-8 node cluster with two node groups, once we
1203
-- allocate an instance on the first node group, we can also change
1204
-- its group.
1205
prop_ClusterAllocChangeGroup =
1206
  forAll (choose (4, 8)) $ \count ->
1207
  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1208
  forAll (genInstanceSmallerThanNode node) $ \inst ->
1209
  case genClusterAlloc count node inst of
1210
    Types.Bad msg -> failTest msg
1211
    Types.Ok (nl, il, inst') ->
1212
      -- we need to add a second node group and nodes to the cluster
1213
      let nl2 = Container.elems $ makeSmallCluster node count
1214
          grp2 = Group.setIdx defGroup (Group.idx defGroup + 1)
1215
          maxndx = maximum . map Node.idx $ nl2
1216
          nl3 = map (\n -> n { Node.group = Group.idx grp2
1217
                             , Node.idx = Node.idx n + maxndx }) nl2
1218
          nl4 = Container.fromList . map (\n -> (Node.idx n, n)) $ nl3
1219
          gl' = Container.add (Group.idx grp2) grp2 defGroupList
1220
          nl' = IntMap.union nl nl4
1221
      in check_EvacMode grp2 inst' $
1222
         Cluster.tryChangeGroup gl' nl' il [] [Instance.idx inst']
1223

    
1224
-- | Check that allocating multiple instances on a cluster, then
1225
-- adding an empty node, results in a valid rebalance.
1226
prop_ClusterAllocBalance =
1227
  forAll (genNode (Just 5) (Just 128)) $ \node ->
1228
  forAll (choose (3, 5)) $ \count ->
1229
  not (Node.offline node) && not (Node.failN1 node) ==>
1230
  let nl = makeSmallCluster node count
1231
      (hnode, nl') = IntMap.deleteFindMax nl
1232
      il = Container.empty
1233
      allocnodes = Cluster.genAllocNodes defGroupList nl' 2 True
1234
      i_templ = createInstance Types.unitMem Types.unitDsk Types.unitCpu
1235
  in case allocnodes >>= \allocnodes' ->
1236
    Cluster.iterateAlloc nl' il (Just 5) i_templ allocnodes' [] [] of
1237
       Types.Bad msg -> failTest $ "Failed to allocate: " ++ msg
1238
       Types.Ok (_, _, _, [], _) -> failTest "Failed to allocate: no instances"
1239
       Types.Ok (_, xnl, il', _, _) ->
1240
         let ynl = Container.add (Node.idx hnode) hnode xnl
1241
             cv = Cluster.compCV ynl
1242
             tbl = Cluster.Table ynl il' cv []
1243
         in printTestCase "Failed to rebalance" $
1244
            canBalance tbl True True False
1245

    
1246
-- | Checks consistency.
1247
prop_ClusterCheckConsistency node inst =
1248
  let nl = makeSmallCluster node 3
1249
      [node1, node2, node3] = Container.elems nl
1250
      node3' = node3 { Node.group = 1 }
1251
      nl' = Container.add (Node.idx node3') node3' nl
1252
      inst1 = Instance.setBoth inst (Node.idx node1) (Node.idx node2)
1253
      inst2 = Instance.setBoth inst (Node.idx node1) Node.noSecondary
1254
      inst3 = Instance.setBoth inst (Node.idx node1) (Node.idx node3)
1255
      ccheck = Cluster.findSplitInstances nl' . Container.fromList
1256
  in null (ccheck [(0, inst1)]) &&
1257
     null (ccheck [(0, inst2)]) &&
1258
     (not . null $ ccheck [(0, inst3)])
1259

    
1260
-- | For now, we only test that we don't lose instances during the split.
1261
prop_ClusterSplitCluster node inst =
1262
  forAll (choose (0, 100)) $ \icnt ->
1263
  let nl = makeSmallCluster node 2
1264
      (nl', il') = foldl (\(ns, is) _ -> assignInstance ns is inst 0 1)
1265
                   (nl, Container.empty) [1..icnt]
1266
      gni = Cluster.splitCluster nl' il'
1267
  in sum (map (Container.size . snd . snd) gni) == icnt &&
1268
     all (\(guuid, (nl'', _)) -> all ((== guuid) . Node.group)
1269
                                 (Container.elems nl'')) gni
1270

    
1271
-- | Helper function to check if we can allocate an instance on a
1272
-- given node list.
1273
canAllocOn :: Node.List -> Int -> Instance.Instance -> Bool
1274
canAllocOn nl reqnodes inst =
1275
  case Cluster.genAllocNodes defGroupList nl reqnodes True >>=
1276
       Cluster.tryAlloc nl (Container.empty) inst of
1277
       Types.Bad _ -> False
1278
       Types.Ok as ->
1279
         case Cluster.asSolution as of
1280
           Nothing -> False
1281
           Just _ -> True
1282

    
1283
-- | Checks that allocation obeys minimum and maximum instance
1284
-- policies. The unittest generates a random node, duplicates it count
1285
-- times, and generates a random instance that can be allocated on
1286
-- this mini-cluster; it then checks that after applying a policy that
1287
-- the instance doesn't fits, the allocation fails.
1288
prop_ClusterAllocPolicy node =
1289
  -- rqn is the required nodes (1 or 2)
1290
  forAll (choose (1, 2)) $ \rqn ->
1291
  forAll (choose (5, 20)) $ \count ->
1292
  forAll (arbitrary `suchThat` (canAllocOn (makeSmallCluster node count) rqn))
1293
         $ \inst ->
1294
  forAll (arbitrary `suchThat` (isFailure .
1295
                                Instance.instMatchesPolicy inst)) $ \ipol ->
1296
  let node' = Node.setPolicy ipol node
1297
      nl = makeSmallCluster node' count
1298
  in not $ canAllocOn nl rqn inst
1299

    
1300
testSuite "Cluster"
1301
            [ 'prop_Score_Zero
1302
            , 'prop_CStats_sane
1303
            , 'prop_ClusterAlloc_sane
1304
            , 'prop_ClusterCanTieredAlloc
1305
            , 'prop_ClusterAllocRelocate
1306
            , 'prop_ClusterAllocEvacuate
1307
            , 'prop_ClusterAllocChangeGroup
1308
            , 'prop_ClusterAllocBalance
1309
            , 'prop_ClusterCheckConsistency
1310
            , 'prop_ClusterSplitCluster
1311
            , 'prop_ClusterAllocPolicy
1312
            ]
1313

    
1314
-- ** OpCodes tests
1315

    
1316
-- | Check that opcode serialization is idempotent.
1317
prop_OpCodes_serialization op =
1318
  case J.readJSON (J.showJSON op) of
1319
    J.Error e -> failTest $ "Cannot deserialise: " ++ e
1320
    J.Ok op' -> op ==? op'
1321
  where _types = op::OpCodes.OpCode
1322

    
1323
testSuite "OpCodes"
1324
            [ 'prop_OpCodes_serialization ]
1325

    
1326
-- ** Jobs tests
1327

    
1328
-- | Check that (queued) job\/opcode status serialization is idempotent.
1329
prop_OpStatus_serialization os =
1330
  case J.readJSON (J.showJSON os) of
1331
    J.Error e -> failTest $ "Cannot deserialise: " ++ e
1332
    J.Ok os' -> os ==? os'
1333
  where _types = os::Jobs.OpStatus
1334

    
1335
prop_JobStatus_serialization js =
1336
  case J.readJSON (J.showJSON js) of
1337
    J.Error e -> failTest $ "Cannot deserialise: " ++ e
1338
    J.Ok js' -> js ==? js'
1339
  where _types = js::Jobs.JobStatus
1340

    
1341
testSuite "Jobs"
1342
            [ 'prop_OpStatus_serialization
1343
            , 'prop_JobStatus_serialization
1344
            ]
1345

    
1346
-- ** Loader tests
1347

    
1348
prop_Loader_lookupNode ktn inst node =
1349
  Loader.lookupNode nl inst node ==? Data.Map.lookup node nl
1350
    where nl = Data.Map.fromList ktn
1351

    
1352
prop_Loader_lookupInstance kti inst =
1353
  Loader.lookupInstance il inst ==? Data.Map.lookup inst il
1354
    where il = Data.Map.fromList kti
1355

    
1356
prop_Loader_assignIndices =
1357
  -- generate nodes with unique names
1358
  forAll (arbitrary `suchThat`
1359
          (\nodes ->
1360
             let names = map Node.name nodes
1361
             in length names == length (nub names))) $ \nodes ->
1362
  let (nassoc, kt) =
1363
        Loader.assignIndices (map (\n -> (Node.name n, n)) nodes)
1364
  in Data.Map.size nassoc == length nodes &&
1365
     Container.size kt == length nodes &&
1366
     if not (null nodes)
1367
       then maximum (IntMap.keys kt) == length nodes - 1
1368
       else True
1369

    
1370
-- | Checks that the number of primary instances recorded on the nodes
1371
-- is zero.
1372
prop_Loader_mergeData ns =
1373
  let na = Container.fromList $ map (\n -> (Node.idx n, n)) ns
1374
  in case Loader.mergeData [] [] [] []
1375
         (Loader.emptyCluster {Loader.cdNodes = na}) of
1376
    Types.Bad _ -> False
1377
    Types.Ok (Loader.ClusterData _ nl il _ _) ->
1378
      let nodes = Container.elems nl
1379
          instances = Container.elems il
1380
      in (sum . map (length . Node.pList)) nodes == 0 &&
1381
         null instances
1382

    
1383
-- | Check that compareNameComponent on equal strings works.
1384
prop_Loader_compareNameComponent_equal :: String -> Bool
1385
prop_Loader_compareNameComponent_equal s =
1386
  Loader.compareNameComponent s s ==
1387
    Loader.LookupResult Loader.ExactMatch s
1388

    
1389
-- | Check that compareNameComponent on prefix strings works.
1390
prop_Loader_compareNameComponent_prefix :: NonEmptyList Char -> String -> Bool
1391
prop_Loader_compareNameComponent_prefix (NonEmpty s1) s2 =
1392
  Loader.compareNameComponent (s1 ++ "." ++ s2) s1 ==
1393
    Loader.LookupResult Loader.PartialMatch s1
1394

    
1395
testSuite "Loader"
1396
            [ 'prop_Loader_lookupNode
1397
            , 'prop_Loader_lookupInstance
1398
            , 'prop_Loader_assignIndices
1399
            , 'prop_Loader_mergeData
1400
            , 'prop_Loader_compareNameComponent_equal
1401
            , 'prop_Loader_compareNameComponent_prefix
1402
            ]
1403

    
1404
-- ** Types tests
1405

    
1406
prop_Types_AllocPolicy_serialisation apol =
1407
  case J.readJSON (J.showJSON apol) of
1408
    J.Ok p -> p ==? apol
1409
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1410
      where _types = apol::Types.AllocPolicy
1411

    
1412
prop_Types_DiskTemplate_serialisation dt =
1413
  case J.readJSON (J.showJSON dt) of
1414
    J.Ok p -> p ==? dt
1415
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1416
      where _types = dt::Types.DiskTemplate
1417

    
1418
prop_Types_ISpec_serialisation ispec =
1419
  case J.readJSON (J.showJSON ispec) of
1420
    J.Ok p -> p ==? ispec
1421
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1422
      where _types = ispec::Types.ISpec
1423

    
1424
prop_Types_IPolicy_serialisation ipol =
1425
  case J.readJSON (J.showJSON ipol) of
1426
    J.Ok p -> p ==? ipol
1427
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1428
      where _types = ipol::Types.IPolicy
1429

    
1430
prop_Types_EvacMode_serialisation em =
1431
  case J.readJSON (J.showJSON em) of
1432
    J.Ok p -> p ==? em
1433
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1434
      where _types = em::Types.EvacMode
1435

    
1436
prop_Types_opToResult op =
1437
  case op of
1438
    Types.OpFail _ -> Types.isBad r
1439
    Types.OpGood v -> case r of
1440
                        Types.Bad _ -> False
1441
                        Types.Ok v' -> v == v'
1442
  where r = Types.opToResult op
1443
        _types = op::Types.OpResult Int
1444

    
1445
prop_Types_eitherToResult ei =
1446
  case ei of
1447
    Left _ -> Types.isBad r
1448
    Right v -> case r of
1449
                 Types.Bad _ -> False
1450
                 Types.Ok v' -> v == v'
1451
    where r = Types.eitherToResult ei
1452
          _types = ei::Either String Int
1453

    
1454
testSuite "Types"
1455
            [ 'prop_Types_AllocPolicy_serialisation
1456
            , 'prop_Types_DiskTemplate_serialisation
1457
            , 'prop_Types_ISpec_serialisation
1458
            , 'prop_Types_IPolicy_serialisation
1459
            , 'prop_Types_EvacMode_serialisation
1460
            , 'prop_Types_opToResult
1461
            , 'prop_Types_eitherToResult
1462
            ]