awspub 0.0.10__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/sns.py ADDED
@@ -0,0 +1,105 @@
1
+ """
2
+ Methods used to handle notifications for AWS using SNS
3
+ """
4
+
5
+ import json
6
+ import logging
7
+ from typing import Any, Dict, List
8
+
9
+ import boto3
10
+ from botocore.exceptions import ClientError
11
+ from mypy_boto3_sns.client import SNSClient
12
+ from mypy_boto3_sts.client import STSClient
13
+
14
+ from awspub.common import _get_regions
15
+ from awspub.context import Context
16
+ from awspub.exceptions import AWSAuthorizationException, AWSNotificationException
17
+ from awspub.s3 import S3
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class SNSNotification(object):
23
+ """
24
+ A data object that contains validation logic and
25
+ structuring rules for SNS notification JSON
26
+ """
27
+
28
+ def __init__(self, context: Context, image_name: str):
29
+ """
30
+ Construct a message and verify that it is valid
31
+ """
32
+ self._ctx: Context = context
33
+ self._image_name: str = image_name
34
+ self._s3: S3 = S3(context)
35
+
36
+ @property
37
+ def conf(self) -> List[Dict[str, Any]]:
38
+ """
39
+ The sns configuration for the current image (based on "image_name") from context
40
+ """
41
+ return self._ctx.conf["images"][self._image_name]["sns"]
42
+
43
+ def _sns_regions(self, topic_config: Dict[Any, Any]) -> List[str]:
44
+ """
45
+ Get the sns regions. Either configured in the sns configuration
46
+ or all available regions.
47
+ If a region is listed that is not available in the currently used partition,
48
+ that region will be ignored (eg. having us-east-1 configured but running in the aws-cn
49
+ partition doesn't include us-east-1 here).
50
+ """
51
+
52
+ regions_configured = topic_config["regions"] if "regions" in topic_config else []
53
+ sns_regions = _get_regions(self._s3.bucket_region, regions_configured)
54
+
55
+ return sns_regions
56
+
57
+ def _get_topic_arn(self, topic_name: str, region_name: str) -> str:
58
+ """
59
+ Calculate topic ARN based on partition, region, account and topic name
60
+ :param topic_name: Name of topic
61
+ :type topic_name: str
62
+ :param region_name: name of region
63
+ :type region_name: str
64
+ :return: return topic ARN
65
+ :rtype: str
66
+ """
67
+
68
+ stsclient: STSClient = boto3.client("sts", region_name=region_name)
69
+ resp = stsclient.get_caller_identity()
70
+
71
+ account = resp["Account"]
72
+ # resp["Arn"] has string format "arn:partition:iam::accountnumber:user/iam_role"
73
+ partition = resp["Arn"].rsplit(":")[1]
74
+
75
+ return f"arn:{partition}:sns:{region_name}:{account}:{topic_name}"
76
+
77
+ def publish(self) -> None:
78
+ """
79
+ send notification to subscribers
80
+ """
81
+
82
+ for topic in self.conf:
83
+ for topic_name, topic_config in topic.items():
84
+ for region_name in self._sns_regions(topic_config):
85
+ snsclient: SNSClient = boto3.client("sns", region_name=region_name)
86
+ try:
87
+ snsclient.publish(
88
+ TopicArn=self._get_topic_arn(topic_name, region_name),
89
+ Subject=topic_config["subject"],
90
+ Message=json.dumps(topic_config["message"]),
91
+ MessageStructure="json",
92
+ )
93
+ except ClientError as e:
94
+ exception_code: str = e.response["Error"]["Code"]
95
+ if exception_code == "AuthorizationError":
96
+ raise AWSAuthorizationException(
97
+ "Profile does not have a permission to send the SNS notification."
98
+ " Please review the policy."
99
+ )
100
+ else:
101
+ raise AWSNotificationException(str(e))
102
+ logger.info(
103
+ f"The SNS notification {topic_config['subject']}"
104
+ f" for the topic {topic_name} in {region_name} has been sent."
105
+ )
File without changes
@@ -0,0 +1,12 @@
1
+ awspub:
2
+ s3:
3
+ bucket_name: "bucket1"
4
+ invalid_field: "not allowed" # This is an invalid field
5
+ source:
6
+ path: "config1.vmdk"
7
+ architecture: "x86_64"
8
+ images:
9
+ test-image:
10
+ description: "Test Image"
11
+ separate_snapshot: "False"
12
+ boot_mode: "uefi-preferred"
@@ -0,0 +1,12 @@
1
+ ---
2
+ awspub:
3
+ source:
4
+ path: "config1.vmdk"
5
+ architecture: "x86_64"
6
+
7
+ s3:
8
+ bucket_name: "bucket1"
9
+
10
+ images:
11
+ "my-custom-image":
12
+ boot_mode: "uefi-preferred"
@@ -0,0 +1,13 @@
1
+ awspub:
2
+ s3:
3
+ bucket_name: "bucket1"
4
+ source:
5
+ path: "config1.vmdk"
6
+ architecture: "x86_64"
7
+ images:
8
+ test-image:
9
+ description: "Test Image"
10
+ separate_snapshot: "False"
11
+ boot_mode: "uefi-preferred"
12
+ notawspub: # to make sure config outside of toplevel `awspub` dict is allowed
13
+ foo_bar: "irrelevant"
Binary file
@@ -0,0 +1,171 @@
1
+ awspub:
2
+ s3:
3
+ bucket_name: "bucket1"
4
+
5
+ source:
6
+ # config1.vmdk generated with
7
+ # dd if=/dev/zero of=config1.raw bs=1K count=1
8
+ # qemu-img convert -f raw -O vmdk -o subformat=streamOptimized config1.raw config1.vmdk
9
+ path: "config1.vmdk"
10
+ architecture: "x86_64"
11
+
12
+ images:
13
+ "test-image-1":
14
+ description: |
15
+ A test image
16
+ boot_mode: "uefi"
17
+ regions:
18
+ - region1
19
+ - region2
20
+ temporary: true
21
+ groups:
22
+ - group1
23
+ - group2
24
+ "test-image-2":
25
+ description: |
26
+ A test image with a separate snapshot
27
+ boot_mode: "uefi"
28
+ separate_snapshot: true
29
+ groups:
30
+ - group1
31
+ "test-image-3":
32
+ description: |
33
+ A test image with a separate snapshot and a billing code
34
+ boot_mode: "uefi"
35
+ separate_snapshot: true
36
+ billing_products:
37
+ - billingcode
38
+ "test-image-4":
39
+ description: |
40
+ A test image without a separate snapshot but a billing product
41
+ boot_mode: "uefi-preferred"
42
+ billing_products:
43
+ - billingcode
44
+ "test-image-5":
45
+ description: |
46
+ A test image without a separate snapshot but multiple billing products
47
+ boot_mode: "uefi-preferred"
48
+ billing_products:
49
+ - billingcode1
50
+ - billingcode2
51
+ "test-image-6":
52
+ description: |
53
+ A test image without a separate snapshot but multiple billing products
54
+ boot_mode: "uefi-preferred"
55
+ regions:
56
+ - "eu-central-1"
57
+ public: true
58
+ tags:
59
+ key1: value1
60
+ "test-image-7":
61
+ description: |
62
+ A test image without a separate snapshot but multiple billing products
63
+ boot_mode: "uefi-preferred"
64
+ regions:
65
+ - "eu-central-1"
66
+ public: true
67
+ temporary: true
68
+ tags:
69
+ key2: name
70
+ name: "not-foobar"
71
+ "test-image-8":
72
+ description: |
73
+ A test image without a separate snapshot but multiple billing products
74
+ boot_mode: "uefi-preferred"
75
+ regions:
76
+ - "eu-central-1"
77
+ - "us-east-1"
78
+ public: true
79
+ tags:
80
+ key1: value1
81
+ share:
82
+ - "123456789123"
83
+ - "221020170000"
84
+ - "aws:290620200000"
85
+ - "aws-cn:334455667788"
86
+ marketplace:
87
+ entity_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
88
+ access_role_arn: "arn:aws:iam::xxxxxxxxxxxx:role/AWSMarketplaceAccess"
89
+ version_title: "1.0.0"
90
+ release_notes: "N/A"
91
+ user_name: "ubuntu"
92
+ scanning_port: 22
93
+ os_name: "UBUNTU"
94
+ os_version: "22.04"
95
+ usage_instructions: |
96
+ You can use me
97
+ recommended_instance_type: "m5.large"
98
+ security_groups:
99
+ -
100
+ from_port: 22
101
+ ip_protocol: "tcp"
102
+ ip_ranges:
103
+ - "0.0.0.0/0"
104
+ to_port: 22
105
+ ssm_parameter:
106
+ -
107
+ name: /test/image
108
+ -
109
+ name: /test/another-image
110
+ "test-image-9":
111
+ boot_mode: "uefi"
112
+ description: |
113
+ A test image without a separate snapshot but multiple billing products
114
+ regions:
115
+ - "eu-central-1"
116
+ - "us-east-1"
117
+ ssm_parameter:
118
+ -
119
+ name: /awspub-test/param2
120
+ allow_overwrite: true
121
+ "test-image-10":
122
+ boot_mode: "uefi"
123
+ description: |
124
+ A test image without a separate snapshot but single sns configs
125
+ regions:
126
+ - "us-east-1"
127
+ sns:
128
+ - "topic1":
129
+ subject: "topic1-subject"
130
+ message:
131
+ default: "default-message"
132
+ email: "email-message"
133
+ regions:
134
+ - "us-east-1"
135
+ "test-image-11":
136
+ boot_mode: "uefi"
137
+ description: |
138
+ A test image without a separate snapshot but multiple sns configs
139
+ regions:
140
+ - "us-east-1"
141
+ - "eu-central-1"
142
+ sns:
143
+ - "topic1":
144
+ subject: "topic1-subject"
145
+ message:
146
+ default: "default-message"
147
+ email: "email-message"
148
+ regions:
149
+ - "us-east-1"
150
+ - "topic2":
151
+ subject: "topic2-subject"
152
+ message:
153
+ default: "default-message"
154
+ regions:
155
+ - "us-gov-1"
156
+ - "eu-central-1"
157
+ "test-image-12":
158
+ boot_mode: "uefi"
159
+ description: |
160
+ A test image without a separate snapshot but single sns configs
161
+ regions:
162
+ - "us-east-1"
163
+ sns:
164
+ - "topic1":
165
+ subject: "topic1-subject"
166
+ message:
167
+ default: "default-message"
168
+ email: "email-message"
169
+
170
+ tags:
171
+ name: "foobar"
@@ -0,0 +1,2 @@
1
+ key1: "value1"
2
+ key2: "value$2"
@@ -0,0 +1,48 @@
1
+ awspub:
2
+ s3:
3
+ bucket_name: "bucket1"
4
+
5
+ source:
6
+ # config1.vmdk generated with
7
+ # dd if=/dev/zero of=config1.raw bs=1K count=1
8
+ # qemu-img convert -f raw -O vmdk -o subformat=streamOptimized config1.raw config1.vmdk
9
+ path: "config1.vmdk"
10
+ architecture: "x86_64"
11
+
12
+ images:
13
+ "test-image-$key1":
14
+ description: |
15
+ A test image
16
+ boot_mode: "uefi"
17
+ regions:
18
+ - region1
19
+ - region2
20
+ temporary: true
21
+ "test-image-$key2":
22
+ description: |
23
+ A test image with a separate snapshot
24
+ boot_mode: "uefi"
25
+ separate_snapshot: true
26
+ "test-image-3":
27
+ description: |
28
+ A test image with a separate snapshot and a billing code
29
+ boot_mode: "uefi"
30
+ separate_snapshot: true
31
+ billing_products:
32
+ - billingcode
33
+ "test-image-4":
34
+ description: |
35
+ A test image without a separate snapshot but a billing product
36
+ boot_mode: "uefi-preferred"
37
+ billing_products:
38
+ - billingcode
39
+ "test-image-5":
40
+ description: |
41
+ A test image without a separate snapshot but multiple billing products
42
+ boot_mode: "uefi-preferred"
43
+ billing_products:
44
+ - billingcode1
45
+ - billingcode2
46
+
47
+ tags:
48
+ name: "foobar"
@@ -0,0 +1,18 @@
1
+ awspub:
2
+ s3:
3
+ bucket_name: "bucket1"
4
+
5
+ source:
6
+ path: "config1.vmdk"
7
+ architecture: "x86_64"
8
+
9
+ images:
10
+ "test-image-1":
11
+ description: |
12
+ A test image
13
+ boot_mode: "uefi"
14
+ # second image with the same key
15
+ "test-image-1":
16
+ description: |
17
+ A test image
18
+ boot_mode: "uefi"
@@ -0,0 +1,89 @@
1
+ import pathlib
2
+
3
+ import pytest
4
+
5
+ from awspub import api, context, image
6
+
7
+ curdir = pathlib.Path(__file__).parent.resolve()
8
+
9
+
10
+ @pytest.mark.parametrize(
11
+ "group,expected_image_names",
12
+ [
13
+ # without any group, all images should be processed
14
+ (
15
+ None,
16
+ [
17
+ "test-image-1",
18
+ "test-image-2",
19
+ "test-image-3",
20
+ "test-image-4",
21
+ "test-image-5",
22
+ "test-image-6",
23
+ "test-image-7",
24
+ "test-image-8",
25
+ "test-image-9",
26
+ "test-image-10",
27
+ "test-image-11",
28
+ "test-image-12",
29
+ ],
30
+ ),
31
+ # with a group that no image as, no image should be processed
32
+ (
33
+ "group-not-used",
34
+ [],
35
+ ),
36
+ # with a group that an image has
37
+ (
38
+ "group2",
39
+ ["test-image-1"],
40
+ ),
41
+ # with a group that multiple images have
42
+ (
43
+ "group1",
44
+ ["test-image-1", "test-image-2"],
45
+ ),
46
+ ],
47
+ )
48
+ def test_api__images_filtered(group, expected_image_names):
49
+ """
50
+ Test the _images_filtered() function
51
+ """
52
+ ctx = context.Context(curdir / "fixtures/config1.yaml", None)
53
+
54
+ image_names = [i[0] for i in api._images_filtered(ctx, group)]
55
+ assert image_names == expected_image_names
56
+
57
+
58
+ @pytest.mark.parametrize(
59
+ "group,expected",
60
+ [
61
+ # without any group, all images should be processed
62
+ (
63
+ None,
64
+ (
65
+ {"test-image-1": {"eu-central-1": "ami-123", "eu-central-2": "ami-456"}},
66
+ {
67
+ "group1": {"test-image-1": {"eu-central-1": "ami-123", "eu-central-2": "ami-456"}},
68
+ "group2": {"test-image-1": {"eu-central-1": "ami-123", "eu-central-2": "ami-456"}},
69
+ },
70
+ ),
71
+ ),
72
+ # with a group that no image as, image should be there but nothing in the group
73
+ ("group-not-used", ({"test-image-1": {"eu-central-1": "ami-123", "eu-central-2": "ami-456"}}, {})),
74
+ ],
75
+ )
76
+ def test_api__images_grouped(group, expected):
77
+ """
78
+ Test the _images_grouped() function
79
+ """
80
+ ctx = context.Context(curdir / "fixtures/config1.yaml", None)
81
+ images = [
82
+ (
83
+ "test-image-1",
84
+ image.Image(ctx, "test-image-1"),
85
+ {"eu-central-1": image._ImageInfo("ami-123", None), "eu-central-2": image._ImageInfo("ami-456", None)},
86
+ )
87
+ ]
88
+ grouped = api._images_grouped(images, group)
89
+ assert grouped == expected
File without changes
@@ -0,0 +1,34 @@
1
+ from unittest.mock import patch
2
+
3
+ import pytest
4
+
5
+ from awspub.common import _get_regions, _split_partition
6
+
7
+
8
+ @pytest.mark.parametrize(
9
+ "input,expected_output",
10
+ [
11
+ ("123456789123", ("aws", "123456789123")),
12
+ ("aws:123456789123", ("aws", "123456789123")),
13
+ ("aws-cn:123456789123", ("aws-cn", "123456789123")),
14
+ ("aws-us-gov:123456789123", ("aws-us-gov", "123456789123")),
15
+ ],
16
+ )
17
+ def test_common__split_partition(input, expected_output):
18
+ assert _split_partition(input) == expected_output
19
+
20
+
21
+ @pytest.mark.parametrize(
22
+ "regions_in_partition,configured_regions,expected_output",
23
+ [
24
+ (["region-1", "region-2"], ["region-1", "region-3"], ["region-1"]),
25
+ (["region-1", "region-2", "region-3"], ["region-4", "region-5"], []),
26
+ (["region-1", "region-2"], [], ["region-1", "region-2"]),
27
+ ],
28
+ )
29
+ def test_common__get_regions(regions_in_partition, configured_regions, expected_output):
30
+ with patch("boto3.client") as bclient_mock:
31
+ instance = bclient_mock.return_value
32
+ instance.describe_regions.return_value = {"Regions": [{"RegionName": r} for r in regions_in_partition]}
33
+
34
+ assert _get_regions("", configured_regions) == expected_output
@@ -0,0 +1,88 @@
1
+ import glob
2
+ import os
3
+ import pathlib
4
+
5
+ import pytest
6
+ from pydantic import ValidationError
7
+ from ruamel.yaml.constructor import DuplicateKeyError
8
+
9
+ from awspub import context
10
+
11
+ curdir = pathlib.Path(__file__).parent.resolve()
12
+
13
+
14
+ def test_context_create():
15
+ """
16
+ Create a Context object from a given configuration
17
+ """
18
+ ctx = context.Context(curdir / "fixtures/config1.yaml", None)
19
+ assert ctx.conf["source"]["path"] == curdir / "fixtures/config1.vmdk"
20
+ assert ctx.source_sha256 == "6252475408b9f9ee64452b611d706a078831a99b123db69d144d878a0488a0a8"
21
+ assert ctx.conf["source"]["architecture"] == "x86_64"
22
+ assert ctx.conf["s3"]["bucket_name"] == "bucket1"
23
+
24
+
25
+ def test_context_create_minimal():
26
+ """
27
+ Create a Context object from a given minimal configuration
28
+ """
29
+ ctx = context.Context(curdir / "fixtures/config-minimal.yaml", None)
30
+ assert ctx.conf["source"]["path"] == curdir / "fixtures/config1.vmdk"
31
+ assert ctx.source_sha256 == "6252475408b9f9ee64452b611d706a078831a99b123db69d144d878a0488a0a8"
32
+ assert ctx.conf["source"]["architecture"] == "x86_64"
33
+ assert ctx.conf["s3"]["bucket_name"] == "bucket1"
34
+
35
+
36
+ def test_context_create_with_mapping():
37
+ """
38
+ Create a Context object from a given configuration
39
+ """
40
+ ctx = context.Context(curdir / "fixtures/config2.yaml", curdir / "fixtures/config2-mapping.yaml")
41
+ assert ctx.conf["source"]["path"] == curdir / "fixtures/config1.vmdk"
42
+ assert ctx.source_sha256 == "6252475408b9f9ee64452b611d706a078831a99b123db69d144d878a0488a0a8"
43
+ assert ctx.conf["images"].get("test-image-value1")
44
+ assert ctx.conf["images"].get("test-image-value$2")
45
+
46
+
47
+ def test_context_with_docs_config_samples():
48
+ """
49
+ Create a Context object with the sample config files used for documentation
50
+ """
51
+ config_samples_dir = curdir.parents[1] / "docs" / "config-samples"
52
+ for f in glob.glob(f"{config_samples_dir}/*.yaml"):
53
+ mapping_file = f + ".mapping"
54
+ if os.path.exists(mapping_file):
55
+ mapping = mapping_file
56
+ else:
57
+ mapping = None
58
+ context.Context(os.path.join(config_samples_dir, f), mapping)
59
+
60
+
61
+ def test_context_with_duplicate_image_name():
62
+ """
63
+ Create a context with a configuration file that contains a duplicate image name key
64
+ """
65
+ with pytest.raises(DuplicateKeyError):
66
+ context.Context(curdir / "fixtures/config3-duplicate-keys.yaml", None)
67
+
68
+
69
+ @pytest.mark.parametrize(
70
+ "config_file",
71
+ ["fixtures/config-minimal.yaml", "fixtures/config-valid-nonawspub.yaml"],
72
+ )
73
+ def test_valid_configuration(config_file):
74
+ """
75
+ Test with a valid configuration file (no extra fields)
76
+ """
77
+ ctx = context.Context(curdir / config_file, None)
78
+ assert ctx.conf is not None
79
+ assert ctx.conf["s3"]["bucket_name"] == "bucket1"
80
+ assert ctx.conf["source"]["architecture"] == "x86_64"
81
+
82
+
83
+ def test_invalid_configuration_extra_field():
84
+ """
85
+ Test with an invalid configuration file that includes an extra field
86
+ """
87
+ with pytest.raises(ValidationError):
88
+ context.Context(curdir / "fixtures/config-invalid-s3-extra.yaml", None)