3030from util import RESOURCE_NAME_PREFIX
3131from util import SERVER_START_TIMEOUT
3232from uuid import uuid4
33+ from warnings import warn
34+
35+
36+ class DADFailed (Exception ):
37+ pass
3338
3439
3540class CloudscaleResource :
@@ -331,7 +336,36 @@ def create_host(self, timeout):
331336
332337 # Validate IPv6 if necessary
333338 if self .spec ['use_ipv6' ] and self .has_public_interface :
334- self .wait_for_non_tentative_ipv6 ()
339+ timeout = 60
340+
341+ for attempt in range (2 ):
342+
343+ # There is an issue with DAD on recently used IPv6 addresses,
344+ # rarely observed during automated tests, which launch loads
345+ # of VMs in quick succession.
346+ #
347+ # When this happens, we force DAD to be repeated once, by
348+ # removing, then re-adding the IPV6 address on the public
349+ # interface.
350+
351+ try :
352+ self .wait_for_non_tentative_ipv6 (timeout = timeout )
353+ except DADFailed as e :
354+ if attempt == 0 :
355+ # Output a warning at the end of the test run
356+ warn (f"DAD failed on first try: { ' ' .join (e .args )} " )
357+
358+ # Wait for the fabric cache to expire
359+ time .sleep (15 )
360+
361+ # Re-add IPv6, to trigger DAD
362+ self .readd_public_ipv6 ()
363+
364+ # Use a lower timeout for the second try
365+ timeout = 30
366+ else :
367+ raise e
368+
335369 self .wait_for_ipv6_default_route ()
336370
337371 # By default, `ndisc_notify` is turned off in most Linux
@@ -384,6 +418,9 @@ def wait_for_non_tentative_ipv6(self, timeout=60):
384418 while datetime .utcnow () <= until :
385419 output = self .output_of ('sudo ip a' )
386420
421+ if 'dadfailed' in output :
422+ raise DADFailed (f"DAD failed. Last output: { output } " )
423+
387424 for line in output .splitlines ():
388425 if preferred .match (line ):
389426 return
@@ -393,6 +430,16 @@ def wait_for_non_tentative_ipv6(self, timeout=60):
393430 raise Timeout (
394431 f'Wait for non-tentative IPv6 timed-out. Last output: { output } ' )
395432
433+ @with_trigger ('server.reset-public-ipv6' )
434+ def readd_public_ipv6 (self ):
435+ """ Removes, then adds the public IPv6 address, to trigger DAD. """
436+
437+ address = self .ip ('public' , 6 )
438+ interface = self .public_interface .name
439+
440+ self .assert_run (f'sudo ip -6 addr del { address } dev { interface } ' )
441+ self .assert_run (f'sudo ip -6 addr add { address } dev { interface } ' )
442+
396443 @with_trigger ('server.wait-for-ipv6-default-route' )
397444 def wait_for_ipv6_default_route (self , timeout = 30 ):
398445 until = datetime .utcnow () + timedelta (seconds = timeout )
0 commit comments