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/image.py
ADDED
@@ -0,0 +1,593 @@
|
|
1
|
+
import hashlib
|
2
|
+
import logging
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from enum import Enum
|
5
|
+
from typing import Any, Dict, List, Optional
|
6
|
+
|
7
|
+
import boto3
|
8
|
+
from mypy_boto3_ec2.client import EC2Client
|
9
|
+
from mypy_boto3_ssm import SSMClient
|
10
|
+
|
11
|
+
from awspub import exceptions
|
12
|
+
from awspub.context import Context
|
13
|
+
from awspub.image_marketplace import ImageMarketplace
|
14
|
+
from awspub.s3 import S3
|
15
|
+
from awspub.snapshot import Snapshot
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
@dataclass
|
21
|
+
class _ImageInfo:
|
22
|
+
"""
|
23
|
+
Information about a image from EC2
|
24
|
+
"""
|
25
|
+
|
26
|
+
image_id: str
|
27
|
+
snapshot_id: Optional[str]
|
28
|
+
|
29
|
+
|
30
|
+
class ImageVerificationErrors(str, Enum):
|
31
|
+
"""
|
32
|
+
Possible errors for image verification
|
33
|
+
"""
|
34
|
+
|
35
|
+
NOT_EXIST = "image does not exist"
|
36
|
+
STATE_NOT_AVAILABLE = "image not available"
|
37
|
+
ROOT_DEVICE_TYPE = "root device type mismatch"
|
38
|
+
ROOT_DEVICE_VOLUME_TYPE = "root device volume type mismatch"
|
39
|
+
ROOT_DEVICE_VOLUME_SIZE = "root device volume size mismatch"
|
40
|
+
ROOT_DEVICE_SNAPSHOT_NOT_COMPLETE = "root device snapshot not complete"
|
41
|
+
BOOT_MODE = "boot mode mismatch"
|
42
|
+
TAGS = "tags mismatch"
|
43
|
+
TPM_SUPPORT = "tpm support mismatch"
|
44
|
+
IMDS_SUPPORT = "imds support mismatch"
|
45
|
+
BILLING_PRODUCTS = "billing products mismatch"
|
46
|
+
|
47
|
+
|
48
|
+
class Image:
|
49
|
+
"""
|
50
|
+
Handle EC2 Image/AMI API interaction
|
51
|
+
"""
|
52
|
+
|
53
|
+
def __init__(self, context: Context, image_name: str):
|
54
|
+
self._ctx: Context = context
|
55
|
+
self._image_name: str = image_name
|
56
|
+
self._image_regions: List[str] = []
|
57
|
+
|
58
|
+
if self._image_name not in self._ctx.conf["images"].keys():
|
59
|
+
raise ValueError(f"image '{self._image_name}' not found in context configuration")
|
60
|
+
|
61
|
+
self._snapshot: Snapshot = Snapshot(context)
|
62
|
+
self._s3: S3 = S3(context)
|
63
|
+
|
64
|
+
def __repr__(self):
|
65
|
+
return f"<{self.__class__} :'{self.image_name}' (snapshot name: {self.snapshot_name})"
|
66
|
+
|
67
|
+
@property
|
68
|
+
def conf(self) -> Dict[str, Any]:
|
69
|
+
"""
|
70
|
+
The configuration for the current image (based on "image_name") from context
|
71
|
+
"""
|
72
|
+
return self._ctx.conf["images"][self._image_name]
|
73
|
+
|
74
|
+
@property
|
75
|
+
def image_name(self) -> str:
|
76
|
+
"""
|
77
|
+
Get the image name
|
78
|
+
"""
|
79
|
+
return self._image_name
|
80
|
+
|
81
|
+
@property
|
82
|
+
def snapshot_name(self) -> str:
|
83
|
+
"""
|
84
|
+
Get the snapshot name which is a sha256 hexdigest
|
85
|
+
|
86
|
+
The snapshot name is the sha256 hexdigest of the source file given in the source->path
|
87
|
+
configuration option.
|
88
|
+
|
89
|
+
if the "separate_snapshot" config option is set to True, the snapshot name is
|
90
|
+
sha256 hexdigest of the source file given in the source->path conf option and then
|
91
|
+
the sha256 hexdigest of the image-name appended and then the sha256 hexdigest
|
92
|
+
calculated of this concatenated string.
|
93
|
+
|
94
|
+
if the "billing_products" config option is set, the snapshot name is
|
95
|
+
sha256 hexdigest of the source file given in the source->path conf option and then
|
96
|
+
the sha256 hexdigest of each entry in the billing_products appended and then the sha256 hexdigest
|
97
|
+
calculated of this concatenated string.
|
98
|
+
|
99
|
+
Note that both options ("separate_snapshot" and "billing_products") can be combined
|
100
|
+
and the snapshot calculation steps would be combined, too.
|
101
|
+
"""
|
102
|
+
s_name = self._ctx.source_sha256
|
103
|
+
if self.conf["separate_snapshot"] is True:
|
104
|
+
s_name += hashlib.sha256(self.image_name.encode("utf-8")).hexdigest()
|
105
|
+
|
106
|
+
if self.conf["billing_products"]:
|
107
|
+
for bp in self.conf["billing_products"]:
|
108
|
+
s_name += hashlib.sha256(bp.encode("utf-8")).hexdigest()
|
109
|
+
|
110
|
+
# in the separate_snapshot and billing_products had no effect, don't do another sha256 of
|
111
|
+
# the source_sha256 to simplify things
|
112
|
+
if s_name == self._ctx.source_sha256:
|
113
|
+
return s_name
|
114
|
+
|
115
|
+
# do a sha256 of the concatenated string
|
116
|
+
return hashlib.sha256(s_name.encode("utf-8")).hexdigest()
|
117
|
+
|
118
|
+
@property
|
119
|
+
def image_regions(self) -> List[str]:
|
120
|
+
"""
|
121
|
+
Get the image regions. Either configured in the image configuration
|
122
|
+
or all available regions
|
123
|
+
"""
|
124
|
+
if not self._image_regions:
|
125
|
+
if self.conf["regions"]:
|
126
|
+
self._image_regions = self.conf["regions"]
|
127
|
+
else:
|
128
|
+
ec2client: EC2Client = boto3.client("ec2", region_name=self._s3.bucket_region)
|
129
|
+
resp = ec2client.describe_regions()
|
130
|
+
self._image_regions = [r["RegionName"] for r in resp["Regions"]]
|
131
|
+
return self._image_regions
|
132
|
+
|
133
|
+
@property
|
134
|
+
def _tags(self):
|
135
|
+
"""
|
136
|
+
Get the tags for this image (common tags + image specific tags)
|
137
|
+
image specific tags override common tags
|
138
|
+
"""
|
139
|
+
tags = []
|
140
|
+
# the common tags
|
141
|
+
tags_dict = self._ctx.tags_dict
|
142
|
+
# the image specific tags
|
143
|
+
tags_dict.update(self.conf.get("tags", {}))
|
144
|
+
for name, value in tags_dict.items():
|
145
|
+
tags.append({"Key": name, "Value": value})
|
146
|
+
return tags
|
147
|
+
|
148
|
+
def _share(self, share_conf: List[str], images: Dict[str, _ImageInfo]):
|
149
|
+
"""
|
150
|
+
Share images with accounts
|
151
|
+
|
152
|
+
:param share_conf: the share configuration. eg. self.conf["share_create"]
|
153
|
+
:type share_conf: List[str]
|
154
|
+
:param images: a Dict with region names as keys and _ImageInfo objects as values
|
155
|
+
:type images: Dict[str, _ImageInfo]
|
156
|
+
"""
|
157
|
+
share_list: List[Dict[str, str]] = [{"UserId": user_id} for user_id in share_conf]
|
158
|
+
|
159
|
+
for region, image_info in images.items():
|
160
|
+
ec2client: EC2Client = boto3.client("ec2", region_name=region)
|
161
|
+
# modify image permissions
|
162
|
+
ec2client.modify_image_attribute(
|
163
|
+
Attribute="LaunchPermission",
|
164
|
+
ImageId=image_info.image_id,
|
165
|
+
LaunchPermission={"Add": share_list}, # type: ignore
|
166
|
+
)
|
167
|
+
|
168
|
+
# modify snapshot permissions
|
169
|
+
if image_info.snapshot_id:
|
170
|
+
ec2client.modify_snapshot_attribute(
|
171
|
+
Attribute="createVolumePermission",
|
172
|
+
SnapshotId=image_info.snapshot_id,
|
173
|
+
CreateVolumePermission={"Add": share_list}, # type: ignore
|
174
|
+
)
|
175
|
+
|
176
|
+
logger.info(f"shared images & snapshots with '{share_conf}'")
|
177
|
+
|
178
|
+
def _get_root_device_snapshot_id(self, image):
|
179
|
+
"""
|
180
|
+
Get the root device snapshot id for a given image
|
181
|
+
:param image: a image structure returned by eg. describe_images()["Images"][0]
|
182
|
+
:type image: dict
|
183
|
+
:return: Either None or a snapshot-id
|
184
|
+
:rtype: Optional[str]
|
185
|
+
"""
|
186
|
+
root_device_name = image.get("RootDeviceName")
|
187
|
+
if not root_device_name:
|
188
|
+
logger.debug(f"can not get RootDeviceName for image {image}")
|
189
|
+
return None
|
190
|
+
for bdm in image["BlockDeviceMappings"]:
|
191
|
+
if bdm["DeviceName"] == root_device_name:
|
192
|
+
ebs = bdm.get("Ebs")
|
193
|
+
if not ebs:
|
194
|
+
logger.debug(
|
195
|
+
f"can not get RootDeviceName. root device {root_device_name} doesn't have a Ebs section"
|
196
|
+
)
|
197
|
+
return None
|
198
|
+
logger.debug(f"found Ebs for root device {root_device_name}: {bdm['Ebs']}")
|
199
|
+
return bdm["Ebs"]["SnapshotId"]
|
200
|
+
|
201
|
+
def _get(self, ec2client: EC2Client) -> Optional[_ImageInfo]:
|
202
|
+
"""
|
203
|
+
Get the a _ImageInfo for the current image which contains the ami id and
|
204
|
+
root device snapshot id.
|
205
|
+
This relies on the image name to be unique and will raise a MultipleImagesException
|
206
|
+
if multiple images are found.
|
207
|
+
|
208
|
+
:param ec2client: EC2Client
|
209
|
+
:type ec2client: EC2Client
|
210
|
+
:return: Either None or a _ImageInfo
|
211
|
+
:rtype: Optional[_ImageInfo]
|
212
|
+
"""
|
213
|
+
resp = ec2client.describe_images(
|
214
|
+
Filters=[
|
215
|
+
{"Name": "name", "Values": [self.image_name]},
|
216
|
+
],
|
217
|
+
Owners=["self"],
|
218
|
+
)
|
219
|
+
|
220
|
+
if len(resp.get("Images", [])) == 1:
|
221
|
+
root_device_snapshot_id = self._get_root_device_snapshot_id(resp["Images"][0])
|
222
|
+
return _ImageInfo(resp["Images"][0]["ImageId"], root_device_snapshot_id)
|
223
|
+
elif len(resp.get("Images", [])) == 0:
|
224
|
+
return None
|
225
|
+
else:
|
226
|
+
images = [i["ImageId"] for i in resp.get("Images", [])]
|
227
|
+
raise exceptions.MultipleImagesException(
|
228
|
+
f"Found {len(images)} images ({', '.join(images)}) with "
|
229
|
+
f"name {self.image_name} in region {ec2client.meta.region_name}. There should be only 1."
|
230
|
+
)
|
231
|
+
|
232
|
+
def _put_ssm_parameters(self) -> None:
|
233
|
+
"""
|
234
|
+
Push the configured SSM parameters to the parameter store
|
235
|
+
"""
|
236
|
+
logger.info(f"Pushing SSM parameters for image {self.image_name} in {len(self.image_regions)} regions ...")
|
237
|
+
for region in self.image_regions:
|
238
|
+
ec2client_region: EC2Client = boto3.client("ec2", region_name=region)
|
239
|
+
image_info: Optional[_ImageInfo] = self._get(ec2client_region)
|
240
|
+
|
241
|
+
# image in region not found
|
242
|
+
if not image_info:
|
243
|
+
logger.error(f"image {self.image_name} not available in region {region}. can not push SSM parameter")
|
244
|
+
continue
|
245
|
+
|
246
|
+
ssmclient_region: SSMClient = boto3.client("ssm", region_name=region)
|
247
|
+
# iterate over all defined parameters
|
248
|
+
for parameter in self.conf["ssm_parameter"]:
|
249
|
+
# if overwrite is not allowed, check if the parameter is already there and if so, do nothing
|
250
|
+
if not parameter["allow_overwrite"]:
|
251
|
+
resp = ssmclient_region.get_parameters(Names=[parameter["name"]])
|
252
|
+
if len(resp["Parameters"]) >= 1:
|
253
|
+
# sanity check if the available parameter matches the value we would (but don't) push
|
254
|
+
if resp["Parameters"][0]["Value"] != image_info.image_id:
|
255
|
+
logger.warning(
|
256
|
+
f"SSM parameter {parameter['name']} exists but value does not match "
|
257
|
+
f"(found {resp['Parameters'][0]['Value']}; expected: {image_info.image_id}"
|
258
|
+
)
|
259
|
+
# parameter exists already and overwrite is not allowed so continue
|
260
|
+
continue
|
261
|
+
# push parameter to store
|
262
|
+
ssmclient_region.put_parameter(
|
263
|
+
Name=parameter["name"],
|
264
|
+
Description=parameter.get("description", ""),
|
265
|
+
Value=image_info.image_id,
|
266
|
+
Type="String",
|
267
|
+
Overwrite=parameter["allow_overwrite"],
|
268
|
+
DataType="aws:ec2:image",
|
269
|
+
# TODO: tags can't be used together with overwrite
|
270
|
+
# Tags=self._ctx.tags,
|
271
|
+
)
|
272
|
+
|
273
|
+
logger.info(
|
274
|
+
f"pushed SSM parameter {parameter['name']} with value {image_info.image_id} in region {region}"
|
275
|
+
)
|
276
|
+
|
277
|
+
def _public(self) -> None:
|
278
|
+
"""
|
279
|
+
Make image and underlying root device snapshot public
|
280
|
+
"""
|
281
|
+
logger.info(f"Make image {self.image_name} in {len(self.image_regions)} regions public ...")
|
282
|
+
|
283
|
+
for region in self.image_regions:
|
284
|
+
ec2client_region: EC2Client = boto3.client("ec2", region_name=region)
|
285
|
+
image_info: Optional[_ImageInfo] = self._get(ec2client_region)
|
286
|
+
if image_info:
|
287
|
+
ec2client_region.modify_image_attribute(
|
288
|
+
ImageId=image_info.image_id,
|
289
|
+
LaunchPermission={
|
290
|
+
"Add": [
|
291
|
+
{
|
292
|
+
"Group": "all",
|
293
|
+
},
|
294
|
+
],
|
295
|
+
},
|
296
|
+
)
|
297
|
+
logger.info(f"image {image_info.image_id} in region {region} public now")
|
298
|
+
|
299
|
+
if image_info.snapshot_id:
|
300
|
+
ec2client_region.modify_snapshot_attribute(
|
301
|
+
SnapshotId=image_info.snapshot_id,
|
302
|
+
Attribute="createVolumePermission",
|
303
|
+
GroupNames=[
|
304
|
+
"all",
|
305
|
+
],
|
306
|
+
OperationType="add",
|
307
|
+
)
|
308
|
+
logger.info(
|
309
|
+
f"snapshot {image_info.snapshot_id} ({image_info.image_id}) in region {region} public now"
|
310
|
+
)
|
311
|
+
else:
|
312
|
+
logger.error(
|
313
|
+
f"snapshot for image {self.image_name} ({image_info.image_id}) not available "
|
314
|
+
f"in region {region}. can not make public"
|
315
|
+
)
|
316
|
+
else:
|
317
|
+
logger.error(f"image {self.image_name} not available in region {region}. can not make public")
|
318
|
+
|
319
|
+
def cleanup(self) -> None:
|
320
|
+
"""
|
321
|
+
Cleanup/delete the temporary images
|
322
|
+
|
323
|
+
If an image is marked as "temporary" in the configuration, do
|
324
|
+
delete that image in all regions.
|
325
|
+
Note: if a temporary image is public, it won't be deleted. A temporary
|
326
|
+
image should never be public
|
327
|
+
Note: the underlying snapshot is currently not deleted. That might change in
|
328
|
+
the future
|
329
|
+
"""
|
330
|
+
if not self.conf["temporary"]:
|
331
|
+
logger.info(f"image {self.image_name} not marked as temporary. no cleanup")
|
332
|
+
return
|
333
|
+
|
334
|
+
# do the cleanup - the image is marked as temporary
|
335
|
+
logger.info(f"Cleanup image {self.image_name} ...")
|
336
|
+
for region in self.image_regions:
|
337
|
+
ec2client_region: EC2Client = boto3.client("ec2", region_name=region)
|
338
|
+
image_info: Optional[_ImageInfo] = self._get(ec2client_region)
|
339
|
+
|
340
|
+
if image_info:
|
341
|
+
resp = ec2client_region.describe_images(
|
342
|
+
Filters=[
|
343
|
+
{"Name": "image-id", "Values": [image_info.image_id]},
|
344
|
+
]
|
345
|
+
)
|
346
|
+
if resp["Images"][0]["Public"] is True:
|
347
|
+
# this shouldn't happen because the image is marked as temporary in the config
|
348
|
+
# so how can it be public?
|
349
|
+
logger.error(
|
350
|
+
f"no cleanup for {self.image_name} in {region} because ({image_info.image_id}) image is public"
|
351
|
+
)
|
352
|
+
else:
|
353
|
+
ec2client_region.deregister_image(ImageId=image_info.image_id)
|
354
|
+
logger.info(f"{self.image_name} in {region} ({image_info.image_id}) deleted")
|
355
|
+
|
356
|
+
def list(self) -> Dict[str, _ImageInfo]:
|
357
|
+
"""
|
358
|
+
Get image based on the available configuration
|
359
|
+
This doesn't change anything - it just tries to get the available image
|
360
|
+
for the different configured regions
|
361
|
+
:return: a Dict with region names as keys and _ImageInfo objects as values
|
362
|
+
:rtype: Dict[str, _ImageInfo]
|
363
|
+
"""
|
364
|
+
images: Dict[str, _ImageInfo] = dict()
|
365
|
+
for region in self.image_regions:
|
366
|
+
ec2client_region: EC2Client = boto3.client("ec2", region_name=region)
|
367
|
+
image_info: Optional[_ImageInfo] = self._get(ec2client_region)
|
368
|
+
if image_info:
|
369
|
+
images[region] = image_info
|
370
|
+
else:
|
371
|
+
logger.warning(f"image {self.image_name} not available in region {region}")
|
372
|
+
return images
|
373
|
+
|
374
|
+
def create(self) -> Dict[str, _ImageInfo]:
|
375
|
+
"""
|
376
|
+
Get or create a image based on the available configuration
|
377
|
+
|
378
|
+
:return: a Dict with region names as keys and _ImageInfo objects as values
|
379
|
+
:rtype: Dict[str, _ImageInfo]
|
380
|
+
"""
|
381
|
+
# this **must** be the region that is used for S3
|
382
|
+
ec2client: EC2Client = boto3.client("ec2", region_name=self._s3.bucket_region)
|
383
|
+
|
384
|
+
# make sure the initial snapshot exists
|
385
|
+
self._snapshot.create(ec2client, self.snapshot_name)
|
386
|
+
|
387
|
+
# make sure the snapshot exist in all required regions
|
388
|
+
snapshot_ids: Dict[str, str] = self._snapshot.copy(
|
389
|
+
self.snapshot_name, self._s3.bucket_region, self.image_regions
|
390
|
+
)
|
391
|
+
|
392
|
+
images: Dict[str, _ImageInfo] = dict()
|
393
|
+
for region in self.image_regions:
|
394
|
+
ec2client_region: EC2Client = boto3.client("ec2", region_name=region)
|
395
|
+
image_info: Optional[_ImageInfo] = self._get(ec2client_region)
|
396
|
+
if image_info:
|
397
|
+
if image_info.snapshot_id != snapshot_ids[region]:
|
398
|
+
logger.warning(
|
399
|
+
f"image with name '{self.image_name}' already exists ({image_info.image_id}) "
|
400
|
+
f"in region {ec2client_region.meta.region_name} but the root device "
|
401
|
+
f"snapshot id is unexpected (got {image_info.snapshot_id} but expected {snapshot_ids[region]})"
|
402
|
+
)
|
403
|
+
else:
|
404
|
+
logger.info(
|
405
|
+
f"image with name '{self.image_name}' already exists ({image_info.image_id}) "
|
406
|
+
f"in region {ec2client_region.meta.region_name}"
|
407
|
+
)
|
408
|
+
images[region] = image_info
|
409
|
+
else:
|
410
|
+
logger.info(
|
411
|
+
f"creating image with name '{self.image_name}' in "
|
412
|
+
f"region {ec2client_region.meta.region_name} ..."
|
413
|
+
)
|
414
|
+
|
415
|
+
register_image_kwargs = dict(
|
416
|
+
Name=self.image_name,
|
417
|
+
Description=self.conf.get("description", ""),
|
418
|
+
Architecture=self._ctx.conf["source"]["architecture"],
|
419
|
+
RootDeviceName=self.conf["root_device_name"],
|
420
|
+
BlockDeviceMappings=[
|
421
|
+
{
|
422
|
+
"Ebs": {
|
423
|
+
"SnapshotId": snapshot_ids[region],
|
424
|
+
"VolumeType": self.conf["root_device_volume_type"],
|
425
|
+
"VolumeSize": self.conf["root_device_volume_size"],
|
426
|
+
},
|
427
|
+
"DeviceName": self.conf["root_device_name"],
|
428
|
+
},
|
429
|
+
# TODO: make those ephemeral block device mappings configurable
|
430
|
+
{"VirtualName": "ephemeral0", "DeviceName": "/dev/sdb"},
|
431
|
+
{"VirtualName": "ephemeral1", "DeviceName": "/dev/sdc"},
|
432
|
+
],
|
433
|
+
EnaSupport=True,
|
434
|
+
SriovNetSupport="simple",
|
435
|
+
VirtualizationType="hvm",
|
436
|
+
BootMode=self.conf["boot_mode"],
|
437
|
+
)
|
438
|
+
|
439
|
+
if self.conf["tpm_support"]:
|
440
|
+
register_image_kwargs["TpmSupport"] = self.conf["tpm_support"]
|
441
|
+
|
442
|
+
if self.conf["imds_support"]:
|
443
|
+
register_image_kwargs["ImdsSupport"] = self.conf["imds_support"]
|
444
|
+
|
445
|
+
if self.conf["uefi_data"]:
|
446
|
+
with open(self.conf["uefi_data"], "r") as f:
|
447
|
+
uefi_data = f.read()
|
448
|
+
register_image_kwargs["UefiData"] = uefi_data
|
449
|
+
|
450
|
+
if self.conf["billing_products"]:
|
451
|
+
register_image_kwargs["BillingProducts"] = self.conf["billing_products"]
|
452
|
+
|
453
|
+
resp = ec2client_region.register_image(**register_image_kwargs)
|
454
|
+
ec2client_region.create_tags(Resources=[resp["ImageId"]], Tags=self._tags)
|
455
|
+
images[region] = _ImageInfo(resp["ImageId"], snapshot_ids[region])
|
456
|
+
|
457
|
+
# wait for the images
|
458
|
+
logger.info(f"Waiting for {len(images)} images to be ready the regions ...")
|
459
|
+
for region, image_info in images.items():
|
460
|
+
ec2client_region_wait: EC2Client = boto3.client("ec2", region_name=region)
|
461
|
+
logger.info(
|
462
|
+
f"Waiting for {image_info.image_id} in {ec2client_region_wait.meta.region_name} "
|
463
|
+
"to exist/be available ..."
|
464
|
+
)
|
465
|
+
waiter_exists = ec2client_region_wait.get_waiter("image_exists")
|
466
|
+
waiter_exists.wait(ImageIds=[image_info.image_id])
|
467
|
+
waiter_available = ec2client_region_wait.get_waiter("image_available")
|
468
|
+
waiter_available.wait(ImageIds=[image_info.image_id])
|
469
|
+
logger.info(f"{len(images)} images are ready")
|
470
|
+
|
471
|
+
# share
|
472
|
+
if self.conf["share"]:
|
473
|
+
self._share(self.conf["share"], images)
|
474
|
+
|
475
|
+
return images
|
476
|
+
|
477
|
+
def publish(self) -> None:
|
478
|
+
"""
|
479
|
+
Handle all publication steps
|
480
|
+
- make image and underlying root device snapshot public if the public flag is set
|
481
|
+
- request a new marketplace version for the image in us-east-1 if the marketplace config is present
|
482
|
+
Note: if the temporary flag is set in the image, this method will do nothing
|
483
|
+
Note: this command doesn't unpublish anything!
|
484
|
+
"""
|
485
|
+
# never publish temporary images
|
486
|
+
if self.conf["temporary"]:
|
487
|
+
logger.warning(f"image {self.image_name} marked as temporary. do not publish")
|
488
|
+
return
|
489
|
+
|
490
|
+
# make snapshot and image public if requested in the image
|
491
|
+
if self.conf["public"]:
|
492
|
+
self._public()
|
493
|
+
else:
|
494
|
+
logger.info(f"image {self.image_name} not marked as public. do not publish")
|
495
|
+
|
496
|
+
# handle marketplace publication
|
497
|
+
if self.conf["marketplace"]:
|
498
|
+
# the "marketplace" configuration is only valid in the "aws" partition
|
499
|
+
partition = boto3.client("ec2").meta.partition
|
500
|
+
if partition == "aws":
|
501
|
+
logger.info(f"marketplace version request for {self.image_name}")
|
502
|
+
# image needs to be in us-east-1
|
503
|
+
ec2client: EC2Client = boto3.client("ec2", region_name="us-east-1")
|
504
|
+
image_info: Optional[_ImageInfo] = self._get(ec2client)
|
505
|
+
if image_info:
|
506
|
+
im = ImageMarketplace(self._ctx, self.image_name)
|
507
|
+
im.request_new_version(image_info.image_id)
|
508
|
+
else:
|
509
|
+
logger.error(
|
510
|
+
f"can not request marketplace version for {self.image_name} because no image found in us-east-1"
|
511
|
+
)
|
512
|
+
else:
|
513
|
+
logger.info(
|
514
|
+
f"found marketplace config for {self.image_name} and partition 'aws' but "
|
515
|
+
f"currently using partition {partition}. Ignoring marketplace config."
|
516
|
+
)
|
517
|
+
|
518
|
+
# handle SSM parameter store
|
519
|
+
if self.conf["ssm_parameter"]:
|
520
|
+
self._put_ssm_parameters()
|
521
|
+
|
522
|
+
def _verify(self, region: str) -> List[ImageVerificationErrors]:
|
523
|
+
"""
|
524
|
+
Verify (but don't modify or create anything) the image in a single region
|
525
|
+
"""
|
526
|
+
problems: List[ImageVerificationErrors] = []
|
527
|
+
ec2client_region: EC2Client = boto3.client("ec2", region_name=region)
|
528
|
+
image_info: Optional[_ImageInfo] = self._get(ec2client_region)
|
529
|
+
|
530
|
+
if not image_info:
|
531
|
+
problems.append(ImageVerificationErrors.NOT_EXIST)
|
532
|
+
return problems
|
533
|
+
|
534
|
+
image_aws = ec2client_region.describe_images(ImageIds=[image_info.image_id])["Images"][0]
|
535
|
+
|
536
|
+
# verify state
|
537
|
+
if image_aws["State"] != "available":
|
538
|
+
problems.append(ImageVerificationErrors.STATE_NOT_AVAILABLE)
|
539
|
+
|
540
|
+
# verify RootDeviceType
|
541
|
+
if image_aws["RootDeviceType"] != "ebs":
|
542
|
+
problems.append(ImageVerificationErrors.ROOT_DEVICE_TYPE)
|
543
|
+
|
544
|
+
# verify BootMode
|
545
|
+
if image_aws["BootMode"] != self.conf["boot_mode"]:
|
546
|
+
problems.append(ImageVerificationErrors.BOOT_MODE)
|
547
|
+
|
548
|
+
# verify RootDeviceVolumeType, RootDeviceVolumeSize and Snapshot
|
549
|
+
for bdm in image_aws["BlockDeviceMappings"]:
|
550
|
+
if bdm.get("DeviceName") and bdm["DeviceName"] == image_aws["RootDeviceName"]:
|
551
|
+
# here's the root device
|
552
|
+
if bdm["Ebs"]["VolumeType"] != self.conf["root_device_volume_type"]:
|
553
|
+
problems.append(ImageVerificationErrors.ROOT_DEVICE_VOLUME_TYPE)
|
554
|
+
if bdm["Ebs"]["VolumeSize"] != self.conf["root_device_volume_size"]:
|
555
|
+
problems.append(ImageVerificationErrors.ROOT_DEVICE_VOLUME_SIZE)
|
556
|
+
|
557
|
+
# verify snapshot
|
558
|
+
snapshot_aws = ec2client_region.describe_snapshots(SnapshotIds=[bdm["Ebs"]["SnapshotId"]])["Snapshots"][
|
559
|
+
0
|
560
|
+
]
|
561
|
+
if snapshot_aws["State"] != "completed":
|
562
|
+
problems.append(ImageVerificationErrors.ROOT_DEVICE_SNAPSHOT_NOT_COMPLETE)
|
563
|
+
|
564
|
+
# verify tpm support
|
565
|
+
if self.conf["tpm_support"] and image_aws.get("TpmSupport") != self.conf["tpm_support"]:
|
566
|
+
problems.append(ImageVerificationErrors.TPM_SUPPORT)
|
567
|
+
|
568
|
+
# verify imds support
|
569
|
+
if self.conf["imds_support"] and image_aws.get("ImdsSupport") != self.conf["imds_support"]:
|
570
|
+
problems.append(ImageVerificationErrors.IMDS_SUPPORT)
|
571
|
+
|
572
|
+
# billing products
|
573
|
+
if self.conf["billing_products"] and image_aws.get("BillingProducts") != self.conf["billing_products"]:
|
574
|
+
problems.append(ImageVerificationErrors.BILLING_PRODUCTS)
|
575
|
+
|
576
|
+
# verify tags
|
577
|
+
for tag in image_aws["Tags"]:
|
578
|
+
if tag["Key"] == "Name" and tag["Value"] != self.snapshot_name:
|
579
|
+
problems.append(ImageVerificationErrors.TAGS)
|
580
|
+
|
581
|
+
return problems
|
582
|
+
|
583
|
+
def verify(self) -> Dict[str, List[ImageVerificationErrors]]:
|
584
|
+
"""
|
585
|
+
Verify (but don't modify or create anything) that the image configuration
|
586
|
+
matches what is on AWS
|
587
|
+
"""
|
588
|
+
logger.info(f"Verifying image {self.image_name} ...")
|
589
|
+
problems: Dict[str, List[ImageVerificationErrors]] = dict()
|
590
|
+
for region in self.image_regions:
|
591
|
+
problems[region] = self._verify(region)
|
592
|
+
|
593
|
+
return problems
|