Statistics
| Branch: | Tag: | Revision:

root / htools / Ganeti / HTools / QC.hs @ 59ed268d

History | View | Annotate | Download (52.4 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
  , testOpCodes
36
  , testJobs
37
  , testCluster
38
  , testLoader
39
  , testTypes
40
  ) where
41

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

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

    
77
import Ganeti.HTools.QCHelper (testSuite)
78

    
79
-- * Constants
80

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

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

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

    
93
-- | All disk templates (used later)
94
allDiskTemplates :: [Types.DiskTemplate]
95
allDiskTemplates = [minBound..maxBound]
96

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

    
120

    
121
defGroup :: Group.Group
122
defGroup = flip Group.setIdx 0 $
123
             Group.create "default" Types.defaultGroupID Types.AllocPreferred
124
                  nullIPolicy
125

    
126
defGroupList :: Group.List
127
defGroupList = Container.fromList [(Group.idx defGroup, defGroup)]
128

    
129
defGroupAssoc :: Data.Map.Map String Types.Gdx
130
defGroupAssoc = Data.Map.singleton (Group.uuid defGroup) (Group.idx defGroup)
131

    
132
-- * Helper functions
133

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

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

    
146
-- | Show a message and fail the test.
147
failTest :: String -> Property
148
failTest msg = printTestCase msg False
149

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

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

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

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

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

    
188
canBalance :: Cluster.Table -> Bool -> Bool -> Bool -> Bool
189
canBalance tbl dm im evac = isJust $ Cluster.tryBalance tbl dm im evac 0 0
190

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

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

    
217
-- * Arbitrary instances
218

    
219
-- | Defines a DNS name.
220
newtype DNSChar = DNSChar { dnsGetChar::Char }
221

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

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

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

    
241
-- | Defines a tag type.
242
newtype TagChar = TagChar { tagGetChar :: Char }
243

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

    
249
instance Arbitrary TagChar where
250
  arbitrary = do
251
    c <- elements tagChar
252
    return (TagChar c)
253

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

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

    
273
instance Arbitrary Types.InstanceStatus where
274
    arbitrary = elements [minBound..maxBound]
275

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

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

    
296
-- let's generate a random instance
297
instance Arbitrary Instance.Instance where
298
  arbitrary = genInstanceSmallerThan maxMem maxDsk maxCpu
299

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

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

    
340
-- and a random node
341
instance Arbitrary Node.Node where
342
  arbitrary = genNode Nothing Nothing
343

    
344
-- replace disks
345
instance Arbitrary OpCodes.ReplaceDisksMode where
346
  arbitrary = elements [minBound..maxBound]
347

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

    
369
instance Arbitrary Jobs.OpStatus where
370
  arbitrary = elements [minBound..maxBound]
371

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

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

    
381
instance Arbitrary Types.AllocPolicy where
382
  arbitrary = elements [minBound..maxBound]
383

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

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

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

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

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

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

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

    
441
-- * Actual tests
442

    
443
-- ** Utils tests
444

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

    
452
-- | Split and join should always be idempotent.
453
prop_Utils_commaSplitJoin s =
454
  Utils.commaJoin (Utils.sepSplit ',' s) ==? s
455

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

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

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

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

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

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

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

    
526
-- ** PeerMap tests
527

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

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

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

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

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

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

    
572
-- ** Container tests
573

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

    
584
prop_Container_nameOf node =
585
  let nl = makeSmallCluster node 1
586
      fnode = head (Container.elems nl)
587
  in Container.nameOf nl (Node.idx fnode) ==? Node.name fnode
588

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

    
610
testSuite "Container"
611
            [ 'prop_Container_addTwo
612
            , 'prop_Container_nameOf
613
            , 'prop_Container_findByName
614
            ]
615

    
616
-- ** Instance tests
617

    
618
-- Simple instance tests, we only have setter/getters
619

    
620
prop_Instance_creat inst =
621
  Instance.name inst ==? Instance.alias inst
622

    
623
prop_Instance_setIdx inst idx =
624
  Instance.idx (Instance.setIdx inst idx) ==? idx
625
    where _types = (inst::Instance.Instance, idx::Types.Idx)
626

    
627
prop_Instance_setName inst name =
628
  Instance.name newinst == name &&
629
  Instance.alias newinst == name
630
    where _types = (inst::Instance.Instance, name::String)
631
          newinst = Instance.setName inst name
632

    
633
prop_Instance_setAlias inst name =
634
  Instance.name newinst == Instance.name inst &&
635
  Instance.alias newinst == name
636
    where _types = (inst::Instance.Instance, name::String)
637
          newinst = Instance.setAlias inst name
638

    
639
prop_Instance_setPri inst pdx =
640
  Instance.pNode (Instance.setPri inst pdx) ==? pdx
641
    where _types = (inst::Instance.Instance, pdx::Types.Ndx)
642

    
643
prop_Instance_setSec inst sdx =
644
  Instance.sNode (Instance.setSec inst sdx) ==? sdx
645
    where _types = (inst::Instance.Instance, sdx::Types.Ndx)
646

    
647
prop_Instance_setBoth inst pdx sdx =
648
  Instance.pNode si == pdx && Instance.sNode si == sdx
649
    where _types = (inst::Instance.Instance, pdx::Types.Ndx, sdx::Types.Ndx)
650
          si = Instance.setBoth inst pdx sdx
651

    
652
prop_Instance_shrinkMG inst =
653
  Instance.mem inst >= 2 * Types.unitMem ==>
654
    case Instance.shrinkByType inst Types.FailMem of
655
      Types.Ok inst' -> Instance.mem inst' == Instance.mem inst - Types.unitMem
656
      _ -> False
657

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

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

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

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

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

    
687
prop_Instance_setMovable inst m =
688
  Instance.movable inst' ==? m
689
    where inst' = Instance.setMovable inst m
690

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

    
708
-- ** Text backend tests
709

    
710
-- Instance text loader tests
711

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

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

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

    
789
prop_Text_Load_NodeFail fields =
790
  length fields /= 8 ==> isNothing $ Text.loadNode Data.Map.empty fields
791

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

    
800
prop_Text_ISpecIdempotent ispec =
801
  case Text.loadISpec "dummy" . Utils.sepSplit ',' .
802
       Text.serializeISpec $ ispec of
803
    Types.Bad msg -> failTest $ "Failed to load ispec: " ++ msg
804
    Types.Ok ispec' -> ispec ==? ispec'
805

    
806
prop_Text_IPolicyIdempotent ipol =
807
  case Text.loadIPolicy . Utils.sepSplit '|' $
808
       Text.serializeIPolicy owner ipol of
809
    Types.Bad msg -> failTest $ "Failed to load ispec: " ++ msg
810
    Types.Ok res -> (owner, ipol) ==? res
811
  where owner = "dummy"
812

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

    
848
testSuite "Text"
849
            [ 'prop_Text_Load_Instance
850
            , 'prop_Text_Load_InstanceFail
851
            , 'prop_Text_Load_Node
852
            , 'prop_Text_Load_NodeFail
853
            , 'prop_Text_NodeLSIdempotent
854
            , 'prop_Text_ISpecIdempotent
855
            , 'prop_Text_IPolicyIdempotent
856
            , 'prop_Text_CreateSerialise
857
            ]
858

    
859
-- ** Node tests
860

    
861
prop_Node_setAlias node name =
862
  Node.name newnode == Node.name node &&
863
  Node.alias newnode == name
864
    where _types = (node::Node.Node, name::String)
865
          newnode = Node.setAlias node name
866

    
867
prop_Node_setOffline node status =
868
  Node.offline newnode ==? status
869
    where newnode = Node.setOffline node status
870

    
871
prop_Node_setXmem node xm =
872
  Node.xMem newnode ==? xm
873
    where newnode = Node.setXmem node xm
874

    
875
prop_Node_setMcpu node mc =
876
  Node.mCpu newnode ==? mc
877
    where newnode = Node.setMcpu node mc
878

    
879
-- | Check that an instance add with too high memory or disk will be
880
-- rejected.
881
prop_Node_addPriFM node inst =
882
  Instance.mem inst >= Node.fMem node && not (Node.failN1 node) &&
883
  not (Instance.instanceOffline inst) ==>
884
  case Node.addPri node inst'' of
885
    Types.OpFail Types.FailMem -> True
886
    _ -> False
887
  where _types = (node::Node.Node, inst::Instance.Instance)
888
        inst' = setInstanceSmallerThanNode node inst
889
        inst'' = inst' { Instance.mem = Instance.mem inst }
890

    
891
prop_Node_addPriFD node inst =
892
  Instance.dsk inst >= Node.fDsk node && not (Node.failN1 node) ==>
893
    case Node.addPri node inst'' of
894
      Types.OpFail Types.FailDisk -> True
895
      _ -> False
896
    where _types = (node::Node.Node, inst::Instance.Instance)
897
          inst' = setInstanceSmallerThanNode node inst
898
          inst'' = inst' { Instance.dsk = Instance.dsk inst }
899

    
900
prop_Node_addPriFC (Positive extra) =
901
  forAll genOnlineNode $ \node ->
902
  forAll (arbitrary `suchThat` Instance.instanceNotOffline) $ \inst ->
903
  let inst' = setInstanceSmallerThanNode node inst
904
      inst'' = inst' { Instance.vcpus = Node.availCpu node + extra }
905
  in case Node.addPri node inst'' of
906
       Types.OpFail Types.FailCPU -> property True
907
       v -> failTest $ "Expected OpFail FailCPU, but got " ++ show v
908

    
909
-- | Check that an instance add with too high memory or disk will be
910
-- rejected.
911
prop_Node_addSec node inst pdx =
912
  ((Instance.mem inst >= (Node.fMem node - Node.rMem node) &&
913
    not (Instance.instanceOffline inst)) ||
914
   Instance.dsk inst >= Node.fDsk node) &&
915
  not (Node.failN1 node) ==>
916
      isFailure (Node.addSec node inst pdx)
917
        where _types = (node::Node.Node, inst::Instance.Instance, pdx::Int)
918

    
919
-- | Check that an offline instance with reasonable disk size can always
920
-- be added.
921
prop_Node_addPriOffline =
922
  forAll (arbitrary `suchThat` ((> 0) . Node.fMem)) $ \node ->
923
  forAll (arbitrary `suchThat`
924
          (\ x ->  (Instance.dsk x  < Node.fDsk node) &&
925
                   Instance.instanceOffline x)) $ \inst ->
926
  case Node.addPri node inst of
927
    Types.OpGood _ -> True
928
    _ -> False
929

    
930
prop_Node_addSecOffline pdx =
931
  forAll genOnlineNode $ \node ->
932
  forAll (arbitrary `suchThat`
933
          (\ inst -> Instance.dsk inst  < Node.availDisk node)) $ \inst ->
934
  case Node.addSec node (inst { Instance.runSt = Types.AdminOffline }) pdx of
935
    Types.OpGood _ -> True
936
    _ -> False
937

    
938
-- | Checks for memory reservation changes.
939
prop_Node_rMem inst =
940
  not (Instance.instanceOffline inst) ==>
941
  forAll (arbitrary `suchThat` ((> Types.unitMem) . Node.fMem)) $ \node ->
942
  -- ab = auto_balance, nb = non-auto_balance
943
  -- we use -1 as the primary node of the instance
944
  let inst' = inst { Instance.pNode = -1, Instance.autoBalance = True }
945
      inst_ab = setInstanceSmallerThanNode node inst'
946
      inst_nb = inst_ab { Instance.autoBalance = False }
947
      -- now we have the two instances, identical except the
948
      -- autoBalance attribute
949
      orig_rmem = Node.rMem node
950
      inst_idx = Instance.idx inst_ab
951
      node_add_ab = Node.addSec node inst_ab (-1)
952
      node_add_nb = Node.addSec node inst_nb (-1)
953
      node_del_ab = liftM (`Node.removeSec` inst_ab) node_add_ab
954
      node_del_nb = liftM (`Node.removeSec` inst_nb) node_add_nb
955
  in case (node_add_ab, node_add_nb, node_del_ab, node_del_nb) of
956
       (Types.OpGood a_ab, Types.OpGood a_nb,
957
        Types.OpGood d_ab, Types.OpGood d_nb) ->
958
         printTestCase "Consistency checks failed" $
959
           Node.rMem a_ab >  orig_rmem &&
960
           Node.rMem a_ab - orig_rmem == Instance.mem inst_ab &&
961
           Node.rMem a_nb == orig_rmem &&
962
           Node.rMem d_ab == orig_rmem &&
963
           Node.rMem d_nb == orig_rmem &&
964
           -- this is not related to rMem, but as good a place to
965
           -- test as any
966
           inst_idx `elem` Node.sList a_ab &&
967
           inst_idx `notElem` Node.sList d_ab
968
       x -> failTest $ "Failed to add/remove instances: " ++ show x
969

    
970
-- | Check mdsk setting.
971
prop_Node_setMdsk node mx =
972
  Node.loDsk node' >= 0 &&
973
  fromIntegral (Node.loDsk node') <= Node.tDsk node &&
974
  Node.availDisk node' >= 0 &&
975
  Node.availDisk node' <= Node.fDsk node' &&
976
  fromIntegral (Node.availDisk node') <= Node.tDsk node' &&
977
  Node.mDsk node' == mx'
978
    where _types = (node::Node.Node, mx::SmallRatio)
979
          node' = Node.setMdsk node mx'
980
          SmallRatio mx' = mx
981

    
982
-- Check tag maps
983
prop_Node_tagMaps_idempotent tags =
984
  Node.delTags (Node.addTags m tags) tags ==? m
985
    where m = Data.Map.empty
986

    
987
prop_Node_tagMaps_reject tags =
988
  not (null tags) ==>
989
  all (\t -> Node.rejectAddTags m [t]) tags
990
    where m = Node.addTags Data.Map.empty tags
991

    
992
prop_Node_showField node =
993
  forAll (elements Node.defaultFields) $ \ field ->
994
  fst (Node.showHeader field) /= Types.unknownField &&
995
  Node.showField node field /= Types.unknownField
996

    
997
prop_Node_computeGroups nodes =
998
  let ng = Node.computeGroups nodes
999
      onlyuuid = map fst ng
1000
  in length nodes == sum (map (length . snd) ng) &&
1001
     all (\(guuid, ns) -> all ((== guuid) . Node.group) ns) ng &&
1002
     length (nub onlyuuid) == length onlyuuid &&
1003
     (null nodes || not (null ng))
1004

    
1005
testSuite "Node"
1006
            [ 'prop_Node_setAlias
1007
            , 'prop_Node_setOffline
1008
            , 'prop_Node_setMcpu
1009
            , 'prop_Node_setXmem
1010
            , 'prop_Node_addPriFM
1011
            , 'prop_Node_addPriFD
1012
            , 'prop_Node_addPriFC
1013
            , 'prop_Node_addSec
1014
            , 'prop_Node_addPriOffline
1015
            , 'prop_Node_addSecOffline
1016
            , 'prop_Node_rMem
1017
            , 'prop_Node_setMdsk
1018
            , 'prop_Node_tagMaps_idempotent
1019
            , 'prop_Node_tagMaps_reject
1020
            , 'prop_Node_showField
1021
            , 'prop_Node_computeGroups
1022
            ]
1023

    
1024
-- ** Cluster tests
1025

    
1026
-- | Check that the cluster score is close to zero for a homogeneous
1027
-- cluster.
1028
prop_Score_Zero node =
1029
  forAll (choose (1, 1024)) $ \count ->
1030
    (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
1031
     (Node.tDsk node > 0) && (Node.tMem node > 0)) ==>
1032
  let fn = Node.buildPeers node Container.empty
1033
      nlst = replicate count fn
1034
      score = Cluster.compCVNodes nlst
1035
  -- we can't say == 0 here as the floating point errors accumulate;
1036
  -- this should be much lower than the default score in CLI.hs
1037
  in score <= 1e-12
1038

    
1039
-- | Check that cluster stats are sane.
1040
prop_CStats_sane =
1041
  forAll (choose (1, 1024)) $ \count ->
1042
  forAll genOnlineNode $ \node ->
1043
  let fn = Node.buildPeers node Container.empty
1044
      nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
1045
      nl = Container.fromList nlst
1046
      cstats = Cluster.totalResources nl
1047
  in Cluster.csAdsk cstats >= 0 &&
1048
     Cluster.csAdsk cstats <= Cluster.csFdsk cstats
1049

    
1050
-- | Check that one instance is allocated correctly, without
1051
-- rebalances needed.
1052
prop_ClusterAlloc_sane inst =
1053
  forAll (choose (5, 20)) $ \count ->
1054
  forAll genOnlineNode $ \node ->
1055
  let (nl, il, inst') = makeSmallEmptyCluster node count inst
1056
  in case Cluster.genAllocNodes defGroupList nl 2 True >>=
1057
     Cluster.tryAlloc nl il inst' of
1058
       Types.Bad _ -> False
1059
       Types.Ok as ->
1060
         case Cluster.asSolution as of
1061
           Nothing -> False
1062
           Just (xnl, xi, _, cv) ->
1063
             let il' = Container.add (Instance.idx xi) xi il
1064
                 tbl = Cluster.Table xnl il' cv []
1065
             in not (canBalance tbl True True False)
1066

    
1067
-- | Checks that on a 2-5 node cluster, we can allocate a random
1068
-- instance spec via tiered allocation (whatever the original instance
1069
-- spec), on either one or two nodes.
1070
prop_ClusterCanTieredAlloc inst =
1071
  forAll (choose (2, 5)) $ \count ->
1072
  forAll (choose (1, 2)) $ \rqnodes ->
1073
  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1074
  let nl = makeSmallCluster node count
1075
      il = Container.empty
1076
      allocnodes = Cluster.genAllocNodes defGroupList nl rqnodes True
1077
  in case allocnodes >>= \allocnodes' ->
1078
    Cluster.tieredAlloc nl il (Just 1) inst allocnodes' [] [] of
1079
       Types.Bad _ -> False
1080
       Types.Ok (_, _, il', ixes, cstats) -> not (null ixes) &&
1081
                                             IntMap.size il' == length ixes &&
1082
                                             length ixes == length cstats
1083

    
1084
-- | Helper function to create a cluster with the given range of nodes
1085
-- and allocate an instance on it.
1086
genClusterAlloc count node inst =
1087
  let nl = makeSmallCluster node count
1088
  in case Cluster.genAllocNodes defGroupList nl 2 True >>=
1089
     Cluster.tryAlloc nl Container.empty inst of
1090
       Types.Bad _ -> Types.Bad "Can't allocate"
1091
       Types.Ok as ->
1092
         case Cluster.asSolution as of
1093
           Nothing -> Types.Bad "Empty solution?"
1094
           Just (xnl, xi, _, _) ->
1095
             let xil = Container.add (Instance.idx xi) xi Container.empty
1096
             in Types.Ok (xnl, xil, xi)
1097

    
1098
-- | Checks that on a 4-8 node cluster, once we allocate an instance,
1099
-- we can also relocate it.
1100
prop_ClusterAllocRelocate =
1101
  forAll (choose (4, 8)) $ \count ->
1102
  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1103
  forAll (genInstanceSmallerThanNode node) $ \inst ->
1104
  case genClusterAlloc count node inst of
1105
    Types.Bad msg -> failTest msg
1106
    Types.Ok (nl, il, inst') ->
1107
      case IAlloc.processRelocate defGroupList nl il
1108
             (Instance.idx inst) 1 [Instance.sNode inst'] of
1109
        Types.Ok _ -> printTestCase "??" True  -- huh, how to make
1110
                                               -- this nicer...
1111
        Types.Bad msg -> failTest $ "Failed to relocate: " ++ msg
1112

    
1113
-- | Helper property checker for the result of a nodeEvac or
1114
-- changeGroup operation.
1115
check_EvacMode grp inst result =
1116
  case result of
1117
    Types.Bad msg -> failTest $ "Couldn't evacuate/change group:" ++ msg
1118
    Types.Ok (_, _, es) ->
1119
      let moved = Cluster.esMoved es
1120
          failed = Cluster.esFailed es
1121
          opcodes = not . null $ Cluster.esOpCodes es
1122
      in failmsg ("'failed' not empty: " ++ show failed) (null failed) .&&.
1123
         failmsg "'opcodes' is null" opcodes .&&.
1124
         case moved of
1125
           [(idx', gdx, _)] -> failmsg "invalid instance moved" (idx == idx')
1126
                               .&&.
1127
                               failmsg "wrong target group"
1128
                                         (gdx == Group.idx grp)
1129
           v -> failmsg  ("invalid solution: " ++ show v) False
1130
  where failmsg = \msg -> printTestCase ("Failed to evacuate: " ++ msg)
1131
        idx = Instance.idx inst
1132

    
1133
-- | Checks that on a 4-8 node cluster, once we allocate an instance,
1134
-- we can also node-evacuate it.
1135
prop_ClusterAllocEvacuate =
1136
  forAll (choose (4, 8)) $ \count ->
1137
  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1138
  forAll (genInstanceSmallerThanNode node) $ \inst ->
1139
  case genClusterAlloc count node inst of
1140
    Types.Bad msg -> failTest msg
1141
    Types.Ok (nl, il, inst') ->
1142
      conjoin $ map (\mode -> check_EvacMode defGroup inst' $
1143
                              Cluster.tryNodeEvac defGroupList nl il mode
1144
                                [Instance.idx inst']) [minBound..maxBound]
1145

    
1146
-- | Checks that on a 4-8 node cluster with two node groups, once we
1147
-- allocate an instance on the first node group, we can also change
1148
-- its group.
1149
prop_ClusterAllocChangeGroup =
1150
  forAll (choose (4, 8)) $ \count ->
1151
  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1152
  forAll (genInstanceSmallerThanNode node) $ \inst ->
1153
  case genClusterAlloc count node inst of
1154
    Types.Bad msg -> failTest msg
1155
    Types.Ok (nl, il, inst') ->
1156
      -- we need to add a second node group and nodes to the cluster
1157
      let nl2 = Container.elems $ makeSmallCluster node count
1158
          grp2 = Group.setIdx defGroup (Group.idx defGroup + 1)
1159
          maxndx = maximum . map Node.idx $ nl2
1160
          nl3 = map (\n -> n { Node.group = Group.idx grp2
1161
                             , Node.idx = Node.idx n + maxndx }) nl2
1162
          nl4 = Container.fromList . map (\n -> (Node.idx n, n)) $ nl3
1163
          gl' = Container.add (Group.idx grp2) grp2 defGroupList
1164
          nl' = IntMap.union nl nl4
1165
      in check_EvacMode grp2 inst' $
1166
         Cluster.tryChangeGroup gl' nl' il [] [Instance.idx inst']
1167

    
1168
-- | Check that allocating multiple instances on a cluster, then
1169
-- adding an empty node, results in a valid rebalance.
1170
prop_ClusterAllocBalance =
1171
  forAll (genNode (Just 5) (Just 128)) $ \node ->
1172
  forAll (choose (3, 5)) $ \count ->
1173
  not (Node.offline node) && not (Node.failN1 node) ==>
1174
  let nl = makeSmallCluster node count
1175
      (hnode, nl') = IntMap.deleteFindMax nl
1176
      il = Container.empty
1177
      allocnodes = Cluster.genAllocNodes defGroupList nl' 2 True
1178
      i_templ = createInstance Types.unitMem Types.unitDsk Types.unitCpu
1179
  in case allocnodes >>= \allocnodes' ->
1180
    Cluster.iterateAlloc nl' il (Just 5) i_templ allocnodes' [] [] of
1181
       Types.Bad msg -> failTest $ "Failed to allocate: " ++ msg
1182
       Types.Ok (_, _, _, [], _) -> failTest "Failed to allocate: no instances"
1183
       Types.Ok (_, xnl, il', _, _) ->
1184
         let ynl = Container.add (Node.idx hnode) hnode xnl
1185
             cv = Cluster.compCV ynl
1186
             tbl = Cluster.Table ynl il' cv []
1187
         in printTestCase "Failed to rebalance" $
1188
            canBalance tbl True True False
1189

    
1190
-- | Checks consistency.
1191
prop_ClusterCheckConsistency node inst =
1192
  let nl = makeSmallCluster node 3
1193
      [node1, node2, node3] = Container.elems nl
1194
      node3' = node3 { Node.group = 1 }
1195
      nl' = Container.add (Node.idx node3') node3' nl
1196
      inst1 = Instance.setBoth inst (Node.idx node1) (Node.idx node2)
1197
      inst2 = Instance.setBoth inst (Node.idx node1) Node.noSecondary
1198
      inst3 = Instance.setBoth inst (Node.idx node1) (Node.idx node3)
1199
      ccheck = Cluster.findSplitInstances nl' . Container.fromList
1200
  in null (ccheck [(0, inst1)]) &&
1201
     null (ccheck [(0, inst2)]) &&
1202
     (not . null $ ccheck [(0, inst3)])
1203

    
1204
-- | For now, we only test that we don't lose instances during the split.
1205
prop_ClusterSplitCluster node inst =
1206
  forAll (choose (0, 100)) $ \icnt ->
1207
  let nl = makeSmallCluster node 2
1208
      (nl', il') = foldl (\(ns, is) _ -> assignInstance ns is inst 0 1)
1209
                   (nl, Container.empty) [1..icnt]
1210
      gni = Cluster.splitCluster nl' il'
1211
  in sum (map (Container.size . snd . snd) gni) == icnt &&
1212
     all (\(guuid, (nl'', _)) -> all ((== guuid) . Node.group)
1213
                                 (Container.elems nl'')) gni
1214

    
1215
-- | Helper function to check if we can allocate an instance on a
1216
-- given node list.
1217
canAllocOn :: Node.List -> Int -> Instance.Instance -> Bool
1218
canAllocOn nl reqnodes inst =
1219
  case Cluster.genAllocNodes defGroupList nl reqnodes True >>=
1220
       Cluster.tryAlloc nl (Container.empty) inst of
1221
       Types.Bad _ -> False
1222
       Types.Ok as ->
1223
         case Cluster.asSolution as of
1224
           Nothing -> False
1225
           Just _ -> True
1226

    
1227
-- | Checks that allocation obeys minimum and maximum instance
1228
-- policies. The unittest generates a random node, duplicates it count
1229
-- times, and generates a random instance that can be allocated on
1230
-- this mini-cluster; it then checks that after applying a policy that
1231
-- the instance doesn't fits, the allocation fails.
1232
prop_ClusterAllocPolicy node =
1233
  -- rqn is the required nodes (1 or 2)
1234
  forAll (choose (1, 2)) $ \rqn ->
1235
  forAll (choose (5, 20)) $ \count ->
1236
  forAll (arbitrary `suchThat` (canAllocOn (makeSmallCluster node count) rqn))
1237
         $ \inst ->
1238
  forAll (arbitrary `suchThat` (isFailure .
1239
                                Instance.instMatchesPolicy inst)) $ \ipol ->
1240
  let node' = Node.setPolicy ipol node
1241
      nl = makeSmallCluster node' count
1242
  in not $ canAllocOn nl rqn inst
1243

    
1244
testSuite "Cluster"
1245
            [ 'prop_Score_Zero
1246
            , 'prop_CStats_sane
1247
            , 'prop_ClusterAlloc_sane
1248
            , 'prop_ClusterCanTieredAlloc
1249
            , 'prop_ClusterAllocRelocate
1250
            , 'prop_ClusterAllocEvacuate
1251
            , 'prop_ClusterAllocChangeGroup
1252
            , 'prop_ClusterAllocBalance
1253
            , 'prop_ClusterCheckConsistency
1254
            , 'prop_ClusterSplitCluster
1255
            , 'prop_ClusterAllocPolicy
1256
            ]
1257

    
1258
-- ** OpCodes tests
1259

    
1260
-- | Check that opcode serialization is idempotent.
1261
prop_OpCodes_serialization op =
1262
  case J.readJSON (J.showJSON op) of
1263
    J.Error e -> failTest $ "Cannot deserialise: " ++ e
1264
    J.Ok op' -> op ==? op'
1265
  where _types = op::OpCodes.OpCode
1266

    
1267
testSuite "OpCodes"
1268
            [ 'prop_OpCodes_serialization ]
1269

    
1270
-- ** Jobs tests
1271

    
1272
-- | Check that (queued) job\/opcode status serialization is idempotent.
1273
prop_OpStatus_serialization os =
1274
  case J.readJSON (J.showJSON os) of
1275
    J.Error e -> failTest $ "Cannot deserialise: " ++ e
1276
    J.Ok os' -> os ==? os'
1277
  where _types = os::Jobs.OpStatus
1278

    
1279
prop_JobStatus_serialization js =
1280
  case J.readJSON (J.showJSON js) of
1281
    J.Error e -> failTest $ "Cannot deserialise: " ++ e
1282
    J.Ok js' -> js ==? js'
1283
  where _types = js::Jobs.JobStatus
1284

    
1285
testSuite "Jobs"
1286
            [ 'prop_OpStatus_serialization
1287
            , 'prop_JobStatus_serialization
1288
            ]
1289

    
1290
-- ** Loader tests
1291

    
1292
prop_Loader_lookupNode ktn inst node =
1293
  Loader.lookupNode nl inst node ==? Data.Map.lookup node nl
1294
    where nl = Data.Map.fromList ktn
1295

    
1296
prop_Loader_lookupInstance kti inst =
1297
  Loader.lookupInstance il inst ==? Data.Map.lookup inst il
1298
    where il = Data.Map.fromList kti
1299

    
1300
prop_Loader_assignIndices =
1301
  -- generate nodes with unique names
1302
  forAll (arbitrary `suchThat`
1303
          (\nodes ->
1304
             let names = map Node.name nodes
1305
             in length names == length (nub names))) $ \nodes ->
1306
  let (nassoc, kt) =
1307
        Loader.assignIndices (map (\n -> (Node.name n, n)) nodes)
1308
  in Data.Map.size nassoc == length nodes &&
1309
     Container.size kt == length nodes &&
1310
     if not (null nodes)
1311
       then maximum (IntMap.keys kt) == length nodes - 1
1312
       else True
1313

    
1314
-- | Checks that the number of primary instances recorded on the nodes
1315
-- is zero.
1316
prop_Loader_mergeData ns =
1317
  let na = Container.fromList $ map (\n -> (Node.idx n, n)) ns
1318
  in case Loader.mergeData [] [] [] []
1319
         (Loader.emptyCluster {Loader.cdNodes = na}) of
1320
    Types.Bad _ -> False
1321
    Types.Ok (Loader.ClusterData _ nl il _ _) ->
1322
      let nodes = Container.elems nl
1323
          instances = Container.elems il
1324
      in (sum . map (length . Node.pList)) nodes == 0 &&
1325
         null instances
1326

    
1327
-- | Check that compareNameComponent on equal strings works.
1328
prop_Loader_compareNameComponent_equal :: String -> Bool
1329
prop_Loader_compareNameComponent_equal s =
1330
  Loader.compareNameComponent s s ==
1331
    Loader.LookupResult Loader.ExactMatch s
1332

    
1333
-- | Check that compareNameComponent on prefix strings works.
1334
prop_Loader_compareNameComponent_prefix :: NonEmptyList Char -> String -> Bool
1335
prop_Loader_compareNameComponent_prefix (NonEmpty s1) s2 =
1336
  Loader.compareNameComponent (s1 ++ "." ++ s2) s1 ==
1337
    Loader.LookupResult Loader.PartialMatch s1
1338

    
1339
testSuite "Loader"
1340
            [ 'prop_Loader_lookupNode
1341
            , 'prop_Loader_lookupInstance
1342
            , 'prop_Loader_assignIndices
1343
            , 'prop_Loader_mergeData
1344
            , 'prop_Loader_compareNameComponent_equal
1345
            , 'prop_Loader_compareNameComponent_prefix
1346
            ]
1347

    
1348
-- ** Types tests
1349

    
1350
prop_Types_AllocPolicy_serialisation apol =
1351
  case J.readJSON (J.showJSON apol) of
1352
    J.Ok p -> p ==? apol
1353
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1354
      where _types = apol::Types.AllocPolicy
1355

    
1356
prop_Types_DiskTemplate_serialisation dt =
1357
  case J.readJSON (J.showJSON dt) of
1358
    J.Ok p -> p ==? dt
1359
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1360
      where _types = dt::Types.DiskTemplate
1361

    
1362
prop_Types_ISpec_serialisation ispec =
1363
  case J.readJSON (J.showJSON ispec) of
1364
    J.Ok p -> p ==? ispec
1365
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1366
      where _types = ispec::Types.ISpec
1367

    
1368
prop_Types_IPolicy_serialisation ipol =
1369
  case J.readJSON (J.showJSON ipol) of
1370
    J.Ok p -> p ==? ipol
1371
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1372
      where _types = ipol::Types.IPolicy
1373

    
1374
prop_Types_EvacMode_serialisation em =
1375
  case J.readJSON (J.showJSON em) of
1376
    J.Ok p -> p ==? em
1377
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1378
      where _types = em::Types.EvacMode
1379

    
1380
prop_Types_opToResult op =
1381
  case op of
1382
    Types.OpFail _ -> Types.isBad r
1383
    Types.OpGood v -> case r of
1384
                        Types.Bad _ -> False
1385
                        Types.Ok v' -> v == v'
1386
  where r = Types.opToResult op
1387
        _types = op::Types.OpResult Int
1388

    
1389
prop_Types_eitherToResult ei =
1390
  case ei of
1391
    Left _ -> Types.isBad r
1392
    Right v -> case r of
1393
                 Types.Bad _ -> False
1394
                 Types.Ok v' -> v == v'
1395
    where r = Types.eitherToResult ei
1396
          _types = ei::Either String Int
1397

    
1398
testSuite "Types"
1399
            [ 'prop_Types_AllocPolicy_serialisation
1400
            , 'prop_Types_DiskTemplate_serialisation
1401
            , 'prop_Types_ISpec_serialisation
1402
            , 'prop_Types_IPolicy_serialisation
1403
            , 'prop_Types_EvacMode_serialisation
1404
            , 'prop_Types_opToResult
1405
            , 'prop_Types_eitherToResult
1406
            ]