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
40 import Test.QuickCheck
41 import qualified Test.HUnit as HUnit
43 import Control.Applicative
46 import qualified Data.List as List
47 import qualified Data.Map as Map
48 import Data.Maybe (fromMaybe)
49 import qualified Data.Set as Set
50 import GHC.Exts (IsString(..))
51 import qualified Text.JSON as J
53 import Test.Ganeti.TestHelper
54 import Test.Ganeti.TestCommon
55 import Test.Ganeti.Types ()
57 import qualified Ganeti.Constants as C
59 import Ganeti.Objects as Objects
63 -- * Arbitrary instances
65 $(genArbitrary ''PartialNDParams)
67 instance Arbitrary Node where
68 arbitrary = Node <$> genFQDN <*> genFQDN <*> genFQDN
69 <*> arbitrary <*> arbitrary <*> arbitrary <*> genFQDN
70 <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
71 <*> arbitrary <*> arbitrary <*> genFQDN <*> arbitrary
72 <*> (Set.fromList <$> genTags)
74 $(genArbitrary ''BlockDriver)
76 $(genArbitrary ''DiskMode)
78 instance Arbitrary DiskLogicalId where
79 arbitrary = oneof [ LIDPlain <$> arbitrary <*> arbitrary
80 , LIDDrbd8 <$> genFQDN <*> genFQDN <*> arbitrary
81 <*> arbitrary <*> arbitrary <*> arbitrary
82 , LIDFile <$> arbitrary <*> arbitrary
83 , LIDBlockDev <$> arbitrary <*> arbitrary
84 , LIDRados <$> arbitrary <*> arbitrary
87 -- | 'Disk' 'arbitrary' instance. Since we don't test disk hierarchy
88 -- properties, we only generate disks with no children (FIXME), as
89 -- generating recursive datastructures is a bit more work.
90 instance Arbitrary Disk where
91 arbitrary = Disk <$> arbitrary <*> pure [] <*> arbitrary
92 <*> arbitrary <*> arbitrary
94 -- FIXME: we should generate proper values, >=0, etc., but this is
95 -- hard for partial ones, where all must be wrapped in a 'Maybe'
96 $(genArbitrary ''PartialBeParams)
98 $(genArbitrary ''AdminState)
100 $(genArbitrary ''PartialNicParams)
102 $(genArbitrary ''PartialNic)
104 instance Arbitrary Instance where
107 <$> genFQDN <*> genFQDN <*> genFQDN -- OS name, but...
109 -- FIXME: add non-empty hvparams when they're a proper type
110 <*> pure (GenericContainer Map.empty) <*> arbitrary
111 -- ... and for OSParams
112 <*> pure (GenericContainer Map.empty) <*> arbitrary <*> arbitrary
113 <*> arbitrary <*> arbitrary <*> arbitrary
115 <*> arbitrary <*> arbitrary
121 <*> (Set.fromList <$> genTags)
123 -- | Generates an instance that is connected to the given networks
124 -- and possibly some other networks
125 genInstWithNets :: [String] -> Gen Instance
126 genInstWithNets nets = do
127 plain_inst <- arbitrary
130 nicparams <- arbitrary
131 -- generate some more networks than the given ones
132 num_more_nets <- choose (0,3)
133 more_nets <- vectorOf num_more_nets genName
134 let partial_nics = map (PartialNic mac ip nicparams . Just)
135 (List.nub (nets ++ more_nets))
136 new_inst = plain_inst { instNics = partial_nics }
139 -- | FIXME: This generates completely random data, without normal
141 $(genArbitrary ''PartialISpecParams)
142 $(genArbitrary ''PartialMinMaxISpecs)
144 -- | FIXME: This generates completely random data, without normal
146 $(genArbitrary ''PartialIPolicy)
148 $(genArbitrary ''FilledISpecParams)
149 $(genArbitrary ''FilledMinMaxISpecs)
150 $(genArbitrary ''FilledIPolicy)
151 $(genArbitrary ''IpFamily)
152 $(genArbitrary ''FilledNDParams)
153 $(genArbitrary ''FilledNicParams)
154 $(genArbitrary ''FilledBeParams)
156 -- | No real arbitrary instance for 'ClusterHvParams' yet.
157 instance Arbitrary ClusterHvParams where
158 arbitrary = return $ GenericContainer Map.empty
160 -- | No real arbitrary instance for 'OsHvParams' yet.
161 instance Arbitrary OsHvParams where
162 arbitrary = return $ GenericContainer Map.empty
164 instance Arbitrary ClusterNicParams where
165 arbitrary = (GenericContainer . Map.singleton C.ppDefault) <$> arbitrary
167 instance Arbitrary OsParams where
168 arbitrary = (GenericContainer . Map.fromList) <$> arbitrary
170 instance Arbitrary ClusterOsParams where
171 arbitrary = (GenericContainer . Map.fromList) <$> arbitrary
173 instance Arbitrary ClusterBeParams where
174 arbitrary = (GenericContainer . Map.fromList) <$> arbitrary
176 instance Arbitrary TagSet where
177 arbitrary = Set.fromList <$> genTags
179 $(genArbitrary ''Cluster)
181 instance Arbitrary Network where
182 arbitrary = genValidNetwork
184 -- | Generates a network instance with minimum netmasks of /24. Generating
185 -- bigger networks slows down the tests, because long bit strings are generated
186 -- for the reservations.
187 genValidNetwork :: Gen Objects.Network
189 -- generate netmask for the IPv4 network
190 netmask <- fromIntegral <$> choose (24::Int, 30)
191 name <- genName >>= mkNonEmpty
192 mac_prefix <- genMaybe genName
194 net6 <- genMaybe genIp6Net
195 gateway <- genMaybe arbitrary
196 gateway6 <- genMaybe genIp6Addr
197 res <- liftM Just (genBitString $ netmask2NumHosts netmask)
198 ext_res <- liftM Just (genBitString $ netmask2NumHosts netmask)
200 let n = Network name mac_prefix (Ip4Network net netmask) net6 gateway
201 gateway6 res ext_res uuid 0 Set.empty
204 -- | Generate an arbitrary string consisting of '0' and '1' of the given length.
205 genBitString :: Int -> Gen String
206 genBitString len = vectorOf len (elements "01")
208 -- | Generate an arbitrary string consisting of '0' and '1' of the maximum given
210 genBitStringMaxLen :: Int -> Gen String
211 genBitStringMaxLen maxLen = choose (0, maxLen) >>= genBitString
213 -- | Generator for config data with an empty cluster (no instances),
214 -- with N defined nodes.
215 genEmptyCluster :: Int -> Gen ConfigData
216 genEmptyCluster ncount = do
217 nodes <- vector ncount
220 let guuid = groupUuid grp
221 nodes' = zipWith (\n idx ->
222 let newname = nodeName n ++ "-" ++ show idx
223 in (newname, n { nodeGroup = guuid,
224 nodeName = newname}))
226 nodemap = Map.fromList nodes'
227 contnodes = if Map.size nodemap /= ncount
228 then error ("Inconsistent node map, duplicates in" ++
229 " node name list? Names: " ++
230 show (map fst nodes'))
231 else GenericContainer nodemap
232 continsts = GenericContainer Map.empty
233 networks = GenericContainer Map.empty
234 let contgroups = GenericContainer $ Map.singleton guuid grp
236 cluster <- resize 8 arbitrary
237 let c = ConfigData version cluster contnodes contgroups continsts networks
241 -- | FIXME: make an even simpler base version of creating a cluster.
243 -- | Generates config data with a couple of networks.
244 genConfigDataWithNetworks :: ConfigData -> Gen ConfigData
245 genConfigDataWithNetworks old_cfg = do
246 num_nets <- choose (0, 3)
247 -- generate a list of network names (no duplicates)
248 net_names <- genUniquesList num_nets genName >>= mapM mkNonEmpty
249 -- generate a random list of networks (possibly with duplicate names)
250 nets <- vectorOf num_nets genValidNetwork
251 -- use unique names for the networks
252 let nets_unique = map ( \(name, net) -> net { networkName = name } )
254 net_map = GenericContainer $ Map.fromList
255 (map (\n -> (networkUuid n, n)) nets_unique)
256 new_cfg = old_cfg { configNetworks = net_map }
261 -- | Tests that fillDict behaves correctly
262 prop_fillDict :: [(Int, Int)] -> [(Int, Int)] -> Property
263 prop_fillDict defaults custom =
264 let d_map = Map.fromList defaults
265 d_keys = map fst defaults
266 c_map = Map.fromList custom
267 c_keys = map fst custom
268 in conjoin [ printTestCase "Empty custom filling"
269 (fillDict d_map Map.empty [] == d_map)
270 , printTestCase "Empty defaults filling"
271 (fillDict Map.empty c_map [] == c_map)
272 , printTestCase "Delete all keys"
273 (fillDict d_map c_map (d_keys++c_keys) == Map.empty)
276 -- | Test that the serialisation of 'DiskLogicalId', which is
277 -- implemented manually, is idempotent. Since we don't have a
278 -- standalone JSON instance for DiskLogicalId (it's a data type that
279 -- expands over two fields in a JSObject), we test this by actially
280 -- testing entire Disk serialisations. So this tests two things at
282 prop_Disk_serialisation :: Disk -> Property
283 prop_Disk_serialisation = testSerialisation
285 -- | Check that node serialisation is idempotent.
286 prop_Node_serialisation :: Node -> Property
287 prop_Node_serialisation = testSerialisation
289 -- | Check that instance serialisation is idempotent.
290 prop_Inst_serialisation :: Instance -> Property
291 prop_Inst_serialisation = testSerialisation
293 -- | Check that network serialisation is idempotent.
294 prop_Network_serialisation :: Network -> Property
295 prop_Network_serialisation = testSerialisation
297 -- | Check config serialisation.
298 prop_Config_serialisation :: Property
299 prop_Config_serialisation =
300 forAll (choose (0, maxNodes `div` 4) >>= genEmptyCluster) testSerialisation
302 -- | Custom HUnit test to check the correspondence between Haskell-generated
303 -- networks and their Python decoded, validated and re-encoded version.
304 -- For the technical background of this unit test, check the documentation
305 -- of "case_py_compat_types" of test/hs/Test/Ganeti/Opcodes.hs
306 casePyCompatNetworks :: HUnit.Assertion
307 casePyCompatNetworks = do
308 let num_networks = 500::Int
309 networks <- genSample (vectorOf num_networks genValidNetwork)
310 let networks_with_properties = map getNetworkProperties networks
311 serialized = J.encode networks
312 -- check for non-ASCII fields, usually due to 'arbitrary :: String'
313 mapM_ (\net -> when (any (not . isAscii) (J.encode net)) .
314 HUnit.assertFailure $
315 "Network has non-ASCII fields: " ++ show net
318 runPython "from ganeti import network\n\
319 \from ganeti import objects\n\
320 \from ganeti import serializer\n\
322 \net_data = serializer.Load(sys.stdin.read())\n\
323 \decoded = [objects.Network.FromDict(n) for n in net_data]\n\
325 \for net in decoded:\n\
326 \ a = network.AddressPool(net)\n\
327 \ encoded.append((a.GetFreeCount(), a.GetReservedCount(), \\\n\
329 \print serializer.Dump(encoded)" serialized
330 >>= checkPythonResult
331 let deserialised = J.decode py_stdout::J.Result [(Int, Int, Network)]
332 decoded <- case deserialised of
333 J.Ok ops -> return ops
335 HUnit.assertFailure ("Unable to decode networks: " ++ msg)
336 -- this already raised an expection, but we need it
338 >> fail "Unable to decode networks"
339 HUnit.assertEqual "Mismatch in number of returned networks"
340 (length decoded) (length networks_with_properties)
341 mapM_ (uncurry (HUnit.assertEqual "Different result after encoding/decoding")
342 ) $ zip decoded networks_with_properties
344 -- | Creates a tuple of the given network combined with some of its properties
345 -- to be compared against the same properties generated by the python code.
346 getNetworkProperties :: Network -> (Int, Int, Network)
347 getNetworkProperties net =
348 let maybePool = createAddressPool net
350 (Just pool) -> (getFreeCount pool, getReservedCount pool, net)
351 Nothing -> (-1, -1, net)
353 -- | Tests the compatibility between Haskell-serialized node groups and their
354 -- python-decoded and encoded version.
355 casePyCompatNodegroups :: HUnit.Assertion
356 casePyCompatNodegroups = do
357 let num_groups = 500::Int
358 groups <- genSample (vectorOf num_groups genNodeGroup)
359 let serialized = J.encode groups
360 -- check for non-ASCII fields, usually due to 'arbitrary :: String'
361 mapM_ (\group -> when (any (not . isAscii) (J.encode group)) .
362 HUnit.assertFailure $
363 "Node group has non-ASCII fields: " ++ show group
366 runPython "from ganeti import objects\n\
367 \from ganeti import serializer\n\
369 \group_data = serializer.Load(sys.stdin.read())\n\
370 \decoded = [objects.NodeGroup.FromDict(g) for g in group_data]\n\
371 \encoded = [g.ToDict() for g in decoded]\n\
372 \print serializer.Dump(encoded)" serialized
373 >>= checkPythonResult
374 let deserialised = J.decode py_stdout::J.Result [NodeGroup]
375 decoded <- case deserialised of
376 J.Ok ops -> return ops
378 HUnit.assertFailure ("Unable to decode node groups: " ++ msg)
379 -- this already raised an expection, but we need it
381 >> fail "Unable to decode node groups"
382 HUnit.assertEqual "Mismatch in number of returned node groups"
383 (length decoded) (length groups)
384 mapM_ (uncurry (HUnit.assertEqual "Different result after encoding/decoding")
385 ) $ zip decoded groups
387 -- | Generates a node group with up to 3 networks.
388 -- | FIXME: This generates still somewhat completely random data, without normal
390 genNodeGroup :: Gen NodeGroup
394 ndparams <- arbitrary
395 alloc_policy <- arbitrary
397 diskparams <- pure (GenericContainer Map.empty)
398 num_networks <- choose (0, 3)
399 net_uuid_list <- vectorOf num_networks (arbitrary::Gen String)
400 nic_param_list <- vectorOf num_networks (arbitrary::Gen PartialNicParams)
401 net_map <- pure (GenericContainer . Map.fromList $
402 zip net_uuid_list nic_param_list)
406 uuid <- genFQDN `suchThat` (/= name)
408 tags <- Set.fromList <$> genTags
409 let group = NodeGroup name members ndparams alloc_policy ipolicy diskparams
410 net_map ctime mtime uuid serial tags
413 instance Arbitrary NodeGroup where
414 arbitrary = genNodeGroup
416 $(genArbitrary ''Ip4Address)
418 $(genArbitrary ''Ip4Network)
420 -- | Helper to compute absolute value of an IPv4 address.
421 ip4AddrValue :: Ip4Address -> Integer
422 ip4AddrValue (Ip4Address a b c d) =
423 fromIntegral a * (2^(24::Integer)) +
424 fromIntegral b * (2^(16::Integer)) +
425 fromIntegral c * (2^(8::Integer)) + fromIntegral d
427 -- | Tests that any difference between IPv4 consecutive addresses is 1.
428 prop_nextIp4Address :: Ip4Address -> Property
429 prop_nextIp4Address ip4 =
430 ip4AddrValue (nextIp4Address ip4) ==? ip4AddrValue ip4 + 1
432 -- | IsString instance for 'Ip4Address', to help write the tests.
433 instance IsString Ip4Address where
435 fromMaybe (error $ "Failed to parse address from " ++ s) (readIp4Address s)
437 -- | Tests a few simple cases of IPv4 next address.
438 caseNextIp4Address :: HUnit.Assertion
439 caseNextIp4Address = do
440 HUnit.assertEqual "" "0.0.0.1" $ nextIp4Address "0.0.0.0"
441 HUnit.assertEqual "" "0.0.0.0" $ nextIp4Address "255.255.255.255"
442 HUnit.assertEqual "" "1.2.3.5" $ nextIp4Address "1.2.3.4"
443 HUnit.assertEqual "" "1.3.0.0" $ nextIp4Address "1.2.255.255"
444 HUnit.assertEqual "" "1.2.255.63" $ nextIp4Address "1.2.255.62"
448 , 'prop_Disk_serialisation
449 , 'prop_Inst_serialisation
450 , 'prop_Network_serialisation
451 , 'prop_Node_serialisation
452 , 'prop_Config_serialisation
453 , 'casePyCompatNetworks
454 , 'casePyCompatNodegroups
455 , 'prop_nextIp4Address
456 , 'caseNextIp4Address