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 +4 -0
- awspub/image.py +78 -47
- awspub/tests/test_image.py +61 -1
- {awspub-0.0.9.dist-info → awspub-0.0.11.dist-info}/METADATA +3 -3
- {awspub-0.0.9.dist-info → awspub-0.0.11.dist-info}/RECORD +8 -8
- {awspub-0.0.9.dist-info → awspub-0.0.11.dist-info}/WHEEL +1 -1
- {awspub-0.0.9.dist-info → awspub-0.0.11.dist-info}/LICENSE +0 -0
- {awspub-0.0.9.dist-info → awspub-0.0.11.dist-info}/entry_points.txt +0 -0
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
|
-
|
439
|
-
|
440
|
-
|
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
|
awspub/tests/test_image.py
CHANGED
@@ -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
|
+
Metadata-Version: 2.3
|
2
2
|
Name: awspub
|
3
|
-
Version: 0.0.
|
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=
|
8
|
-
awspub/image.py,sha256=
|
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=
|
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.
|
32
|
-
awspub-0.0.
|
33
|
-
awspub-0.0.
|
34
|
-
awspub-0.0.
|
35
|
-
awspub-0.0.
|
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,,
|
File without changes
|
File without changes
|