awspub 0.0.9__py3-none-any.whl → 0.0.11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
awspub/exceptions.py CHANGED
@@ -10,6 +10,10 @@ class MultipleImagesException(Exception):
10
10
  pass
11
11
 
12
12
 
13
+ class IncompleteImageSetException(Exception):
14
+ pass
15
+
16
+
13
17
  class BucketDoesNotExistException(Exception):
14
18
  def __init__(self, bucket_name: str, *args, **kwargs):
15
19
  msg = f"The bucket named '{bucket_name}' does not exist. You will need to create the bucket before proceeding."
awspub/image.py CHANGED
@@ -5,6 +5,7 @@ from enum import Enum
5
5
  from typing import Any, Dict, List, Optional
6
6
 
7
7
  import boto3
8
+ import botocore.exceptions
8
9
  from mypy_boto3_ec2.client import EC2Client
9
10
  from mypy_boto3_ssm import SSMClient
10
11
 
@@ -418,6 +419,7 @@ class Image:
418
419
  )
419
420
 
420
421
  images: Dict[str, _ImageInfo] = dict()
422
+ missing_regions: List[str] = []
421
423
  for region in self.image_regions:
422
424
  ec2client_region: EC2Client = boto3.client("ec2", region_name=region)
423
425
  image_info: Optional[_ImageInfo] = self._get(ec2client_region)
@@ -435,53 +437,10 @@ class Image:
435
437
  )
436
438
  images[region] = image_info
437
439
  else:
438
- logger.info(
439
- f"creating image with name '{self.image_name}' in "
440
- f"region {ec2client_region.meta.region_name} ..."
441
- )
442
-
443
- register_image_kwargs = dict(
444
- Name=self.image_name,
445
- Description=self.conf.get("description", ""),
446
- Architecture=self._ctx.conf["source"]["architecture"],
447
- RootDeviceName=self.conf["root_device_name"],
448
- BlockDeviceMappings=[
449
- {
450
- "Ebs": {
451
- "SnapshotId": snapshot_ids[region],
452
- "VolumeType": self.conf["root_device_volume_type"],
453
- "VolumeSize": self.conf["root_device_volume_size"],
454
- },
455
- "DeviceName": self.conf["root_device_name"],
456
- },
457
- # TODO: make those ephemeral block device mappings configurable
458
- {"VirtualName": "ephemeral0", "DeviceName": "/dev/sdb"},
459
- {"VirtualName": "ephemeral1", "DeviceName": "/dev/sdc"},
460
- ],
461
- EnaSupport=True,
462
- SriovNetSupport="simple",
463
- VirtualizationType="hvm",
464
- BootMode=self.conf["boot_mode"],
465
- )
466
-
467
- if self.conf["tpm_support"]:
468
- register_image_kwargs["TpmSupport"] = self.conf["tpm_support"]
469
-
470
- if self.conf["imds_support"]:
471
- register_image_kwargs["ImdsSupport"] = self.conf["imds_support"]
472
-
473
- if self.conf["uefi_data"]:
474
- with open(self.conf["uefi_data"], "r") as f:
475
- uefi_data = f.read()
476
- register_image_kwargs["UefiData"] = uefi_data
477
-
478
- if self.conf["billing_products"]:
479
- register_image_kwargs["BillingProducts"] = self.conf["billing_products"]
480
-
481
- resp = ec2client_region.register_image(**register_image_kwargs)
482
- ec2client_region.create_tags(Resources=[resp["ImageId"]], Tags=self._tags)
483
- images[region] = _ImageInfo(resp["ImageId"], snapshot_ids[region])
484
-
440
+ if image := self._register_image(snapshot_ids[region], ec2client_region):
441
+ images[region] = image
442
+ else:
443
+ missing_regions.append(region)
485
444
  # wait for the images
486
445
  logger.info(f"Waiting for {len(images)} images to be ready the regions ...")
487
446
  for region, image_info in images.items():
@@ -500,8 +459,80 @@ class Image:
500
459
  if self.conf["share"]:
501
460
  self._share(self.conf["share"], images)
502
461
 
462
+ if missing_regions:
463
+ logger.error("Failed to publish images to all regions", extra={"missing_regions": missing_regions})
464
+ raise exceptions.IncompleteImageSetException("Incomplete image set published")
465
+
503
466
  return images
504
467
 
468
+ def _register_image(self, snapshot_id: str, ec2client: EC2Client) -> Optional[_ImageInfo]:
469
+ """
470
+ Register snapshot_id in region configured for ec2client_region
471
+
472
+ :param snapshot_id: snapshot id to use for image registration
473
+ :type snapshot_id: str
474
+ :param ec2client: EC2Client for the region to register image to
475
+ :type ec2client: EC2Client
476
+ :return: _ImageInfo containing the ImageId SnapshotId pair
477
+ :rtype: _ImageInfo
478
+ """
479
+ logger.info(f"creating image with name '{self.image_name}' in " f"region {ec2client.meta.region_name} ...")
480
+
481
+ register_image_kwargs = dict(
482
+ Name=self.image_name,
483
+ Description=self.conf.get("description", ""),
484
+ Architecture=self._ctx.conf["source"]["architecture"],
485
+ RootDeviceName=self.conf["root_device_name"],
486
+ BlockDeviceMappings=[
487
+ {
488
+ "Ebs": {
489
+ "SnapshotId": snapshot_id,
490
+ "VolumeType": self.conf["root_device_volume_type"],
491
+ "VolumeSize": self.conf["root_device_volume_size"],
492
+ },
493
+ "DeviceName": self.conf["root_device_name"],
494
+ },
495
+ # TODO: make those ephemeral block device mappings configurable
496
+ {"VirtualName": "ephemeral0", "DeviceName": "/dev/sdb"},
497
+ {"VirtualName": "ephemeral1", "DeviceName": "/dev/sdc"},
498
+ ],
499
+ EnaSupport=True,
500
+ SriovNetSupport="simple",
501
+ VirtualizationType="hvm",
502
+ BootMode=self.conf["boot_mode"],
503
+ )
504
+
505
+ if self.conf["tpm_support"]:
506
+ register_image_kwargs["TpmSupport"] = self.conf["tpm_support"]
507
+
508
+ if self.conf["imds_support"]:
509
+ register_image_kwargs["ImdsSupport"] = self.conf["imds_support"]
510
+
511
+ if self.conf["uefi_data"]:
512
+ with open(self.conf["uefi_data"], "r") as f:
513
+ uefi_data = f.read()
514
+ register_image_kwargs["UefiData"] = uefi_data
515
+
516
+ if self.conf["billing_products"]:
517
+ register_image_kwargs["BillingProducts"] = self.conf["billing_products"]
518
+
519
+ try:
520
+ resp = ec2client.register_image(**register_image_kwargs)
521
+ except botocore.exceptions.ClientError as e:
522
+ if e.response.get("Error", {}).get("Code", None) == "OperationNotPermitted":
523
+ logger.exception(
524
+ "Unable to register image",
525
+ extra={
526
+ "registration_options": register_image_kwargs,
527
+ "region": ec2client.meta.region_name,
528
+ },
529
+ )
530
+ return None
531
+ raise e
532
+
533
+ ec2client.create_tags(Resources=[resp["ImageId"]], Tags=self._tags)
534
+ return _ImageInfo(resp["ImageId"], snapshot_id)
535
+
505
536
  def publish(self) -> None:
506
537
  """
507
538
  Handle all publication steps
@@ -1,6 +1,7 @@
1
1
  import pathlib
2
- from unittest.mock import patch
2
+ from unittest.mock import MagicMock, patch
3
3
 
4
+ import botocore.exceptions
4
5
  import pytest
5
6
 
6
7
  from awspub import context, exceptions, image
@@ -494,3 +495,62 @@ def test_image__share_list_filtered(partition, imagename, share_list_expected):
494
495
  ctx = context.Context(curdir / "fixtures/config1.yaml", None)
495
496
  img = image.Image(ctx, imagename)
496
497
  assert img._share_list_filtered(img.conf["share"]) == share_list_expected
498
+
499
+
500
+ @patch("awspub.s3.S3.bucket_region", return_value="region1")
501
+ def test_create__should_allow_partial_registration(s3_bucket_mock):
502
+ """
503
+ Test that the create() method allows a partial upload set
504
+ """
505
+ with patch("boto3.client") as bclient_mock:
506
+ instance = bclient_mock.return_value
507
+
508
+ ctx = context.Context(curdir / "fixtures/config1.yaml", None)
509
+ img = image.Image(ctx, "test-image-6")
510
+ img._image_regions = ["region1", "region2"]
511
+ img._image_regions_cached = True
512
+ with patch.object(img, "_get") as get_mock, patch.object(img._snapshot, "copy") as copy_mock:
513
+ copy_mock.return_value = {r: f"snapshot{i}" for i, r in enumerate(img.image_regions)}
514
+ get_mock.return_value = None
515
+ instance.register_image.side_effect = [
516
+ botocore.exceptions.ClientError(
517
+ {
518
+ "Error": {
519
+ "Code": "OperationNotPermitted",
520
+ "Message": "Intentional permission failure for snapshot0",
521
+ }
522
+ },
523
+ "awspub Testing",
524
+ ),
525
+ {"ImageId": "id1"},
526
+ ]
527
+ with pytest.raises(exceptions.IncompleteImageSetException):
528
+ img.create() == {"region2": image._ImageInfo("id1", "snapshot1")}
529
+ # register and create_tags should be called since at least one snapshot made it
530
+ assert instance.register_image.called
531
+ assert instance.create_tags.called
532
+
533
+
534
+ def test_register_image__should_return_none_on_permission_failures():
535
+ instance = MagicMock()
536
+
537
+ instance.register_image.side_effect = botocore.exceptions.ClientError(
538
+ {"Error": {"Code": "OperationNotPermitted", "Message": "Testing"}}, "Testing"
539
+ )
540
+ ctx = context.Context(curdir / "fixtures/config1.yaml", None)
541
+ img = image.Image(ctx, "test-image-6")
542
+ snapshot_ids = {"eu-central-1": "my-snapshot"}
543
+ assert img._register_image(snapshot_ids["eu-central-1"], instance) is None
544
+
545
+
546
+ def test_register_image__should_raise_on_unhandled_client_error():
547
+ instance = MagicMock()
548
+
549
+ instance.register_image.side_effect = botocore.exceptions.ClientError(
550
+ {"Error": {"Code": "UnsupportedOperation", "Message": "Testing"}}, "Testing"
551
+ )
552
+ ctx = context.Context(curdir / "fixtures/config1.yaml", None)
553
+ img = image.Image(ctx, "test-image-6")
554
+ snapshot_ids = {"eu-central-1": "my-snapshot"}
555
+ with pytest.raises(botocore.exceptions.ClientError):
556
+ img._register_image(snapshot_ids["eu-central-1"], instance) is None
@@ -1,8 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: awspub
3
- Version: 0.0.9
3
+ Version: 0.0.11
4
4
  Summary: Publish images to AWS EC2
5
- Home-page: https://github.com/canonical/awspub
6
5
  License: GPL-3.0-or-later
7
6
  Keywords: AWS,EC2,publication
8
7
  Author: Thomas Bechtold
@@ -19,6 +18,7 @@ Requires-Dist: boto3
19
18
  Requires-Dist: boto3-stubs[essential,marketplace-catalog,s3,sns,ssm,sts] (>=1.28.83,<2.0.0)
20
19
  Requires-Dist: pydantic (>=2,<3)
21
20
  Requires-Dist: ruamel-yaml (>=0.18.6,<0.19.0)
21
+ Project-URL: Homepage, https://github.com/canonical/awspub
22
22
  Project-URL: Repository, https://github.com/canonical/awspub
23
23
  Description-Content-Type: text/x-rst
24
24
 
@@ -4,8 +4,8 @@ awspub/cli/__init__.py,sha256=-zCBEbnt5zbvSZ8PxQALpPAy0CiQUf-qZnikJ7U4Sf0,5621
4
4
  awspub/common.py,sha256=RFNPfPAw_C0mtdEyBoWWMP6n6CyESdakXhzAvDoyAfQ,2340
5
5
  awspub/configmodels.py,sha256=_6I_-1YzfJiSY81QWK-T3afiQUMx810lQQ3EjoEVuNg,9396
6
6
  awspub/context.py,sha256=LDkp9Sz5AqRxQq70ICgFIJn5g2qrc5qiVawTyS_rXZE,4064
7
- awspub/exceptions.py,sha256=2JUEPhZ3sir2NoAuqteFYlh84LsrD7vaeIpkiWtiBpc,556
8
- awspub/image.py,sha256=JVJEOnlGR34QUxH-APiWtxTmF6rp2EV846Uimi8o-uU,26878
7
+ awspub/exceptions.py,sha256=edWb03Gv35nDv3eoQGwI7rvsX5FNZT1otkBV8ly3W1A,613
8
+ awspub/image.py,sha256=RuJ8gOAHhnqGUBEBMT7WYSruI688r_1pOTf0nzzB3tU,27965
9
9
  awspub/image_marketplace.py,sha256=oiD7yNU5quG5CQG9Ql5Ut9hLWA1yewg6qVwTbyadGwc,5314
10
10
  awspub/s3.py,sha256=ivR8DuAkYilph73EjFkTgUelkXxU7pZfosnsHHyoZkQ,11274
11
11
  awspub/snapshot.py,sha256=V5e_07SnmCwEPjRmwZh43spWparhH8X4ugG16uQfGuo,10040
@@ -23,13 +23,13 @@ awspub/tests/test_api.py,sha256=eC8iqpGFgFygSbqlyWLiLjSW7TwZeEtngJ-g7CPQT7I,2603
23
23
  awspub/tests/test_cli.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
24
  awspub/tests/test_common.py,sha256=K61eBmBt8NPqyME8co_dt6dr_yvlw3UZ54klcw7m2RA,1217
25
25
  awspub/tests/test_context.py,sha256=wMXQqj4vi2U3q5w1xPV-stB3mp3K6puUyXhsShJG4wA,3115
26
- awspub/tests/test_image.py,sha256=B1X4iy4B9o45InnsEbU8YLghhq7BZKvkU1hVC7l0j1k,19523
26
+ awspub/tests/test_image.py,sha256=CNcy8_ySSlPmTl1CwBJncPm4p9TAopUNyjbaNEqemaE,22170
27
27
  awspub/tests/test_image_marketplace.py,sha256=JP7PrFjix1AyQg7eEaQ-wCROVoIOb873koseniOqGQQ,1456
28
28
  awspub/tests/test_s3.py,sha256=UJL8CQDEvhA42MwPGeSvSbQFj8h86c1LrLFDvcMcRws,2857
29
29
  awspub/tests/test_snapshot.py,sha256=8KPTqGVyzrpivWuq3HE7ZhgtLllcr3rA_3hZcxu2xjg,4123
30
30
  awspub/tests/test_sns.py,sha256=XdZh0ETwRHSp_77UFkot-07BlS8pKVMEJIs2HS9EdaQ,6622
31
- awspub-0.0.9.dist-info/LICENSE,sha256=9GbrzFQ3rWjVKj-IZnX1kGDsIGIdjc25KGRmAp03Jn0,35150
32
- awspub-0.0.9.dist-info/METADATA,sha256=3QPgFi4EUPU2fR5M8t3xU69Sb70QDvmTpwRYzmwCQt0,1405
33
- awspub-0.0.9.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
34
- awspub-0.0.9.dist-info/entry_points.txt,sha256=hrQzy9P5yO58nj6W0UDPdQPUTqEkQLpMvuyDDRu7LRQ,42
35
- awspub-0.0.9.dist-info/RECORD,,
31
+ awspub-0.0.11.dist-info/LICENSE,sha256=9GbrzFQ3rWjVKj-IZnX1kGDsIGIdjc25KGRmAp03Jn0,35150
32
+ awspub-0.0.11.dist-info/METADATA,sha256=gRDUHcUTxQNeZZqgdXB_-Pg3sFrQb7PLmgGH0XqWcrg,1418
33
+ awspub-0.0.11.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
34
+ awspub-0.0.11.dist-info/entry_points.txt,sha256=hrQzy9P5yO58nj6W0UDPdQPUTqEkQLpMvuyDDRu7LRQ,42
35
+ awspub-0.0.11.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.1
2
+ Generator: poetry-core 2.1.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any