Skip to content

Commit 5d4fad7

Browse files
committed
record deletion of images
1 parent 36b0fe7 commit 5d4fad7

File tree

11 files changed

+77
-26
lines changed

11 files changed

+77
-26
lines changed

pycloudlib/azure/cloud.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -627,7 +627,7 @@ def delete_image(self, image_id, **kwargs):
627627
if delete_poller.status() == "Succeeded":
628628
if image_id in self.registered_images:
629629
del self.registered_images[image_id]
630-
self._log.debug("Image %s was deleted", image_id)
630+
self._record_image_deletion(image_id)
631631
else:
632632
self._log.debug(
633633
"Error deleting %s. Status: %d",

pycloudlib/cloud.py

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,16 @@ class ImageType(enum.Enum):
4343
class ImageInfo:
4444
"""Dataclass to hold image information."""
4545

46-
id: str
47-
name: str
46+
image_id: str
47+
image_name: str
48+
49+
def __eq__(self, value: object) -> bool:
50+
"""
51+
Check if two ImageInfo objects are equal.
52+
53+
It only checks if the image_id is the same, since this should be the unique identifier.
54+
"""
55+
return isinstance(value, ImageInfo) and self.image_id == value.image_id
4856

4957

5058
class BaseCloud(ABC):
@@ -225,15 +233,15 @@ def clean(self) -> List[Exception]:
225233
exceptions.append(e)
226234
for image_info in self.created_images:
227235
try:
228-
self.delete_image(image_id=image_info.id)
236+
self.delete_image(image_id=image_info.image_id)
229237
except Exception as e:
230238
exceptions.append(e)
231239
for image_info in self.preserved_images:
232240
# noop - just log that we're not cleaning up these images
233241
self._log.info(
234242
"Preserved image %s [id:%s] is NOT being cleaned up.",
235-
image_info.name,
236-
image_info.id,
243+
image_info.image_name,
244+
image_info.image_id,
237245
)
238246
return exceptions
239247

@@ -329,6 +337,18 @@ def _validate_tag(tag: str):
329337
raise InvalidTagNameError(tag=tag, rules_failed=rules_failed)
330338

331339
def _get_ssh_keys(self) -> KeyPair:
340+
"""
341+
Get the ssh key pair to use for the cloud instance.
342+
343+
If no key pair is provided in the config file, the default key pair
344+
will be used. The default key pair is the id_rsa or id_ed25519 key
345+
in the user's .ssh directory.
346+
347+
:raises PycloudlibError: if no public key path is provided and no default key is found
348+
:raises PycloudlibError: if the public key path provided in the config does not exist
349+
350+
:return: KeyPair object with the public and private key paths
351+
"""
332352
user = getpass.getuser()
333353
# check if id_rsa or id_ed25519 keys exist in the user's .ssh directory
334354
possible_default_keys = [
@@ -380,21 +400,48 @@ def _store_snapshot_info(
380400
:return: ImageInfo object with the snapshot information
381401
"""
382402
image_info = ImageInfo(
383-
id=snapshot_id,
384-
name=snapshot_name,
403+
image_id=snapshot_id,
404+
image_name=snapshot_name,
385405
)
386406
if not keep_snapshot:
387407
self.created_images.append(image_info)
388408
self._log.info(
389-
"Created temporary snapshot %s [id:%s]",
390-
image_info.name,
391-
image_info.id,
409+
"Created temporary snapshot %s",
410+
image_info,
392411
)
393412
else:
394413
self.preserved_images.append(image_info)
395414
self._log.info(
396-
"Created permanent snapshot %s [id:%s]",
397-
image_info.name,
398-
image_info.id,
415+
"Created permanent snapshot %s",
416+
image_info,
399417
)
400418
return image_info
419+
420+
def _record_image_deletion(self, image_id: str):
421+
"""
422+
Record the deletion of an image.
423+
424+
This method should be called after an image is successfully deleted.
425+
It will remove the image from the list of created_images or preserved_images
426+
so that the cloud does not attempt to re-clean it up later. It will also log
427+
the deletion of the image.
428+
429+
:param image_id: ID of the image that was deleted
430+
"""
431+
if match := [i for i in self.created_images if i.image_id == image_id]:
432+
deleted_image = match[0]
433+
self.created_images.remove(deleted_image)
434+
self._log.debug(
435+
"Snapshot %s has been deleted. Will no longer need to be cleaned up later.",
436+
deleted_image,
437+
)
438+
elif match := [i for i in self.preserved_images if i.image_id == image_id]:
439+
deleted_image = match[0]
440+
self.preserved_images.remove(deleted_image)
441+
self._log.debug(
442+
"Snapshot %s has been deleted. This snapshot was taken with keep=True, "
443+
"but since it has been manually deleted, it will not be preserved.",
444+
deleted_image,
445+
)
446+
else:
447+
self._log.debug("Deleted image %s", image_id)

pycloudlib/ec2/cloud.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,8 @@ def delete_image(self, image_id, **kwargs):
291291
self._log.debug("removing custom snapshot %s", snapshot_id)
292292
self.client.delete_snapshot(SnapshotId=snapshot_id)
293293

294+
self._record_image_deletion(image_id)
295+
294296
def delete_key(self, name):
295297
"""Delete an uploaded key.
296298

pycloudlib/gce/cloud.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@ def delete_image(self, image_id, **kwargs):
314314
raise_on_error(operation)
315315
except GoogleAPICallError as e:
316316
raise_on_error(e)
317+
self._record_image_deletion(image_id)
317318

318319
def get_instance(
319320
self,

pycloudlib/ibm/cloud.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
from pycloudlib.cloud import BaseCloud
1515
from pycloudlib.config import ConfigFile
16-
from pycloudlib.errors import InvalidTagNameError
16+
from pycloudlib.errors import InvalidTagNameError, ResourceNotFoundError, ResourceType
1717
from pycloudlib.ibm._util import get_first as _get_first
1818
from pycloudlib.ibm._util import iter_resources as _iter_resources
1919
from pycloudlib.ibm._util import wait_until as _wait_until
@@ -130,7 +130,9 @@ def delete_image(self, image_id: str, **kwargs):
130130
self._client.delete_image(image_id).get_result()
131131
except ApiException as e:
132132
if "does not exist" not in str(e):
133-
raise
133+
raise ResourceNotFoundError(ResourceType.IMAGE, image_id) from e
134+
else:
135+
self._record_image_deletion(image_id)
134136

135137
def released_image(self, release, *, arch: str = "amd64", **kwargs):
136138
"""ID of the latest released image for a particular release.

pycloudlib/ibm_classic/cloud.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ def delete_image(self, image_id: str, **kwargs):
8181
) from e
8282
except SoftLayer.SoftLayerAPIError as e:
8383
raise IBMClassicException(f"Error deleting image {image_id}") from e
84+
else:
85+
self._record_image_deletion(image_id)
8486

8587
def released_image(self, release, *, disk_size: str = "25G", **kwargs):
8688
"""ID (globalIdentifier) of the latest released image for a particular release.

pycloudlib/lxd/cloud.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -393,11 +393,10 @@ def delete_image(self, image_id, **kwargs):
393393
image_id: string, LXD image fingerprint
394394
"""
395395
self._log.debug("Deleting image: '%s'", image_id)
396-
397396
subp(["lxc", "image", "delete", image_id])
398-
self._log.debug("Deleted %s", image_id)
397+
self._record_image_deletion(image_id)
399398

400-
def snapshot(self, instance, *, clean=True, keep=False, name=None):
399+
def snapshot(self, instance: LXDInstance, *, clean=True, keep=False, name=None):
401400
"""Take a snapshot of the passed in instance for use as image.
402401
403402
:param instance: The instance to create an image from
@@ -428,13 +427,6 @@ def clean(self) -> List[Exception]:
428427
"""
429428
exceptions = super().clean()
430429

431-
for snapshot in self.created_snapshots:
432-
try:
433-
subp(["lxc", "image", "delete", snapshot])
434-
except RuntimeError as e:
435-
if "Image not found" not in str(e):
436-
exceptions.append(e)
437-
438430
for profile in self.created_profiles:
439431
try:
440432
subp(["lxc", "profile", "delete", profile])

pycloudlib/oci/cloud.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ def delete_image(self, image_id, **kwargs):
126126
image_id: string, id of the image to delete
127127
"""
128128
self.compute_client.delete_image(image_id, **kwargs)
129+
self._record_image_deletion(image_id)
129130

130131
def released_image(self, release, operating_system="Canonical Ubuntu"):
131132
"""Get the released image.

pycloudlib/openstack/cloud.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def delete_image(self, image_id, **kwargs):
5656
image_id: string, id of the image to delete
5757
"""
5858
self.conn.delete_image(image_id, wait=True)
59+
self._record_image_deletion(image_id)
5960

6061
def released_image(self, release, **kwargs):
6162
"""Not supported for openstack."""

pycloudlib/qemu/cloud.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ def delete_image(self, image_id, **kwargs):
107107
image_file = Path(image_id)
108108
if image_file.exists():
109109
image_file.unlink()
110+
self._record_image_deletion(image_id)
110111
else:
111112
self._log.debug("Cannot delete image %s as it does not exist", image_file)
112113

0 commit comments

Comments
 (0)