a55ba02c2b1a4c5b52c13d1efb06cb74b0a488d0
[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 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.Loader as Loader
59 import qualified Ganeti.HTools.Luxi
60 import qualified Ganeti.HTools.Node as Node
61 import qualified Ganeti.HTools.Group as Group
62 import qualified Ganeti.HTools.PeerMap as PeerMap
63 import qualified Ganeti.HTools.Rapi
64 import qualified Ganeti.HTools.Simu
65 import qualified Ganeti.HTools.Text as Text
66 import qualified Ganeti.HTools.Types as Types
67 import qualified Ganeti.HTools.Utils as Utils
68 import qualified Ganeti.HTools.Version
69 import qualified Ganeti.Constants as C
70
71 import qualified Ganeti.HTools.Program.Hail
72 import qualified Ganeti.HTools.Program.Hbal
73 import qualified Ganeti.HTools.Program.Hscan
74 import qualified Ganeti.HTools.Program.Hspace
75
76 import Ganeti.HTools.QCHelper (testSuite)
77
78 -- * Constants
79
80 -- | Maximum memory (1TiB, somewhat random value).
81 maxMem :: Int
82 maxMem = 1024 * 1024
83
84 -- | Maximum disk (8TiB, somewhat random value).
85 maxDsk :: Int
86 maxDsk = 1024 * 1024 * 8
87
88 -- | Max CPUs (1024, somewhat random value).
89 maxCpu :: Int
90 maxCpu = 1024
91
92 defGroup :: Group.Group
93 defGroup = flip Group.setIdx 0 $
94                Group.create "default" Utils.defaultGroupID
95                     Types.AllocPreferred
96
97 defGroupList :: Group.List
98 defGroupList = Container.fromList [(Group.idx defGroup, defGroup)]
99
100 defGroupAssoc :: Data.Map.Map String Types.Gdx
101 defGroupAssoc = Data.Map.singleton (Group.uuid defGroup) (Group.idx defGroup)
102
103 -- * Helper functions
104
105 -- | Simple checker for whether OpResult is fail or pass.
106 isFailure :: Types.OpResult a -> Bool
107 isFailure (Types.OpFail _) = True
108 isFailure _ = False
109
110 -- | Checks for equality with proper annotation.
111 (==?) :: (Show a, Eq a) => a -> a -> Property
112 (==?) x y = printTestCase
113             ("Expected equality, but '" ++
114              show x ++ "' /= '" ++ show y ++ "'") (x == y)
115 infix 3 ==?
116
117 -- | Update an instance to be smaller than a node.
118 setInstanceSmallerThanNode node inst =
119     inst { Instance.mem = Node.availMem node `div` 2
120          , Instance.dsk = Node.availDisk node `div` 2
121          , Instance.vcpus = Node.availCpu node `div` 2
122          }
123
124 -- | Create an instance given its spec.
125 createInstance mem dsk vcpus =
126     Instance.create "inst-unnamed" mem dsk vcpus Types.Running [] True (-1) (-1)
127                     Types.DTDrbd8
128
129 -- | Create a small cluster by repeating a node spec.
130 makeSmallCluster :: Node.Node -> Int -> Node.List
131 makeSmallCluster node count =
132     let fn = Node.buildPeers node Container.empty
133         namelst = map (\n -> (Node.name n, n)) (replicate count fn)
134         (_, nlst) = Loader.assignIndices namelst
135     in nlst
136
137 -- | Checks if a node is "big" enough.
138 isNodeBig :: Node.Node -> Int -> Bool
139 isNodeBig node size = Node.availDisk node > size * Types.unitDsk
140                       && Node.availMem node > size * Types.unitMem
141                       && Node.availCpu node > size * Types.unitCpu
142
143 canBalance :: Cluster.Table -> Bool -> Bool -> Bool -> Bool
144 canBalance tbl dm im evac = isJust $ Cluster.tryBalance tbl dm im evac 0 0
145
146 -- | Assigns a new fresh instance to a cluster; this is not
147 -- allocation, so no resource checks are done.
148 assignInstance :: Node.List -> Instance.List -> Instance.Instance ->
149                   Types.Idx -> Types.Idx ->
150                   (Node.List, Instance.List)
151 assignInstance nl il inst pdx sdx =
152   let pnode = Container.find pdx nl
153       snode = Container.find sdx nl
154       maxiidx = if Container.null il
155                 then 0
156                 else fst (Container.findMax il) + 1
157       inst' = inst { Instance.idx = maxiidx,
158                      Instance.pNode = pdx, Instance.sNode = sdx }
159       pnode' = Node.setPri pnode inst'
160       snode' = Node.setSec snode inst'
161       nl' = Container.addTwo pdx pnode' sdx snode' nl
162       il' = Container.add maxiidx inst' il
163   in (nl', il')
164
165 -- * Arbitrary instances
166
167 -- | Defines a DNS name.
168 newtype DNSChar = DNSChar { dnsGetChar::Char }
169
170 instance Arbitrary DNSChar where
171     arbitrary = do
172       x <- elements (['a'..'z'] ++ ['0'..'9'] ++ "_-")
173       return (DNSChar x)
174
175 getName :: Gen String
176 getName = do
177   n <- choose (1, 64)
178   dn <- vector n::Gen [DNSChar]
179   return (map dnsGetChar dn)
180
181
182 getFQDN :: Gen String
183 getFQDN = do
184   felem <- getName
185   ncomps <- choose (1, 4)
186   frest <- vector ncomps::Gen [[DNSChar]]
187   let frest' = map (map dnsGetChar) frest
188   return (felem ++ "." ++ intercalate "." frest')
189
190 instance Arbitrary Types.InstanceStatus where
191     arbitrary = elements [ Types.AdminDown
192                          , Types.AdminOffline
193                          , Types.ErrorDown
194                          , Types.ErrorUp
195                          , Types.NodeDown
196                          , Types.NodeOffline
197                          , Types.Running
198                          , Types.WrongNode]
199
200 -- let's generate a random instance
201 instance Arbitrary Instance.Instance where
202     arbitrary = do
203       name <- getFQDN
204       mem <- choose (0, maxMem)
205       dsk <- choose (0, maxDsk)
206       run_st <- arbitrary
207       pn <- arbitrary
208       sn <- arbitrary
209       vcpus <- choose (0, maxCpu)
210       return $ Instance.create name mem dsk vcpus run_st [] True pn sn
211                                Types.DTDrbd8
212
213 -- | Generas an arbitrary node based on sizing information.
214 genNode :: Maybe Int -- ^ Minimum node size in terms of units
215         -> Maybe Int -- ^ Maximum node size (when Nothing, bounded
216                      -- just by the max... constants)
217         -> Gen Node.Node
218 genNode min_multiplier max_multiplier = do
219   let (base_mem, base_dsk, base_cpu) =
220           case min_multiplier of
221             Just mm -> (mm * Types.unitMem,
222                         mm * Types.unitDsk,
223                         mm * Types.unitCpu)
224             Nothing -> (0, 0, 0)
225       (top_mem, top_dsk, top_cpu)  =
226           case max_multiplier of
227             Just mm -> (mm * Types.unitMem,
228                         mm * Types.unitDsk,
229                         mm * Types.unitCpu)
230             Nothing -> (maxMem, maxDsk, maxCpu)
231   name  <- getFQDN
232   mem_t <- choose (base_mem, top_mem)
233   mem_f <- choose (base_mem, mem_t)
234   mem_n <- choose (0, mem_t - mem_f)
235   dsk_t <- choose (base_dsk, top_dsk)
236   dsk_f <- choose (base_dsk, dsk_t)
237   cpu_t <- choose (base_cpu, top_cpu)
238   offl  <- arbitrary
239   let n = Node.create name (fromIntegral mem_t) mem_n mem_f
240           (fromIntegral dsk_t) dsk_f (fromIntegral cpu_t) offl 0
241   return $ Node.buildPeers n Container.empty
242
243 -- and a random node
244 instance Arbitrary Node.Node where
245     arbitrary = genNode Nothing Nothing
246
247 -- replace disks
248 instance Arbitrary OpCodes.ReplaceDisksMode where
249   arbitrary = elements [ OpCodes.ReplaceOnPrimary
250                        , OpCodes.ReplaceOnSecondary
251                        , OpCodes.ReplaceNewSecondary
252                        , OpCodes.ReplaceAuto
253                        ]
254
255 instance Arbitrary OpCodes.OpCode where
256   arbitrary = do
257     op_id <- elements [ "OP_TEST_DELAY"
258                       , "OP_INSTANCE_REPLACE_DISKS"
259                       , "OP_INSTANCE_FAILOVER"
260                       , "OP_INSTANCE_MIGRATE"
261                       ]
262     (case op_id of
263         "OP_TEST_DELAY" ->
264           liftM3 OpCodes.OpTestDelay arbitrary arbitrary arbitrary
265         "OP_INSTANCE_REPLACE_DISKS" ->
266           liftM5 OpCodes.OpInstanceReplaceDisks arbitrary arbitrary
267           arbitrary arbitrary arbitrary
268         "OP_INSTANCE_FAILOVER" ->
269           liftM3 OpCodes.OpInstanceFailover arbitrary arbitrary
270                  arbitrary
271         "OP_INSTANCE_MIGRATE" ->
272           liftM5 OpCodes.OpInstanceMigrate arbitrary arbitrary
273                  arbitrary arbitrary
274           arbitrary
275         _ -> fail "Wrong opcode")
276
277 instance Arbitrary Jobs.OpStatus where
278   arbitrary = elements [minBound..maxBound]
279
280 instance Arbitrary Jobs.JobStatus where
281   arbitrary = elements [minBound..maxBound]
282
283 newtype SmallRatio = SmallRatio Double deriving Show
284 instance Arbitrary SmallRatio where
285     arbitrary = do
286       v <- choose (0, 1)
287       return $ SmallRatio v
288
289 instance Arbitrary Types.AllocPolicy where
290   arbitrary = elements [minBound..maxBound]
291
292 instance Arbitrary Types.DiskTemplate where
293   arbitrary = elements [minBound..maxBound]
294
295 instance Arbitrary Types.FailMode where
296     arbitrary = elements [minBound..maxBound]
297
298 instance Arbitrary a => Arbitrary (Types.OpResult a) where
299     arbitrary = arbitrary >>= \c ->
300                 case c of
301                   False -> liftM Types.OpFail arbitrary
302                   True -> liftM Types.OpGood arbitrary
303
304 -- * Actual tests
305
306 -- ** Utils tests
307
308 -- | If the list is not just an empty element, and if the elements do
309 -- not contain commas, then join+split should be idempotent.
310 prop_Utils_commaJoinSplit =
311     forAll (arbitrary `suchThat`
312             (\l -> l /= [""] && all (not . elem ',') l )) $ \lst ->
313     Utils.sepSplit ',' (Utils.commaJoin lst) ==? lst
314
315 -- | Split and join should always be idempotent.
316 prop_Utils_commaSplitJoin s =
317     Utils.commaJoin (Utils.sepSplit ',' s) ==? s
318
319 -- | fromObjWithDefault, we test using the Maybe monad and an integer
320 -- value.
321 prop_Utils_fromObjWithDefault def_value random_key =
322     -- a missing key will be returned with the default
323     Utils.fromObjWithDefault [] random_key def_value == Just def_value &&
324     -- a found key will be returned as is, not with default
325     Utils.fromObjWithDefault [(random_key, J.showJSON def_value)]
326          random_key (def_value+1) == Just def_value
327         where _types = def_value :: Integer
328
329 -- | Test that functional if' behaves like the syntactic sugar if.
330 prop_Utils_if'if :: Bool -> Int -> Int -> Gen Prop
331 prop_Utils_if'if cnd a b =
332     Utils.if' cnd a b ==? if cnd then a else b
333
334 -- | Test basic select functionality
335 prop_Utils_select :: Int      -- ^ Default result
336                   -> [Int]    -- ^ List of False values
337                   -> [Int]    -- ^ List of True values
338                   -> Gen Prop -- ^ Test result
339 prop_Utils_select def lst1 lst2 =
340   Utils.select def cndlist ==? expectedresult
341   where expectedresult = Utils.if' (null lst2) def (head lst2)
342         flist = map (\e -> (False, e)) lst1
343         tlist = map (\e -> (True, e)) lst2
344         cndlist = flist ++ tlist
345
346 -- | Test basic select functionality with undefined default
347 prop_Utils_select_undefd :: [Int]            -- ^ List of False values
348                          -> NonEmptyList Int -- ^ List of True values
349                          -> Gen Prop         -- ^ Test result
350 prop_Utils_select_undefd lst1 (NonEmpty lst2) =
351   Utils.select undefined cndlist ==? head lst2
352   where flist = map (\e -> (False, e)) lst1
353         tlist = map (\e -> (True, e)) lst2
354         cndlist = flist ++ tlist
355
356 -- | Test basic select functionality with undefined list values
357 prop_Utils_select_undefv :: [Int]            -- ^ List of False values
358                          -> NonEmptyList Int -- ^ List of True values
359                          -> Gen Prop         -- ^ Test result
360 prop_Utils_select_undefv lst1 (NonEmpty lst2) =
361   Utils.select undefined cndlist ==? head lst2
362   where flist = map (\e -> (False, e)) lst1
363         tlist = map (\e -> (True, e)) lst2
364         cndlist = flist ++ tlist ++ [undefined]
365
366 prop_Utils_parseUnit (NonNegative n) =
367     Utils.parseUnit (show n) == Types.Ok n &&
368     Utils.parseUnit (show n ++ "m") == Types.Ok n &&
369     (case Utils.parseUnit (show n ++ "M") of
370       Types.Ok m -> if n > 0
371                     then m < n  -- for positive values, X MB is less than X MiB
372                     else m == 0 -- but for 0, 0 MB == 0 MiB
373       Types.Bad _ -> False) &&
374     Utils.parseUnit (show n ++ "g") == Types.Ok (n*1024) &&
375     Utils.parseUnit (show n ++ "t") == Types.Ok (n*1048576) &&
376     Types.isBad (Utils.parseUnit (show n ++ "x")::Types.Result Int)
377     where _types = n::Int
378
379 -- | Test list for the Utils module.
380 testSuite "Utils"
381               [ 'prop_Utils_commaJoinSplit
382               , 'prop_Utils_commaSplitJoin
383               , 'prop_Utils_fromObjWithDefault
384               , 'prop_Utils_if'if
385               , 'prop_Utils_select
386               , 'prop_Utils_select_undefd
387               , 'prop_Utils_select_undefv
388               , 'prop_Utils_parseUnit
389               ]
390
391 -- ** PeerMap tests
392
393 -- | Make sure add is idempotent.
394 prop_PeerMap_addIdempotent pmap key em =
395     fn puniq ==? fn (fn puniq)
396     where _types = (pmap::PeerMap.PeerMap,
397                     key::PeerMap.Key, em::PeerMap.Elem)
398           fn = PeerMap.add key em
399           puniq = PeerMap.accumArray const pmap
400
401 -- | Make sure remove is idempotent.
402 prop_PeerMap_removeIdempotent pmap key =
403     fn puniq ==? fn (fn puniq)
404     where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
405           fn = PeerMap.remove key
406           puniq = PeerMap.accumArray const pmap
407
408 -- | Make sure a missing item returns 0.
409 prop_PeerMap_findMissing pmap key =
410     PeerMap.find key (PeerMap.remove key puniq) ==? 0
411     where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
412           puniq = PeerMap.accumArray const pmap
413
414 -- | Make sure an added item is found.
415 prop_PeerMap_addFind pmap key em =
416     PeerMap.find key (PeerMap.add key em puniq) ==? em
417     where _types = (pmap::PeerMap.PeerMap,
418                     key::PeerMap.Key, em::PeerMap.Elem)
419           puniq = PeerMap.accumArray const pmap
420
421 -- | Manual check that maxElem returns the maximum indeed, or 0 for null.
422 prop_PeerMap_maxElem pmap =
423     PeerMap.maxElem puniq ==? if null puniq then 0
424                               else (maximum . snd . unzip) puniq
425     where _types = pmap::PeerMap.PeerMap
426           puniq = PeerMap.accumArray const pmap
427
428 -- | List of tests for the PeerMap module.
429 testSuite "PeerMap"
430               [ 'prop_PeerMap_addIdempotent
431               , 'prop_PeerMap_removeIdempotent
432               , 'prop_PeerMap_maxElem
433               , 'prop_PeerMap_addFind
434               , 'prop_PeerMap_findMissing
435               ]
436
437 -- ** Container tests
438
439 prop_Container_addTwo cdata i1 i2 =
440     fn i1 i2 cont == fn i2 i1 cont &&
441        fn i1 i2 cont == fn i1 i2 (fn i1 i2 cont)
442     where _types = (cdata::[Int],
443                     i1::Int, i2::Int)
444           cont = foldl (\c x -> Container.add x x c) Container.empty cdata
445           fn x1 x2 = Container.addTwo x1 x1 x2 x2
446
447 prop_Container_nameOf node =
448   let nl = makeSmallCluster node 1
449       fnode = head (Container.elems nl)
450   in Container.nameOf nl (Node.idx fnode) ==? Node.name fnode
451
452 -- | We test that in a cluster, given a random node, we can find it by
453 -- its name and alias, as long as all names and aliases are unique,
454 -- and that we fail to find a non-existing name.
455 prop_Container_findByName node othername =
456   forAll (choose (1, 20)) $ \ cnt ->
457   forAll (choose (0, cnt - 1)) $ \ fidx ->
458   forAll (vector cnt) $ \ names ->
459   (length . nub) (map fst names ++ map snd names) ==
460   length names * 2 &&
461   not (othername `elem` (map fst names ++ map snd names)) ==>
462   let nl = makeSmallCluster node cnt
463       nodes = Container.elems nl
464       nodes' = map (\((name, alias), nn) -> (Node.idx nn,
465                                              nn { Node.name = name,
466                                                   Node.alias = alias }))
467                $ zip names nodes
468       nl' = Container.fromList nodes'
469       target = snd (nodes' !! fidx)
470   in Container.findByName nl' (Node.name target) == Just target &&
471      Container.findByName nl' (Node.alias target) == Just target &&
472      Container.findByName nl' othername == Nothing
473
474 testSuite "Container"
475               [ 'prop_Container_addTwo
476               , 'prop_Container_nameOf
477               , 'prop_Container_findByName
478               ]
479
480 -- ** Instance tests
481
482 -- Simple instance tests, we only have setter/getters
483
484 prop_Instance_creat inst =
485     Instance.name inst ==? Instance.alias inst
486
487 prop_Instance_setIdx inst idx =
488     Instance.idx (Instance.setIdx inst idx) ==? idx
489     where _types = (inst::Instance.Instance, idx::Types.Idx)
490
491 prop_Instance_setName inst name =
492     Instance.name newinst == name &&
493     Instance.alias newinst == name
494     where _types = (inst::Instance.Instance, name::String)
495           newinst = Instance.setName inst name
496
497 prop_Instance_setAlias inst name =
498     Instance.name newinst == Instance.name inst &&
499     Instance.alias newinst == name
500     where _types = (inst::Instance.Instance, name::String)
501           newinst = Instance.setAlias inst name
502
503 prop_Instance_setPri inst pdx =
504     Instance.pNode (Instance.setPri inst pdx) ==? pdx
505     where _types = (inst::Instance.Instance, pdx::Types.Ndx)
506
507 prop_Instance_setSec inst sdx =
508     Instance.sNode (Instance.setSec inst sdx) ==? sdx
509     where _types = (inst::Instance.Instance, sdx::Types.Ndx)
510
511 prop_Instance_setBoth inst pdx sdx =
512     Instance.pNode si == pdx && Instance.sNode si == sdx
513     where _types = (inst::Instance.Instance, pdx::Types.Ndx, sdx::Types.Ndx)
514           si = Instance.setBoth inst pdx sdx
515
516 prop_Instance_shrinkMG inst =
517     Instance.mem inst >= 2 * Types.unitMem ==>
518         case Instance.shrinkByType inst Types.FailMem of
519           Types.Ok inst' ->
520               Instance.mem inst' == Instance.mem inst - Types.unitMem
521           _ -> False
522
523 prop_Instance_shrinkMF inst =
524     forAll (choose (0, 2 * Types.unitMem - 1)) $ \mem ->
525     let inst' = inst { Instance.mem = mem}
526     in Types.isBad $ Instance.shrinkByType inst' Types.FailMem
527
528 prop_Instance_shrinkCG inst =
529     Instance.vcpus inst >= 2 * Types.unitCpu ==>
530         case Instance.shrinkByType inst Types.FailCPU of
531           Types.Ok inst' ->
532               Instance.vcpus inst' == Instance.vcpus inst - Types.unitCpu
533           _ -> False
534
535 prop_Instance_shrinkCF inst =
536     forAll (choose (0, 2 * Types.unitCpu - 1)) $ \vcpus ->
537     let inst' = inst { Instance.vcpus = vcpus }
538     in Types.isBad $ Instance.shrinkByType inst' Types.FailCPU
539
540 prop_Instance_shrinkDG inst =
541     Instance.dsk inst >= 2 * Types.unitDsk ==>
542         case Instance.shrinkByType inst Types.FailDisk of
543           Types.Ok inst' ->
544               Instance.dsk inst' == Instance.dsk inst - Types.unitDsk
545           _ -> False
546
547 prop_Instance_shrinkDF inst =
548     forAll (choose (0, 2 * Types.unitDsk - 1)) $ \dsk ->
549     let inst' = inst { Instance.dsk = dsk }
550     in Types.isBad $ Instance.shrinkByType inst' Types.FailDisk
551
552 prop_Instance_setMovable inst m =
553     Instance.movable inst' ==? m
554     where inst' = Instance.setMovable inst m
555
556 testSuite "Instance"
557               [ 'prop_Instance_creat
558               , 'prop_Instance_setIdx
559               , 'prop_Instance_setName
560               , 'prop_Instance_setAlias
561               , 'prop_Instance_setPri
562               , 'prop_Instance_setSec
563               , 'prop_Instance_setBoth
564               , 'prop_Instance_shrinkMG
565               , 'prop_Instance_shrinkMF
566               , 'prop_Instance_shrinkCG
567               , 'prop_Instance_shrinkCF
568               , 'prop_Instance_shrinkDG
569               , 'prop_Instance_shrinkDF
570               , 'prop_Instance_setMovable
571               ]
572
573 -- ** Text backend tests
574
575 -- Instance text loader tests
576
577 prop_Text_Load_Instance name mem dsk vcpus status
578                         (NonEmpty pnode) snode
579                         (NonNegative pdx) (NonNegative sdx) autobal dt =
580     pnode /= snode && pdx /= sdx ==>
581     let vcpus_s = show vcpus
582         dsk_s = show dsk
583         mem_s = show mem
584         status_s = Types.instanceStatusToRaw status
585         ndx = if null snode
586               then [(pnode, pdx)]
587               else [(pnode, pdx), (snode, sdx)]
588         nl = Data.Map.fromList ndx
589         tags = ""
590         sbal = if autobal then "Y" else "N"
591         sdt = Types.diskTemplateToRaw dt
592         inst = Text.loadInst nl
593                [name, mem_s, dsk_s, vcpus_s, status_s,
594                 sbal, pnode, snode, sdt, tags]
595         fail1 = Text.loadInst nl
596                [name, mem_s, dsk_s, vcpus_s, status_s,
597                 sbal, pnode, pnode, tags]
598         _types = ( name::String, mem::Int, dsk::Int
599                  , vcpus::Int, status::Types.InstanceStatus
600                  , snode::String
601                  , autobal::Bool)
602     in
603       case inst of
604         Types.Bad msg -> printTestCase ("Failed to load instance: " ++ msg)
605                          False
606         Types.Ok (_, i) -> printTestCase "Mismatch in some field while\
607                                          \ loading the instance" $
608             Instance.name i == name &&
609             Instance.vcpus i == vcpus &&
610             Instance.mem i == mem &&
611             Instance.pNode i == pdx &&
612             Instance.sNode i == (if null snode
613                                  then Node.noSecondary
614                                  else sdx) &&
615             Instance.autoBalance i == autobal &&
616             Types.isBad fail1
617
618 prop_Text_Load_InstanceFail ktn fields =
619     length fields /= 10 ==>
620     case Text.loadInst nl fields of
621       Types.Ok _ -> printTestCase "Managed to load instance from invalid\
622                                   \ data" False
623       Types.Bad msg -> printTestCase ("Unrecognised error message: " ++ msg) $
624                        "Invalid/incomplete instance data: '" `isPrefixOf` msg
625     where nl = Data.Map.fromList ktn
626
627 prop_Text_Load_Node name tm nm fm td fd tc fo =
628     let conv v = if v < 0
629                     then "?"
630                     else show v
631         tm_s = conv tm
632         nm_s = conv nm
633         fm_s = conv fm
634         td_s = conv td
635         fd_s = conv fd
636         tc_s = conv tc
637         fo_s = if fo
638                then "Y"
639                else "N"
640         any_broken = any (< 0) [tm, nm, fm, td, fd, tc]
641         gid = Group.uuid defGroup
642     in case Text.loadNode defGroupAssoc
643            [name, tm_s, nm_s, fm_s, td_s, fd_s, tc_s, fo_s, gid] of
644          Nothing -> False
645          Just (name', node) ->
646              if fo || any_broken
647              then Node.offline node
648              else Node.name node == name' && name' == name &&
649                   Node.alias node == name &&
650                   Node.tMem node == fromIntegral tm &&
651                   Node.nMem node == nm &&
652                   Node.fMem node == fm &&
653                   Node.tDsk node == fromIntegral td &&
654                   Node.fDsk node == fd &&
655                   Node.tCpu node == fromIntegral tc
656
657 prop_Text_Load_NodeFail fields =
658     length fields /= 8 ==> isNothing $ Text.loadNode Data.Map.empty fields
659
660 prop_Text_NodeLSIdempotent node =
661     (Text.loadNode defGroupAssoc.
662          Utils.sepSplit '|' . Text.serializeNode defGroupList) n ==
663     Just (Node.name n, n)
664     -- override failN1 to what loadNode returns by default
665     where n = node { Node.failN1 = True, Node.offline = False }
666
667 testSuite "Text"
668               [ 'prop_Text_Load_Instance
669               , 'prop_Text_Load_InstanceFail
670               , 'prop_Text_Load_Node
671               , 'prop_Text_Load_NodeFail
672               , 'prop_Text_NodeLSIdempotent
673               ]
674
675 -- ** Node tests
676
677 prop_Node_setAlias node name =
678     Node.name newnode == Node.name node &&
679     Node.alias newnode == name
680     where _types = (node::Node.Node, name::String)
681           newnode = Node.setAlias node name
682
683 prop_Node_setOffline node status =
684     Node.offline newnode ==? status
685     where newnode = Node.setOffline node status
686
687 prop_Node_setXmem node xm =
688     Node.xMem newnode ==? xm
689     where newnode = Node.setXmem node xm
690
691 prop_Node_setMcpu node mc =
692     Node.mCpu newnode ==? mc
693     where newnode = Node.setMcpu node mc
694
695 -- | Check that an instance add with too high memory or disk will be
696 -- rejected.
697 prop_Node_addPriFM node inst = Instance.mem inst >= Node.fMem node &&
698                                not (Node.failN1 node)
699                                ==>
700                                case Node.addPri node inst'' of
701                                  Types.OpFail Types.FailMem -> True
702                                  _ -> False
703     where _types = (node::Node.Node, inst::Instance.Instance)
704           inst' = setInstanceSmallerThanNode node inst
705           inst'' = inst' { Instance.mem = Instance.mem inst }
706
707 prop_Node_addPriFD node inst = Instance.dsk inst >= Node.fDsk node &&
708                                not (Node.failN1 node)
709                                ==>
710                                case Node.addPri node inst'' of
711                                  Types.OpFail Types.FailDisk -> True
712                                  _ -> False
713     where _types = (node::Node.Node, inst::Instance.Instance)
714           inst' = setInstanceSmallerThanNode node inst
715           inst'' = inst' { Instance.dsk = Instance.dsk inst }
716
717 prop_Node_addPriFC node inst (Positive extra) =
718     not (Node.failN1 node) ==>
719         case Node.addPri node inst'' of
720           Types.OpFail Types.FailCPU -> True
721           _ -> False
722     where _types = (node::Node.Node, inst::Instance.Instance)
723           inst' = setInstanceSmallerThanNode node inst
724           inst'' = inst' { Instance.vcpus = Node.availCpu node + extra }
725
726 -- | Check that an instance add with too high memory or disk will be
727 -- rejected.
728 prop_Node_addSec node inst pdx =
729     (Instance.mem inst >= (Node.fMem node - Node.rMem node) ||
730      Instance.dsk inst >= Node.fDsk node) &&
731     not (Node.failN1 node)
732     ==> isFailure (Node.addSec node inst pdx)
733         where _types = (node::Node.Node, inst::Instance.Instance, pdx::Int)
734
735 -- | Checks for memory reservation changes.
736 prop_Node_rMem inst =
737     forAll (arbitrary `suchThat` ((> Types.unitMem) . Node.fMem)) $ \node ->
738     -- ab = auto_balance, nb = non-auto_balance
739     -- we use -1 as the primary node of the instance
740     let inst' = inst { Instance.pNode = -1, Instance.autoBalance = True }
741         inst_ab = setInstanceSmallerThanNode node inst'
742         inst_nb = inst_ab { Instance.autoBalance = False }
743         -- now we have the two instances, identical except the
744         -- autoBalance attribute
745         orig_rmem = Node.rMem node
746         inst_idx = Instance.idx inst_ab
747         node_add_ab = Node.addSec node inst_ab (-1)
748         node_add_nb = Node.addSec node inst_nb (-1)
749         node_del_ab = liftM (`Node.removeSec` inst_ab) node_add_ab
750         node_del_nb = liftM (`Node.removeSec` inst_nb) node_add_nb
751     in case (node_add_ab, node_add_nb, node_del_ab, node_del_nb) of
752          (Types.OpGood a_ab, Types.OpGood a_nb,
753           Types.OpGood d_ab, Types.OpGood d_nb) ->
754              printTestCase "Consistency checks failed" $
755              Node.rMem a_ab >  orig_rmem &&
756              Node.rMem a_ab - orig_rmem == Instance.mem inst_ab &&
757              Node.rMem a_nb == orig_rmem &&
758              Node.rMem d_ab == orig_rmem &&
759              Node.rMem d_nb == orig_rmem &&
760              -- this is not related to rMem, but as good a place to
761              -- test as any
762              inst_idx `elem` Node.sList a_ab &&
763              not (inst_idx `elem` Node.sList d_ab)
764          x -> printTestCase ("Failed to add/remove instances: " ++ show x)
765               False
766
767 -- | Check mdsk setting.
768 prop_Node_setMdsk node mx =
769     Node.loDsk node' >= 0 &&
770     fromIntegral (Node.loDsk node') <= Node.tDsk node &&
771     Node.availDisk node' >= 0 &&
772     Node.availDisk node' <= Node.fDsk node' &&
773     fromIntegral (Node.availDisk node') <= Node.tDsk node' &&
774     Node.mDsk node' == mx'
775     where _types = (node::Node.Node, mx::SmallRatio)
776           node' = Node.setMdsk node mx'
777           SmallRatio mx' = mx
778
779 -- Check tag maps
780 prop_Node_tagMaps_idempotent tags =
781     Node.delTags (Node.addTags m tags) tags ==? m
782     where m = Data.Map.empty
783
784 prop_Node_tagMaps_reject tags =
785     not (null tags) ==>
786     all (\t -> Node.rejectAddTags m [t]) tags
787     where m = Node.addTags Data.Map.empty tags
788
789 prop_Node_showField node =
790   forAll (elements Node.defaultFields) $ \ field ->
791   fst (Node.showHeader field) /= Types.unknownField &&
792   Node.showField node field /= Types.unknownField
793
794 prop_Node_computeGroups nodes =
795   let ng = Node.computeGroups nodes
796       onlyuuid = map fst ng
797   in length nodes == sum (map (length . snd) ng) &&
798      all (\(guuid, ns) -> all ((== guuid) . Node.group) ns) ng &&
799      length (nub onlyuuid) == length onlyuuid &&
800      (null nodes || not (null ng))
801
802 testSuite "Node"
803               [ 'prop_Node_setAlias
804               , 'prop_Node_setOffline
805               , 'prop_Node_setMcpu
806               , 'prop_Node_setXmem
807               , 'prop_Node_addPriFM
808               , 'prop_Node_addPriFD
809               , 'prop_Node_addPriFC
810               , 'prop_Node_addSec
811               , 'prop_Node_rMem
812               , 'prop_Node_setMdsk
813               , 'prop_Node_tagMaps_idempotent
814               , 'prop_Node_tagMaps_reject
815               , 'prop_Node_showField
816               , 'prop_Node_computeGroups
817               ]
818
819 -- ** Cluster tests
820
821 -- | Check that the cluster score is close to zero for a homogeneous
822 -- cluster.
823 prop_Score_Zero node =
824     forAll (choose (1, 1024)) $ \count ->
825     (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
826      (Node.tDsk node > 0) && (Node.tMem node > 0)) ==>
827     let fn = Node.buildPeers node Container.empty
828         nlst = replicate count fn
829         score = Cluster.compCVNodes nlst
830     -- we can't say == 0 here as the floating point errors accumulate;
831     -- this should be much lower than the default score in CLI.hs
832     in score <= 1e-12
833
834 -- | Check that cluster stats are sane.
835 prop_CStats_sane node =
836     forAll (choose (1, 1024)) $ \count ->
837     (not (Node.offline node) && not (Node.failN1 node) &&
838      (Node.availDisk node > 0) && (Node.availMem node > 0)) ==>
839     let fn = Node.buildPeers node Container.empty
840         nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
841         nl = Container.fromList nlst
842         cstats = Cluster.totalResources nl
843     in Cluster.csAdsk cstats >= 0 &&
844        Cluster.csAdsk cstats <= Cluster.csFdsk cstats
845
846 -- | Check that one instance is allocated correctly, without
847 -- rebalances needed.
848 prop_ClusterAlloc_sane node inst =
849     forAll (choose (5, 20)) $ \count ->
850     not (Node.offline node)
851             && not (Node.failN1 node)
852             && Node.availDisk node > 0
853             && Node.availMem node > 0
854             ==>
855     let nl = makeSmallCluster node count
856         il = Container.empty
857         inst' = setInstanceSmallerThanNode node inst
858     in case Cluster.genAllocNodes defGroupList nl 2 True >>=
859        Cluster.tryAlloc nl il inst' of
860          Types.Bad _ -> False
861          Types.Ok as ->
862              case Cluster.asSolution as of
863                Nothing -> False
864                Just (xnl, xi, _, cv) ->
865                    let il' = Container.add (Instance.idx xi) xi il
866                        tbl = Cluster.Table xnl il' cv []
867                    in not (canBalance tbl True True False)
868
869 -- | Checks that on a 2-5 node cluster, we can allocate a random
870 -- instance spec via tiered allocation (whatever the original instance
871 -- spec), on either one or two nodes.
872 prop_ClusterCanTieredAlloc node inst =
873     forAll (choose (2, 5)) $ \count ->
874     forAll (choose (1, 2)) $ \rqnodes ->
875     not (Node.offline node)
876             && not (Node.failN1 node)
877             && isNodeBig node 4
878             ==>
879     let nl = makeSmallCluster node count
880         il = Container.empty
881         allocnodes = Cluster.genAllocNodes defGroupList nl rqnodes True
882     in case allocnodes >>= \allocnodes' ->
883         Cluster.tieredAlloc nl il (Just 1) inst allocnodes' [] [] of
884          Types.Bad _ -> False
885          Types.Ok (_, _, il', ixes, cstats) -> not (null ixes) &&
886                                       IntMap.size il' == length ixes &&
887                                       length ixes == length cstats
888
889 -- | Checks that on a 4-8 node cluster, once we allocate an instance,
890 -- we can also evacuate it.
891 prop_ClusterAllocEvac node inst =
892     forAll (choose (4, 8)) $ \count ->
893     not (Node.offline node)
894             && not (Node.failN1 node)
895             && isNodeBig node 4
896             ==>
897     let nl = makeSmallCluster node count
898         il = Container.empty
899         inst' = setInstanceSmallerThanNode node inst
900     in case Cluster.genAllocNodes defGroupList nl 2 True >>=
901        Cluster.tryAlloc nl il inst' of
902          Types.Bad _ -> False
903          Types.Ok as ->
904              case Cluster.asSolution as of
905                Nothing -> False
906                Just (xnl, xi, _, _) ->
907                    let sdx = Instance.sNode xi
908                        il' = Container.add (Instance.idx xi) xi il
909                    in case IAlloc.processRelocate defGroupList xnl il'
910                           (Instance.idx xi) 1 [sdx] of
911                         Types.Ok _ -> True
912                         _ -> False
913
914 -- | Check that allocating multiple instances on a cluster, then
915 -- adding an empty node, results in a valid rebalance.
916 prop_ClusterAllocBalance =
917     forAll (genNode (Just 5) (Just 128)) $ \node ->
918     forAll (choose (3, 5)) $ \count ->
919     not (Node.offline node) && not (Node.failN1 node) ==>
920     let nl = makeSmallCluster node count
921         (hnode, nl') = IntMap.deleteFindMax nl
922         il = Container.empty
923         allocnodes = Cluster.genAllocNodes defGroupList nl' 2 True
924         i_templ = createInstance Types.unitMem Types.unitDsk Types.unitCpu
925     in case allocnodes >>= \allocnodes' ->
926         Cluster.iterateAlloc nl' il (Just 5) i_templ allocnodes' [] [] of
927          Types.Bad _ -> False
928          Types.Ok (_, xnl, il', _, _) ->
929                    let ynl = Container.add (Node.idx hnode) hnode xnl
930                        cv = Cluster.compCV ynl
931                        tbl = Cluster.Table ynl il' cv []
932                    in canBalance tbl True True False
933
934 -- | Checks consistency.
935 prop_ClusterCheckConsistency node inst =
936   let nl = makeSmallCluster node 3
937       [node1, node2, node3] = Container.elems nl
938       node3' = node3 { Node.group = 1 }
939       nl' = Container.add (Node.idx node3') node3' nl
940       inst1 = Instance.setBoth inst (Node.idx node1) (Node.idx node2)
941       inst2 = Instance.setBoth inst (Node.idx node1) Node.noSecondary
942       inst3 = Instance.setBoth inst (Node.idx node1) (Node.idx node3)
943       ccheck = Cluster.findSplitInstances nl' . Container.fromList
944   in null (ccheck [(0, inst1)]) &&
945      null (ccheck [(0, inst2)]) &&
946      (not . null $ ccheck [(0, inst3)])
947
948 -- | For now, we only test that we don't lose instances during the split.
949 prop_ClusterSplitCluster node inst =
950   forAll (choose (0, 100)) $ \icnt ->
951   let nl = makeSmallCluster node 2
952       (nl', il') = foldl (\(ns, is) _ -> assignInstance ns is inst 0 1)
953                    (nl, Container.empty) [1..icnt]
954       gni = Cluster.splitCluster nl' il'
955   in sum (map (Container.size . snd . snd) gni) == icnt &&
956      all (\(guuid, (nl'', _)) -> all ((== guuid) . Node.group)
957                                  (Container.elems nl'')) gni
958
959 testSuite "Cluster"
960               [ 'prop_Score_Zero
961               , 'prop_CStats_sane
962               , 'prop_ClusterAlloc_sane
963               , 'prop_ClusterCanTieredAlloc
964               , 'prop_ClusterAllocEvac
965               , 'prop_ClusterAllocBalance
966               , 'prop_ClusterCheckConsistency
967               , 'prop_ClusterSplitCluster
968               ]
969
970 -- ** OpCodes tests
971
972 -- | Check that opcode serialization is idempotent.
973 prop_OpCodes_serialization op =
974   case J.readJSON (J.showJSON op) of
975     J.Error e -> printTestCase ("Cannot deserialise: " ++ e) False
976     J.Ok op' -> op ==? op'
977   where _types = op::OpCodes.OpCode
978
979 testSuite "OpCodes"
980               [ 'prop_OpCodes_serialization ]
981
982 -- ** Jobs tests
983
984 -- | Check that (queued) job\/opcode status serialization is idempotent.
985 prop_OpStatus_serialization os =
986   case J.readJSON (J.showJSON os) of
987     J.Error e -> printTestCase ("Cannot deserialise: " ++ e) False
988     J.Ok os' -> os ==? os'
989   where _types = os::Jobs.OpStatus
990
991 prop_JobStatus_serialization js =
992   case J.readJSON (J.showJSON js) of
993     J.Error e -> printTestCase ("Cannot deserialise: " ++ e) False
994     J.Ok js' -> js ==? js'
995   where _types = js::Jobs.JobStatus
996
997 testSuite "Jobs"
998               [ 'prop_OpStatus_serialization
999               , 'prop_JobStatus_serialization
1000               ]
1001
1002 -- ** Loader tests
1003
1004 prop_Loader_lookupNode ktn inst node =
1005   Loader.lookupNode nl inst node ==? Data.Map.lookup node nl
1006   where nl = Data.Map.fromList ktn
1007
1008 prop_Loader_lookupInstance kti inst =
1009   Loader.lookupInstance il inst ==? Data.Map.lookup inst il
1010   where il = Data.Map.fromList kti
1011
1012 prop_Loader_assignIndices nodes =
1013   Data.Map.size nassoc == length nodes &&
1014   Container.size kt == length nodes &&
1015   (if not (null nodes)
1016    then maximum (IntMap.keys kt) == length nodes - 1
1017    else True)
1018   where (nassoc, kt) = Loader.assignIndices (map (\n -> (Node.name n, n)) nodes)
1019
1020 -- | Checks that the number of primary instances recorded on the nodes
1021 -- is zero.
1022 prop_Loader_mergeData ns =
1023   let na = Container.fromList $ map (\n -> (Node.idx n, n)) ns
1024   in case Loader.mergeData [] [] [] []
1025          (Loader.emptyCluster {Loader.cdNodes = na}) of
1026     Types.Bad _ -> False
1027     Types.Ok (Loader.ClusterData _ nl il _) ->
1028       let nodes = Container.elems nl
1029           instances = Container.elems il
1030       in (sum . map (length . Node.pList)) nodes == 0 &&
1031          null instances
1032
1033 -- | Check that compareNameComponent on equal strings works.
1034 prop_Loader_compareNameComponent_equal :: String -> Bool
1035 prop_Loader_compareNameComponent_equal s =
1036   Loader.compareNameComponent s s ==
1037     Loader.LookupResult Loader.ExactMatch s
1038
1039 -- | Check that compareNameComponent on prefix strings works.
1040 prop_Loader_compareNameComponent_prefix :: NonEmptyList Char -> String -> Bool
1041 prop_Loader_compareNameComponent_prefix (NonEmpty s1) s2 =
1042   Loader.compareNameComponent (s1 ++ "." ++ s2) s1 ==
1043     Loader.LookupResult Loader.PartialMatch s1
1044
1045 testSuite "Loader"
1046               [ 'prop_Loader_lookupNode
1047               , 'prop_Loader_lookupInstance
1048               , 'prop_Loader_assignIndices
1049               , 'prop_Loader_mergeData
1050               , 'prop_Loader_compareNameComponent_equal
1051               , 'prop_Loader_compareNameComponent_prefix
1052               ]
1053
1054 -- ** Types tests
1055
1056 prop_Types_AllocPolicy_serialisation apol =
1057     case J.readJSON (J.showJSON apol) of
1058       J.Ok p -> printTestCase ("invalid deserialisation " ++ show p) $
1059                 p == apol
1060       J.Error s -> printTestCase ("failed to deserialise: " ++ s) False
1061     where _types = apol::Types.AllocPolicy
1062
1063 prop_Types_DiskTemplate_serialisation dt =
1064     case J.readJSON (J.showJSON dt) of
1065       J.Ok p -> printTestCase ("invalid deserialisation " ++ show p) $
1066                 p == dt
1067       J.Error s -> printTestCase ("failed to deserialise: " ++ s)
1068                    False
1069     where _types = dt::Types.DiskTemplate
1070
1071 prop_Types_opToResult op =
1072     case op of
1073       Types.OpFail _ -> Types.isBad r
1074       Types.OpGood v -> case r of
1075                           Types.Bad _ -> False
1076                           Types.Ok v' -> v == v'
1077     where r = Types.opToResult op
1078           _types = op::Types.OpResult Int
1079
1080 prop_Types_eitherToResult ei =
1081     case ei of
1082       Left _ -> Types.isBad r
1083       Right v -> case r of
1084                    Types.Bad _ -> False
1085                    Types.Ok v' -> v == v'
1086     where r = Types.eitherToResult ei
1087           _types = ei::Either String Int
1088
1089 testSuite "Types"
1090               [ 'prop_Types_AllocPolicy_serialisation
1091               , 'prop_Types_DiskTemplate_serialisation
1092               , 'prop_Types_opToResult
1093               , 'prop_Types_eitherToResult
1094               ]