Skip to content

Commit

Permalink
feat!: add ability to retain snapshot after cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
a-dubs committed Oct 15, 2024
1 parent cf6b0d1 commit 1f5f311
Show file tree
Hide file tree
Showing 12 changed files with 131 additions and 29 deletions.
9 changes: 7 additions & 2 deletions pycloudlib/azure/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -1092,12 +1092,13 @@ class compatibility.

raise InstanceNotFoundError(resource_id=instance_id)

def snapshot(self, instance, clean=True, delete_provisioned_user=True, **kwargs):
def snapshot(self, instance, *, clean=True, keep=False, delete_provisioned_user=True, **kwargs):
"""Snapshot an instance and generate an image from it.
Args:
instance: Instance to snapshot
clean: Run instance clean method before taking snapshot
keep: keep the snapshot after the cloud instance is cleaned up
delete_provisioned_user: Deletes the last provisioned user
kwargs: Other named arguments specific to this implementation
Expand Down Expand Up @@ -1129,7 +1130,11 @@ def snapshot(self, instance, clean=True, delete_provisioned_user=True, **kwargs)
image_id = image.id
image_name = image.name

self.created_images.append(image_id)
self._store_snapshot_info(
snapshot_id=image_id,
snapshot_name=image_name,
keep_snapshot=keep,
)

self.registered_images[image_id] = {
"name": image_name,
Expand Down
61 changes: 57 additions & 4 deletions pycloudlib/cloud.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# This file is part of pycloudlib. See LICENSE file for license information.
"""Base class for all other clouds to provide consistent set of functions."""

import dataclasses
import enum
import getpass
import io
Expand Down Expand Up @@ -34,6 +35,14 @@ class ImageType(enum.Enum):
PRO_FIPS = "Pro FIPS"


@dataclasses.dataclass
class ImageInfo:
"""Dataclass to hold image information."""

id: str
name: str


class BaseCloud(ABC):
"""Base Cloud Class."""

Expand All @@ -54,7 +63,8 @@ def __init__(
config_file: path to pycloudlib configuration file
"""
self.created_instances: List[BaseInstance] = []
self.created_images: List[str] = []
self.created_images: List[ImageInfo] = []
self.preserved_images: List[ImageInfo] = [] # each dict will hold an id and name

self._log = logging.getLogger("{}.{}".format(__name__, self.__class__.__name__))
self._check_and_set_config(config_file, required_values)
Expand Down Expand Up @@ -188,12 +198,13 @@ def launch(
raise NotImplementedError

@abstractmethod
def snapshot(self, instance, clean=True, **kwargs):
def snapshot(self, instance, *, clean=True, keep=False, **kwargs):
"""Snapshot an instance and generate an image from it.
Args:
instance: Instance to snapshot
clean: run instance clean method before taking snapshot
keep: keep the snapshot after the cloud instance is cleaned up
Returns:
An image id
Expand All @@ -215,11 +226,18 @@ def clean(self) -> List[Exception]:
instance.delete()
except Exception as e:
exceptions.append(e)
for image_id in self.created_images:
for image_info in self.created_images:
try:
self.delete_image(image_id)
self.delete_image(image_id=image_info.id)
except Exception as e:
exceptions.append(e)
for image_info in self.preserved_images:
# noop - just log that we're not cleaning up these images
self._log.info(
"Preserved image %s [id:%s] is NOT being cleaned up.",
image_info.name,
image_info.id,
)
return exceptions

def list_keys(self):
Expand Down Expand Up @@ -312,3 +330,38 @@ def _validate_tag(tag: str):

if rules_failed:
raise InvalidTagNameError(tag=tag, rules_failed=rules_failed)

def _store_snapshot_info(
self,
snapshot_id: str,
snapshot_name: str,
keep_snapshot: bool,
) -> ImageInfo:
"""
Store the snapshot information in the appropriate list.
:param snapshot_id: ID of the snapshot
:param snapshot_name: Name of the snapshot
:param keep_snapshot: Whether or not to keep the snapshot
:return: ImageInfo object with the snapshot information
"""
image_info = ImageInfo(
id=snapshot_id,
name=snapshot_name,
)
if not keep_snapshot:
self.created_images.append(image_info)
self._log.info(
"Created temporary snapshot %s [id:%s]",
image_info.name,
image_info.id,
)
else:
self.preserved_images.append(image_info)
self._log.info(
"Created permanent snapshot %s [id:%s]",
image_info.name,
image_info.id,
)
return image_info
10 changes: 8 additions & 2 deletions pycloudlib/ec2/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,12 +413,13 @@ def list_keys(self):
keypair_names.append(keypair["KeyName"])
return keypair_names

def snapshot(self, instance, clean=True):
def snapshot(self, instance, *, clean=True, keep=False, **kwargs):
"""Snapshot an instance and generate an image from it.
Args:
instance: Instance to snapshot
clean: run instance clean method before taking snapshot
keep: keep the snapshot after the cloud instance is cleaned up
Returns:
An image id
Expand All @@ -437,7 +438,12 @@ def snapshot(self, instance, clean=True):
)
image_ami_edited = response["ImageId"]
image = self.resource.Image(image_ami_edited)
self.created_images.append(image.id)

self._store_snapshot_info(
snapshot_id=image.id,
snapshot_name=image.name,
keep_snapshot=keep,
)

self._wait_for_snapshot(image)
_tag_resource(image, self.tag)
Expand Down
9 changes: 7 additions & 2 deletions pycloudlib/gce/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,12 +427,13 @@ def launch(
self.created_instances.append(instance)
return instance

def snapshot(self, instance: GceInstance, clean=True, **kwargs):
def snapshot(self, instance: GceInstance, *, clean=True, keep=False, **kwargs):
"""Snapshot an instance and generate an image from it.
Args:
instance: Instance to snapshot
clean: run instance clean method before taking snapshot
keep: keep the snapshot after the cloud instance is cleaned up
Returns:
An image id
Expand Down Expand Up @@ -470,7 +471,11 @@ def snapshot(self, instance: GceInstance, clean=True, **kwargs):
self._wait_for_operation(operation)

image_id = "projects/{}/global/images/{}".format(self.project, snapshot_name)
self.created_images.append(image_id)
self._store_snapshot_info(
snapshot_name=snapshot_name,
snapshot_id=image_id,
keep_snapshot=keep,
)
return image_id

def _wait_for_operation(self, operation, operation_type="global", sleep_seconds=300):
Expand Down
9 changes: 7 additions & 2 deletions pycloudlib/ibm/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,12 +312,13 @@ def launch(

return instance

def snapshot(self, instance: IBMInstance, clean: bool = True, **kwargs) -> str:
def snapshot(self, instance: IBMInstance, *, clean=True, keep=False, **kwargs) -> str:
"""Snapshot an instance and generate an image from it.
Args:
instance: Instance to snapshot
clean: run instance clean method before taking snapshot
keep: keep the snapshot after the cloud instance is cleaned up
Returns:
An image id
Expand Down Expand Up @@ -347,7 +348,11 @@ def snapshot(self, instance: IBMInstance, clean: bool = True, **kwargs) -> str:
f"Snapshot not available after {timeout_seconds} seconds. Check IBM VPC console."
),
)
self.created_images.append(snapshot_id)
self._store_snapshot_info(
snapshot_name=str(image_prototype["name"]),
snapshot_id=snapshot_id,
keep_snapshot=keep,
)
return snapshot_id

def list_keys(self) -> List[str]:
Expand Down
11 changes: 7 additions & 4 deletions pycloudlib/ibm_classic/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,9 @@ def launch(
def snapshot(
self,
instance,
*,
clean=True,
keep=False,
note: Optional[str] = None,
**kwargs,
):
Expand All @@ -276,6 +278,7 @@ def snapshot(
Args:
instance: Instance to snapshot
clean: run instance clean method before taking snapshot
keep: keep the snapshot after the cloud instance is cleaned up
note: optional note to add to the snapshot
Returns:
Expand All @@ -290,10 +293,10 @@ def snapshot(
name=f"{self.tag}-snapshot",
notes=note,
)
self._log.info(
"Successfully created snapshot '%s' with ID: %s",
snapshot_result["name"],
snapshot_result["id"],
self._store_snapshot_info(
snapshot_name=snapshot_result["name"],
snapshot_id=snapshot_result["id"],
keep_snapshot=keep,
)
return snapshot_result["id"]

Expand Down
8 changes: 6 additions & 2 deletions pycloudlib/lxd/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ def delete_image(self, image_id, **kwargs):
subp(["lxc", "image", "delete", image_id])
self._log.debug("Deleted %s", image_id)

def snapshot(self, instance, clean=True, name=None):
def snapshot(self, instance, *, clean=True, keep=False, name=None):
"""Take a snapshot of the passed in instance for use as image.
:param instance: The instance to create an image from
Expand All @@ -412,7 +412,11 @@ def snapshot(self, instance, clean=True, name=None):
instance.clean()

snapshot_name = instance.snapshot(name)
self.created_snapshots.append(snapshot_name)
self._store_snapshot_info(
snapshot_name=snapshot_name,
snapshot_id=snapshot_name,
keep_snapshot=keep,
)
return snapshot_name

# pylint: disable=broad-except
Expand Down
14 changes: 10 additions & 4 deletions pycloudlib/oci/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,15 +313,17 @@ def launch(
self.created_instances.append(instance)
return instance

def snapshot(self, instance, clean=True, name=None):
def snapshot(self, instance, *, clean=True, keep=False, name=None):
"""Snapshot an instance and generate an image from it.
Args:
instance: Instance to snapshot
clean: run instance clean method before taking snapshot
name: (Optional) Name of created image
keep: Keep the image after the cloud instance is cleaned up
name: Name of created image
Returns:
An image object
The image id of the snapshot
"""
if clean:
instance.clean()
Expand All @@ -340,6 +342,10 @@ def snapshot(self, instance, clean=True, name=None):
desired_state="AVAILABLE",
)

self.created_images.append(image_data.id)
self._store_snapshot_info(
snapshot_name=image_data.display_name,
snapshot_id=image_data.id,
keep_snapshot=keep,
)

return image_data.id
9 changes: 7 additions & 2 deletions pycloudlib/openstack/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,12 +171,13 @@ def launch(
self.created_instances.append(instance)
return instance

def snapshot(self, instance, clean=True, **kwargs):
def snapshot(self, instance, *, clean=True, keep=False, **kwargs):
"""Snapshot an instance and generate an image from it.
Args:
instance: Instance to snapshot
clean: run instance clean method before taking snapshot
keep: keep the snapshot after the cloud instance is cleaned up
Returns:
An image id
Expand All @@ -188,7 +189,11 @@ def snapshot(self, instance, clean=True, **kwargs):
image = self.conn.create_image_snapshot(
"{}-snapshot".format(self.tag), instance.server.id, wait=True
)
self.created_images.append(image.id)
self._store_snapshot_info(
snapshot_name=image.name,
snapshot_id=image.id,
keep_snapshot=keep,
)
return image.id

def use_key(self, public_key_path, private_key_path=None, name=None):
Expand Down
9 changes: 7 additions & 2 deletions pycloudlib/qemu/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -542,12 +542,13 @@ def launch(

return instance

def snapshot(self, instance: QemuInstance, clean=True, **kwargs) -> str:
def snapshot(self, instance: QemuInstance, *, clean=True, keep=False, **kwargs) -> str:
"""Snapshot an instance and generate an image from it.
Args:
instance: Instance to snapshot
clean: run instance clean method before taking snapshot
keep: keep the snapshot after the cloud instance is cleaned up
Returns:
An image id
Expand Down Expand Up @@ -596,7 +597,11 @@ def snapshot(self, instance: QemuInstance, clean=True, **kwargs) -> str:
snapshot_path,
instance.instance_path,
)
self.created_images.append(str(snapshot_path))
self._store_snapshot_info(
snapshot_name=snapshot_path.stem,
snapshot_id=str(snapshot_path),
keep_snapshot=keep,
)

return str(snapshot_path)

Expand Down
9 changes: 7 additions & 2 deletions pycloudlib/vmware/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,12 +220,13 @@ def launch(
instance.start()
return instance

def snapshot(self, instance, clean=True, **kwargs):
def snapshot(self, instance, *, clean=True, keep=False, **kwargs):
"""Snapshot an instance and generate an image from it.
Args:
instance: Instance to snapshot
clean: run instance clean method before taking snapshot
keep: keep the snapshot after the cloud instance is cleaned up
Returns:
An image id
Expand All @@ -246,6 +247,10 @@ def snapshot(self, instance, clean=True, **kwargs):
check=True,
)

self.created_images.append(image_name)
self._store_snapshot_info(
snapshot_name=image_name,
snapshot_id=image_name,
keep_snapshot=keep,
)

return image_name
Loading

0 comments on commit 1f5f311

Please sign in to comment.