Skip to content

Commit 1f5f311

Browse files
committed
feat!: add ability to retain snapshot after cleanup
1 parent cf6b0d1 commit 1f5f311

File tree

12 files changed

+131
-29
lines changed

12 files changed

+131
-29
lines changed

pycloudlib/azure/cloud.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,12 +1092,13 @@ class compatibility.
10921092

10931093
raise InstanceNotFoundError(resource_id=instance_id)
10941094

1095-
def snapshot(self, instance, clean=True, delete_provisioned_user=True, **kwargs):
1095+
def snapshot(self, instance, *, clean=True, keep=False, delete_provisioned_user=True, **kwargs):
10961096
"""Snapshot an instance and generate an image from it.
10971097
10981098
Args:
10991099
instance: Instance to snapshot
11001100
clean: Run instance clean method before taking snapshot
1101+
keep: keep the snapshot after the cloud instance is cleaned up
11011102
delete_provisioned_user: Deletes the last provisioned user
11021103
kwargs: Other named arguments specific to this implementation
11031104
@@ -1129,7 +1130,11 @@ def snapshot(self, instance, clean=True, delete_provisioned_user=True, **kwargs)
11291130
image_id = image.id
11301131
image_name = image.name
11311132

1132-
self.created_images.append(image_id)
1133+
self._store_snapshot_info(
1134+
snapshot_id=image_id,
1135+
snapshot_name=image_name,
1136+
keep_snapshot=keep,
1137+
)
11331138

11341139
self.registered_images[image_id] = {
11351140
"name": image_name,

pycloudlib/cloud.py

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# This file is part of pycloudlib. See LICENSE file for license information.
22
"""Base class for all other clouds to provide consistent set of functions."""
33

4+
import dataclasses
45
import enum
56
import getpass
67
import io
@@ -34,6 +35,14 @@ class ImageType(enum.Enum):
3435
PRO_FIPS = "Pro FIPS"
3536

3637

38+
@dataclasses.dataclass
39+
class ImageInfo:
40+
"""Dataclass to hold image information."""
41+
42+
id: str
43+
name: str
44+
45+
3746
class BaseCloud(ABC):
3847
"""Base Cloud Class."""
3948

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

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

190200
@abstractmethod
191-
def snapshot(self, instance, clean=True, **kwargs):
201+
def snapshot(self, instance, *, clean=True, keep=False, **kwargs):
192202
"""Snapshot an instance and generate an image from it.
193203
194204
Args:
195205
instance: Instance to snapshot
196206
clean: run instance clean method before taking snapshot
207+
keep: keep the snapshot after the cloud instance is cleaned up
197208
198209
Returns:
199210
An image id
@@ -215,11 +226,18 @@ def clean(self) -> List[Exception]:
215226
instance.delete()
216227
except Exception as e:
217228
exceptions.append(e)
218-
for image_id in self.created_images:
229+
for image_info in self.created_images:
219230
try:
220-
self.delete_image(image_id)
231+
self.delete_image(image_id=image_info.id)
221232
except Exception as e:
222233
exceptions.append(e)
234+
for image_info in self.preserved_images:
235+
# noop - just log that we're not cleaning up these images
236+
self._log.info(
237+
"Preserved image %s [id:%s] is NOT being cleaned up.",
238+
image_info.name,
239+
image_info.id,
240+
)
223241
return exceptions
224242

225243
def list_keys(self):
@@ -312,3 +330,38 @@ def _validate_tag(tag: str):
312330

313331
if rules_failed:
314332
raise InvalidTagNameError(tag=tag, rules_failed=rules_failed)
333+
334+
def _store_snapshot_info(
335+
self,
336+
snapshot_id: str,
337+
snapshot_name: str,
338+
keep_snapshot: bool,
339+
) -> ImageInfo:
340+
"""
341+
Store the snapshot information in the appropriate list.
342+
343+
:param snapshot_id: ID of the snapshot
344+
:param snapshot_name: Name of the snapshot
345+
:param keep_snapshot: Whether or not to keep the snapshot
346+
347+
:return: ImageInfo object with the snapshot information
348+
"""
349+
image_info = ImageInfo(
350+
id=snapshot_id,
351+
name=snapshot_name,
352+
)
353+
if not keep_snapshot:
354+
self.created_images.append(image_info)
355+
self._log.info(
356+
"Created temporary snapshot %s [id:%s]",
357+
image_info.name,
358+
image_info.id,
359+
)
360+
else:
361+
self.preserved_images.append(image_info)
362+
self._log.info(
363+
"Created permanent snapshot %s [id:%s]",
364+
image_info.name,
365+
image_info.id,
366+
)
367+
return image_info

pycloudlib/ec2/cloud.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -413,12 +413,13 @@ def list_keys(self):
413413
keypair_names.append(keypair["KeyName"])
414414
return keypair_names
415415

416-
def snapshot(self, instance, clean=True):
416+
def snapshot(self, instance, *, clean=True, keep=False, **kwargs):
417417
"""Snapshot an instance and generate an image from it.
418418
419419
Args:
420420
instance: Instance to snapshot
421421
clean: run instance clean method before taking snapshot
422+
keep: keep the snapshot after the cloud instance is cleaned up
422423
423424
Returns:
424425
An image id
@@ -437,7 +438,12 @@ def snapshot(self, instance, clean=True):
437438
)
438439
image_ami_edited = response["ImageId"]
439440
image = self.resource.Image(image_ami_edited)
440-
self.created_images.append(image.id)
441+
442+
self._store_snapshot_info(
443+
snapshot_id=image.id,
444+
snapshot_name=image.name,
445+
keep_snapshot=keep,
446+
)
441447

442448
self._wait_for_snapshot(image)
443449
_tag_resource(image, self.tag)

pycloudlib/gce/cloud.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -427,12 +427,13 @@ def launch(
427427
self.created_instances.append(instance)
428428
return instance
429429

430-
def snapshot(self, instance: GceInstance, clean=True, **kwargs):
430+
def snapshot(self, instance: GceInstance, *, clean=True, keep=False, **kwargs):
431431
"""Snapshot an instance and generate an image from it.
432432
433433
Args:
434434
instance: Instance to snapshot
435435
clean: run instance clean method before taking snapshot
436+
keep: keep the snapshot after the cloud instance is cleaned up
436437
437438
Returns:
438439
An image id
@@ -470,7 +471,11 @@ def snapshot(self, instance: GceInstance, clean=True, **kwargs):
470471
self._wait_for_operation(operation)
471472

472473
image_id = "projects/{}/global/images/{}".format(self.project, snapshot_name)
473-
self.created_images.append(image_id)
474+
self._store_snapshot_info(
475+
snapshot_name=snapshot_name,
476+
snapshot_id=image_id,
477+
keep_snapshot=keep,
478+
)
474479
return image_id
475480

476481
def _wait_for_operation(self, operation, operation_type="global", sleep_seconds=300):

pycloudlib/ibm/cloud.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,12 +312,13 @@ def launch(
312312

313313
return instance
314314

315-
def snapshot(self, instance: IBMInstance, clean: bool = True, **kwargs) -> str:
315+
def snapshot(self, instance: IBMInstance, *, clean=True, keep=False, **kwargs) -> str:
316316
"""Snapshot an instance and generate an image from it.
317317
318318
Args:
319319
instance: Instance to snapshot
320320
clean: run instance clean method before taking snapshot
321+
keep: keep the snapshot after the cloud instance is cleaned up
321322
322323
Returns:
323324
An image id
@@ -347,7 +348,11 @@ def snapshot(self, instance: IBMInstance, clean: bool = True, **kwargs) -> str:
347348
f"Snapshot not available after {timeout_seconds} seconds. Check IBM VPC console."
348349
),
349350
)
350-
self.created_images.append(snapshot_id)
351+
self._store_snapshot_info(
352+
snapshot_name=str(image_prototype["name"]),
353+
snapshot_id=snapshot_id,
354+
keep_snapshot=keep,
355+
)
351356
return snapshot_id
352357

353358
def list_keys(self) -> List[str]:

pycloudlib/ibm_classic/cloud.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,9 @@ def launch(
267267
def snapshot(
268268
self,
269269
instance,
270+
*,
270271
clean=True,
272+
keep=False,
271273
note: Optional[str] = None,
272274
**kwargs,
273275
):
@@ -276,6 +278,7 @@ def snapshot(
276278
Args:
277279
instance: Instance to snapshot
278280
clean: run instance clean method before taking snapshot
281+
keep: keep the snapshot after the cloud instance is cleaned up
279282
note: optional note to add to the snapshot
280283
281284
Returns:
@@ -290,10 +293,10 @@ def snapshot(
290293
name=f"{self.tag}-snapshot",
291294
notes=note,
292295
)
293-
self._log.info(
294-
"Successfully created snapshot '%s' with ID: %s",
295-
snapshot_result["name"],
296-
snapshot_result["id"],
296+
self._store_snapshot_info(
297+
snapshot_name=snapshot_result["name"],
298+
snapshot_id=snapshot_result["id"],
299+
keep_snapshot=keep,
297300
)
298301
return snapshot_result["id"]
299302

pycloudlib/lxd/cloud.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,7 @@ def delete_image(self, image_id, **kwargs):
397397
subp(["lxc", "image", "delete", image_id])
398398
self._log.debug("Deleted %s", image_id)
399399

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

414414
snapshot_name = instance.snapshot(name)
415-
self.created_snapshots.append(snapshot_name)
415+
self._store_snapshot_info(
416+
snapshot_name=snapshot_name,
417+
snapshot_id=snapshot_name,
418+
keep_snapshot=keep,
419+
)
416420
return snapshot_name
417421

418422
# pylint: disable=broad-except

pycloudlib/oci/cloud.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -313,15 +313,17 @@ def launch(
313313
self.created_instances.append(instance)
314314
return instance
315315

316-
def snapshot(self, instance, clean=True, name=None):
316+
def snapshot(self, instance, *, clean=True, keep=False, name=None):
317317
"""Snapshot an instance and generate an image from it.
318318
319319
Args:
320320
instance: Instance to snapshot
321321
clean: run instance clean method before taking snapshot
322-
name: (Optional) Name of created image
322+
keep: Keep the image after the cloud instance is cleaned up
323+
name: Name of created image
324+
323325
Returns:
324-
An image object
326+
The image id of the snapshot
325327
"""
326328
if clean:
327329
instance.clean()
@@ -340,6 +342,10 @@ def snapshot(self, instance, clean=True, name=None):
340342
desired_state="AVAILABLE",
341343
)
342344

343-
self.created_images.append(image_data.id)
345+
self._store_snapshot_info(
346+
snapshot_name=image_data.display_name,
347+
snapshot_id=image_data.id,
348+
keep_snapshot=keep,
349+
)
344350

345351
return image_data.id

pycloudlib/openstack/cloud.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,12 +171,13 @@ def launch(
171171
self.created_instances.append(instance)
172172
return instance
173173

174-
def snapshot(self, instance, clean=True, **kwargs):
174+
def snapshot(self, instance, *, clean=True, keep=False, **kwargs):
175175
"""Snapshot an instance and generate an image from it.
176176
177177
Args:
178178
instance: Instance to snapshot
179179
clean: run instance clean method before taking snapshot
180+
keep: keep the snapshot after the cloud instance is cleaned up
180181
181182
Returns:
182183
An image id
@@ -188,7 +189,11 @@ def snapshot(self, instance, clean=True, **kwargs):
188189
image = self.conn.create_image_snapshot(
189190
"{}-snapshot".format(self.tag), instance.server.id, wait=True
190191
)
191-
self.created_images.append(image.id)
192+
self._store_snapshot_info(
193+
snapshot_name=image.name,
194+
snapshot_id=image.id,
195+
keep_snapshot=keep,
196+
)
192197
return image.id
193198

194199
def use_key(self, public_key_path, private_key_path=None, name=None):

pycloudlib/qemu/cloud.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -542,12 +542,13 @@ def launch(
542542

543543
return instance
544544

545-
def snapshot(self, instance: QemuInstance, clean=True, **kwargs) -> str:
545+
def snapshot(self, instance: QemuInstance, *, clean=True, keep=False, **kwargs) -> str:
546546
"""Snapshot an instance and generate an image from it.
547547
548548
Args:
549549
instance: Instance to snapshot
550550
clean: run instance clean method before taking snapshot
551+
keep: keep the snapshot after the cloud instance is cleaned up
551552
552553
Returns:
553554
An image id
@@ -596,7 +597,11 @@ def snapshot(self, instance: QemuInstance, clean=True, **kwargs) -> str:
596597
snapshot_path,
597598
instance.instance_path,
598599
)
599-
self.created_images.append(str(snapshot_path))
600+
self._store_snapshot_info(
601+
snapshot_name=snapshot_path.stem,
602+
snapshot_id=str(snapshot_path),
603+
keep_snapshot=keep,
604+
)
600605

601606
return str(snapshot_path)
602607

0 commit comments

Comments
 (0)