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