@@ -213,7 +213,10 @@ def get_connected_container(
213213 if not network :
214214 return []
215215
216- return network .attrs ["Containers" ].keys ()
216+ containers = network .attrs .get ("Containers" )
217+ if containers is None :
218+ return []
219+ return containers .keys ()
217220
218221 def get_connected_networks (
219222 self , container : Union [str , docker .models .containers .Container ]
@@ -305,7 +308,10 @@ def container_get_ip(
305308 network = self .network (network , raise_on_not_found = True )
306309
307310 network .reload ()
308- for k , v in network .attrs ["Containers" ].items ():
311+ containers = network .attrs .get ("Containers" )
312+ if containers is None :
313+ return None
314+ for k , v in containers .items ():
309315 if k == container .id :
310316 return v ["IPv4Address" ]
311317 return None
@@ -401,7 +407,7 @@ def _get_used_subnets(self) -> set[ipaddress.IPv4Network]:
401407 used = set ()
402408 for network in self .client .networks .list ():
403409 try :
404- ipam_config = network .attrs .get ("IPAM" , {}).get ("Config" , [])
410+ ipam_config = network .attrs .get ("IPAM" , {}).get ("Config" ) or []
405411 for config in ipam_config :
406412 subnet_str = config .get ("Subnet" )
407413 if subnet_str :
@@ -444,23 +450,48 @@ def create_network(self, name=None, driver="bridge", internal=False):
444450 random .choices (string .ascii_uppercase , k = 10 )
445451 )
446452
447- # Allocate a /29 subnet from our pool
448- subnet = self ._allocate_subnet ()
449- if subnet is None :
450- raise RuntimeError (
451- "No available subnet in instance network pool. "
452- "Consider cleaning up unused networks."
453- )
453+ # Retry loop to handle race conditions when multiple processes
454+ # try to allocate the same subnet concurrently
455+ max_retries = 10
456+ last_error = None
457+
458+ for attempt in range (max_retries ):
459+ # Allocate a /29 subnet from our pool
460+ subnet = self ._allocate_subnet ()
461+ if subnet is None :
462+ raise RuntimeError (
463+ "No available subnet in instance network pool. "
464+ "Consider cleaning up unused networks."
465+ )
466+
467+ # First usable host is the gateway
468+ gateway = str (list (subnet .hosts ())[0 ])
454469
455- # First usable host is the gateway
456- gateway = str ( list ( subnet . hosts ())[ 0 ])
470+ ipam_pool = IPAMPool ( subnet = str ( subnet ), gateway = gateway )
471+ ipam_config = IPAMConfig ( pool_configs = [ ipam_pool ])
457472
458- ipam_pool = IPAMPool (subnet = str (subnet ), gateway = gateway )
459- ipam_config = IPAMConfig (pool_configs = [ipam_pool ])
473+ log .debug (
474+ f"Creating network { name } with subnet { subnet } (attempt { attempt + 1 } )"
475+ )
476+ try :
477+ return self .client .networks .create (
478+ name , driver = driver , internal = internal , ipam = ipam_config
479+ )
480+ except errors .APIError as e :
481+ # Check if this is a subnet overlap error (race condition)
482+ if "Pool overlaps" in str (e ):
483+ log .warning (
484+ f"Subnet { subnet } was allocated by another process, retrying..."
485+ )
486+ last_error = e
487+ continue
488+ # Re-raise other API errors
489+ raise
460490
461- log .debug (f"Creating network { name } with subnet { subnet } " )
462- return self .client .networks .create (
463- name , driver = driver , internal = internal , ipam = ipam_config
491+ # All retries exhausted
492+ raise RuntimeError (
493+ f"Failed to allocate subnet after { max_retries } attempts. "
494+ f"Last error: { last_error } "
464495 )
465496
466497 def network (self , network_id , raise_on_not_found = False ):
0 commit comments