d4239966dd73c8b680552d16c5c02c087eb8230b
[ganeti-local] / htools / Ganeti / HTools / QC.hs
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   , testCLI
42   ) where
43
44 import Test.QuickCheck
45 import Text.Printf (printf)
46 import Data.List (findIndex, intercalate, nub, isPrefixOf)
47 import qualified Data.Set as Set
48 import Data.Maybe
49 import Control.Monad
50 import qualified System.Console.GetOpt as GetOpt
51 import qualified Text.JSON as J
52 import qualified Data.Map
53 import qualified Data.IntMap as IntMap
54
55 import qualified Ganeti.OpCodes as OpCodes
56 import qualified Ganeti.Jobs as Jobs
57 import qualified Ganeti.Luxi
58 import qualified Ganeti.HTools.CLI as CLI
59 import qualified Ganeti.HTools.Cluster as Cluster
60 import qualified Ganeti.HTools.Container as Container
61 import qualified Ganeti.HTools.ExtLoader
62 import qualified Ganeti.HTools.IAlloc as IAlloc
63 import qualified Ganeti.HTools.Instance as Instance
64 import qualified Ganeti.HTools.JSON as JSON
65 import qualified Ganeti.HTools.Loader as Loader
66 import qualified Ganeti.HTools.Luxi
67 import qualified Ganeti.HTools.Node as Node
68 import qualified Ganeti.HTools.Group as Group
69 import qualified Ganeti.HTools.PeerMap as PeerMap
70 import qualified Ganeti.HTools.Rapi
71 import qualified Ganeti.HTools.Simu as Simu
72 import qualified Ganeti.HTools.Text as Text
73 import qualified Ganeti.HTools.Types as Types
74 import qualified Ganeti.HTools.Utils as Utils
75 import qualified Ganeti.HTools.Version
76 import qualified Ganeti.Constants as C
77
78 import qualified Ganeti.HTools.Program as Program
79 import qualified Ganeti.HTools.Program.Hail
80 import qualified Ganeti.HTools.Program.Hbal
81 import qualified Ganeti.HTools.Program.Hscan
82 import qualified Ganeti.HTools.Program.Hspace
83
84 import Ganeti.HTools.QCHelper (testSuite)
85
86 -- * Constants
87
88 -- | Maximum memory (1TiB, somewhat random value).
89 maxMem :: Int
90 maxMem = 1024 * 1024
91
92 -- | Maximum disk (8TiB, somewhat random value).
93 maxDsk :: Int
94 maxDsk = 1024 * 1024 * 8
95
96 -- | Max CPUs (1024, somewhat random value).
97 maxCpu :: Int
98 maxCpu = 1024
99
100 -- | All disk templates (used later)
101 allDiskTemplates :: [Types.DiskTemplate]
102 allDiskTemplates = [minBound..maxBound]
103
104 -- | Null iPolicy, and by null we mean very liberal.
105 nullIPolicy = Types.IPolicy
106   { Types.iPolicyMinSpec = Types.ISpec { Types.iSpecMemorySize = 0
107                                        , Types.iSpecCpuCount   = 0
108                                        , Types.iSpecDiskSize   = 0
109                                        , Types.iSpecDiskCount  = 0
110                                        , Types.iSpecNicCount   = 0
111                                        }
112   , Types.iPolicyMaxSpec = Types.ISpec { Types.iSpecMemorySize = maxBound
113                                        , Types.iSpecCpuCount   = maxBound
114                                        , Types.iSpecDiskSize   = maxBound
115                                        , Types.iSpecDiskCount  = C.maxDisks
116                                        , Types.iSpecNicCount   = C.maxNics
117                                        }
118   , Types.iPolicyStdSpec = Types.ISpec { Types.iSpecMemorySize = Types.unitMem
119                                        , Types.iSpecCpuCount   = Types.unitCpu
120                                        , Types.iSpecDiskSize   = Types.unitDsk
121                                        , Types.iSpecDiskCount  = 1
122                                        , Types.iSpecNicCount   = 1
123                                        }
124   , Types.iPolicyDiskTemplates = [Types.DTDrbd8, Types.DTPlain]
125   , Types.iPolicyVcpuRatio = 1024 -- somewhat random value, high
126                                   -- enough to not impact us
127   }
128
129
130 defGroup :: Group.Group
131 defGroup = flip Group.setIdx 0 $
132              Group.create "default" Types.defaultGroupID Types.AllocPreferred
133                   nullIPolicy
134
135 defGroupList :: Group.List
136 defGroupList = Container.fromList [(Group.idx defGroup, defGroup)]
137
138 defGroupAssoc :: Data.Map.Map String Types.Gdx
139 defGroupAssoc = Data.Map.singleton (Group.uuid defGroup) (Group.idx defGroup)
140
141 -- * Helper functions
142
143 -- | Simple checker for whether OpResult is fail or pass.
144 isFailure :: Types.OpResult a -> Bool
145 isFailure (Types.OpFail _) = True
146 isFailure _ = False
147
148 -- | Checks for equality with proper annotation.
149 (==?) :: (Show a, Eq a) => a -> a -> Property
150 (==?) x y = printTestCase
151             ("Expected equality, but '" ++
152              show x ++ "' /= '" ++ show y ++ "'") (x == y)
153 infix 3 ==?
154
155 -- | Show a message and fail the test.
156 failTest :: String -> Property
157 failTest msg = printTestCase msg False
158
159 -- | Update an instance to be smaller than a node.
160 setInstanceSmallerThanNode node inst =
161   inst { Instance.mem = Node.availMem node `div` 2
162        , Instance.dsk = Node.availDisk node `div` 2
163        , Instance.vcpus = Node.availCpu node `div` 2
164        }
165
166 -- | Create an instance given its spec.
167 createInstance mem dsk vcpus =
168   Instance.create "inst-unnamed" mem dsk vcpus Types.Running [] True (-1) (-1)
169     Types.DTDrbd8
170
171 -- | Create a small cluster by repeating a node spec.
172 makeSmallCluster :: Node.Node -> Int -> Node.List
173 makeSmallCluster node count =
174   let origname = Node.name node
175       origalias = Node.alias node
176       nodes = map (\idx -> node { Node.name = origname ++ "-" ++ show idx
177                                 , Node.alias = origalias ++ "-" ++ show idx })
178               [1..count]
179       fn = flip Node.buildPeers Container.empty
180       namelst = map (\n -> (Node.name n, fn n)) nodes
181       (_, nlst) = Loader.assignIndices namelst
182   in nlst
183
184 -- | Make a small cluster, both nodes and instances.
185 makeSmallEmptyCluster :: Node.Node -> Int -> Instance.Instance
186                       -> (Node.List, Instance.List, Instance.Instance)
187 makeSmallEmptyCluster node count inst =
188   (makeSmallCluster node count, Container.empty,
189    setInstanceSmallerThanNode node inst)
190
191 -- | Checks if a node is "big" enough.
192 isNodeBig :: Int -> Node.Node -> Bool
193 isNodeBig size node = Node.availDisk node > size * Types.unitDsk
194                       && Node.availMem node > size * Types.unitMem
195                       && Node.availCpu node > size * Types.unitCpu
196
197 canBalance :: Cluster.Table -> Bool -> Bool -> Bool -> Bool
198 canBalance tbl dm im evac = isJust $ Cluster.tryBalance tbl dm im evac 0 0
199
200 -- | Assigns a new fresh instance to a cluster; this is not
201 -- allocation, so no resource checks are done.
202 assignInstance :: Node.List -> Instance.List -> Instance.Instance ->
203                   Types.Idx -> Types.Idx ->
204                   (Node.List, Instance.List)
205 assignInstance nl il inst pdx sdx =
206   let pnode = Container.find pdx nl
207       snode = Container.find sdx nl
208       maxiidx = if Container.null il
209                   then 0
210                   else fst (Container.findMax il) + 1
211       inst' = inst { Instance.idx = maxiidx,
212                      Instance.pNode = pdx, Instance.sNode = sdx }
213       pnode' = Node.setPri pnode inst'
214       snode' = Node.setSec snode inst'
215       nl' = Container.addTwo pdx pnode' sdx snode' nl
216       il' = Container.add maxiidx inst' il
217   in (nl', il')
218
219 -- | Generates a list of a given size with non-duplicate elements.
220 genUniquesList :: (Eq a, Arbitrary a) => Int -> Gen [a]
221 genUniquesList cnt =
222   foldM (\lst _ -> do
223            newelem <- arbitrary `suchThat` (`notElem` lst)
224            return (newelem:lst)) [] [1..cnt]
225
226 -- * Arbitrary instances
227
228 -- | Defines a DNS name.
229 newtype DNSChar = DNSChar { dnsGetChar::Char }
230
231 instance Arbitrary DNSChar where
232   arbitrary = do
233     x <- elements (['a'..'z'] ++ ['0'..'9'] ++ "_-")
234     return (DNSChar x)
235
236 -- | Generates a single name component.
237 getName :: Gen String
238 getName = do
239   n <- choose (1, 64)
240   dn <- vector n::Gen [DNSChar]
241   return (map dnsGetChar dn)
242
243 -- | Generates an entire FQDN.
244 getFQDN :: Gen String
245 getFQDN = do
246   ncomps <- choose (1, 4)
247   names <- mapM (const getName) [1..ncomps::Int]
248   return $ intercalate "." names
249
250 -- | Defines a tag type.
251 newtype TagChar = TagChar { tagGetChar :: Char }
252
253 -- | All valid tag chars. This doesn't need to match _exactly_
254 -- Ganeti's own tag regex, just enough for it to be close.
255 tagChar :: [Char]
256 tagChar = ['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9'] ++ ".+*/:@-"
257
258 instance Arbitrary TagChar where
259   arbitrary = do
260     c <- elements tagChar
261     return (TagChar c)
262
263 -- | Generates a tag
264 genTag :: Gen [TagChar]
265 genTag = do
266   -- the correct value would be C.maxTagLen, but that's way too
267   -- verbose in unittests, and at the moment I don't see any possible
268   -- bugs with longer tags and the way we use tags in htools
269   n <- choose (1, 10)
270   vector n
271
272 -- | Generates a list of tags (correctly upper bounded).
273 genTags :: Gen [String]
274 genTags = do
275   -- the correct value would be C.maxTagsPerObj, but per the comment
276   -- in genTag, we don't use tags enough in htools to warrant testing
277   -- such big values
278   n <- choose (0, 10::Int)
279   tags <- mapM (const genTag) [1..n]
280   return $ map (map tagGetChar) tags
281
282 instance Arbitrary Types.InstanceStatus where
283     arbitrary = elements [minBound..maxBound]
284
285 -- | Generates a random instance with maximum disk/mem/cpu values.
286 genInstanceSmallerThan :: Int -> Int -> Int -> Gen Instance.Instance
287 genInstanceSmallerThan lim_mem lim_dsk lim_cpu = do
288   name <- getFQDN
289   mem <- choose (0, lim_mem)
290   dsk <- choose (0, lim_dsk)
291   run_st <- arbitrary
292   pn <- arbitrary
293   sn <- arbitrary
294   vcpus <- choose (0, lim_cpu)
295   return $ Instance.create name mem dsk vcpus run_st [] True pn sn
296          Types.DTDrbd8
297
298 -- | Generates an instance smaller than a node.
299 genInstanceSmallerThanNode :: Node.Node -> Gen Instance.Instance
300 genInstanceSmallerThanNode node =
301   genInstanceSmallerThan (Node.availMem node `div` 2)
302                          (Node.availDisk node `div` 2)
303                          (Node.availCpu node `div` 2)
304
305 -- let's generate a random instance
306 instance Arbitrary Instance.Instance where
307   arbitrary = genInstanceSmallerThan maxMem maxDsk maxCpu
308
309 -- | Generas an arbitrary node based on sizing information.
310 genNode :: Maybe Int -- ^ Minimum node size in terms of units
311         -> Maybe Int -- ^ Maximum node size (when Nothing, bounded
312                      -- just by the max... constants)
313         -> Gen Node.Node
314 genNode min_multiplier max_multiplier = do
315   let (base_mem, base_dsk, base_cpu) =
316         case min_multiplier of
317           Just mm -> (mm * Types.unitMem,
318                       mm * Types.unitDsk,
319                       mm * Types.unitCpu)
320           Nothing -> (0, 0, 0)
321       (top_mem, top_dsk, top_cpu)  =
322         case max_multiplier of
323           Just mm -> (mm * Types.unitMem,
324                       mm * Types.unitDsk,
325                       mm * Types.unitCpu)
326           Nothing -> (maxMem, maxDsk, maxCpu)
327   name  <- getFQDN
328   mem_t <- choose (base_mem, top_mem)
329   mem_f <- choose (base_mem, mem_t)
330   mem_n <- choose (0, mem_t - mem_f)
331   dsk_t <- choose (base_dsk, top_dsk)
332   dsk_f <- choose (base_dsk, dsk_t)
333   cpu_t <- choose (base_cpu, top_cpu)
334   offl  <- arbitrary
335   let n = Node.create name (fromIntegral mem_t) mem_n mem_f
336           (fromIntegral dsk_t) dsk_f (fromIntegral cpu_t) offl 0
337       n' = Node.setPolicy nullIPolicy n
338   return $ Node.buildPeers n' Container.empty
339
340 -- | Helper function to generate a sane node.
341 genOnlineNode :: Gen Node.Node
342 genOnlineNode = do
343   arbitrary `suchThat` (\n -> not (Node.offline n) &&
344                               not (Node.failN1 n) &&
345                               Node.availDisk n > 0 &&
346                               Node.availMem n > 0 &&
347                               Node.availCpu n > 0)
348
349 -- and a random node
350 instance Arbitrary Node.Node where
351   arbitrary = genNode Nothing Nothing
352
353 -- replace disks
354 instance Arbitrary OpCodes.ReplaceDisksMode where
355   arbitrary = elements [minBound..maxBound]
356
357 instance Arbitrary OpCodes.OpCode where
358   arbitrary = do
359     op_id <- elements [ "OP_TEST_DELAY"
360                       , "OP_INSTANCE_REPLACE_DISKS"
361                       , "OP_INSTANCE_FAILOVER"
362                       , "OP_INSTANCE_MIGRATE"
363                       ]
364     case op_id of
365       "OP_TEST_DELAY" ->
366         liftM3 OpCodes.OpTestDelay arbitrary arbitrary arbitrary
367       "OP_INSTANCE_REPLACE_DISKS" ->
368         liftM5 OpCodes.OpInstanceReplaceDisks arbitrary arbitrary
369           arbitrary arbitrary arbitrary
370       "OP_INSTANCE_FAILOVER" ->
371         liftM3 OpCodes.OpInstanceFailover arbitrary arbitrary
372           arbitrary
373       "OP_INSTANCE_MIGRATE" ->
374         liftM5 OpCodes.OpInstanceMigrate arbitrary arbitrary
375           arbitrary arbitrary arbitrary
376       _ -> fail "Wrong opcode"
377
378 instance Arbitrary Jobs.OpStatus where
379   arbitrary = elements [minBound..maxBound]
380
381 instance Arbitrary Jobs.JobStatus where
382   arbitrary = elements [minBound..maxBound]
383
384 newtype SmallRatio = SmallRatio Double deriving Show
385 instance Arbitrary SmallRatio where
386   arbitrary = do
387     v <- choose (0, 1)
388     return $ SmallRatio v
389
390 instance Arbitrary Types.AllocPolicy where
391   arbitrary = elements [minBound..maxBound]
392
393 instance Arbitrary Types.DiskTemplate where
394   arbitrary = elements [minBound..maxBound]
395
396 instance Arbitrary Types.FailMode where
397   arbitrary = elements [minBound..maxBound]
398
399 instance Arbitrary Types.EvacMode where
400   arbitrary = elements [minBound..maxBound]
401
402 instance Arbitrary a => Arbitrary (Types.OpResult a) where
403   arbitrary = arbitrary >>= \c ->
404               if c
405                 then liftM Types.OpGood arbitrary
406                 else liftM Types.OpFail arbitrary
407
408 instance Arbitrary Types.ISpec where
409   arbitrary = do
410     mem_s <- arbitrary::Gen (NonNegative Int)
411     dsk_c <- arbitrary::Gen (NonNegative Int)
412     dsk_s <- arbitrary::Gen (NonNegative Int)
413     cpu_c <- arbitrary::Gen (NonNegative Int)
414     nic_c <- arbitrary::Gen (NonNegative Int)
415     return Types.ISpec { Types.iSpecMemorySize = fromIntegral mem_s
416                        , Types.iSpecCpuCount   = fromIntegral cpu_c
417                        , Types.iSpecDiskSize   = fromIntegral dsk_s
418                        , Types.iSpecDiskCount  = fromIntegral dsk_c
419                        , Types.iSpecNicCount   = fromIntegral nic_c
420                        }
421
422 -- | Generates an ispec bigger than the given one.
423 genBiggerISpec :: Types.ISpec -> Gen Types.ISpec
424 genBiggerISpec imin = do
425   mem_s <- choose (Types.iSpecMemorySize imin, maxBound)
426   dsk_c <- choose (Types.iSpecDiskCount imin, maxBound)
427   dsk_s <- choose (Types.iSpecDiskSize imin, maxBound)
428   cpu_c <- choose (Types.iSpecCpuCount imin, maxBound)
429   nic_c <- choose (Types.iSpecNicCount imin, maxBound)
430   return Types.ISpec { Types.iSpecMemorySize = fromIntegral mem_s
431                      , Types.iSpecCpuCount   = fromIntegral cpu_c
432                      , Types.iSpecDiskSize   = fromIntegral dsk_s
433                      , Types.iSpecDiskCount  = fromIntegral dsk_c
434                      , Types.iSpecNicCount   = fromIntegral nic_c
435                      }
436
437 instance Arbitrary Types.IPolicy where
438   arbitrary = do
439     imin <- arbitrary
440     istd <- genBiggerISpec imin
441     imax <- genBiggerISpec istd
442     num_tmpl <- choose (0, length allDiskTemplates)
443     dts  <- genUniquesList num_tmpl
444     vcpu_ratio <- arbitrary
445     return Types.IPolicy { Types.iPolicyMinSpec = imin
446                          , Types.iPolicyStdSpec = istd
447                          , Types.iPolicyMaxSpec = imax
448                          , Types.iPolicyDiskTemplates = dts
449                          , Types.iPolicyVcpuRatio = vcpu_ratio
450                          }
451
452 -- * Actual tests
453
454 -- ** Utils tests
455
456 -- | Helper to generate a small string that doesn't contain commas.
457 genNonCommaString = do
458   size <- choose (0, 20) -- arbitrary max size
459   vectorOf size (arbitrary `suchThat` ((/=) ','))
460
461 -- | If the list is not just an empty element, and if the elements do
462 -- not contain commas, then join+split should be idempotent.
463 prop_Utils_commaJoinSplit =
464   forAll (choose (0, 20)) $ \llen ->
465   forAll (vectorOf llen genNonCommaString `suchThat` ((/=) [""])) $ \lst ->
466   Utils.sepSplit ',' (Utils.commaJoin lst) ==? lst
467
468 -- | Split and join should always be idempotent.
469 prop_Utils_commaSplitJoin s =
470   Utils.commaJoin (Utils.sepSplit ',' s) ==? s
471
472 -- | fromObjWithDefault, we test using the Maybe monad and an integer
473 -- value.
474 prop_Utils_fromObjWithDefault def_value random_key =
475   -- a missing key will be returned with the default
476   JSON.fromObjWithDefault [] random_key def_value == Just def_value &&
477   -- a found key will be returned as is, not with default
478   JSON.fromObjWithDefault [(random_key, J.showJSON def_value)]
479        random_key (def_value+1) == Just def_value
480     where _types = def_value :: Integer
481
482 -- | Test that functional if' behaves like the syntactic sugar if.
483 prop_Utils_if'if :: Bool -> Int -> Int -> Gen Prop
484 prop_Utils_if'if cnd a b =
485   Utils.if' cnd a b ==? if cnd then a else b
486
487 -- | Test basic select functionality
488 prop_Utils_select :: Int      -- ^ Default result
489                   -> [Int]    -- ^ List of False values
490                   -> [Int]    -- ^ List of True values
491                   -> Gen Prop -- ^ Test result
492 prop_Utils_select def lst1 lst2 =
493   Utils.select def (flist ++ tlist) ==? expectedresult
494     where expectedresult = Utils.if' (null lst2) def (head lst2)
495           flist = zip (repeat False) lst1
496           tlist = zip (repeat True)  lst2
497
498 -- | Test basic select functionality with undefined default
499 prop_Utils_select_undefd :: [Int]            -- ^ List of False values
500                          -> NonEmptyList Int -- ^ List of True values
501                          -> Gen Prop         -- ^ Test result
502 prop_Utils_select_undefd lst1 (NonEmpty lst2) =
503   Utils.select undefined (flist ++ tlist) ==? head lst2
504     where flist = zip (repeat False) lst1
505           tlist = zip (repeat True)  lst2
506
507 -- | Test basic select functionality with undefined list values
508 prop_Utils_select_undefv :: [Int]            -- ^ List of False values
509                          -> NonEmptyList Int -- ^ List of True values
510                          -> Gen Prop         -- ^ Test result
511 prop_Utils_select_undefv lst1 (NonEmpty lst2) =
512   Utils.select undefined cndlist ==? head lst2
513     where flist = zip (repeat False) lst1
514           tlist = zip (repeat True)  lst2
515           cndlist = flist ++ tlist ++ [undefined]
516
517 prop_Utils_parseUnit (NonNegative n) =
518   Utils.parseUnit (show n) == Types.Ok n &&
519   Utils.parseUnit (show n ++ "m") == Types.Ok n &&
520   (case Utils.parseUnit (show n ++ "M") of
521      Types.Ok m -> if n > 0
522                      then m < n  -- for positive values, X MB is < than X MiB
523                      else m == 0 -- but for 0, 0 MB == 0 MiB
524      Types.Bad _ -> False) &&
525   Utils.parseUnit (show n ++ "g") == Types.Ok (n*1024) &&
526   Utils.parseUnit (show n ++ "t") == Types.Ok (n*1048576) &&
527   Types.isBad (Utils.parseUnit (show n ++ "x")::Types.Result Int)
528     where _types = n::Int
529
530 -- | Test list for the Utils module.
531 testSuite "Utils"
532             [ 'prop_Utils_commaJoinSplit
533             , 'prop_Utils_commaSplitJoin
534             , 'prop_Utils_fromObjWithDefault
535             , 'prop_Utils_if'if
536             , 'prop_Utils_select
537             , 'prop_Utils_select_undefd
538             , 'prop_Utils_select_undefv
539             , 'prop_Utils_parseUnit
540             ]
541
542 -- ** PeerMap tests
543
544 -- | Make sure add is idempotent.
545 prop_PeerMap_addIdempotent pmap key em =
546   fn puniq ==? fn (fn puniq)
547     where _types = (pmap::PeerMap.PeerMap,
548                     key::PeerMap.Key, em::PeerMap.Elem)
549           fn = PeerMap.add key em
550           puniq = PeerMap.accumArray const pmap
551
552 -- | Make sure remove is idempotent.
553 prop_PeerMap_removeIdempotent pmap key =
554   fn puniq ==? fn (fn puniq)
555     where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
556           fn = PeerMap.remove key
557           puniq = PeerMap.accumArray const pmap
558
559 -- | Make sure a missing item returns 0.
560 prop_PeerMap_findMissing pmap key =
561   PeerMap.find key (PeerMap.remove key puniq) ==? 0
562     where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
563           puniq = PeerMap.accumArray const pmap
564
565 -- | Make sure an added item is found.
566 prop_PeerMap_addFind pmap key em =
567   PeerMap.find key (PeerMap.add key em puniq) ==? em
568     where _types = (pmap::PeerMap.PeerMap,
569                     key::PeerMap.Key, em::PeerMap.Elem)
570           puniq = PeerMap.accumArray const pmap
571
572 -- | Manual check that maxElem returns the maximum indeed, or 0 for null.
573 prop_PeerMap_maxElem pmap =
574   PeerMap.maxElem puniq ==? if null puniq then 0
575                               else (maximum . snd . unzip) puniq
576     where _types = pmap::PeerMap.PeerMap
577           puniq = PeerMap.accumArray const pmap
578
579 -- | List of tests for the PeerMap module.
580 testSuite "PeerMap"
581             [ 'prop_PeerMap_addIdempotent
582             , 'prop_PeerMap_removeIdempotent
583             , 'prop_PeerMap_maxElem
584             , 'prop_PeerMap_addFind
585             , 'prop_PeerMap_findMissing
586             ]
587
588 -- ** Container tests
589
590 -- we silence the following due to hlint bug fixed in later versions
591 {-# ANN prop_Container_addTwo "HLint: ignore Avoid lambda" #-}
592 prop_Container_addTwo cdata i1 i2 =
593   fn i1 i2 cont == fn i2 i1 cont &&
594   fn i1 i2 cont == fn i1 i2 (fn i1 i2 cont)
595     where _types = (cdata::[Int],
596                     i1::Int, i2::Int)
597           cont = foldl (\c x -> Container.add x x c) Container.empty cdata
598           fn x1 x2 = Container.addTwo x1 x1 x2 x2
599
600 prop_Container_nameOf node =
601   let nl = makeSmallCluster node 1
602       fnode = head (Container.elems nl)
603   in Container.nameOf nl (Node.idx fnode) ==? Node.name fnode
604
605 -- | We test that in a cluster, given a random node, we can find it by
606 -- its name and alias, as long as all names and aliases are unique,
607 -- and that we fail to find a non-existing name.
608 prop_Container_findByName node =
609   forAll (choose (1, 20)) $ \ cnt ->
610   forAll (choose (0, cnt - 1)) $ \ fidx ->
611   forAll (genUniquesList (cnt * 2)) $ \ allnames ->
612   forAll (arbitrary `suchThat` (`notElem` allnames)) $ \ othername ->
613   let names = zip (take cnt allnames) (drop cnt allnames)
614       nl = makeSmallCluster node cnt
615       nodes = Container.elems nl
616       nodes' = map (\((name, alias), nn) -> (Node.idx nn,
617                                              nn { Node.name = name,
618                                                   Node.alias = alias }))
619                $ zip names nodes
620       nl' = Container.fromList nodes'
621       target = snd (nodes' !! fidx)
622   in Container.findByName nl' (Node.name target) == Just target &&
623      Container.findByName nl' (Node.alias target) == Just target &&
624      isNothing (Container.findByName nl' othername)
625
626 testSuite "Container"
627             [ 'prop_Container_addTwo
628             , 'prop_Container_nameOf
629             , 'prop_Container_findByName
630             ]
631
632 -- ** Instance tests
633
634 -- Simple instance tests, we only have setter/getters
635
636 prop_Instance_creat inst =
637   Instance.name inst ==? Instance.alias inst
638
639 prop_Instance_setIdx inst idx =
640   Instance.idx (Instance.setIdx inst idx) ==? idx
641     where _types = (inst::Instance.Instance, idx::Types.Idx)
642
643 prop_Instance_setName inst name =
644   Instance.name newinst == name &&
645   Instance.alias newinst == name
646     where _types = (inst::Instance.Instance, name::String)
647           newinst = Instance.setName inst name
648
649 prop_Instance_setAlias inst name =
650   Instance.name newinst == Instance.name inst &&
651   Instance.alias newinst == name
652     where _types = (inst::Instance.Instance, name::String)
653           newinst = Instance.setAlias inst name
654
655 prop_Instance_setPri inst pdx =
656   Instance.pNode (Instance.setPri inst pdx) ==? pdx
657     where _types = (inst::Instance.Instance, pdx::Types.Ndx)
658
659 prop_Instance_setSec inst sdx =
660   Instance.sNode (Instance.setSec inst sdx) ==? sdx
661     where _types = (inst::Instance.Instance, sdx::Types.Ndx)
662
663 prop_Instance_setBoth inst pdx sdx =
664   Instance.pNode si == pdx && Instance.sNode si == sdx
665     where _types = (inst::Instance.Instance, pdx::Types.Ndx, sdx::Types.Ndx)
666           si = Instance.setBoth inst pdx sdx
667
668 prop_Instance_shrinkMG inst =
669   Instance.mem inst >= 2 * Types.unitMem ==>
670     case Instance.shrinkByType inst Types.FailMem of
671       Types.Ok inst' -> Instance.mem inst' == Instance.mem inst - Types.unitMem
672       _ -> False
673
674 prop_Instance_shrinkMF inst =
675   forAll (choose (0, 2 * Types.unitMem - 1)) $ \mem ->
676     let inst' = inst { Instance.mem = mem}
677     in Types.isBad $ Instance.shrinkByType inst' Types.FailMem
678
679 prop_Instance_shrinkCG inst =
680   Instance.vcpus inst >= 2 * Types.unitCpu ==>
681     case Instance.shrinkByType inst Types.FailCPU of
682       Types.Ok inst' ->
683         Instance.vcpus inst' == Instance.vcpus inst - Types.unitCpu
684       _ -> False
685
686 prop_Instance_shrinkCF inst =
687   forAll (choose (0, 2 * Types.unitCpu - 1)) $ \vcpus ->
688     let inst' = inst { Instance.vcpus = vcpus }
689     in Types.isBad $ Instance.shrinkByType inst' Types.FailCPU
690
691 prop_Instance_shrinkDG inst =
692   Instance.dsk inst >= 2 * Types.unitDsk ==>
693     case Instance.shrinkByType inst Types.FailDisk of
694       Types.Ok inst' ->
695         Instance.dsk inst' == Instance.dsk inst - Types.unitDsk
696       _ -> False
697
698 prop_Instance_shrinkDF inst =
699   forAll (choose (0, 2 * Types.unitDsk - 1)) $ \dsk ->
700     let inst' = inst { Instance.dsk = dsk }
701     in Types.isBad $ Instance.shrinkByType inst' Types.FailDisk
702
703 prop_Instance_setMovable inst m =
704   Instance.movable inst' ==? m
705     where inst' = Instance.setMovable inst m
706
707 testSuite "Instance"
708             [ 'prop_Instance_creat
709             , 'prop_Instance_setIdx
710             , 'prop_Instance_setName
711             , 'prop_Instance_setAlias
712             , 'prop_Instance_setPri
713             , 'prop_Instance_setSec
714             , 'prop_Instance_setBoth
715             , 'prop_Instance_shrinkMG
716             , 'prop_Instance_shrinkMF
717             , 'prop_Instance_shrinkCG
718             , 'prop_Instance_shrinkCF
719             , 'prop_Instance_shrinkDG
720             , 'prop_Instance_shrinkDF
721             , 'prop_Instance_setMovable
722             ]
723
724 -- ** Backends
725
726 -- *** Text backend tests
727
728 -- Instance text loader tests
729
730 prop_Text_Load_Instance name mem dsk vcpus status
731                         (NonEmpty pnode) snode
732                         (NonNegative pdx) (NonNegative sdx) autobal dt =
733   pnode /= snode && pdx /= sdx ==>
734   let vcpus_s = show vcpus
735       dsk_s = show dsk
736       mem_s = show mem
737       status_s = Types.instanceStatusToRaw status
738       ndx = if null snode
739               then [(pnode, pdx)]
740               else [(pnode, pdx), (snode, sdx)]
741       nl = Data.Map.fromList ndx
742       tags = ""
743       sbal = if autobal then "Y" else "N"
744       sdt = Types.diskTemplateToRaw dt
745       inst = Text.loadInst nl
746              [name, mem_s, dsk_s, vcpus_s, status_s,
747               sbal, pnode, snode, sdt, tags]
748       fail1 = Text.loadInst nl
749               [name, mem_s, dsk_s, vcpus_s, status_s,
750                sbal, pnode, pnode, tags]
751       _types = ( name::String, mem::Int, dsk::Int
752                , vcpus::Int, status::Types.InstanceStatus
753                , snode::String
754                , autobal::Bool)
755   in case inst of
756        Types.Bad msg -> failTest $ "Failed to load instance: " ++ msg
757        Types.Ok (_, i) -> printTestCase "Mismatch in some field while\
758                                         \ loading the instance" $
759                Instance.name i == name &&
760                Instance.vcpus i == vcpus &&
761                Instance.mem i == mem &&
762                Instance.pNode i == pdx &&
763                Instance.sNode i == (if null snode
764                                       then Node.noSecondary
765                                       else sdx) &&
766                Instance.autoBalance i == autobal &&
767                Types.isBad fail1
768
769 prop_Text_Load_InstanceFail ktn fields =
770   length fields /= 10 ==>
771     case Text.loadInst nl fields of
772       Types.Ok _ -> failTest "Managed to load instance from invalid data"
773       Types.Bad msg -> printTestCase ("Unrecognised error message: " ++ msg) $
774                        "Invalid/incomplete instance data: '" `isPrefixOf` msg
775     where nl = Data.Map.fromList ktn
776
777 prop_Text_Load_Node name tm nm fm td fd tc fo =
778   let conv v = if v < 0
779                  then "?"
780                  else show v
781       tm_s = conv tm
782       nm_s = conv nm
783       fm_s = conv fm
784       td_s = conv td
785       fd_s = conv fd
786       tc_s = conv tc
787       fo_s = if fo
788                then "Y"
789                else "N"
790       any_broken = any (< 0) [tm, nm, fm, td, fd, tc]
791       gid = Group.uuid defGroup
792   in case Text.loadNode defGroupAssoc
793        [name, tm_s, nm_s, fm_s, td_s, fd_s, tc_s, fo_s, gid] of
794        Nothing -> False
795        Just (name', node) ->
796          if fo || any_broken
797            then Node.offline node
798            else Node.name node == name' && name' == name &&
799                 Node.alias node == name &&
800                 Node.tMem node == fromIntegral tm &&
801                 Node.nMem node == nm &&
802                 Node.fMem node == fm &&
803                 Node.tDsk node == fromIntegral td &&
804                 Node.fDsk node == fd &&
805                 Node.tCpu node == fromIntegral tc
806
807 prop_Text_Load_NodeFail fields =
808   length fields /= 8 ==> isNothing $ Text.loadNode Data.Map.empty fields
809
810 prop_Text_NodeLSIdempotent node =
811   (Text.loadNode defGroupAssoc.
812        Utils.sepSplit '|' . Text.serializeNode defGroupList) n ==?
813   Just (Node.name n, n)
814     -- override failN1 to what loadNode returns by default
815     where n = Node.setPolicy Types.defIPolicy $
816               node { Node.failN1 = True, Node.offline = False }
817
818 prop_Text_ISpecIdempotent ispec =
819   case Text.loadISpec "dummy" . Utils.sepSplit ',' .
820        Text.serializeISpec $ ispec of
821     Types.Bad msg -> failTest $ "Failed to load ispec: " ++ msg
822     Types.Ok ispec' -> ispec ==? ispec'
823
824 prop_Text_IPolicyIdempotent ipol =
825   case Text.loadIPolicy . Utils.sepSplit '|' $
826        Text.serializeIPolicy owner ipol of
827     Types.Bad msg -> failTest $ "Failed to load ispec: " ++ msg
828     Types.Ok res -> (owner, ipol) ==? res
829   where owner = "dummy"
830
831 -- | This property, while being in the text tests, does more than just
832 -- test end-to-end the serialisation and loading back workflow; it
833 -- also tests the Loader.mergeData and the actuall
834 -- Cluster.iterateAlloc (for well-behaving w.r.t. instance
835 -- allocations, not for the business logic). As such, it's a quite
836 -- complex and slow test, and that's the reason we restrict it to
837 -- small cluster sizes.
838 prop_Text_CreateSerialise =
839   forAll genTags $ \ctags ->
840   forAll (choose (1, 2)) $ \reqnodes ->
841   forAll (choose (1, 20)) $ \maxiter ->
842   forAll (choose (2, 10)) $ \count ->
843   forAll genOnlineNode $ \node ->
844   forAll (genInstanceSmallerThanNode node) $ \inst ->
845   let inst' = Instance.setMovable inst (reqnodes == 2)
846       nl = makeSmallCluster node count
847   in case Cluster.genAllocNodes defGroupList nl reqnodes True >>= \allocn ->
848      Cluster.iterateAlloc nl Container.empty (Just maxiter) inst' allocn [] []
849      of
850        Types.Bad msg -> failTest $ "Failed to allocate: " ++ msg
851        Types.Ok (_, _, _, [], _) -> printTestCase
852                                     "Failed to allocate: no allocations" False
853        Types.Ok (_, nl', il', _, _) ->
854          let cdata = Loader.ClusterData defGroupList nl' il' ctags
855                      Types.defIPolicy
856              saved = Text.serializeCluster cdata
857          in case Text.parseData saved >>= Loader.mergeData [] [] [] [] of
858               Types.Bad msg -> failTest $ "Failed to load/merge: " ++ msg
859               Types.Ok (Loader.ClusterData gl2 nl2 il2 ctags2 cpol2) ->
860                 ctags ==? ctags2 .&&.
861                 Types.defIPolicy ==? cpol2 .&&.
862                 il' ==? il2 .&&.
863                 defGroupList ==? gl2 .&&.
864                 nl' ==? nl2
865
866 testSuite "Text"
867             [ 'prop_Text_Load_Instance
868             , 'prop_Text_Load_InstanceFail
869             , 'prop_Text_Load_Node
870             , 'prop_Text_Load_NodeFail
871             , 'prop_Text_NodeLSIdempotent
872             , 'prop_Text_ISpecIdempotent
873             , 'prop_Text_IPolicyIdempotent
874             , 'prop_Text_CreateSerialise
875             ]
876
877 -- *** Simu backend
878
879 -- | Generates a tuple of specs for simulation.
880 genSimuSpec :: Gen (String, Int, Int, Int, Int)
881 genSimuSpec = do
882   pol <- elements [C.allocPolicyPreferred,
883                    C.allocPolicyLastResort, C.allocPolicyUnallocable,
884                   "p", "a", "u"]
885  -- should be reasonable (nodes/group), bigger values only complicate
886  -- the display of failed tests, and we don't care (in this particular
887  -- test) about big node groups
888   nodes <- choose (0, 20)
889   dsk <- choose (0, maxDsk)
890   mem <- choose (0, maxMem)
891   cpu <- choose (0, maxCpu)
892   return (pol, nodes, dsk, mem, cpu)
893
894 -- | Checks that given a set of corrects specs, we can load them
895 -- successfully, and that at high-level the values look right.
896 prop_SimuLoad =
897   forAll (choose (0, 10)) $ \ngroups ->
898   forAll (replicateM ngroups genSimuSpec) $ \specs ->
899   let strspecs = map (\(p, n, d, m, c) -> printf "%s,%d,%d,%d,%d"
900                                           p n d m c::String) specs
901       totnodes = sum $ map (\(_, n, _, _, _) -> n) specs
902       mdc_in = concatMap (\(_, n, d, m, c) ->
903                             replicate n (fromIntegral m, fromIntegral d,
904                                          fromIntegral c,
905                                          fromIntegral m, fromIntegral d)) specs
906   in case Simu.parseData strspecs of
907        Types.Bad msg -> failTest $ "Failed to load specs: " ++ msg
908        Types.Ok (Loader.ClusterData gl nl il tags ipol) ->
909          let nodes = map snd $ IntMap.toAscList nl
910              nidx = map Node.idx nodes
911              mdc_out = map (\n -> (Node.tMem n, Node.tDsk n, Node.tCpu n,
912                                    Node.fMem n, Node.fDsk n)) nodes
913          in
914          Container.size gl ==? ngroups .&&.
915          Container.size nl ==? totnodes .&&.
916          Container.size il ==? 0 .&&.
917          length tags ==? 0 .&&.
918          ipol ==? Types.defIPolicy .&&.
919          nidx ==? [1..totnodes] .&&.
920          mdc_in ==? mdc_out .&&.
921          map Group.iPolicy (Container.elems gl) ==?
922              replicate ngroups Types.defIPolicy
923
924 testSuite "Simu"
925             [ 'prop_SimuLoad
926             ]
927
928 -- ** Node tests
929
930 prop_Node_setAlias node name =
931   Node.name newnode == Node.name node &&
932   Node.alias newnode == name
933     where _types = (node::Node.Node, name::String)
934           newnode = Node.setAlias node name
935
936 prop_Node_setOffline node status =
937   Node.offline newnode ==? status
938     where newnode = Node.setOffline node status
939
940 prop_Node_setXmem node xm =
941   Node.xMem newnode ==? xm
942     where newnode = Node.setXmem node xm
943
944 prop_Node_setMcpu node mc =
945   Types.iPolicyVcpuRatio (Node.iPolicy newnode) ==? mc
946     where newnode = Node.setMcpu node mc
947
948 -- | Check that an instance add with too high memory or disk will be
949 -- rejected.
950 prop_Node_addPriFM node inst =
951   Instance.mem inst >= Node.fMem node && not (Node.failN1 node) &&
952   not (Instance.instanceOffline inst) ==>
953   case Node.addPri node inst'' of
954     Types.OpFail Types.FailMem -> True
955     _ -> False
956   where _types = (node::Node.Node, inst::Instance.Instance)
957         inst' = setInstanceSmallerThanNode node inst
958         inst'' = inst' { Instance.mem = Instance.mem inst }
959
960 prop_Node_addPriFD node inst =
961   Instance.dsk inst >= Node.fDsk node && not (Node.failN1 node) ==>
962     case Node.addPri node inst'' of
963       Types.OpFail Types.FailDisk -> True
964       _ -> False
965     where _types = (node::Node.Node, inst::Instance.Instance)
966           inst' = setInstanceSmallerThanNode node inst
967           inst'' = inst' { Instance.dsk = Instance.dsk inst }
968
969 prop_Node_addPriFC (Positive extra) =
970   forAll genOnlineNode $ \node ->
971   forAll (arbitrary `suchThat` Instance.instanceNotOffline) $ \inst ->
972   let inst' = setInstanceSmallerThanNode node inst
973       inst'' = inst' { Instance.vcpus = Node.availCpu node + extra }
974   in case Node.addPri node inst'' of
975        Types.OpFail Types.FailCPU -> property True
976        v -> failTest $ "Expected OpFail FailCPU, but got " ++ show v
977
978 -- | Check that an instance add with too high memory or disk will be
979 -- rejected.
980 prop_Node_addSec node inst pdx =
981   ((Instance.mem inst >= (Node.fMem node - Node.rMem node) &&
982     not (Instance.instanceOffline inst)) ||
983    Instance.dsk inst >= Node.fDsk node) &&
984   not (Node.failN1 node) ==>
985       isFailure (Node.addSec node inst pdx)
986         where _types = (node::Node.Node, inst::Instance.Instance, pdx::Int)
987
988 -- | Check that an offline instance with reasonable disk size but
989 -- extra mem/cpu can always be added.
990 prop_Node_addOffline (NonNegative extra_mem) (NonNegative extra_cpu) pdx =
991   forAll genOnlineNode $ \node ->
992   forAll (genInstanceSmallerThanNode node) $ \inst ->
993   let inst' = inst { Instance.runSt = Types.AdminOffline
994                    , Instance.mem = Node.availMem node + extra_mem
995                    , Instance.vcpus = Node.availCpu node + extra_cpu }
996   in case (Node.addPri node inst', Node.addSec node inst' pdx) of
997        (Types.OpGood _, Types.OpGood _) -> property True
998        v -> failTest $ "Expected OpGood/OpGood, but got: " ++ show v
999
1000 -- | Checks for memory reservation changes.
1001 prop_Node_rMem inst =
1002   not (Instance.instanceOffline inst) ==>
1003   forAll (arbitrary `suchThat` ((> Types.unitMem) . Node.fMem)) $ \node ->
1004   -- ab = auto_balance, nb = non-auto_balance
1005   -- we use -1 as the primary node of the instance
1006   let inst' = inst { Instance.pNode = -1, Instance.autoBalance = True }
1007       inst_ab = setInstanceSmallerThanNode node inst'
1008       inst_nb = inst_ab { Instance.autoBalance = False }
1009       -- now we have the two instances, identical except the
1010       -- autoBalance attribute
1011       orig_rmem = Node.rMem node
1012       inst_idx = Instance.idx inst_ab
1013       node_add_ab = Node.addSec node inst_ab (-1)
1014       node_add_nb = Node.addSec node inst_nb (-1)
1015       node_del_ab = liftM (`Node.removeSec` inst_ab) node_add_ab
1016       node_del_nb = liftM (`Node.removeSec` inst_nb) node_add_nb
1017   in case (node_add_ab, node_add_nb, node_del_ab, node_del_nb) of
1018        (Types.OpGood a_ab, Types.OpGood a_nb,
1019         Types.OpGood d_ab, Types.OpGood d_nb) ->
1020          printTestCase "Consistency checks failed" $
1021            Node.rMem a_ab >  orig_rmem &&
1022            Node.rMem a_ab - orig_rmem == Instance.mem inst_ab &&
1023            Node.rMem a_nb == orig_rmem &&
1024            Node.rMem d_ab == orig_rmem &&
1025            Node.rMem d_nb == orig_rmem &&
1026            -- this is not related to rMem, but as good a place to
1027            -- test as any
1028            inst_idx `elem` Node.sList a_ab &&
1029            inst_idx `notElem` Node.sList d_ab
1030        x -> failTest $ "Failed to add/remove instances: " ++ show x
1031
1032 -- | Check mdsk setting.
1033 prop_Node_setMdsk node mx =
1034   Node.loDsk node' >= 0 &&
1035   fromIntegral (Node.loDsk node') <= Node.tDsk node &&
1036   Node.availDisk node' >= 0 &&
1037   Node.availDisk node' <= Node.fDsk node' &&
1038   fromIntegral (Node.availDisk node') <= Node.tDsk node' &&
1039   Node.mDsk node' == mx'
1040     where _types = (node::Node.Node, mx::SmallRatio)
1041           node' = Node.setMdsk node mx'
1042           SmallRatio mx' = mx
1043
1044 -- Check tag maps
1045 prop_Node_tagMaps_idempotent =
1046   forAll genTags $ \tags ->
1047   Node.delTags (Node.addTags m tags) tags ==? m
1048     where m = Data.Map.empty
1049
1050 prop_Node_tagMaps_reject =
1051   forAll (genTags `suchThat` (not . null)) $ \tags ->
1052   let m = Node.addTags Data.Map.empty tags
1053   in all (\t -> Node.rejectAddTags m [t]) tags
1054
1055 prop_Node_showField node =
1056   forAll (elements Node.defaultFields) $ \ field ->
1057   fst (Node.showHeader field) /= Types.unknownField &&
1058   Node.showField node field /= Types.unknownField
1059
1060 prop_Node_computeGroups nodes =
1061   let ng = Node.computeGroups nodes
1062       onlyuuid = map fst ng
1063   in length nodes == sum (map (length . snd) ng) &&
1064      all (\(guuid, ns) -> all ((== guuid) . Node.group) ns) ng &&
1065      length (nub onlyuuid) == length onlyuuid &&
1066      (null nodes || not (null ng))
1067
1068 testSuite "Node"
1069             [ 'prop_Node_setAlias
1070             , 'prop_Node_setOffline
1071             , 'prop_Node_setMcpu
1072             , 'prop_Node_setXmem
1073             , 'prop_Node_addPriFM
1074             , 'prop_Node_addPriFD
1075             , 'prop_Node_addPriFC
1076             , 'prop_Node_addSec
1077             , 'prop_Node_addOffline
1078             , 'prop_Node_rMem
1079             , 'prop_Node_setMdsk
1080             , 'prop_Node_tagMaps_idempotent
1081             , 'prop_Node_tagMaps_reject
1082             , 'prop_Node_showField
1083             , 'prop_Node_computeGroups
1084             ]
1085
1086 -- ** Cluster tests
1087
1088 -- | Check that the cluster score is close to zero for a homogeneous
1089 -- cluster.
1090 prop_Score_Zero node =
1091   forAll (choose (1, 1024)) $ \count ->
1092     (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
1093      (Node.tDsk node > 0) && (Node.tMem node > 0)) ==>
1094   let fn = Node.buildPeers node Container.empty
1095       nlst = replicate count fn
1096       score = Cluster.compCVNodes nlst
1097   -- we can't say == 0 here as the floating point errors accumulate;
1098   -- this should be much lower than the default score in CLI.hs
1099   in score <= 1e-12
1100
1101 -- | Check that cluster stats are sane.
1102 prop_CStats_sane =
1103   forAll (choose (1, 1024)) $ \count ->
1104   forAll genOnlineNode $ \node ->
1105   let fn = Node.buildPeers node Container.empty
1106       nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
1107       nl = Container.fromList nlst
1108       cstats = Cluster.totalResources nl
1109   in Cluster.csAdsk cstats >= 0 &&
1110      Cluster.csAdsk cstats <= Cluster.csFdsk cstats
1111
1112 -- | Check that one instance is allocated correctly, without
1113 -- rebalances needed.
1114 prop_ClusterAlloc_sane inst =
1115   forAll (choose (5, 20)) $ \count ->
1116   forAll genOnlineNode $ \node ->
1117   let (nl, il, inst') = makeSmallEmptyCluster node count inst
1118   in case Cluster.genAllocNodes defGroupList nl 2 True >>=
1119      Cluster.tryAlloc nl il inst' of
1120        Types.Bad _ -> False
1121        Types.Ok as ->
1122          case Cluster.asSolution as of
1123            Nothing -> False
1124            Just (xnl, xi, _, cv) ->
1125              let il' = Container.add (Instance.idx xi) xi il
1126                  tbl = Cluster.Table xnl il' cv []
1127              in not (canBalance tbl True True False)
1128
1129 -- | Checks that on a 2-5 node cluster, we can allocate a random
1130 -- instance spec via tiered allocation (whatever the original instance
1131 -- spec), on either one or two nodes. Furthermore, we test that
1132 -- computed allocation statistics are correct.
1133 prop_ClusterCanTieredAlloc inst =
1134   forAll (choose (2, 5)) $ \count ->
1135   forAll (choose (1, 2)) $ \rqnodes ->
1136   forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1137   let nl = makeSmallCluster node count
1138       il = Container.empty
1139       allocnodes = Cluster.genAllocNodes defGroupList nl rqnodes True
1140   in case allocnodes >>= \allocnodes' ->
1141     Cluster.tieredAlloc nl il (Just 1) inst allocnodes' [] [] of
1142        Types.Bad msg -> failTest $ "Failed to tiered alloc: " ++ msg
1143        Types.Ok (_, nl', il', ixes, cstats) ->
1144          let (ai_alloc, ai_pool, ai_unav) =
1145                Cluster.computeAllocationDelta
1146                 (Cluster.totalResources nl)
1147                 (Cluster.totalResources nl')
1148              all_nodes = Container.elems nl
1149          in property (not (null ixes)) .&&.
1150             IntMap.size il' ==? length ixes .&&.
1151             length ixes ==? length cstats .&&.
1152             sum (map Types.allocInfoVCpus [ai_alloc, ai_pool, ai_unav]) ==?
1153               sum (map Node.hiCpu all_nodes) .&&.
1154             sum (map Types.allocInfoNCpus [ai_alloc, ai_pool, ai_unav]) ==?
1155               sum (map Node.tCpu all_nodes) .&&.
1156             sum (map Types.allocInfoMem [ai_alloc, ai_pool, ai_unav]) ==?
1157               truncate (sum (map Node.tMem all_nodes)) .&&.
1158             sum (map Types.allocInfoDisk [ai_alloc, ai_pool, ai_unav]) ==?
1159               truncate (sum (map Node.tDsk all_nodes))
1160
1161 -- | Helper function to create a cluster with the given range of nodes
1162 -- and allocate an instance on it.
1163 genClusterAlloc count node inst =
1164   let nl = makeSmallCluster node count
1165   in case Cluster.genAllocNodes defGroupList nl 2 True >>=
1166      Cluster.tryAlloc nl Container.empty inst of
1167        Types.Bad _ -> Types.Bad "Can't allocate"
1168        Types.Ok as ->
1169          case Cluster.asSolution as of
1170            Nothing -> Types.Bad "Empty solution?"
1171            Just (xnl, xi, _, _) ->
1172              let xil = Container.add (Instance.idx xi) xi Container.empty
1173              in Types.Ok (xnl, xil, xi)
1174
1175 -- | Checks that on a 4-8 node cluster, once we allocate an instance,
1176 -- we can also relocate it.
1177 prop_ClusterAllocRelocate =
1178   forAll (choose (4, 8)) $ \count ->
1179   forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1180   forAll (genInstanceSmallerThanNode node) $ \inst ->
1181   case genClusterAlloc count node inst of
1182     Types.Bad msg -> failTest msg
1183     Types.Ok (nl, il, inst') ->
1184       case IAlloc.processRelocate defGroupList nl il
1185              (Instance.idx inst) 1 [Instance.sNode inst'] of
1186         Types.Ok _ -> printTestCase "??" True  -- huh, how to make
1187                                                -- this nicer...
1188         Types.Bad msg -> failTest $ "Failed to relocate: " ++ msg
1189
1190 -- | Helper property checker for the result of a nodeEvac or
1191 -- changeGroup operation.
1192 check_EvacMode grp inst result =
1193   case result of
1194     Types.Bad msg -> failTest $ "Couldn't evacuate/change group:" ++ msg
1195     Types.Ok (_, _, es) ->
1196       let moved = Cluster.esMoved es
1197           failed = Cluster.esFailed es
1198           opcodes = not . null $ Cluster.esOpCodes es
1199       in failmsg ("'failed' not empty: " ++ show failed) (null failed) .&&.
1200          failmsg "'opcodes' is null" opcodes .&&.
1201          case moved of
1202            [(idx', gdx, _)] -> failmsg "invalid instance moved" (idx == idx')
1203                                .&&.
1204                                failmsg "wrong target group"
1205                                          (gdx == Group.idx grp)
1206            v -> failmsg  ("invalid solution: " ++ show v) False
1207   where failmsg = \msg -> printTestCase ("Failed to evacuate: " ++ msg)
1208         idx = Instance.idx inst
1209
1210 -- | Checks that on a 4-8 node cluster, once we allocate an instance,
1211 -- we can also node-evacuate it.
1212 prop_ClusterAllocEvacuate =
1213   forAll (choose (4, 8)) $ \count ->
1214   forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1215   forAll (genInstanceSmallerThanNode node) $ \inst ->
1216   case genClusterAlloc count node inst of
1217     Types.Bad msg -> failTest msg
1218     Types.Ok (nl, il, inst') ->
1219       conjoin $ map (\mode -> check_EvacMode defGroup inst' $
1220                               Cluster.tryNodeEvac defGroupList nl il mode
1221                                 [Instance.idx inst']) [minBound..maxBound]
1222
1223 -- | Checks that on a 4-8 node cluster with two node groups, once we
1224 -- allocate an instance on the first node group, we can also change
1225 -- its group.
1226 prop_ClusterAllocChangeGroup =
1227   forAll (choose (4, 8)) $ \count ->
1228   forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1229   forAll (genInstanceSmallerThanNode node) $ \inst ->
1230   case genClusterAlloc count node inst of
1231     Types.Bad msg -> failTest msg
1232     Types.Ok (nl, il, inst') ->
1233       -- we need to add a second node group and nodes to the cluster
1234       let nl2 = Container.elems $ makeSmallCluster node count
1235           grp2 = Group.setIdx defGroup (Group.idx defGroup + 1)
1236           maxndx = maximum . map Node.idx $ nl2
1237           nl3 = map (\n -> n { Node.group = Group.idx grp2
1238                              , Node.idx = Node.idx n + maxndx }) nl2
1239           nl4 = Container.fromList . map (\n -> (Node.idx n, n)) $ nl3
1240           gl' = Container.add (Group.idx grp2) grp2 defGroupList
1241           nl' = IntMap.union nl nl4
1242       in check_EvacMode grp2 inst' $
1243          Cluster.tryChangeGroup gl' nl' il [] [Instance.idx inst']
1244
1245 -- | Check that allocating multiple instances on a cluster, then
1246 -- adding an empty node, results in a valid rebalance.
1247 prop_ClusterAllocBalance =
1248   forAll (genNode (Just 5) (Just 128)) $ \node ->
1249   forAll (choose (3, 5)) $ \count ->
1250   not (Node.offline node) && not (Node.failN1 node) ==>
1251   let nl = makeSmallCluster node count
1252       (hnode, nl') = IntMap.deleteFindMax nl
1253       il = Container.empty
1254       allocnodes = Cluster.genAllocNodes defGroupList nl' 2 True
1255       i_templ = createInstance Types.unitMem Types.unitDsk Types.unitCpu
1256   in case allocnodes >>= \allocnodes' ->
1257     Cluster.iterateAlloc nl' il (Just 5) i_templ allocnodes' [] [] of
1258        Types.Bad msg -> failTest $ "Failed to allocate: " ++ msg
1259        Types.Ok (_, _, _, [], _) -> failTest "Failed to allocate: no instances"
1260        Types.Ok (_, xnl, il', _, _) ->
1261          let ynl = Container.add (Node.idx hnode) hnode xnl
1262              cv = Cluster.compCV ynl
1263              tbl = Cluster.Table ynl il' cv []
1264          in printTestCase "Failed to rebalance" $
1265             canBalance tbl True True False
1266
1267 -- | Checks consistency.
1268 prop_ClusterCheckConsistency node inst =
1269   let nl = makeSmallCluster node 3
1270       [node1, node2, node3] = Container.elems nl
1271       node3' = node3 { Node.group = 1 }
1272       nl' = Container.add (Node.idx node3') node3' nl
1273       inst1 = Instance.setBoth inst (Node.idx node1) (Node.idx node2)
1274       inst2 = Instance.setBoth inst (Node.idx node1) Node.noSecondary
1275       inst3 = Instance.setBoth inst (Node.idx node1) (Node.idx node3)
1276       ccheck = Cluster.findSplitInstances nl' . Container.fromList
1277   in null (ccheck [(0, inst1)]) &&
1278      null (ccheck [(0, inst2)]) &&
1279      (not . null $ ccheck [(0, inst3)])
1280
1281 -- | For now, we only test that we don't lose instances during the split.
1282 prop_ClusterSplitCluster node inst =
1283   forAll (choose (0, 100)) $ \icnt ->
1284   let nl = makeSmallCluster node 2
1285       (nl', il') = foldl (\(ns, is) _ -> assignInstance ns is inst 0 1)
1286                    (nl, Container.empty) [1..icnt]
1287       gni = Cluster.splitCluster nl' il'
1288   in sum (map (Container.size . snd . snd) gni) == icnt &&
1289      all (\(guuid, (nl'', _)) -> all ((== guuid) . Node.group)
1290                                  (Container.elems nl'')) gni
1291
1292 -- | Helper function to check if we can allocate an instance on a
1293 -- given node list.
1294 canAllocOn :: Node.List -> Int -> Instance.Instance -> Bool
1295 canAllocOn nl reqnodes inst =
1296   case Cluster.genAllocNodes defGroupList nl reqnodes True >>=
1297        Cluster.tryAlloc nl (Container.empty) inst of
1298        Types.Bad _ -> False
1299        Types.Ok as ->
1300          case Cluster.asSolution as of
1301            Nothing -> False
1302            Just _ -> True
1303
1304 -- | Checks that allocation obeys minimum and maximum instance
1305 -- policies. The unittest generates a random node, duplicates it count
1306 -- times, and generates a random instance that can be allocated on
1307 -- this mini-cluster; it then checks that after applying a policy that
1308 -- the instance doesn't fits, the allocation fails.
1309 prop_ClusterAllocPolicy node =
1310   -- rqn is the required nodes (1 or 2)
1311   forAll (choose (1, 2)) $ \rqn ->
1312   forAll (choose (5, 20)) $ \count ->
1313   forAll (arbitrary `suchThat` (canAllocOn (makeSmallCluster node count) rqn))
1314          $ \inst ->
1315   forAll (arbitrary `suchThat` (isFailure .
1316                                 Instance.instMatchesPolicy inst)) $ \ipol ->
1317   let node' = Node.setPolicy ipol node
1318       nl = makeSmallCluster node' count
1319   in not $ canAllocOn nl rqn inst
1320
1321 testSuite "Cluster"
1322             [ 'prop_Score_Zero
1323             , 'prop_CStats_sane
1324             , 'prop_ClusterAlloc_sane
1325             , 'prop_ClusterCanTieredAlloc
1326             , 'prop_ClusterAllocRelocate
1327             , 'prop_ClusterAllocEvacuate
1328             , 'prop_ClusterAllocChangeGroup
1329             , 'prop_ClusterAllocBalance
1330             , 'prop_ClusterCheckConsistency
1331             , 'prop_ClusterSplitCluster
1332             , 'prop_ClusterAllocPolicy
1333             ]
1334
1335 -- ** OpCodes tests
1336
1337 -- | Check that opcode serialization is idempotent.
1338 prop_OpCodes_serialization op =
1339   case J.readJSON (J.showJSON op) of
1340     J.Error e -> failTest $ "Cannot deserialise: " ++ e
1341     J.Ok op' -> op ==? op'
1342   where _types = op::OpCodes.OpCode
1343
1344 testSuite "OpCodes"
1345             [ 'prop_OpCodes_serialization ]
1346
1347 -- ** Jobs tests
1348
1349 -- | Check that (queued) job\/opcode status serialization is idempotent.
1350 prop_OpStatus_serialization os =
1351   case J.readJSON (J.showJSON os) of
1352     J.Error e -> failTest $ "Cannot deserialise: " ++ e
1353     J.Ok os' -> os ==? os'
1354   where _types = os::Jobs.OpStatus
1355
1356 prop_JobStatus_serialization js =
1357   case J.readJSON (J.showJSON js) of
1358     J.Error e -> failTest $ "Cannot deserialise: " ++ e
1359     J.Ok js' -> js ==? js'
1360   where _types = js::Jobs.JobStatus
1361
1362 testSuite "Jobs"
1363             [ 'prop_OpStatus_serialization
1364             , 'prop_JobStatus_serialization
1365             ]
1366
1367 -- ** Loader tests
1368
1369 prop_Loader_lookupNode ktn inst node =
1370   Loader.lookupNode nl inst node ==? Data.Map.lookup node nl
1371     where nl = Data.Map.fromList ktn
1372
1373 prop_Loader_lookupInstance kti inst =
1374   Loader.lookupInstance il inst ==? Data.Map.lookup inst il
1375     where il = Data.Map.fromList kti
1376
1377 prop_Loader_assignIndices =
1378   -- generate nodes with unique names
1379   forAll (arbitrary `suchThat`
1380           (\nodes ->
1381              let names = map Node.name nodes
1382              in length names == length (nub names))) $ \nodes ->
1383   let (nassoc, kt) =
1384         Loader.assignIndices (map (\n -> (Node.name n, n)) nodes)
1385   in Data.Map.size nassoc == length nodes &&
1386      Container.size kt == length nodes &&
1387      if not (null nodes)
1388        then maximum (IntMap.keys kt) == length nodes - 1
1389        else True
1390
1391 -- | Checks that the number of primary instances recorded on the nodes
1392 -- is zero.
1393 prop_Loader_mergeData ns =
1394   let na = Container.fromList $ map (\n -> (Node.idx n, n)) ns
1395   in case Loader.mergeData [] [] [] []
1396          (Loader.emptyCluster {Loader.cdNodes = na}) of
1397     Types.Bad _ -> False
1398     Types.Ok (Loader.ClusterData _ nl il _ _) ->
1399       let nodes = Container.elems nl
1400           instances = Container.elems il
1401       in (sum . map (length . Node.pList)) nodes == 0 &&
1402          null instances
1403
1404 -- | Check that compareNameComponent on equal strings works.
1405 prop_Loader_compareNameComponent_equal :: String -> Bool
1406 prop_Loader_compareNameComponent_equal s =
1407   Loader.compareNameComponent s s ==
1408     Loader.LookupResult Loader.ExactMatch s
1409
1410 -- | Check that compareNameComponent on prefix strings works.
1411 prop_Loader_compareNameComponent_prefix :: NonEmptyList Char -> String -> Bool
1412 prop_Loader_compareNameComponent_prefix (NonEmpty s1) s2 =
1413   Loader.compareNameComponent (s1 ++ "." ++ s2) s1 ==
1414     Loader.LookupResult Loader.PartialMatch s1
1415
1416 testSuite "Loader"
1417             [ 'prop_Loader_lookupNode
1418             , 'prop_Loader_lookupInstance
1419             , 'prop_Loader_assignIndices
1420             , 'prop_Loader_mergeData
1421             , 'prop_Loader_compareNameComponent_equal
1422             , 'prop_Loader_compareNameComponent_prefix
1423             ]
1424
1425 -- ** Types tests
1426
1427 prop_Types_AllocPolicy_serialisation apol =
1428   case J.readJSON (J.showJSON apol) of
1429     J.Ok p -> p ==? apol
1430     J.Error s -> failTest $ "Failed to deserialise: " ++ s
1431       where _types = apol::Types.AllocPolicy
1432
1433 prop_Types_DiskTemplate_serialisation dt =
1434   case J.readJSON (J.showJSON dt) of
1435     J.Ok p -> p ==? dt
1436     J.Error s -> failTest $ "Failed to deserialise: " ++ s
1437       where _types = dt::Types.DiskTemplate
1438
1439 prop_Types_ISpec_serialisation ispec =
1440   case J.readJSON (J.showJSON ispec) of
1441     J.Ok p -> p ==? ispec
1442     J.Error s -> failTest $ "Failed to deserialise: " ++ s
1443       where _types = ispec::Types.ISpec
1444
1445 prop_Types_IPolicy_serialisation ipol =
1446   case J.readJSON (J.showJSON ipol) of
1447     J.Ok p -> p ==? ipol
1448     J.Error s -> failTest $ "Failed to deserialise: " ++ s
1449       where _types = ipol::Types.IPolicy
1450
1451 prop_Types_EvacMode_serialisation em =
1452   case J.readJSON (J.showJSON em) of
1453     J.Ok p -> p ==? em
1454     J.Error s -> failTest $ "Failed to deserialise: " ++ s
1455       where _types = em::Types.EvacMode
1456
1457 prop_Types_opToResult op =
1458   case op of
1459     Types.OpFail _ -> Types.isBad r
1460     Types.OpGood v -> case r of
1461                         Types.Bad _ -> False
1462                         Types.Ok v' -> v == v'
1463   where r = Types.opToResult op
1464         _types = op::Types.OpResult Int
1465
1466 prop_Types_eitherToResult ei =
1467   case ei of
1468     Left _ -> Types.isBad r
1469     Right v -> case r of
1470                  Types.Bad _ -> False
1471                  Types.Ok v' -> v == v'
1472     where r = Types.eitherToResult ei
1473           _types = ei::Either String Int
1474
1475 testSuite "Types"
1476             [ 'prop_Types_AllocPolicy_serialisation
1477             , 'prop_Types_DiskTemplate_serialisation
1478             , 'prop_Types_ISpec_serialisation
1479             , 'prop_Types_IPolicy_serialisation
1480             , 'prop_Types_EvacMode_serialisation
1481             , 'prop_Types_opToResult
1482             , 'prop_Types_eitherToResult
1483             ]
1484
1485 -- ** CLI tests
1486
1487 -- | Test correct parsing.
1488 prop_CLI_parseISpec descr dsk mem cpu =
1489   let str = printf "%d,%d,%d" dsk mem cpu
1490   in CLI.parseISpecString descr str ==? Types.Ok (Types.RSpec cpu mem dsk)
1491
1492 -- | Test parsing failure due to wrong section count.
1493 prop_CLI_parseISpecFail descr =
1494   forAll (choose (0,100) `suchThat` ((/=) 3)) $ \nelems ->
1495   forAll (replicateM nelems arbitrary) $ \values ->
1496   let str = intercalate "," $ map show (values::[Int])
1497   in case CLI.parseISpecString descr str of
1498        Types.Ok v -> failTest $ "Expected failure, got " ++ show v
1499        _ -> property True
1500
1501 -- | Test parseYesNo.
1502 prop_CLI_parseYesNo def testval val =
1503   forAll (elements [val, "yes", "no"]) $ \actual_val ->
1504   if testval
1505     then CLI.parseYesNo def Nothing ==? Types.Ok def
1506     else let result = CLI.parseYesNo def (Just actual_val)
1507          in if actual_val `elem` ["yes", "no"]
1508               then result ==? Types.Ok (actual_val == "yes")
1509               else property $ Types.isBad result
1510
1511 -- | Helper to check for correct parsing of string arg.
1512 checkStringArg val (opt, fn) =
1513   let GetOpt.Option _ longs _ _ = opt
1514   in case longs of
1515        [] -> failTest "no long options?"
1516        cmdarg:_ ->
1517          case CLI.parseOptsInner ["--" ++ cmdarg ++ "=" ++ val] "prog" [opt] of
1518            Left e -> failTest $ "Failed to parse option: " ++ show e
1519            Right (options, _) -> fn options ==? Just val
1520
1521 -- | Test a few string arguments.
1522 prop_CLI_StringArg argument =
1523   let args = [ (CLI.oDataFile,      CLI.optDataFile)
1524              , (CLI.oDynuFile,      CLI.optDynuFile)
1525              , (CLI.oSaveCluster,   CLI.optSaveCluster)
1526              , (CLI.oReplay,        CLI.optReplay)
1527              , (CLI.oPrintCommands, CLI.optShowCmds)
1528              , (CLI.oLuxiSocket,    CLI.optLuxi)
1529              ]
1530   in conjoin $ map (checkStringArg argument) args
1531
1532 -- | Helper to test that a given option is accepted OK with quick exit.
1533 checkEarlyExit name options param =
1534   case CLI.parseOptsInner [param] name options of
1535     Left (code, _) -> if code == 0
1536                           then property True
1537                           else failTest $ "Program " ++ name ++
1538                                  " returns invalid code " ++ show code ++
1539                                  " for option " ++ param
1540     _ -> failTest $ "Program " ++ name ++ " doesn't consider option " ++
1541          param ++ " as early exit one"
1542
1543 -- | Test that all binaries support some common options. There is
1544 -- nothing actually random about this test...
1545 prop_CLI_stdopts =
1546   let params = ["-h", "--help", "-V", "--version"]
1547       opts = map (\(name, (_, o)) -> (name, o)) Program.personalities
1548       -- apply checkEarlyExit across the cartesian product of params and opts
1549   in conjoin [checkEarlyExit n o p | p <- params, (n, o) <- opts]
1550
1551 testSuite "CLI"
1552           [ 'prop_CLI_parseISpec
1553           , 'prop_CLI_parseISpecFail
1554           , 'prop_CLI_parseYesNo
1555           , 'prop_CLI_StringArg
1556           , 'prop_CLI_stdopts
1557           ]