Skip to content

Commit 774bbbe

Browse files
committed
record deletion of images
1 parent 36b0fe7 commit 774bbbe

File tree

11 files changed

+92
-27
lines changed

11 files changed

+92
-27
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: 75 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,32 @@ class ImageType(enum.Enum):
4141

4242
@dataclasses.dataclass
4343
class ImageInfo:
44-
"""Dataclass to hold image information."""
44+
"""Dataclass that represents an image on any given cloud."""
4545

46-
id: str
47-
name: str
46+
image_id: str
47+
image_name: str
48+
49+
def __str__(self):
50+
"""Return a human readable string representation of the image."""
51+
return f"{self.image_name} [id: {self.image_id}]"
52+
53+
def __repr__(self):
54+
"""Return a string representation of the image."""
55+
return f"ImageInfo(id={self.image_id}, name={self.image_name})"
56+
57+
def __eq__(self, other):
58+
"""
59+
Check if two ImageInfo objects represent the same image.
60+
61+
Only the id is used for comparison since this should be the unique identifier for an image.
62+
"""
63+
if not isinstance(other, ImageInfo):
64+
return False
65+
return self.image_id == other.image_id
66+
67+
def __dict__(self):
68+
"""Return a dictionary representation of the image."""
69+
return {"image_id": self.image_id, "image_name": self.image_name}
4870

4971

5072
class BaseCloud(ABC):
@@ -225,15 +247,15 @@ def clean(self) -> List[Exception]:
225247
exceptions.append(e)
226248
for image_info in self.created_images:
227249
try:
228-
self.delete_image(image_id=image_info.id)
250+
self.delete_image(image_id=image_info.image_id)
229251
except Exception as e:
230252
exceptions.append(e)
231253
for image_info in self.preserved_images:
232254
# noop - just log that we're not cleaning up these images
233255
self._log.info(
234256
"Preserved image %s [id:%s] is NOT being cleaned up.",
235-
image_info.name,
236-
image_info.id,
257+
image_info.image_name,
258+
image_info.image_id,
237259
)
238260
return exceptions
239261

@@ -329,6 +351,18 @@ def _validate_tag(tag: str):
329351
raise InvalidTagNameError(tag=tag, rules_failed=rules_failed)
330352

331353
def _get_ssh_keys(self) -> KeyPair:
354+
"""
355+
Get the ssh key pair to use for the cloud instance.
356+
357+
If no key pair is provided in the config file, the default key pair
358+
will be used. The default key pair is the id_rsa or id_ed25519 key
359+
in the user's .ssh directory.
360+
361+
:raises PycloudlibError: if no public key path is provided and no default key is found
362+
:raises PycloudlibError: if the public key path provided in the config does not exist
363+
364+
:return: KeyPair object with the public and private key paths
365+
"""
332366
user = getpass.getuser()
333367
# check if id_rsa or id_ed25519 keys exist in the user's .ssh directory
334368
possible_default_keys = [
@@ -380,21 +414,48 @@ def _store_snapshot_info(
380414
:return: ImageInfo object with the snapshot information
381415
"""
382416
image_info = ImageInfo(
383-
id=snapshot_id,
384-
name=snapshot_name,
417+
image_id=snapshot_id,
418+
image_name=snapshot_name,
385419
)
386420
if not keep_snapshot:
387421
self.created_images.append(image_info)
388422
self._log.info(
389-
"Created temporary snapshot %s [id:%s]",
390-
image_info.name,
391-
image_info.id,
423+
"Created temporary snapshot %s",
424+
image_info,
392425
)
393426
else:
394427
self.preserved_images.append(image_info)
395428
self._log.info(
396-
"Created permanent snapshot %s [id:%s]",
397-
image_info.name,
398-
image_info.id,
429+
"Created permanent snapshot %s",
430+
image_info,
399431
)
400432
return image_info
433+
434+
def _record_image_deletion(self, image_id: str):
435+
"""
436+
Record the deletion of an image.
437+
438+
This method should be called after an image is successfully deleted.
439+
It will remove the image from the list of created_images or preserved_images
440+
so that the cloud does not attempt to re-clean it up later. It will also log
441+
the deletion of the image.
442+
443+
:param image_id: ID of the image that was deleted
444+
"""
445+
if match := [i for i in self.created_images if i.image_id == image_id]:
446+
deleted_image = match[0]
447+
self.created_images.remove(deleted_image)
448+
self._log.debug(
449+
"Snapshot %s has been deleted. Will no longer need to be cleaned up later.",
450+
deleted_image,
451+
)
452+
elif match := [i for i in self.preserved_images if i.image_id == image_id]:
453+
deleted_image = match[0]
454+
self.preserved_images.remove(deleted_image)
455+
self._log.debug(
456+
"Snapshot %s has been deleted. This snapshot was taken with keep=True, "
457+
"but since it has been manually deleted, it will not be preserved.",
458+
deleted_image,
459+
)
460+
else:
461+
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): # type: ignore
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)