1 {-# LANGUAGE TemplateHaskell, TypeSynonymInstances, FlexibleInstances,
3 {-# OPTIONS_GHC -fno-warn-orphans #-}
5 {-| Unittests for ganeti-htools.
11 Copyright (C) 2009, 2010, 2011, 2012, 2013 Google Inc.
13 This program is free software; you can redistribute it and/or modify
14 it under the terms of the GNU General Public License as published by
15 the Free Software Foundation; either version 2 of the License, or
16 (at your option) any later version.
18 This program is distributed in the hope that it will be useful, but
19 WITHOUT ANY WARRANTY; without even the implied warranty of
20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 General Public License for more details.
23 You should have received a copy of the GNU General Public License
24 along with this program; if not, write to the Free Software
25 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
30 module Test.Ganeti.Objects
33 , genConfigDataWithNetworks
42 import Test.QuickCheck
43 import qualified Test.HUnit as HUnit
45 import Control.Applicative
48 import qualified Data.List as List
49 import qualified Data.Map as Map
50 import Data.Maybe (fromMaybe)
51 import qualified Data.Set as Set
52 import GHC.Exts (IsString(..))
53 import qualified Text.JSON as J
55 import Test.Ganeti.TestHelper
56 import Test.Ganeti.TestCommon
57 import Test.Ganeti.Types ()
59 import qualified Ganeti.Constants as C
61 import Ganeti.Objects as Objects
65 -- * Arbitrary instances
67 $(genArbitrary ''PartialNDParams)
69 instance Arbitrary Node where
70 arbitrary = Node <$> genFQDN <*> genFQDN <*> genFQDN
71 <*> arbitrary <*> arbitrary <*> arbitrary <*> genFQDN
72 <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
73 <*> arbitrary <*> arbitrary <*> genFQDN <*> arbitrary
74 <*> (Set.fromList <$> genTags)
76 $(genArbitrary ''BlockDriver)
78 $(genArbitrary ''DiskMode)
80 instance Arbitrary DiskLogicalId where
81 arbitrary = oneof [ LIDPlain <$> arbitrary <*> arbitrary
82 , LIDDrbd8 <$> genFQDN <*> genFQDN <*> arbitrary
83 <*> arbitrary <*> arbitrary <*> arbitrary
84 , LIDFile <$> arbitrary <*> arbitrary
85 , LIDBlockDev <$> arbitrary <*> arbitrary
86 , LIDRados <$> arbitrary <*> arbitrary
89 -- | 'Disk' 'arbitrary' instance. Since we don't test disk hierarchy
90 -- properties, we only generate disks with no children (FIXME), as
91 -- generating recursive datastructures is a bit more work.
92 instance Arbitrary Disk where
93 arbitrary = Disk <$> arbitrary <*> pure [] <*> arbitrary
94 <*> arbitrary <*> arbitrary <*> arbitrary
97 -- FIXME: we should generate proper values, >=0, etc., but this is
98 -- hard for partial ones, where all must be wrapped in a 'Maybe'
99 $(genArbitrary ''PartialBeParams)
101 $(genArbitrary ''AdminState)
103 $(genArbitrary ''PartialNicParams)
105 $(genArbitrary ''PartialNic)
107 instance Arbitrary Instance where
119 -- FIXME: add non-empty hvparams when they're a proper type
120 <*> pure (GenericContainer Map.empty)
124 <*> pure (GenericContainer Map.empty)
136 <*> arbitrary <*> arbitrary
142 <*> (Set.fromList <$> genTags)
144 -- | Generates an instance that is connected to the given networks
145 -- and possibly some other networks
146 genInstWithNets :: [String] -> Gen Instance
147 genInstWithNets nets = do
148 plain_inst <- arbitrary
151 nicparams <- arbitrary
154 -- generate some more networks than the given ones
155 num_more_nets <- choose (0,3)
156 more_nets <- vectorOf num_more_nets genName
157 let genNic net = PartialNic mac ip nicparams net name uuid
158 partial_nics = map (genNic . Just)
159 (List.nub (nets ++ more_nets))
160 new_inst = plain_inst { instNics = partial_nics }
163 genDiskWithChildren :: Int -> Gen Disk
164 genDiskWithChildren num_children = do
165 logicalid <- arbitrary
166 children <- vectorOf num_children (genDiskWithChildren 0)
170 name <- genMaybe genName
172 let disk = Disk logicalid children ivname size mode name uuid
176 genDisk = genDiskWithChildren 3
178 -- | FIXME: This generates completely random data, without normal
180 $(genArbitrary ''PartialISpecParams)
182 -- | FIXME: This generates completely random data, without normal
184 $(genArbitrary ''PartialIPolicy)
186 $(genArbitrary ''FilledISpecParams)
187 $(genArbitrary ''MinMaxISpecs)
188 $(genArbitrary ''FilledIPolicy)
189 $(genArbitrary ''IpFamily)
190 $(genArbitrary ''FilledNDParams)
191 $(genArbitrary ''FilledNicParams)
192 $(genArbitrary ''FilledBeParams)
194 -- | No real arbitrary instance for 'ClusterHvParams' yet.
195 instance Arbitrary ClusterHvParams where
196 arbitrary = return $ GenericContainer Map.empty
198 -- | No real arbitrary instance for 'OsHvParams' yet.
199 instance Arbitrary OsHvParams where
200 arbitrary = return $ GenericContainer Map.empty
202 instance Arbitrary ClusterNicParams where
203 arbitrary = (GenericContainer . Map.singleton C.ppDefault) <$> arbitrary
205 instance Arbitrary OsParams where
206 arbitrary = (GenericContainer . Map.fromList) <$> arbitrary
208 instance Arbitrary ClusterOsParams where
209 arbitrary = (GenericContainer . Map.fromList) <$> arbitrary
211 instance Arbitrary ClusterBeParams where
212 arbitrary = (GenericContainer . Map.fromList) <$> arbitrary
214 instance Arbitrary TagSet where
215 arbitrary = Set.fromList <$> genTags
217 $(genArbitrary ''Cluster)
219 instance Arbitrary Network where
220 arbitrary = genValidNetwork
222 -- | Generates a network instance with minimum netmasks of /24. Generating
223 -- bigger networks slows down the tests, because long bit strings are generated
224 -- for the reservations.
225 genValidNetwork :: Gen Objects.Network
227 -- generate netmask for the IPv4 network
228 netmask <- fromIntegral <$> choose (24::Int, 30)
229 name <- genName >>= mkNonEmpty
230 mac_prefix <- genMaybe genName
232 net6 <- genMaybe genIp6Net
233 gateway <- genMaybe arbitrary
234 gateway6 <- genMaybe genIp6Addr
235 res <- liftM Just (genBitString $ netmask2NumHosts netmask)
236 ext_res <- liftM Just (genBitString $ netmask2NumHosts netmask)
238 let n = Network name mac_prefix (Ip4Network net netmask) net6 gateway
239 gateway6 res ext_res uuid 0 Set.empty
242 -- | Generate an arbitrary string consisting of '0' and '1' of the given length.
243 genBitString :: Int -> Gen String
244 genBitString len = vectorOf len (elements "01")
246 -- | Generate an arbitrary string consisting of '0' and '1' of the maximum given
248 genBitStringMaxLen :: Int -> Gen String
249 genBitStringMaxLen maxLen = choose (0, maxLen) >>= genBitString
251 -- | Generator for config data with an empty cluster (no instances),
252 -- with N defined nodes.
253 genEmptyCluster :: Int -> Gen ConfigData
254 genEmptyCluster ncount = do
255 nodes <- vector ncount
258 let guuid = groupUuid grp
259 nodes' = zipWith (\n idx ->
260 let newname = nodeName n ++ "-" ++ show idx
261 in (newname, n { nodeGroup = guuid,
262 nodeName = newname}))
264 nodemap = Map.fromList nodes'
265 contnodes = if Map.size nodemap /= ncount
266 then error ("Inconsistent node map, duplicates in" ++
267 " node name list? Names: " ++
268 show (map fst nodes'))
269 else GenericContainer nodemap
270 continsts = GenericContainer Map.empty
271 networks = GenericContainer Map.empty
272 let contgroups = GenericContainer $ Map.singleton guuid grp
274 cluster <- resize 8 arbitrary
275 let c = ConfigData version cluster contnodes contgroups continsts networks
279 -- | FIXME: make an even simpler base version of creating a cluster.
281 -- | Generates config data with a couple of networks.
282 genConfigDataWithNetworks :: ConfigData -> Gen ConfigData
283 genConfigDataWithNetworks old_cfg = do
284 num_nets <- choose (0, 3)
285 -- generate a list of network names (no duplicates)
286 net_names <- genUniquesList num_nets genName >>= mapM mkNonEmpty
287 -- generate a random list of networks (possibly with duplicate names)
288 nets <- vectorOf num_nets genValidNetwork
289 -- use unique names for the networks
290 let nets_unique = map ( \(name, net) -> net { networkName = name } )
292 net_map = GenericContainer $ Map.fromList
293 (map (\n -> (networkUuid n, n)) nets_unique)
294 new_cfg = old_cfg { configNetworks = net_map }
299 -- | Tests that fillDict behaves correctly
300 prop_fillDict :: [(Int, Int)] -> [(Int, Int)] -> Property
301 prop_fillDict defaults custom =
302 let d_map = Map.fromList defaults
303 d_keys = map fst defaults
304 c_map = Map.fromList custom
305 c_keys = map fst custom
306 in conjoin [ printTestCase "Empty custom filling"
307 (fillDict d_map Map.empty [] == d_map)
308 , printTestCase "Empty defaults filling"
309 (fillDict Map.empty c_map [] == c_map)
310 , printTestCase "Delete all keys"
311 (fillDict d_map c_map (d_keys++c_keys) == Map.empty)
314 -- | Test that the serialisation of 'DiskLogicalId', which is
315 -- implemented manually, is idempotent. Since we don't have a
316 -- standalone JSON instance for DiskLogicalId (it's a data type that
317 -- expands over two fields in a JSObject), we test this by actially
318 -- testing entire Disk serialisations. So this tests two things at
320 prop_Disk_serialisation :: Disk -> Property
321 prop_Disk_serialisation = testSerialisation
323 -- | Check that node serialisation is idempotent.
324 prop_Node_serialisation :: Node -> Property
325 prop_Node_serialisation = testSerialisation
327 -- | Check that instance serialisation is idempotent.
328 prop_Inst_serialisation :: Instance -> Property
329 prop_Inst_serialisation = testSerialisation
331 -- | Check that network serialisation is idempotent.
332 prop_Network_serialisation :: Network -> Property
333 prop_Network_serialisation = testSerialisation
335 -- | Check config serialisation.
336 prop_Config_serialisation :: Property
337 prop_Config_serialisation =
338 forAll (choose (0, maxNodes `div` 4) >>= genEmptyCluster) testSerialisation
340 -- | Custom HUnit test to check the correspondence between Haskell-generated
341 -- networks and their Python decoded, validated and re-encoded version.
342 -- For the technical background of this unit test, check the documentation
343 -- of "case_py_compat_types" of test/hs/Test/Ganeti/Opcodes.hs
344 casePyCompatNetworks :: HUnit.Assertion
345 casePyCompatNetworks = do
346 let num_networks = 500::Int
347 networks <- genSample (vectorOf num_networks genValidNetwork)
348 let networks_with_properties = map getNetworkProperties networks
349 serialized = J.encode networks
350 -- check for non-ASCII fields, usually due to 'arbitrary :: String'
351 mapM_ (\net -> when (any (not . isAscii) (J.encode net)) .
352 HUnit.assertFailure $
353 "Network has non-ASCII fields: " ++ show net
356 runPython "from ganeti import network\n\
357 \from ganeti import objects\n\
358 \from ganeti import serializer\n\
360 \net_data = serializer.Load(sys.stdin.read())\n\
361 \decoded = [objects.Network.FromDict(n) for n in net_data]\n\
363 \for net in decoded:\n\
364 \ a = network.AddressPool(net)\n\
365 \ encoded.append((a.GetFreeCount(), a.GetReservedCount(), \\\n\
367 \print serializer.Dump(encoded)" serialized
368 >>= checkPythonResult
369 let deserialised = J.decode py_stdout::J.Result [(Int, Int, Network)]
370 decoded <- case deserialised of
371 J.Ok ops -> return ops
373 HUnit.assertFailure ("Unable to decode networks: " ++ msg)
374 -- this already raised an expection, but we need it
376 >> fail "Unable to decode networks"
377 HUnit.assertEqual "Mismatch in number of returned networks"
378 (length decoded) (length networks_with_properties)
379 mapM_ (uncurry (HUnit.assertEqual "Different result after encoding/decoding")
380 ) $ zip decoded networks_with_properties
382 -- | Creates a tuple of the given network combined with some of its properties
383 -- to be compared against the same properties generated by the python code.
384 getNetworkProperties :: Network -> (Int, Int, Network)
385 getNetworkProperties net =
386 let maybePool = createAddressPool net
388 (Just pool) -> (getFreeCount pool, getReservedCount pool, net)
389 Nothing -> (-1, -1, net)
391 -- | Tests the compatibility between Haskell-serialized node groups and their
392 -- python-decoded and encoded version.
393 casePyCompatNodegroups :: HUnit.Assertion
394 casePyCompatNodegroups = do
395 let num_groups = 500::Int
396 groups <- genSample (vectorOf num_groups genNodeGroup)
397 let serialized = J.encode groups
398 -- check for non-ASCII fields, usually due to 'arbitrary :: String'
399 mapM_ (\group -> when (any (not . isAscii) (J.encode group)) .
400 HUnit.assertFailure $
401 "Node group has non-ASCII fields: " ++ show group
404 runPython "from ganeti import objects\n\
405 \from ganeti import serializer\n\
407 \group_data = serializer.Load(sys.stdin.read())\n\
408 \decoded = [objects.NodeGroup.FromDict(g) for g in group_data]\n\
409 \encoded = [g.ToDict() for g in decoded]\n\
410 \print serializer.Dump(encoded)" serialized
411 >>= checkPythonResult
412 let deserialised = J.decode py_stdout::J.Result [NodeGroup]
413 decoded <- case deserialised of
414 J.Ok ops -> return ops
416 HUnit.assertFailure ("Unable to decode node groups: " ++ msg)
417 -- this already raised an expection, but we need it
419 >> fail "Unable to decode node groups"
420 HUnit.assertEqual "Mismatch in number of returned node groups"
421 (length decoded) (length groups)
422 mapM_ (uncurry (HUnit.assertEqual "Different result after encoding/decoding")
423 ) $ zip decoded groups
425 -- | Generates a node group with up to 3 networks.
426 -- | FIXME: This generates still somewhat completely random data, without normal
428 genNodeGroup :: Gen NodeGroup
432 ndparams <- arbitrary
433 alloc_policy <- arbitrary
435 diskparams <- pure (GenericContainer Map.empty)
436 num_networks <- choose (0, 3)
437 net_uuid_list <- vectorOf num_networks (arbitrary::Gen String)
438 nic_param_list <- vectorOf num_networks (arbitrary::Gen PartialNicParams)
439 net_map <- pure (GenericContainer . Map.fromList $
440 zip net_uuid_list nic_param_list)
444 uuid <- genFQDN `suchThat` (/= name)
446 tags <- Set.fromList <$> genTags
447 let group = NodeGroup name members ndparams alloc_policy ipolicy diskparams
448 net_map ctime mtime uuid serial tags
451 instance Arbitrary NodeGroup where
452 arbitrary = genNodeGroup
454 $(genArbitrary ''Ip4Address)
456 $(genArbitrary ''Ip4Network)
458 -- | Helper to compute absolute value of an IPv4 address.
459 ip4AddrValue :: Ip4Address -> Integer
460 ip4AddrValue (Ip4Address a b c d) =
461 fromIntegral a * (2^(24::Integer)) +
462 fromIntegral b * (2^(16::Integer)) +
463 fromIntegral c * (2^(8::Integer)) + fromIntegral d
465 -- | Tests that any difference between IPv4 consecutive addresses is 1.
466 prop_nextIp4Address :: Ip4Address -> Property
467 prop_nextIp4Address ip4 =
468 ip4AddrValue (nextIp4Address ip4) ==? ip4AddrValue ip4 + 1
470 -- | IsString instance for 'Ip4Address', to help write the tests.
471 instance IsString Ip4Address where
473 fromMaybe (error $ "Failed to parse address from " ++ s) (readIp4Address s)
475 -- | Tests a few simple cases of IPv4 next address.
476 caseNextIp4Address :: HUnit.Assertion
477 caseNextIp4Address = do
478 HUnit.assertEqual "" "0.0.0.1" $ nextIp4Address "0.0.0.0"
479 HUnit.assertEqual "" "0.0.0.0" $ nextIp4Address "255.255.255.255"
480 HUnit.assertEqual "" "1.2.3.5" $ nextIp4Address "1.2.3.4"
481 HUnit.assertEqual "" "1.3.0.0" $ nextIp4Address "1.2.255.255"
482 HUnit.assertEqual "" "1.2.255.63" $ nextIp4Address "1.2.255.62"
486 , 'prop_Disk_serialisation
487 , 'prop_Inst_serialisation
488 , 'prop_Network_serialisation
489 , 'prop_Node_serialisation
490 , 'prop_Config_serialisation
491 , 'casePyCompatNetworks
492 , 'casePyCompatNodegroups
493 , 'prop_nextIp4Address
494 , 'caseNextIp4Address