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/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