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 <*> arbitrary
95 -- FIXME: we should generate proper values, >=0, etc., but this is
96 -- hard for partial ones, where all must be wrapped in a 'Maybe'
97 $(genArbitrary ''PartialBeParams)
99 $(genArbitrary ''AdminState)
101 $(genArbitrary ''PartialNicParams)
103 $(genArbitrary ''PartialNic)
105 instance Arbitrary Instance where
108 <$> genFQDN <*> genFQDN <*> genFQDN -- OS name, but...
110 -- FIXME: add non-empty hvparams when they're a proper type
111 <*> pure (GenericContainer Map.empty) <*> arbitrary
112 -- ... and for OSParams
113 <*> pure (GenericContainer Map.empty) <*> arbitrary <*> arbitrary
114 <*> arbitrary <*> arbitrary <*> arbitrary
116 <*> arbitrary <*> arbitrary
122 <*> (Set.fromList <$> genTags)
124 -- | Generates an instance that is connected to the given networks
125 -- and possibly some other networks
126 genInstWithNets :: [String] -> Gen Instance
127 genInstWithNets nets = do
128 plain_inst <- arbitrary
131 nicparams <- arbitrary
134 -- generate some more networks than the given ones
135 num_more_nets <- choose (0,3)
136 more_nets <- vectorOf num_more_nets genName
137 let genNic net = PartialNic mac ip nicparams net name uuid
138 partial_nics = map (genNic . Just)
139 (List.nub (nets ++ more_nets))
140 new_inst = plain_inst { instNics = partial_nics }
143 -- | FIXME: This generates completely random data, without normal
145 $(genArbitrary ''PartialISpecParams)
147 -- | FIXME: This generates completely random data, without normal
149 $(genArbitrary ''PartialIPolicy)
151 $(genArbitrary ''FilledISpecParams)
152 $(genArbitrary ''MinMaxISpecs)
153 $(genArbitrary ''FilledIPolicy)
154 $(genArbitrary ''IpFamily)
155 $(genArbitrary ''FilledNDParams)
156 $(genArbitrary ''FilledNicParams)
157 $(genArbitrary ''FilledBeParams)
159 -- | No real arbitrary instance for 'ClusterHvParams' yet.
160 instance Arbitrary ClusterHvParams where
161 arbitrary = return $ GenericContainer Map.empty
163 -- | No real arbitrary instance for 'OsHvParams' yet.
164 instance Arbitrary OsHvParams where
165 arbitrary = return $ GenericContainer Map.empty
167 instance Arbitrary ClusterNicParams where
168 arbitrary = (GenericContainer . Map.singleton C.ppDefault) <$> arbitrary
170 instance Arbitrary OsParams where
171 arbitrary = (GenericContainer . Map.fromList) <$> arbitrary
173 instance Arbitrary ClusterOsParams where
174 arbitrary = (GenericContainer . Map.fromList) <$> arbitrary
176 instance Arbitrary ClusterBeParams where
177 arbitrary = (GenericContainer . Map.fromList) <$> arbitrary
179 instance Arbitrary TagSet where
180 arbitrary = Set.fromList <$> genTags
182 $(genArbitrary ''Cluster)
184 instance Arbitrary Network where
185 arbitrary = genValidNetwork
187 -- | Generates a network instance with minimum netmasks of /24. Generating
188 -- bigger networks slows down the tests, because long bit strings are generated
189 -- for the reservations.
190 genValidNetwork :: Gen Objects.Network
192 -- generate netmask for the IPv4 network
193 netmask <- fromIntegral <$> choose (24::Int, 30)
194 name <- genName >>= mkNonEmpty
195 mac_prefix <- genMaybe genName
197 net6 <- genMaybe genIp6Net
198 gateway <- genMaybe arbitrary
199 gateway6 <- genMaybe genIp6Addr
200 res <- liftM Just (genBitString $ netmask2NumHosts netmask)
201 ext_res <- liftM Just (genBitString $ netmask2NumHosts netmask)
203 let n = Network name mac_prefix (Ip4Network net netmask) net6 gateway
204 gateway6 res ext_res uuid 0 Set.empty
207 -- | Generate an arbitrary string consisting of '0' and '1' of the given length.
208 genBitString :: Int -> Gen String
209 genBitString len = vectorOf len (elements "01")
211 -- | Generate an arbitrary string consisting of '0' and '1' of the maximum given
213 genBitStringMaxLen :: Int -> Gen String
214 genBitStringMaxLen maxLen = choose (0, maxLen) >>= genBitString
216 -- | Generator for config data with an empty cluster (no instances),
217 -- with N defined nodes.
218 genEmptyCluster :: Int -> Gen ConfigData
219 genEmptyCluster ncount = do
220 nodes <- vector ncount
223 let guuid = groupUuid grp
224 nodes' = zipWith (\n idx ->
225 let newname = nodeName n ++ "-" ++ show idx
226 in (newname, n { nodeGroup = guuid,
227 nodeName = newname}))
229 nodemap = Map.fromList nodes'
230 contnodes = if Map.size nodemap /= ncount
231 then error ("Inconsistent node map, duplicates in" ++
232 " node name list? Names: " ++
233 show (map fst nodes'))
234 else GenericContainer nodemap
235 continsts = GenericContainer Map.empty
236 networks = GenericContainer Map.empty
237 let contgroups = GenericContainer $ Map.singleton guuid grp
239 cluster <- resize 8 arbitrary
240 let c = ConfigData version cluster contnodes contgroups continsts networks
244 -- | FIXME: make an even simpler base version of creating a cluster.
246 -- | Generates config data with a couple of networks.
247 genConfigDataWithNetworks :: ConfigData -> Gen ConfigData
248 genConfigDataWithNetworks old_cfg = do
249 num_nets <- choose (0, 3)
250 -- generate a list of network names (no duplicates)
251 net_names <- genUniquesList num_nets genName >>= mapM mkNonEmpty
252 -- generate a random list of networks (possibly with duplicate names)
253 nets <- vectorOf num_nets genValidNetwork
254 -- use unique names for the networks
255 let nets_unique = map ( \(name, net) -> net { networkName = name } )
257 net_map = GenericContainer $ Map.fromList
258 (map (\n -> (networkUuid n, n)) nets_unique)
259 new_cfg = old_cfg { configNetworks = net_map }
264 -- | Tests that fillDict behaves correctly
265 prop_fillDict :: [(Int, Int)] -> [(Int, Int)] -> Property
266 prop_fillDict defaults custom =
267 let d_map = Map.fromList defaults
268 d_keys = map fst defaults
269 c_map = Map.fromList custom
270 c_keys = map fst custom
271 in conjoin [ printTestCase "Empty custom filling"
272 (fillDict d_map Map.empty [] == d_map)
273 , printTestCase "Empty defaults filling"
274 (fillDict Map.empty c_map [] == c_map)
275 , printTestCase "Delete all keys"
276 (fillDict d_map c_map (d_keys++c_keys) == Map.empty)
279 -- | Test that the serialisation of 'DiskLogicalId', which is
280 -- implemented manually, is idempotent. Since we don't have a
281 -- standalone JSON instance for DiskLogicalId (it's a data type that
282 -- expands over two fields in a JSObject), we test this by actially
283 -- testing entire Disk serialisations. So this tests two things at
285 prop_Disk_serialisation :: Disk -> Property
286 prop_Disk_serialisation = testSerialisation
288 -- | Check that node serialisation is idempotent.
289 prop_Node_serialisation :: Node -> Property
290 prop_Node_serialisation = testSerialisation
292 -- | Check that instance serialisation is idempotent.
293 prop_Inst_serialisation :: Instance -> Property
294 prop_Inst_serialisation = testSerialisation
296 -- | Check that network serialisation is idempotent.
297 prop_Network_serialisation :: Network -> Property
298 prop_Network_serialisation = testSerialisation
300 -- | Check config serialisation.
301 prop_Config_serialisation :: Property
302 prop_Config_serialisation =
303 forAll (choose (0, maxNodes `div` 4) >>= genEmptyCluster) testSerialisation
305 -- | Custom HUnit test to check the correspondence between Haskell-generated
306 -- networks and their Python decoded, validated and re-encoded version.
307 -- For the technical background of this unit test, check the documentation
308 -- of "case_py_compat_types" of test/hs/Test/Ganeti/Opcodes.hs
309 casePyCompatNetworks :: HUnit.Assertion
310 casePyCompatNetworks = do
311 let num_networks = 500::Int
312 networks <- genSample (vectorOf num_networks genValidNetwork)
313 let networks_with_properties = map getNetworkProperties networks
314 serialized = J.encode networks
315 -- check for non-ASCII fields, usually due to 'arbitrary :: String'
316 mapM_ (\net -> when (any (not . isAscii) (J.encode net)) .
317 HUnit.assertFailure $
318 "Network has non-ASCII fields: " ++ show net
321 runPython "from ganeti import network\n\
322 \from ganeti import objects\n\
323 \from ganeti import serializer\n\
325 \net_data = serializer.Load(sys.stdin.read())\n\
326 \decoded = [objects.Network.FromDict(n) for n in net_data]\n\
328 \for net in decoded:\n\
329 \ a = network.AddressPool(net)\n\
330 \ encoded.append((a.GetFreeCount(), a.GetReservedCount(), \\\n\
332 \print serializer.Dump(encoded)" serialized
333 >>= checkPythonResult
334 let deserialised = J.decode py_stdout::J.Result [(Int, Int, Network)]
335 decoded <- case deserialised of
336 J.Ok ops -> return ops
338 HUnit.assertFailure ("Unable to decode networks: " ++ msg)
339 -- this already raised an expection, but we need it
341 >> fail "Unable to decode networks"
342 HUnit.assertEqual "Mismatch in number of returned networks"
343 (length decoded) (length networks_with_properties)
344 mapM_ (uncurry (HUnit.assertEqual "Different result after encoding/decoding")
345 ) $ zip decoded networks_with_properties
347 -- | Creates a tuple of the given network combined with some of its properties
348 -- to be compared against the same properties generated by the python code.
349 getNetworkProperties :: Network -> (Int, Int, Network)
350 getNetworkProperties net =
351 let maybePool = createAddressPool net
353 (Just pool) -> (getFreeCount pool, getReservedCount pool, net)
354 Nothing -> (-1, -1, net)
356 -- | Tests the compatibility between Haskell-serialized node groups and their
357 -- python-decoded and encoded version.
358 casePyCompatNodegroups :: HUnit.Assertion
359 casePyCompatNodegroups = do
360 let num_groups = 500::Int
361 groups <- genSample (vectorOf num_groups genNodeGroup)
362 let serialized = J.encode groups
363 -- check for non-ASCII fields, usually due to 'arbitrary :: String'
364 mapM_ (\group -> when (any (not . isAscii) (J.encode group)) .
365 HUnit.assertFailure $
366 "Node group has non-ASCII fields: " ++ show group
369 runPython "from ganeti import objects\n\
370 \from ganeti import serializer\n\
372 \group_data = serializer.Load(sys.stdin.read())\n\
373 \decoded = [objects.NodeGroup.FromDict(g) for g in group_data]\n\
374 \encoded = [g.ToDict() for g in decoded]\n\
375 \print serializer.Dump(encoded)" serialized
376 >>= checkPythonResult
377 let deserialised = J.decode py_stdout::J.Result [NodeGroup]
378 decoded <- case deserialised of
379 J.Ok ops -> return ops
381 HUnit.assertFailure ("Unable to decode node groups: " ++ msg)
382 -- this already raised an expection, but we need it
384 >> fail "Unable to decode node groups"
385 HUnit.assertEqual "Mismatch in number of returned node groups"
386 (length decoded) (length groups)
387 mapM_ (uncurry (HUnit.assertEqual "Different result after encoding/decoding")
388 ) $ zip decoded groups
390 -- | Generates a node group with up to 3 networks.
391 -- | FIXME: This generates still somewhat completely random data, without normal
393 genNodeGroup :: Gen NodeGroup
397 ndparams <- arbitrary
398 alloc_policy <- arbitrary
400 diskparams <- pure (GenericContainer Map.empty)
401 num_networks <- choose (0, 3)
402 net_uuid_list <- vectorOf num_networks (arbitrary::Gen String)
403 nic_param_list <- vectorOf num_networks (arbitrary::Gen PartialNicParams)
404 net_map <- pure (GenericContainer . Map.fromList $
405 zip net_uuid_list nic_param_list)
409 uuid <- genFQDN `suchThat` (/= name)
411 tags <- Set.fromList <$> genTags
412 let group = NodeGroup name members ndparams alloc_policy ipolicy diskparams
413 net_map ctime mtime uuid serial tags
416 instance Arbitrary NodeGroup where
417 arbitrary = genNodeGroup
419 $(genArbitrary ''Ip4Address)
421 $(genArbitrary ''Ip4Network)
423 -- | Helper to compute absolute value of an IPv4 address.
424 ip4AddrValue :: Ip4Address -> Integer
425 ip4AddrValue (Ip4Address a b c d) =
426 fromIntegral a * (2^(24::Integer)) +
427 fromIntegral b * (2^(16::Integer)) +
428 fromIntegral c * (2^(8::Integer)) + fromIntegral d
430 -- | Tests that any difference between IPv4 consecutive addresses is 1.
431 prop_nextIp4Address :: Ip4Address -> Property
432 prop_nextIp4Address ip4 =
433 ip4AddrValue (nextIp4Address ip4) ==? ip4AddrValue ip4 + 1
435 -- | IsString instance for 'Ip4Address', to help write the tests.
436 instance IsString Ip4Address where
438 fromMaybe (error $ "Failed to parse address from " ++ s) (readIp4Address s)
440 -- | Tests a few simple cases of IPv4 next address.
441 caseNextIp4Address :: HUnit.Assertion
442 caseNextIp4Address = do
443 HUnit.assertEqual "" "0.0.0.1" $ nextIp4Address "0.0.0.0"
444 HUnit.assertEqual "" "0.0.0.0" $ nextIp4Address "255.255.255.255"
445 HUnit.assertEqual "" "1.2.3.5" $ nextIp4Address "1.2.3.4"
446 HUnit.assertEqual "" "1.3.0.0" $ nextIp4Address "1.2.255.255"
447 HUnit.assertEqual "" "1.2.255.63" $ nextIp4Address "1.2.255.62"
451 , 'prop_Disk_serialisation
452 , 'prop_Inst_serialisation
453 , 'prop_Network_serialisation
454 , 'prop_Node_serialisation
455 , 'prop_Config_serialisation
456 , 'casePyCompatNetworks
457 , 'casePyCompatNodegroups
458 , 'prop_nextIp4Address
459 , 'caseNextIp4Address