awspub 0.0.10__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- awspub/__init__.py +3 -0
- awspub/api.py +165 -0
- awspub/cli/__init__.py +146 -0
- awspub/common.py +64 -0
- awspub/configmodels.py +216 -0
- awspub/context.py +108 -0
- awspub/exceptions.py +28 -0
- awspub/image.py +656 -0
- awspub/image_marketplace.py +120 -0
- awspub/s3.py +262 -0
- awspub/snapshot.py +241 -0
- awspub/sns.py +105 -0
- awspub/tests/__init__.py +0 -0
- awspub/tests/fixtures/config-invalid-s3-extra.yaml +12 -0
- awspub/tests/fixtures/config-minimal.yaml +12 -0
- awspub/tests/fixtures/config-valid-nonawspub.yaml +13 -0
- awspub/tests/fixtures/config1.vmdk +0 -0
- awspub/tests/fixtures/config1.yaml +171 -0
- awspub/tests/fixtures/config2-mapping.yaml +2 -0
- awspub/tests/fixtures/config2.yaml +48 -0
- awspub/tests/fixtures/config3-duplicate-keys.yaml +18 -0
- awspub/tests/test_api.py +89 -0
- awspub/tests/test_cli.py +0 -0
- awspub/tests/test_common.py +34 -0
- awspub/tests/test_context.py +88 -0
- awspub/tests/test_image.py +556 -0
- awspub/tests/test_image_marketplace.py +44 -0
- awspub/tests/test_s3.py +74 -0
- awspub/tests/test_snapshot.py +122 -0
- awspub/tests/test_sns.py +189 -0
- awspub-0.0.10.dist-info/LICENSE +675 -0
- awspub-0.0.10.dist-info/METADATA +46 -0
- awspub-0.0.10.dist-info/RECORD +35 -0
- awspub-0.0.10.dist-info/WHEEL +4 -0
- awspub-0.0.10.dist-info/entry_points.txt +3 -0
awspub/__init__.py
ADDED
awspub/api.py
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
import logging
|
2
|
+
import pathlib
|
3
|
+
from typing import Dict, Iterator, List, Optional, Tuple
|
4
|
+
|
5
|
+
from awspub.context import Context
|
6
|
+
from awspub.image import Image, _ImageInfo
|
7
|
+
from awspub.s3 import S3
|
8
|
+
|
9
|
+
logger = logging.getLogger(__name__)
|
10
|
+
|
11
|
+
|
12
|
+
def _images_grouped(
|
13
|
+
images: List[Tuple[str, Image, Dict[str, _ImageInfo]]], group: Optional[str]
|
14
|
+
) -> Tuple[Dict[str, Dict[str, str]], Dict[str, Dict[str, Dict[str, str]]]]:
|
15
|
+
"""
|
16
|
+
Group the given images by name and by group
|
17
|
+
|
18
|
+
:param images: the images
|
19
|
+
:type images: List[Tuple[str, Image, Dict[str, _ImageInfo]]]
|
20
|
+
:param group: a optional group name
|
21
|
+
:type group: Optional[str]
|
22
|
+
:return: the images grouped by name and by group
|
23
|
+
:rtype: Tuple[Dict[str, Dict[str, str]], Dict[str, Dict[str, Dict[str, str]]]
|
24
|
+
"""
|
25
|
+
images_by_name: Dict[str, Dict[str, str]] = dict()
|
26
|
+
images_by_group: Dict[str, Dict[str, Dict[str, str]]] = dict()
|
27
|
+
for image_name, image, image_result in images:
|
28
|
+
images_region_id: Dict[str, str] = {key: val.image_id for (key, val) in image_result.items()}
|
29
|
+
images_by_name[image_name] = images_region_id
|
30
|
+
for image_group in image.conf.get("groups", []):
|
31
|
+
if group and image_group != group:
|
32
|
+
continue
|
33
|
+
if not images_by_group.get(image_group):
|
34
|
+
images_by_group[image_group] = {}
|
35
|
+
images_by_group[image_group][image_name] = images_region_id
|
36
|
+
return images_by_name, images_by_group
|
37
|
+
|
38
|
+
|
39
|
+
def _images_filtered(context: Context, group: Optional[str]) -> Iterator[Tuple[str, Image]]:
|
40
|
+
"""
|
41
|
+
Filter the images from ctx based on the given args
|
42
|
+
|
43
|
+
:param context: the context
|
44
|
+
:type context: a awspub.context.Context instance
|
45
|
+
:param group: a optional group name
|
46
|
+
:type group: Optional[str]
|
47
|
+
"""
|
48
|
+
for image_name in context.conf["images"].keys():
|
49
|
+
image = Image(context, image_name)
|
50
|
+
if group:
|
51
|
+
# limit the images to process to the group given on the command line
|
52
|
+
if group not in image.conf.get("groups", []):
|
53
|
+
logger.info(f"skipping image {image_name} because not part of group {group}")
|
54
|
+
continue
|
55
|
+
|
56
|
+
logger.info(f"processing image {image_name} (group: {group})")
|
57
|
+
yield image_name, image
|
58
|
+
|
59
|
+
|
60
|
+
def create(
|
61
|
+
config: pathlib.Path, config_mapping: pathlib.Path, group: Optional[str]
|
62
|
+
) -> Tuple[Dict[str, Dict[str, str]], Dict[str, Dict[str, Dict[str, str]]]]:
|
63
|
+
"""
|
64
|
+
Create images in the partition of the used account based on
|
65
|
+
the given configuration file and the config mapping
|
66
|
+
|
67
|
+
:param config: the configuration file path
|
68
|
+
:type config: pathlib.Path
|
69
|
+
:param config_mapping: the config template mapping file path
|
70
|
+
:type config_mapping: pathlib.Path
|
71
|
+
:param group: only handles images from given group
|
72
|
+
:type group: Optional[str]
|
73
|
+
:return: the images grouped by name and by group
|
74
|
+
:rtype: Tuple[Dict[str, Dict[str, str]], Dict[str, Dict[str, Dict[str, str]]]
|
75
|
+
"""
|
76
|
+
|
77
|
+
ctx = Context(config, config_mapping)
|
78
|
+
s3 = S3(ctx)
|
79
|
+
s3.upload_file(ctx.conf["source"]["path"])
|
80
|
+
images: List[Tuple[str, Image, Dict[str, _ImageInfo]]] = []
|
81
|
+
for image_name, image in _images_filtered(ctx, group):
|
82
|
+
image_result: Dict[str, _ImageInfo] = image.create()
|
83
|
+
images.append((image_name, image, image_result))
|
84
|
+
images_by_name, images_by_group = _images_grouped(images, group)
|
85
|
+
return images_by_name, images_by_group
|
86
|
+
|
87
|
+
|
88
|
+
def list(
|
89
|
+
config: pathlib.Path, config_mapping: pathlib.Path, group: Optional[str]
|
90
|
+
) -> Tuple[Dict[str, Dict[str, str]], Dict[str, Dict[str, Dict[str, str]]]]:
|
91
|
+
"""
|
92
|
+
List images in the partition of the used account based on
|
93
|
+
the given configuration file and the config mapping
|
94
|
+
|
95
|
+
:param config: the configuration file path
|
96
|
+
:type config: pathlib.Path
|
97
|
+
:param config_mapping: the config template mapping file path
|
98
|
+
:type config_mapping: pathlib.Path
|
99
|
+
:param group: only handles images from given group
|
100
|
+
:type group: Optional[str]
|
101
|
+
:return: the images grouped by name and by group
|
102
|
+
:rtype: Tuple[Dict[str, Dict[str, str]], Dict[str, Dict[str, Dict[str, str]]]
|
103
|
+
"""
|
104
|
+
ctx = Context(config, config_mapping)
|
105
|
+
images: List[Tuple[str, Image, Dict[str, _ImageInfo]]] = []
|
106
|
+
for image_name, image in _images_filtered(ctx, group):
|
107
|
+
image_result: Dict[str, _ImageInfo] = image.list()
|
108
|
+
images.append((image_name, image, image_result))
|
109
|
+
|
110
|
+
images_by_name, images_by_group = _images_grouped(images, group)
|
111
|
+
return images_by_name, images_by_group
|
112
|
+
|
113
|
+
|
114
|
+
def publish(config: pathlib.Path, config_mapping: pathlib.Path, group: Optional[str]):
|
115
|
+
"""
|
116
|
+
Make available images in the partition of the used account based on
|
117
|
+
the given configuration file public
|
118
|
+
|
119
|
+
:param config: the configuration file path
|
120
|
+
:type config: pathlib.Path
|
121
|
+
:param config_mapping: the config template mapping file path
|
122
|
+
:type config_mapping: pathlib.Path
|
123
|
+
:param group: only handles images from given group
|
124
|
+
:type group: Optional[str]
|
125
|
+
"""
|
126
|
+
ctx = Context(config, config_mapping)
|
127
|
+
for image_name, image in _images_filtered(ctx, group):
|
128
|
+
image.publish()
|
129
|
+
|
130
|
+
|
131
|
+
def cleanup(config: pathlib.Path, config_mapping: pathlib.Path, group: Optional[str]):
|
132
|
+
"""
|
133
|
+
Cleanup available images in the partition of the used account based on
|
134
|
+
the given configuration file
|
135
|
+
|
136
|
+
:param config: the configuration file path
|
137
|
+
:type config: pathlib.Path
|
138
|
+
:param config_mapping: the config template mapping file path
|
139
|
+
:type config_mapping: pathlib.Path
|
140
|
+
:param group: only handles images from given group
|
141
|
+
:type group: Optional[str]
|
142
|
+
"""
|
143
|
+
ctx = Context(config, config_mapping)
|
144
|
+
for image_name, image in _images_filtered(ctx, group):
|
145
|
+
image.cleanup()
|
146
|
+
|
147
|
+
|
148
|
+
def verify(config: pathlib.Path, config_mapping: pathlib.Path, group: Optional[str]) -> Dict[str, Dict]:
|
149
|
+
"""
|
150
|
+
Verify available images in the partition of the used account based on
|
151
|
+
the given configuration file.
|
152
|
+
This is EXPERIMENTAL and doesn't work reliable yet!
|
153
|
+
|
154
|
+
:param config: the configuration file path
|
155
|
+
:type config: pathlib.Path
|
156
|
+
:param config_mapping: the config template mapping file path
|
157
|
+
:type config_mapping: pathlib.Path
|
158
|
+
:param group: only handles images from given group
|
159
|
+
:type group: Optional[str]
|
160
|
+
"""
|
161
|
+
problems: Dict[str, Dict] = dict()
|
162
|
+
ctx = Context(config, config_mapping)
|
163
|
+
for image_name, image in _images_filtered(ctx, group):
|
164
|
+
problems[image_name] = image.verify()
|
165
|
+
return problems
|
awspub/cli/__init__.py
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
#!/usr/bin/python3
|
2
|
+
|
3
|
+
import argparse
|
4
|
+
import json
|
5
|
+
import logging
|
6
|
+
import pathlib
|
7
|
+
import sys
|
8
|
+
|
9
|
+
import awspub
|
10
|
+
|
11
|
+
logger = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
|
14
|
+
def _create(args) -> None:
|
15
|
+
"""
|
16
|
+
Create images based on the given configuration and write json
|
17
|
+
data to the given output
|
18
|
+
"""
|
19
|
+
images_by_name, images_by_group = awspub.create(args.config, args.config_mapping, args.group)
|
20
|
+
images_json = json.dumps({"images": images_by_name, "images-by-group": images_by_group}, indent=4)
|
21
|
+
args.output.write(images_json)
|
22
|
+
|
23
|
+
|
24
|
+
def _list(args) -> None:
|
25
|
+
"""
|
26
|
+
List images based on the given configuration and write json
|
27
|
+
data to the given output
|
28
|
+
"""
|
29
|
+
images_by_name, images_by_group = awspub.list(args.config, args.config_mapping, args.group)
|
30
|
+
images_json = json.dumps({"images": images_by_name, "images-by-group": images_by_group}, indent=4)
|
31
|
+
args.output.write(images_json)
|
32
|
+
|
33
|
+
|
34
|
+
def _verify(args) -> None:
|
35
|
+
"""
|
36
|
+
Verify available images against configuration
|
37
|
+
"""
|
38
|
+
problems = awspub.verify(args.config, args.config_mapping, args.group)
|
39
|
+
args.output.write((json.dumps({"problems": problems}, indent=4)))
|
40
|
+
|
41
|
+
|
42
|
+
def _cleanup(args) -> None:
|
43
|
+
"""
|
44
|
+
Cleanup available images
|
45
|
+
"""
|
46
|
+
awspub.cleanup(args.config, args.config_mapping, args.group)
|
47
|
+
|
48
|
+
|
49
|
+
def _publish(args) -> None:
|
50
|
+
"""
|
51
|
+
Make available images public
|
52
|
+
"""
|
53
|
+
awspub.publish(args.config, args.config_mapping, args.group)
|
54
|
+
|
55
|
+
|
56
|
+
def _parser():
|
57
|
+
parser = argparse.ArgumentParser(description="AWS EC2 publication tool")
|
58
|
+
parser.add_argument("--log-level", choices=["info", "debug"], default="info")
|
59
|
+
parser.add_argument("--log-file", type=pathlib.Path, help="write log to given file instead of stdout")
|
60
|
+
parser.add_argument("--log-console", action="store_true", help="write log to stdout")
|
61
|
+
p_sub = parser.add_subparsers(help="sub-command help")
|
62
|
+
|
63
|
+
# create
|
64
|
+
p_create = p_sub.add_parser("create", help="Create images")
|
65
|
+
p_create.add_argument(
|
66
|
+
"--output", type=argparse.FileType("w+"), help="output file path. defaults to stdout", default=sys.stdout
|
67
|
+
)
|
68
|
+
p_create.add_argument("--config-mapping", type=pathlib.Path, help="the image config template mapping file path")
|
69
|
+
p_create.add_argument("--group", type=str, help="only handles images from given group")
|
70
|
+
p_create.add_argument("config", type=pathlib.Path, help="the image configuration file path")
|
71
|
+
p_create.set_defaults(func=_create)
|
72
|
+
|
73
|
+
# list
|
74
|
+
p_list = p_sub.add_parser("list", help="List images (but don't modify anything)")
|
75
|
+
p_list.add_argument(
|
76
|
+
"--output", type=argparse.FileType("w+"), help="output file path. defaults to stdout", default=sys.stdout
|
77
|
+
)
|
78
|
+
p_list.add_argument("--config-mapping", type=pathlib.Path, help="the image config template mapping file path")
|
79
|
+
p_list.add_argument("--group", type=str, help="only handles images from given group")
|
80
|
+
p_list.add_argument("config", type=pathlib.Path, help="the image configuration file path")
|
81
|
+
p_list.set_defaults(func=_list)
|
82
|
+
|
83
|
+
# verify
|
84
|
+
p_verify = p_sub.add_parser("verify", help="Verify images")
|
85
|
+
p_verify.add_argument(
|
86
|
+
"--output", type=argparse.FileType("w+"), help="output file path. defaults to stdout", default=sys.stdout
|
87
|
+
)
|
88
|
+
p_verify.add_argument("--config-mapping", type=pathlib.Path, help="the image config template mapping file path")
|
89
|
+
p_verify.add_argument("--group", type=str, help="only handles images from given group")
|
90
|
+
p_verify.add_argument("config", type=pathlib.Path, help="the image configuration file path")
|
91
|
+
|
92
|
+
p_verify.set_defaults(func=_verify)
|
93
|
+
|
94
|
+
# cleanup
|
95
|
+
p_cleanup = p_sub.add_parser("cleanup", help="Cleanup images")
|
96
|
+
p_cleanup.add_argument(
|
97
|
+
"--output", type=argparse.FileType("w+"), help="output file path. defaults to stdout", default=sys.stdout
|
98
|
+
)
|
99
|
+
p_cleanup.add_argument("--config-mapping", type=pathlib.Path, help="the image config template mapping file path")
|
100
|
+
p_cleanup.add_argument("--group", type=str, help="only handles images from given group")
|
101
|
+
p_cleanup.add_argument("config", type=pathlib.Path, help="the image configuration file path")
|
102
|
+
|
103
|
+
p_cleanup.set_defaults(func=_cleanup)
|
104
|
+
|
105
|
+
# publish
|
106
|
+
p_publish = p_sub.add_parser("publish", help="Publish images")
|
107
|
+
p_publish.add_argument(
|
108
|
+
"--output", type=argparse.FileType("w+"), help="output file path. defaults to stdout", default=sys.stdout
|
109
|
+
)
|
110
|
+
p_publish.add_argument("--config-mapping", type=pathlib.Path, help="the image config template mapping file path")
|
111
|
+
p_publish.add_argument("--group", type=str, help="only handles images from given group")
|
112
|
+
p_publish.add_argument("config", type=pathlib.Path, help="the image configuration file path")
|
113
|
+
|
114
|
+
p_publish.set_defaults(func=_publish)
|
115
|
+
|
116
|
+
return parser
|
117
|
+
|
118
|
+
|
119
|
+
def main():
|
120
|
+
parser = _parser()
|
121
|
+
args = parser.parse_args()
|
122
|
+
log_formatter = logging.Formatter("%(asctime)s:%(name)s:%(levelname)s:%(message)s")
|
123
|
+
# log level
|
124
|
+
loglevel = logging.INFO
|
125
|
+
if args.log_level == "debug":
|
126
|
+
loglevel = logging.DEBUG
|
127
|
+
root_logger = logging.getLogger()
|
128
|
+
root_logger.setLevel(loglevel)
|
129
|
+
# log file
|
130
|
+
if args.log_file:
|
131
|
+
file_handler = logging.FileHandler(filename=args.log_file)
|
132
|
+
file_handler.setFormatter(log_formatter)
|
133
|
+
root_logger.addHandler(file_handler)
|
134
|
+
# log console
|
135
|
+
if args.log_console:
|
136
|
+
console_handler = logging.StreamHandler()
|
137
|
+
console_handler.setFormatter(log_formatter)
|
138
|
+
root_logger.addHandler(console_handler)
|
139
|
+
if "func" not in args:
|
140
|
+
sys.exit(parser.print_help())
|
141
|
+
args.func(args)
|
142
|
+
sys.exit(0)
|
143
|
+
|
144
|
+
|
145
|
+
if __name__ == "__main__":
|
146
|
+
main()
|
awspub/common.py
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
import logging
|
2
|
+
from typing import List, Tuple
|
3
|
+
|
4
|
+
import boto3
|
5
|
+
from mypy_boto3_ec2.client import EC2Client
|
6
|
+
|
7
|
+
logger = logging.getLogger(__name__)
|
8
|
+
|
9
|
+
|
10
|
+
def _split_partition(val: str) -> Tuple[str, str]:
|
11
|
+
"""
|
12
|
+
Split a string into partition and resource, separated by a colon. If no partition is given, assume "aws"
|
13
|
+
:param val: the string to split
|
14
|
+
:type val: str
|
15
|
+
:return: the partition and the resource
|
16
|
+
:rtype: Tuple[str, str]
|
17
|
+
"""
|
18
|
+
if ":" in val:
|
19
|
+
partition, resource = val.split(":")
|
20
|
+
else:
|
21
|
+
# if no partition is given, assume default commercial partition "aws"
|
22
|
+
partition = "aws"
|
23
|
+
resource = val
|
24
|
+
return partition, resource
|
25
|
+
|
26
|
+
|
27
|
+
def _get_regions(region_to_query: str, regions_allowlist: List[str]) -> List[str]:
|
28
|
+
"""
|
29
|
+
Get a list of region names querying the `region_to_query` for all regions and
|
30
|
+
then filtering by `regions_allowlist`.
|
31
|
+
If no `regions_allowlist` is given, all queried regions are returned for the
|
32
|
+
current partition.
|
33
|
+
If `regions_allowlist` is given, all regions from that list are returned if
|
34
|
+
the listed region exist in the current partition.
|
35
|
+
Eg. `us-east-1` listed in `regions_allowlist` won't be returned if the current
|
36
|
+
partition is `aws-cn`.
|
37
|
+
:param region_to_query: region name of current partition
|
38
|
+
:type region_to_query: str
|
39
|
+
:praram regions_allowlist: list of regions in config file
|
40
|
+
:type regions_allowlist: List[str]
|
41
|
+
:return: list of regions names
|
42
|
+
:rtype: List[str]
|
43
|
+
"""
|
44
|
+
|
45
|
+
# get all available regions
|
46
|
+
ec2client: EC2Client = boto3.client("ec2", region_name=region_to_query)
|
47
|
+
resp = ec2client.describe_regions()
|
48
|
+
ec2_regions_all = [r["RegionName"] for r in resp["Regions"]]
|
49
|
+
|
50
|
+
if regions_allowlist:
|
51
|
+
# filter out regions that are not available in the current partition
|
52
|
+
regions_allowlist_set = set(regions_allowlist)
|
53
|
+
ec2_regions_all_set = set(ec2_regions_all)
|
54
|
+
regions = list(regions_allowlist_set.intersection(ec2_regions_all_set))
|
55
|
+
diff = regions_allowlist_set.difference(ec2_regions_all_set)
|
56
|
+
if diff:
|
57
|
+
logger.warning(
|
58
|
+
f"regions {diff} listed in regions allowlist are not available in the current partition."
|
59
|
+
" Ignoring those."
|
60
|
+
)
|
61
|
+
else:
|
62
|
+
regions = ec2_regions_all
|
63
|
+
|
64
|
+
return regions
|
awspub/configmodels.py
ADDED
@@ -0,0 +1,216 @@
|
|
1
|
+
import pathlib
|
2
|
+
from enum import Enum
|
3
|
+
from typing import Dict, List, Literal, Optional
|
4
|
+
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
6
|
+
|
7
|
+
from awspub.common import _split_partition
|
8
|
+
|
9
|
+
|
10
|
+
class ConfigS3Model(BaseModel):
|
11
|
+
"""
|
12
|
+
S3 configuration.
|
13
|
+
This is required for uploading source files (usually .vmdk) to a bucket so
|
14
|
+
snapshots can be created out of the s3 file
|
15
|
+
"""
|
16
|
+
|
17
|
+
model_config = ConfigDict(extra="forbid")
|
18
|
+
|
19
|
+
bucket_name: str = Field(description="The S3 bucket name")
|
20
|
+
|
21
|
+
|
22
|
+
class ConfigSourceModel(BaseModel):
|
23
|
+
"""
|
24
|
+
Source configuration.
|
25
|
+
This defines the source (usually .vmdk) that is uploaded
|
26
|
+
to S3 and then used to create EC2 snapshots in different regions.
|
27
|
+
"""
|
28
|
+
|
29
|
+
model_config = ConfigDict(extra="forbid")
|
30
|
+
|
31
|
+
path: pathlib.Path = Field(description="Path to a local .vmdk image")
|
32
|
+
architecture: Literal["x86_64", "arm64"] = Field(description="The architecture of the given .vmdk image")
|
33
|
+
|
34
|
+
|
35
|
+
class ConfigImageMarketplaceSecurityGroupModel(BaseModel):
|
36
|
+
"""
|
37
|
+
Image/AMI Marketplace specific configuration for a security group
|
38
|
+
"""
|
39
|
+
|
40
|
+
model_config = ConfigDict(extra="forbid")
|
41
|
+
|
42
|
+
from_port: int = Field(description="The source port")
|
43
|
+
ip_protocol: Literal["tcp", "udp"] = Field(description="The IP protocol (either 'tcp' or 'udp')")
|
44
|
+
ip_ranges: List[str] = Field(description="IP ranges to allow, in CIDR format (eg. '192.0.2.0/24')")
|
45
|
+
to_port: int = Field(description="The destination port")
|
46
|
+
|
47
|
+
|
48
|
+
class ConfigImageMarketplaceModel(BaseModel):
|
49
|
+
"""
|
50
|
+
Image/AMI Marketplace specific configuration to request new Marketplace versions
|
51
|
+
See https://docs.aws.amazon.com/marketplace-catalog/latest/api-reference/ami-products.html
|
52
|
+
for further information
|
53
|
+
"""
|
54
|
+
|
55
|
+
model_config = ConfigDict(extra="forbid")
|
56
|
+
|
57
|
+
entity_id: str = Field(description="The entity ID (product ID)")
|
58
|
+
# see https://docs.aws.amazon.com/marketplace/latest/userguide/ami-single-ami-products.html#single-ami-marketplace-ami-access # noqa:E501
|
59
|
+
access_role_arn: str = Field(
|
60
|
+
description="IAM role Amazon Resource Name (ARN) used by AWS Marketplace to access the provided AMI"
|
61
|
+
)
|
62
|
+
version_title: str = Field(description="The version title. Must be unique across the product")
|
63
|
+
release_notes: str = Field(description="The release notes")
|
64
|
+
user_name: str = Field(description="The login username to access the operating system")
|
65
|
+
scanning_port: int = Field(description="Port to access the operating system (default: 22)", default=22)
|
66
|
+
os_name: str = Field(description="Operating system name displayed to Marketplace buyers")
|
67
|
+
os_version: str = Field(description="Operating system version displayed to Marketplace buyers")
|
68
|
+
usage_instructions: str = Field(
|
69
|
+
description=" Instructions for using the AMI, or a link to more information about the AMI"
|
70
|
+
)
|
71
|
+
recommended_instance_type: str = Field(
|
72
|
+
description="The instance type that is recommended to run the service with the AMI and is the "
|
73
|
+
"default for 1-click installs of your service"
|
74
|
+
)
|
75
|
+
security_groups: Optional[List[ConfigImageMarketplaceSecurityGroupModel]]
|
76
|
+
|
77
|
+
|
78
|
+
class ConfigImageSSMParameterModel(BaseModel):
|
79
|
+
"""
|
80
|
+
Image/AMI SSM specific configuration to push parameters of type `aws:ec2:image` to the parameter store
|
81
|
+
"""
|
82
|
+
|
83
|
+
model_config = ConfigDict(extra="forbid")
|
84
|
+
|
85
|
+
name: str = Field(
|
86
|
+
description="The fully qualified name of the parameter that you want to add to the system. "
|
87
|
+
"A parameter name must be unique within an Amazon Web Services Region"
|
88
|
+
)
|
89
|
+
description: Optional[str] = Field(
|
90
|
+
description="Information about the parameter that you want to add to the system", default=None
|
91
|
+
)
|
92
|
+
allow_overwrite: Optional[bool] = Field(
|
93
|
+
description="allow to overwrite an existing parameter. Useful for keep a 'latest' parameter (default: false)",
|
94
|
+
default=False,
|
95
|
+
)
|
96
|
+
|
97
|
+
|
98
|
+
class SNSNotificationProtocol(str, Enum):
|
99
|
+
DEFAULT = "default"
|
100
|
+
EMAIL = "email"
|
101
|
+
|
102
|
+
|
103
|
+
class ConfigImageSNSNotificationModel(BaseModel):
|
104
|
+
"""
|
105
|
+
Image/AMI SNS Notification specific configuration to notify subscribers about new images availability
|
106
|
+
"""
|
107
|
+
|
108
|
+
model_config = ConfigDict(extra="forbid")
|
109
|
+
|
110
|
+
subject: str = Field(description="The subject of SNS Notification", min_length=1, max_length=99)
|
111
|
+
message: Dict[SNSNotificationProtocol, str] = Field(
|
112
|
+
description="The body of the message to be sent to subscribers.",
|
113
|
+
default={SNSNotificationProtocol.DEFAULT: ""},
|
114
|
+
)
|
115
|
+
regions: Optional[List[str]] = Field(
|
116
|
+
description="Optional list of regions for sending notification. If not given, regions where the image "
|
117
|
+
"registered will be used from the currently used parition. If a region doesn't exist in the currently "
|
118
|
+
"used partition, it will be ignored.",
|
119
|
+
default=None,
|
120
|
+
)
|
121
|
+
|
122
|
+
@field_validator("message")
|
123
|
+
def check_message(cls, value):
|
124
|
+
# Check message protocols have default key
|
125
|
+
# Message should contain at least a top-level JSON key of “default”
|
126
|
+
# with a value that is a string
|
127
|
+
if SNSNotificationProtocol.DEFAULT not in value:
|
128
|
+
raise ValueError(f"{SNSNotificationProtocol.DEFAULT.value} key is required to send SNS notification")
|
129
|
+
return value
|
130
|
+
|
131
|
+
|
132
|
+
class ConfigImageModel(BaseModel):
|
133
|
+
"""
|
134
|
+
Image/AMI configuration.
|
135
|
+
"""
|
136
|
+
|
137
|
+
model_config = ConfigDict(extra="forbid")
|
138
|
+
|
139
|
+
description: Optional[str] = Field(description="Optional image description", default=None)
|
140
|
+
regions: Optional[List[str]] = Field(
|
141
|
+
description="Optional list of regions for this image. If not given, all available regions will"
|
142
|
+
"be used from the currently used partition. If a region doesn't exist in the currently used partition,"
|
143
|
+
" it will be ignored.",
|
144
|
+
default=None,
|
145
|
+
)
|
146
|
+
separate_snapshot: bool = Field(description="Use a separate snapshot for this image?", default=False)
|
147
|
+
billing_products: Optional[List[str]] = Field(description="Optional list of billing codes", default=None)
|
148
|
+
boot_mode: Literal["legacy-bios", "uefi", "uefi-preferred"] = Field(
|
149
|
+
description="The boot mode. For arm64, this needs to be 'uefi'"
|
150
|
+
)
|
151
|
+
root_device_name: Optional[str] = Field(description="The root device name", default="/dev/sda1")
|
152
|
+
root_device_volume_type: Optional[Literal["gp2", "gp3"]] = Field(
|
153
|
+
description="The root device volume type", default="gp3"
|
154
|
+
)
|
155
|
+
root_device_volume_size: Optional[int] = Field(description="The root device volume size (in GB)", default=8)
|
156
|
+
uefi_data: Optional[pathlib.Path] = Field(
|
157
|
+
description="Optional path to a non-volatile UEFI variable store (must be already base64 encoded)", default=None
|
158
|
+
)
|
159
|
+
tpm_support: Optional[Literal["v2.0"]] = Field(
|
160
|
+
description="Optional TPM support. If this is set, 'boot_mode' must be 'uefi'", default=None
|
161
|
+
)
|
162
|
+
imds_support: Optional[Literal["v2.0"]] = Field(description="Optional IMDS support", default=None)
|
163
|
+
share: Optional[List[str]] = Field(
|
164
|
+
description="Optional list of account IDs the image and snapshot will be shared with. The account"
|
165
|
+
"ID can be prefixed with the partition and separated by ':'. Eg 'aws-cn:123456789123'",
|
166
|
+
default=None,
|
167
|
+
)
|
168
|
+
temporary: Optional[bool] = Field(
|
169
|
+
description="Optional boolean field indicates that a image is only temporary", default=False
|
170
|
+
)
|
171
|
+
public: Optional[bool] = Field(
|
172
|
+
description="Optional boolean field indicates if the image should be public", default=False
|
173
|
+
)
|
174
|
+
marketplace: Optional[ConfigImageMarketplaceModel] = Field(
|
175
|
+
description="Optional structure containing Marketplace related configuration for the commercial "
|
176
|
+
"'aws' partition",
|
177
|
+
default=None,
|
178
|
+
)
|
179
|
+
ssm_parameter: Optional[List[ConfigImageSSMParameterModel]] = Field(
|
180
|
+
description="Optional list of SSM parameter paths of type `aws:ec2:image` which will "
|
181
|
+
"be pushed to the parameter store",
|
182
|
+
default=None,
|
183
|
+
)
|
184
|
+
groups: Optional[List[str]] = Field(description="Optional list of groups this image is part of", default=[])
|
185
|
+
tags: Optional[Dict[str, str]] = Field(description="Optional Tags to apply to this image only", default={})
|
186
|
+
sns: Optional[List[Dict[str, ConfigImageSNSNotificationModel]]] = Field(
|
187
|
+
description="Optional list of SNS Notification related configuration", default=None
|
188
|
+
)
|
189
|
+
|
190
|
+
@field_validator("share")
|
191
|
+
@classmethod
|
192
|
+
def check_share(cls, v: Optional[List[str]]) -> Optional[List[str]]:
|
193
|
+
"""
|
194
|
+
Make sure the account IDs are valid and if given the partition is correct
|
195
|
+
"""
|
196
|
+
if v is not None:
|
197
|
+
for val in v:
|
198
|
+
partition, account_id = _split_partition(val)
|
199
|
+
if len(account_id) != 12:
|
200
|
+
raise ValueError("Account ID must be 12 characters long")
|
201
|
+
if partition not in ["aws", "aws-cn", "aws-us-gov"]:
|
202
|
+
raise ValueError("Partition must be one of 'aws', 'aws-cn', 'aws-us-gov'")
|
203
|
+
return v
|
204
|
+
|
205
|
+
|
206
|
+
class ConfigModel(BaseModel):
|
207
|
+
"""
|
208
|
+
The base model for the whole configuration
|
209
|
+
"""
|
210
|
+
|
211
|
+
model_config = ConfigDict(extra="forbid")
|
212
|
+
|
213
|
+
s3: ConfigS3Model
|
214
|
+
source: ConfigSourceModel
|
215
|
+
images: Dict[str, ConfigImageModel]
|
216
|
+
tags: Optional[Dict[str, str]] = Field(description="Optional Tags to apply to all resources", default={})
|