awspub 0.0.1__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/__init__.py +3 -0
- awspub/api.py +165 -0
- awspub/cli/__init__.py +146 -0
- awspub/configmodels.py +157 -0
- awspub/context.py +108 -0
- awspub/exceptions.py +16 -0
- awspub/image.py +593 -0
- awspub/image_marketplace.py +120 -0
- awspub/s3.py +262 -0
- awspub/snapshot.py +241 -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 +118 -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 +86 -0
- awspub/tests/test_cli.py +0 -0
- awspub/tests/test_context.py +88 -0
- awspub/tests/test_image.py +433 -0
- awspub/tests/test_image_marketplace.py +44 -0
- awspub/tests/test_s3.py +74 -0
- awspub/tests/test_snapshot.py +122 -0
- awspub-0.0.1.dist-info/LICENSE +675 -0
- awspub-0.0.1.dist-info/METADATA +700 -0
- awspub-0.0.1.dist-info/RECORD +31 -0
- awspub-0.0.1.dist-info/WHEEL +4 -0
- awspub-0.0.1.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=argparse.BooleanOptionalAction, 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/configmodels.py
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
import pathlib
|
2
|
+
from typing import Dict, List, Literal, Optional
|
3
|
+
|
4
|
+
from pydantic import BaseModel, ConfigDict, Field
|
5
|
+
|
6
|
+
|
7
|
+
class ConfigS3Model(BaseModel):
|
8
|
+
"""
|
9
|
+
S3 configuration.
|
10
|
+
This is required for uploading source files (usually .vmdk) to a bucket so
|
11
|
+
snapshots can be created out of the s3 file
|
12
|
+
"""
|
13
|
+
|
14
|
+
model_config = ConfigDict(extra="forbid")
|
15
|
+
|
16
|
+
bucket_name: str = Field(description="The S3 bucket name")
|
17
|
+
|
18
|
+
|
19
|
+
class ConfigSourceModel(BaseModel):
|
20
|
+
"""
|
21
|
+
Source configuration.
|
22
|
+
This defines the source (usually .vmdk) that is uploaded
|
23
|
+
to S3 and then used to create EC2 snapshots in different regions.
|
24
|
+
"""
|
25
|
+
|
26
|
+
model_config = ConfigDict(extra="forbid")
|
27
|
+
|
28
|
+
path: pathlib.Path = Field(description="Path to a local .vmdk image")
|
29
|
+
architecture: Literal["x86_64", "arm64"] = Field(description="The architecture of the given .vmdk image")
|
30
|
+
|
31
|
+
|
32
|
+
class ConfigImageMarketplaceSecurityGroupModel(BaseModel):
|
33
|
+
"""
|
34
|
+
Image/AMI Marketplace specific configuration for a security group
|
35
|
+
"""
|
36
|
+
|
37
|
+
model_config = ConfigDict(extra="forbid")
|
38
|
+
|
39
|
+
from_port: int = Field(description="The source port")
|
40
|
+
ip_protocol: Literal["tcp", "udp"] = Field(description="The IP protocol (either 'tcp' or 'udp')")
|
41
|
+
ip_ranges: List[str] = Field(description="IP ranges to allow, in CIDR format (eg. '192.0.2.0/24')")
|
42
|
+
to_port: int = Field(description="The destination port")
|
43
|
+
|
44
|
+
|
45
|
+
class ConfigImageMarketplaceModel(BaseModel):
|
46
|
+
"""
|
47
|
+
Image/AMI Marketplace specific configuration to request new Marketplace versions
|
48
|
+
See https://docs.aws.amazon.com/marketplace-catalog/latest/api-reference/ami-products.html
|
49
|
+
for further information
|
50
|
+
"""
|
51
|
+
|
52
|
+
model_config = ConfigDict(extra="forbid")
|
53
|
+
|
54
|
+
entity_id: str = Field(description="The entity ID (product ID)")
|
55
|
+
# see https://docs.aws.amazon.com/marketplace/latest/userguide/ami-single-ami-products.html#single-ami-marketplace-ami-access # noqa:E501
|
56
|
+
access_role_arn: str = Field(
|
57
|
+
description="IAM role Amazon Resource Name (ARN) used by AWS Marketplace to access the provided AMI"
|
58
|
+
)
|
59
|
+
version_title: str = Field(description="The version title. Must be unique across the product")
|
60
|
+
release_notes: str = Field(description="The release notes")
|
61
|
+
user_name: str = Field(description="The login username to access the operating system")
|
62
|
+
scanning_port: int = Field(description="Port to access the operating system (default: 22)", default=22)
|
63
|
+
os_name: str = Field(description="Operating system name displayed to Marketplace buyers")
|
64
|
+
os_version: str = Field(description="Operating system version displayed to Marketplace buyers")
|
65
|
+
usage_instructions: str = Field(
|
66
|
+
description=" Instructions for using the AMI, or a link to more information about the AMI"
|
67
|
+
)
|
68
|
+
recommended_instance_type: str = Field(
|
69
|
+
description="The instance type that is recommended to run the service with the AMI and is the "
|
70
|
+
"default for 1-click installs of your service"
|
71
|
+
)
|
72
|
+
security_groups: Optional[List[ConfigImageMarketplaceSecurityGroupModel]]
|
73
|
+
|
74
|
+
|
75
|
+
class ConfigImageSSMParameterModel(BaseModel):
|
76
|
+
"""
|
77
|
+
Image/AMI SSM specific configuration to push parameters of type `aws:ec2:image` to the parameter store
|
78
|
+
"""
|
79
|
+
|
80
|
+
model_config = ConfigDict(extra="forbid")
|
81
|
+
|
82
|
+
name: str = Field(
|
83
|
+
description="The fully qualified name of the parameter that you want to add to the system. "
|
84
|
+
"A parameter name must be unique within an Amazon Web Services Region"
|
85
|
+
)
|
86
|
+
description: Optional[str] = Field(
|
87
|
+
description="Information about the parameter that you want to add to the system", default=None
|
88
|
+
)
|
89
|
+
allow_overwrite: Optional[bool] = Field(
|
90
|
+
description="allow to overwrite an existing parameter. Useful for keep a 'latest' parameter (default: false)",
|
91
|
+
default=False,
|
92
|
+
)
|
93
|
+
|
94
|
+
|
95
|
+
class ConfigImageModel(BaseModel):
|
96
|
+
"""
|
97
|
+
Image/AMI configuration.
|
98
|
+
"""
|
99
|
+
|
100
|
+
model_config = ConfigDict(extra="forbid")
|
101
|
+
|
102
|
+
description: Optional[str] = Field(description="Optional image description", default=None)
|
103
|
+
regions: Optional[List[str]] = Field(
|
104
|
+
description="Optional list of regions for this image. If not given, all available regions will be used",
|
105
|
+
default=None,
|
106
|
+
)
|
107
|
+
separate_snapshot: bool = Field(description="Use a separate snapshot for this image?", default=False)
|
108
|
+
billing_products: Optional[List[str]] = Field(description="Optional list of billing codes", default=None)
|
109
|
+
boot_mode: Literal["legacy-bios", "uefi", "uefi-preferred"] = Field(
|
110
|
+
description="The boot mode. For arm64, this needs to be 'uefi'"
|
111
|
+
)
|
112
|
+
root_device_name: Optional[str] = Field(description="The root device name", default="/dev/sda1")
|
113
|
+
root_device_volume_type: Optional[Literal["gp2", "gp3"]] = Field(
|
114
|
+
description="The root device volume type", default="gp3"
|
115
|
+
)
|
116
|
+
root_device_volume_size: Optional[int] = Field(description="The root device volume size (in GB)", default=8)
|
117
|
+
uefi_data: Optional[pathlib.Path] = Field(
|
118
|
+
description="Optional path to a non-volatile UEFI variable store (must be already base64 encoded)", default=None
|
119
|
+
)
|
120
|
+
tpm_support: Optional[Literal["v2.0"]] = Field(
|
121
|
+
description="Optional TPM support. If this is set, 'boot_mode' must be 'uefi'", default=None
|
122
|
+
)
|
123
|
+
imds_support: Optional[Literal["v2.0"]] = Field(description="Optional IMDS support", default=None)
|
124
|
+
share: Optional[List[str]] = Field(
|
125
|
+
description="Optional list of account IDs the image and snapshot will be shared with", default=None
|
126
|
+
)
|
127
|
+
temporary: Optional[bool] = Field(
|
128
|
+
description="Optional boolean field indicates that a image is only temporary", default=False
|
129
|
+
)
|
130
|
+
public: Optional[bool] = Field(
|
131
|
+
description="Optional boolean field indicates if the image should be public", default=False
|
132
|
+
)
|
133
|
+
marketplace: Optional[ConfigImageMarketplaceModel] = Field(
|
134
|
+
description="Optional structure containing Marketplace related configuration for the commercial "
|
135
|
+
"'aws' partition",
|
136
|
+
default=None,
|
137
|
+
)
|
138
|
+
ssm_parameter: Optional[List[ConfigImageSSMParameterModel]] = Field(
|
139
|
+
description="Optional list of SSM parameter paths of type `aws:ec2:image` which will "
|
140
|
+
"be pushed to the parameter store",
|
141
|
+
default=None,
|
142
|
+
)
|
143
|
+
groups: Optional[List[str]] = Field(description="Optional list of groups this image is part of", default=[])
|
144
|
+
tags: Optional[Dict[str, str]] = Field(description="Optional Tags to apply to this image only", default={})
|
145
|
+
|
146
|
+
|
147
|
+
class ConfigModel(BaseModel):
|
148
|
+
"""
|
149
|
+
The base model for the whole configuration
|
150
|
+
"""
|
151
|
+
|
152
|
+
model_config = ConfigDict(extra="forbid")
|
153
|
+
|
154
|
+
s3: ConfigS3Model
|
155
|
+
source: ConfigSourceModel
|
156
|
+
images: Dict[str, ConfigImageModel]
|
157
|
+
tags: Optional[Dict[str, str]] = Field(description="Optional Tags to apply to all resources", default={})
|
awspub/context.py
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
import hashlib
|
2
|
+
import logging
|
3
|
+
import pathlib
|
4
|
+
from string import Template
|
5
|
+
from typing import Dict
|
6
|
+
|
7
|
+
from ruamel.yaml import YAML
|
8
|
+
|
9
|
+
from awspub.configmodels import ConfigModel
|
10
|
+
|
11
|
+
logger = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
|
14
|
+
class Context:
|
15
|
+
"""
|
16
|
+
Context holds the used configuration and some
|
17
|
+
automatically calculated values
|
18
|
+
"""
|
19
|
+
|
20
|
+
def __init__(self, conf_path: pathlib.Path, conf_template_mapping_path: pathlib.Path):
|
21
|
+
self._conf_path = conf_path
|
22
|
+
self._conf = None
|
23
|
+
self._conf_template_mapping_path = conf_template_mapping_path
|
24
|
+
self._conf_template_mapping = {}
|
25
|
+
yaml = YAML(typ="safe")
|
26
|
+
|
27
|
+
# read the config mapping first
|
28
|
+
if self._conf_template_mapping_path:
|
29
|
+
with open(self._conf_template_mapping_path, "r") as ctm:
|
30
|
+
self._conf_template_mapping = yaml.load(ctm)
|
31
|
+
logger.debug(f"loaded config template mapping for substitution: {self._conf_template_mapping}")
|
32
|
+
|
33
|
+
# read the config itself
|
34
|
+
with open(self._conf_path, "r") as f:
|
35
|
+
template = Template(f.read())
|
36
|
+
# substitute the values in the config with values from the config template mapping
|
37
|
+
ft = template.substitute(**self._conf_template_mapping)
|
38
|
+
y = yaml.load(ft)["awspub"]
|
39
|
+
self._conf = ConfigModel(**y).model_dump()
|
40
|
+
logger.debug(f"config loaded and validated as: {self._conf}")
|
41
|
+
|
42
|
+
# handle relative paths in config files. those are relative to the config file dirname
|
43
|
+
if not self.conf["source"]["path"].is_absolute():
|
44
|
+
self.conf["source"]["path"] = pathlib.Path(self._conf_path).parent / self.conf["source"]["path"]
|
45
|
+
|
46
|
+
for image_name, props in self.conf["images"].items():
|
47
|
+
if props["uefi_data"] and not self.conf["images"][image_name]["uefi_data"].is_absolute():
|
48
|
+
self.conf["images"][image_name]["uefi_data"] = (
|
49
|
+
pathlib.Path(self._conf_path).parent / self.conf["images"][image_name]["uefi_data"]
|
50
|
+
)
|
51
|
+
|
52
|
+
# calculate the sha256 sum of the source file once
|
53
|
+
self._source_sha256_obj = self._sha256sum(self.conf["source"]["path"])
|
54
|
+
self._source_sha256 = self._source_sha256_obj.hexdigest()
|
55
|
+
|
56
|
+
@property
|
57
|
+
def conf(self):
|
58
|
+
return self._conf
|
59
|
+
|
60
|
+
@property
|
61
|
+
def source_sha256(self):
|
62
|
+
"""
|
63
|
+
The sha256 sum hexdigest of the source->path value from the given
|
64
|
+
configuration. This value is used in different places (eg. to automatically
|
65
|
+
upload to S3 with this value as key)
|
66
|
+
"""
|
67
|
+
return self._source_sha256
|
68
|
+
|
69
|
+
@property
|
70
|
+
def tags_dict(self) -> Dict[str, str]:
|
71
|
+
"""
|
72
|
+
Common tags which will be used for all AWS resources
|
73
|
+
This includes tags defined in the configuration file
|
74
|
+
but doesn't include image group specific tags.
|
75
|
+
Usually the tags() method should be used.
|
76
|
+
"""
|
77
|
+
tags = dict()
|
78
|
+
tags["awspub:source:filename"] = self.conf["source"]["path"].name
|
79
|
+
tags["awspub:source:architecture"] = self.conf["source"]["architecture"]
|
80
|
+
tags["awspub:source:sha256"] = self.source_sha256
|
81
|
+
tags.update(self.conf.get("tags", {}))
|
82
|
+
return tags
|
83
|
+
|
84
|
+
@property
|
85
|
+
def tags(self):
|
86
|
+
"""
|
87
|
+
Helper to make tags directly usable by the AWS EC2 API
|
88
|
+
which requires a list of dicts with "Key" and "Value" defined.
|
89
|
+
"""
|
90
|
+
tags = []
|
91
|
+
for name, value in self.tags_dict.items():
|
92
|
+
tags.append({"Key": name, "Value": value})
|
93
|
+
return tags
|
94
|
+
|
95
|
+
def _sha256sum(self, file_path: pathlib.Path):
|
96
|
+
"""
|
97
|
+
Calculate a sha256 sum for a given file
|
98
|
+
|
99
|
+
:param file_path: the path to the local file to upload
|
100
|
+
:type file_path: pathlib.Path
|
101
|
+
:return: a haslib Hash object
|
102
|
+
:rtype: _hashlib.HASH
|
103
|
+
"""
|
104
|
+
sha256_hash = hashlib.sha256()
|
105
|
+
with open(file_path.resolve(), "rb") as f:
|
106
|
+
for byte_block in iter(lambda: f.read(4096), b""):
|
107
|
+
sha256_hash.update(byte_block)
|
108
|
+
return sha256_hash
|
awspub/exceptions.py
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
class MultipleSnapshotsException(Exception):
|
2
|
+
pass
|
3
|
+
|
4
|
+
|
5
|
+
class MultipleImportSnapshotTasksException(Exception):
|
6
|
+
pass
|
7
|
+
|
8
|
+
|
9
|
+
class MultipleImagesException(Exception):
|
10
|
+
pass
|
11
|
+
|
12
|
+
|
13
|
+
class BucketDoesNotExistException(Exception):
|
14
|
+
def __init__(self, bucket_name: str, *args, **kwargs):
|
15
|
+
msg = f"The bucket named '{bucket_name}' does not exist. You will need to create the bucket before proceeding."
|
16
|
+
super().__init__(msg, *args, **kwargs)
|