1010from typing import Any , Optional
1111
1212import backoff
13- from requests import HTTPError
13+ from requests import HTTPError , RequestException
1414import requests
1515
1616from resalloc_ibm_cloud .argparsers import powervs_arg_parser
@@ -162,6 +162,39 @@ def _delete_volume_with_backoff(self, volume_id: str) -> None:
162162 self .client .delete_volume (volume_id )
163163 logger .info ("Deleted volume with ID %s" , volume_id )
164164
165+ def _wait_for_instance_gone (
166+ self , instance_id : str , timeout : int = 600 , interval : int = 10 ,
167+ ) -> None :
168+ """
169+ Poll until the instance no longer exists (404) so that its network
170+ interfaces are safe to delete.
171+ """
172+ start_time = time .time ()
173+ while True :
174+ if time .time () - start_time > timeout :
175+ logger .warning (
176+ "Timed out waiting for instance %s to be fully deleted" ,
177+ instance_id ,
178+ )
179+ break
180+
181+ try :
182+ instance = self .client .get_instance (instance_id )
183+ status = instance .get ("status" , "unknown" )
184+ logger .info (
185+ "Waiting for instance %s to be deleted (status: %s)" ,
186+ instance_id , status ,
187+ )
188+ except RequestException as e :
189+ if e .response .status_code == 404 :
190+ logger .info ("Instance %s is gone" , instance_id )
191+ return
192+
193+ logger .error ("Failed to get instance %s: %s" , instance_id , str (e ))
194+ break
195+
196+ sleep (interval )
197+
165198 def _force_delete_volume_by_instance_name (self , instance_name : str ) -> None :
166199 # if powervs decides to fail and keep the volume around, force delete any
167200 # volume that starts with the instance name
@@ -198,6 +231,18 @@ def delete_vm(self, name: str) -> None:
198231 instance_information = self .client .get_instance (instance_id )
199232 volume_ids = instance_information .get ("volumeIDs" , [])
200233
234+ # IBM Cloud does not automatically clean up network interfaces (ports)
235+ # when a VM is deleted, which eventually exhausts the subnet IP pool.
236+ # We need to remember them now and delete them after the instance is gone.
237+ # This is a new thing in PowerVS, maybe in the future it will be handled automatically
238+ # again???
239+ network_interfaces = []
240+ for net in instance_information .get ("networks" , []):
241+ network_id = net .get ("networkID" )
242+ interface_id = net .get ("networkInterfaceID" )
243+ if network_id and interface_id :
244+ network_interfaces .append ((network_id , interface_id ))
245+
201246 # the data volumes tends to remain undeleted even if the delete_instance
202247 # call is with delete_data_volumes, so this needs to be assured manually
203248 for volume_id in volume_ids :
@@ -218,6 +263,19 @@ def delete_vm(self, name: str) -> None:
218263 instance_id ,
219264 )
220265
266+ if not network_interfaces :
267+ return
268+
269+ self ._wait_for_instance_gone (instance_id )
270+ for network_id , interface_id in network_interfaces :
271+ try :
272+ self .client .delete_network_interface (network_id , interface_id )
273+ except RequestException as e :
274+ logger .error (
275+ "Failed to delete network interface %s: %s" ,
276+ interface_id , str (e ),
277+ )
278+
221279 def _parse_volumes (self , volumes_list : list [str ]) -> list [dict ]:
222280 """
223281 Parse volume specifications from a list of strings.
0 commit comments