1919 a /32 (or /128) with the shared link-local gateway, delivered via ConfigDrive.
2020"""
2121
22+ import random
23+
2224from marvin .cloudstackTestCase import cloudstackTestCase
23- from marvin .cloudstackException import CloudstackAPIException
2425from marvin .lib .base import (Account ,
2526 Network ,
2627 NetworkOffering ,
@@ -48,7 +49,6 @@ def setUpClass(cls):
4849 cls .template = get_template (cls .apiclient , cls .zone .id , cls .services ["ostype" ])
4950
5051 cls ._cleanup = []
51- cls .hypervisor = testClient .getHypervisorInfo ()
5252 cls .skip = False
5353
5454 zone = Zone (cls .zone .__dict__ )
@@ -136,6 +136,20 @@ def create_l3_network(self, startip="203.0.113.10", endip="203.0.113.50"):
136136 domainid = self .account .domainid
137137 )
138138
139+ def deploy_vm (self , network , ** kwargs ):
140+ virtual_machine = VirtualMachine .create (
141+ self .apiclient ,
142+ self .services ["virtual_machine" ],
143+ accountid = self .account .name ,
144+ domainid = self .account .domainid ,
145+ serviceofferingid = self .service_offering .id ,
146+ networkids = [network .id ],
147+ ** kwargs
148+ )
149+ self .cleanup .append (virtual_machine )
150+ self .assertEqual (virtual_machine .state , "Running" )
151+ return virtual_machine
152+
139153 @attr (tags = ["advanced" , "smoke" ], required_hardware = "false" )
140154 def test_01_create_l3_network (self ):
141155 """ An L3 network is created like a Shared network: with a subnet. The subnet is an
@@ -162,17 +176,28 @@ def test_01b_create_l3_network_with_operator_specified_id(self):
162176 vlan parameter, so bridge names (brdr-<id>) are plannable before the network
163177 exists. The id must lie outside the physical network's dynamic range. """
164178 services = dict (self .services ["l3_network" ])
165- network = Network .create (
166- self .apiclient ,
167- services ,
168- zoneid = self .zone .id ,
169- networkofferingid = self .network_offering_specifyid .id ,
170- accountid = self .account .name ,
171- domainid = self .account .domainid ,
172- vlan = "5928"
173- )
179+ network = None
180+ # Outside the physical network's dynamic range (5800-5899); retried once in case the
181+ # id is already taken by a pre-existing network or public range in the zone.
182+ for attempt in range (2 ):
183+ routed_id = random .randint (6000 , 9999 )
184+ try :
185+ network = Network .create (
186+ self .apiclient ,
187+ services ,
188+ zoneid = self .zone .id ,
189+ networkofferingid = self .network_offering_specifyid .id ,
190+ accountid = self .account .name ,
191+ domainid = self .account .domainid ,
192+ vlan = str (routed_id )
193+ )
194+ break
195+ except Exception as e :
196+ if attempt == 0 and "already" in str (e ):
197+ continue
198+ raise
174199 self .cleanup .append (network )
175- self .assertEqual (network .broadcasturi , "routed://5928" ,
200+ self .assertEqual (network .broadcasturi , "routed://%d" % routed_id ,
176201 "the operator-specified routed id must be carried verbatim, got %s" % network .broadcasturi )
177202
178203 @attr (tags = ["advanced" , "smoke" ], required_hardware = "false" )
@@ -182,22 +207,41 @@ def test_02_deploy_vm_in_l3_network(self):
182207 network = self .create_l3_network ()
183208 self .cleanup .append (network )
184209
185- virtual_machine = VirtualMachine .create (
186- self .apiclient ,
187- self .services ["virtual_machine" ],
188- accountid = self .account .name ,
189- domainid = self .account .domainid ,
190- serviceofferingid = self .service_offering .id ,
191- networkids = [network .id ]
192- )
193- self .cleanup .append (virtual_machine )
194-
195- self .assertEqual (virtual_machine .state , "Running" )
210+ virtual_machine = self .deploy_vm (network )
196211 nic = virtual_machine .nic [0 ]
197212 self .assertEqual (nic .netmask , "255.255.255.255" , "an L3 NIC address is a host route (/32)" )
198213 self .assertEqual (nic .gateway , "169.254.0.1" , "an L3 NIC uses the shared link-local gateway" )
199214 self .assertTrue (nic .ipaddress .startswith ("203.0.113." ), "the address must come from the network's subnet" )
200215
216+ @attr (tags = ["advanced" , "smoke" ], required_hardware = "false" )
217+ def test_02b_deploy_vm_with_requested_ip_in_l3_network (self ):
218+ """ As on a Shared network, a user may ask for a specific address from the network's
219+ range at deploy time and the Instance receives exactly that address. """
220+ network = self .create_l3_network ()
221+ self .cleanup .append (network )
222+
223+ virtual_machine = self .deploy_vm (network , ipaddress = "203.0.113.25" )
224+ nic = virtual_machine .nic [0 ]
225+ self .assertEqual (nic .ipaddress , "203.0.113.25" , "the Instance must receive the requested address" )
226+ self .assertEqual (nic .netmask , "255.255.255.255" , "an L3 NIC address is a host route (/32)" )
227+ self .assertEqual (nic .gateway , "169.254.0.1" , "an L3 NIC uses the shared link-local gateway" )
228+
229+ @attr (tags = ["advanced" , "smoke" ], required_hardware = "false" )
230+ def test_02c_network_address_is_assignable (self ):
231+ """ There is no broadcast domain, so the subnet's first (.0) and last (.255) addresses
232+ are ordinary routable addresses: every Instance is a /32 behind the host's routing
233+ table and nothing broadcasts to them. A range consisting of only the network
234+ address must therefore be accepted, and an Instance must receive it. """
235+ network = self .create_l3_network (startip = "203.0.115.0" , endip = "203.0.115.0" )
236+ self .cleanup .append (network )
237+ self .assertEqual (network .cidr , "203.0.115.0/24" , "the subnet must derive from the range and netmask" )
238+
239+ virtual_machine = self .deploy_vm (network )
240+ nic = virtual_machine .nic [0 ]
241+ self .assertEqual (nic .ipaddress , "203.0.115.0" , "the network address must be assignable to an Instance" )
242+ self .assertEqual (nic .netmask , "255.255.255.255" , "an L3 NIC address is a host route (/32)" )
243+ self .assertEqual (nic .gateway , "169.254.0.1" , "an L3 NIC uses the shared link-local gateway" )
244+
201245 @attr (tags = ["advanced" , "smoke" ], required_hardware = "false" )
202246 def test_03_l3_offering_rejects_dhcp (self ):
203247 """ DHCP is not supported and not needed on L3 networks: ConfigDrive carries the
@@ -231,12 +275,9 @@ def test_05_l3_subnets_may_not_overlap_zone_wide(self):
231275 network = self .create_l3_network (startip = "203.0.113.10" , endip = "203.0.113.30" )
232276 self .cleanup .append (network )
233277
234- try :
278+ with self . assertRaises ( Exception ) :
235279 overlapping = self .create_l3_network (startip = "203.0.113.20" , endip = "203.0.113.40" )
236280 self .cleanup .append (overlapping )
237- self .fail ("creating an L3 network overlapping another must fail" )
238- except (CloudstackAPIException , Exception ):
239- pass
240281
241282 def create_ipv6_only_l3_network (self , ip6cidr = "2001:db8:113::/64" ):
242283 services = {
@@ -258,7 +299,7 @@ def test_06_create_ipv6_only_l3_network(self):
258299 """ Nothing on an L3 network depends on IPv4 - no DHCP, no password or metadata
259300 service - so IPv4 is optional and an IPv6-only network is valid. Addresses
260301 derive from the subnet and the NIC MAC (EUI-64), so no IPv6 range is needed
261- either: ip6gateway and ip6cidr alone define the network. """
302+ either: ip6cidr alone defines the network, and a given ip6gateway is ignored . """
262303 network = self .create_ipv6_only_l3_network ()
263304 self .cleanup .append (network )
264305
@@ -274,17 +315,7 @@ def test_07_deploy_vm_in_ipv6_only_l3_network(self):
274315 network = self .create_ipv6_only_l3_network ()
275316 self .cleanup .append (network )
276317
277- virtual_machine = VirtualMachine .create (
278- self .apiclient ,
279- self .services ["virtual_machine" ],
280- accountid = self .account .name ,
281- domainid = self .account .domainid ,
282- serviceofferingid = self .service_offering .id ,
283- networkids = [network .id ]
284- )
285- self .cleanup .append (virtual_machine )
286-
287- self .assertEqual (virtual_machine .state , "Running" )
318+ virtual_machine = self .deploy_vm (network )
288319 nic = virtual_machine .nic [0 ]
289320 self .assertTrue (getattr (nic , "ip6address" , None ), "the NIC must carry an IPv6 address" )
290321 self .assertEqual (nic .ip6gateway , "fe80::1" , "an L3 NIC uses the shared link-local IPv6 gateway" )
@@ -320,7 +351,8 @@ def test_08_l3_network_rejects_incomplete_address_family(self):
320351 def test_09_l3_network_ignores_gateways (self ):
321352 """ Gateways play no part on an L3 network - the Instance's gateway is always the
322353 shared link-local address - so given gateways are accepted but ignored, and no
323- address in the subnet is burnt for one. """
354+ address in the subnet is burnt for one. An Instance on this dual-stack network
355+ gets a /32 and a /128, each with its shared link-local gateway. """
324356 services = dict (self .services ["l3_network" ])
325357 services ["gateway" ] = "203.0.113.1"
326358 services ["ip6gateway" ] = "2001:db8:113::1"
@@ -337,6 +369,16 @@ def test_09_l3_network_ignores_gateways(self):
337369 self .assertFalse (getattr (network , "gateway" , None ), "the given IPv4 gateway must be ignored" )
338370 self .assertFalse (getattr (network , "ip6gateway" , None ), "the given IPv6 gateway must be ignored" )
339371
372+ virtual_machine = self .deploy_vm (network )
373+ nic = virtual_machine .nic [0 ]
374+ self .assertTrue (getattr (nic , "ipaddress" , None ), "the NIC must carry an IPv4 address" )
375+ self .assertEqual (nic .netmask , "255.255.255.255" , "an L3 NIC address is a host route (/32)" )
376+ self .assertEqual (nic .gateway , "169.254.0.1" , "an L3 NIC uses the shared link-local gateway" )
377+ self .assertTrue (getattr (nic , "ip6address" , None ), "the NIC must carry an IPv6 address" )
378+ self .assertTrue (getattr (nic , "ip6cidr" , "" ).endswith ("/128" ),
379+ "an L3 NIC IPv6 address is a host route (/128), got %s" % getattr (nic , "ip6cidr" , None ))
380+ self .assertEqual (nic .ip6gateway , "fe80::1" , "an L3 NIC uses the shared link-local IPv6 gateway" )
381+
340382 @attr (tags = ["advanced" , "smoke" ], required_hardware = "false" )
341383 def test_10_create_l3_network_by_cidr (self ):
342384 """ The IPv4 subnet can be given as a CIDR, like IPv6: CloudStack derives the netmask
0 commit comments