cloudpub 0.9.2__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.
cloudpub/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
@@ -0,0 +1,2 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ from .service import AWSProductService, AWSVersionMetadata # noqa: F401
@@ -0,0 +1,526 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ import json
3
+ import logging
4
+ from copy import deepcopy
5
+ from typing import Dict, List, Optional
6
+
7
+ from boto3.session import Session
8
+ from tenacity import RetryError, Retrying
9
+ from tenacity.retry import retry_if_result
10
+ from tenacity.stop import stop_after_attempt
11
+ from tenacity.wait import wait_fixed
12
+
13
+ from cloudpub.aws.utils import (
14
+ create_version_tree,
15
+ get_restricted_major_versions,
16
+ get_restricted_minor_versions,
17
+ get_restricted_patch_versions,
18
+ pprint_debug_logging,
19
+ )
20
+ from cloudpub.common import BaseService, PublishingMetadata
21
+ from cloudpub.error import InvalidStateError, NotFoundError, Timeout
22
+ from cloudpub.models.aws import (
23
+ ChangeSetResponse,
24
+ DeliveryOption,
25
+ DescribeChangeSetReponse,
26
+ DescribeEntityResponse,
27
+ GroupedVersions,
28
+ ListChangeSet,
29
+ ListChangeSetsResponse,
30
+ ListEntitiesResponse,
31
+ ProductDetailResponse,
32
+ ProductVersionsResponse,
33
+ ProductVersionsVirtualizationSource,
34
+ VersionMapping,
35
+ )
36
+
37
+ log = logging.getLogger(__name__)
38
+
39
+
40
+ class AWSVersionMetadata(PublishingMetadata):
41
+ """A collection of metadata necessary for publishing a AMI into a product."""
42
+
43
+ def __init__(self, version_mapping: VersionMapping, marketplace_entity_type: str, **kwargs):
44
+ """
45
+ Create a new AWS Version Metadata object.
46
+
47
+ Args:
48
+ version_mapping (VersionMapping)
49
+ A mapping of all the information to add a new version
50
+ marketplace_entity_type (str)
51
+ Product type of the AWS product
52
+ Example: AmiProduct
53
+ """
54
+ self.marketplace_entity_type = marketplace_entity_type
55
+ self.version_mapping = version_mapping
56
+
57
+ super(AWSVersionMetadata, self).__init__(**kwargs)
58
+
59
+
60
+ class AWSProductService(BaseService[AWSVersionMetadata]):
61
+ """Create a new service provider for AWS using Boto3."""
62
+
63
+ # Boto3 docs
64
+ # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
65
+
66
+ def __init__(
67
+ self,
68
+ access_id: str,
69
+ secret_key: str,
70
+ region: str = "us-east-1",
71
+ attempts: int = 288,
72
+ interval: int = 600,
73
+ ) -> None:
74
+ """
75
+ AWS cloud provider service.
76
+
77
+ Args:
78
+ access_id (str)
79
+ AWS account access ID
80
+ secret_key (str)
81
+ AWS account secret access key
82
+ region (str, optional)
83
+ AWS region for compute operations
84
+ This defaults to 'us-east-1'
85
+ attempts (int, optional)
86
+ Max number of times to poll
87
+ while waiting for changeset
88
+ Defaults to 288
89
+ interval (int, optional)
90
+ Seconds between polling
91
+ while waiting for changeset
92
+ Defaults to 600
93
+ """
94
+ self.session = Session(
95
+ aws_access_key_id=access_id,
96
+ aws_secret_access_key=secret_key,
97
+ region_name=region,
98
+ )
99
+
100
+ self.marketplace = self.session.client("marketplace-catalog")
101
+ self.wait_for_changeset_attempts = attempts
102
+ self.wait_for_changeset_interval = interval
103
+
104
+ super(AWSProductService, self).__init__()
105
+
106
+ def _check_product_versions(self, details: ProductDetailResponse) -> None:
107
+ if not details.versions:
108
+ pprint_debug_logging(log, details.to_json(), "The details from the response are: ")
109
+ self._raise_error(NotFoundError, "This product has no versions")
110
+
111
+ def get_product_by_id(self, entity_id: str) -> ProductDetailResponse:
112
+ """
113
+ Get a product detail by it's id.
114
+
115
+ Args:
116
+ entity_id (str)
117
+ Entity id to get details from. If not set will default to
118
+ class setting for EntityId.
119
+ Returns:
120
+ ProductDetailResponse: The details for a product
121
+ Raises:
122
+ NotFoundError when the product is not found.
123
+ """
124
+ rsp = DescribeEntityResponse.from_json(
125
+ self.marketplace.describe_entity(Catalog="AWSMarketplace", EntityId=entity_id)
126
+ )
127
+
128
+ if not rsp.details_document:
129
+ pprint_debug_logging(log, rsp)
130
+ self._raise_error(NotFoundError, f"No such product with EntityId: \"{entity_id}\"")
131
+
132
+ return rsp.details_document
133
+
134
+ def get_product_by_name(
135
+ self, marketplace_entity_type: str, product_name: str
136
+ ) -> ProductDetailResponse:
137
+ """
138
+ Get a product detail by it's name.
139
+
140
+ Args:
141
+ marketplace_entity_type (str)
142
+ Product type of the AWS product
143
+ Example: AmiProduct
144
+ product_name (str)
145
+ Name of a product
146
+ Returns:
147
+ str: A dict of details for the first response of a product
148
+ Raises:
149
+ NotFoundError when the product is not found.
150
+ InvalidStateError when more than one product is found.
151
+ """
152
+ filter_list = [{"Name": "Name", "ValueList": [product_name]}]
153
+
154
+ entity_rsp = ListEntitiesResponse.from_json(
155
+ self.marketplace.list_entities(
156
+ Catalog="AWSMarketplace",
157
+ EntityType=marketplace_entity_type,
158
+ FilterList=filter_list,
159
+ )
160
+ )
161
+
162
+ if len(entity_rsp.entity_summary_list) == 0:
163
+ pprint_debug_logging(log, entity_rsp)
164
+ self._raise_error(NotFoundError, f"No such product with name \"{product_name}\"")
165
+
166
+ elif len(entity_rsp.entity_summary_list) > 1:
167
+ pprint_debug_logging(log, entity_rsp)
168
+ self._raise_error(InvalidStateError, f"Multiple responses found for \"{product_name}\"")
169
+
170
+ # We should only get one response based on filtering
171
+ elif hasattr(entity_rsp.entity_summary_list[0], "entity_id"):
172
+ return self.get_product_by_id(entity_rsp.entity_summary_list[0].entity_id)
173
+
174
+ self._raise_error(NotFoundError, f"No such product with name \"{product_name}\"")
175
+
176
+ def get_product_version_details(
177
+ self, entity_id: str, version_id: str
178
+ ) -> ProductVersionsResponse:
179
+ """
180
+ Get a product detail by it's name.
181
+
182
+ Args:
183
+ entity_id (str)
184
+ The Id of the entity to get version details from
185
+ version_id (str)
186
+ The version id of a product to get the details of
187
+ Returns:
188
+ ProductVersionsResponse: The details for the first response of a product
189
+ Raises:
190
+ NotFoundError when the product is not found.
191
+ """
192
+ details = self.get_product_by_id(entity_id)
193
+ self._check_product_versions(details)
194
+
195
+ for version in details.versions:
196
+ for delivery_option in version.delivery_options:
197
+ if delivery_option.id == version_id:
198
+ return version
199
+
200
+ self._raise_error(NotFoundError, f"No such version with id \"{version_id}\"")
201
+
202
+ def get_product_versions(self, entity_id: str) -> Dict[str, GroupedVersions]:
203
+ """
204
+ Get the titles, ids, and date created of all the versions of a product.
205
+
206
+ Args:
207
+ entity_id (str)
208
+ The Id of the entity to get versions from
209
+ Returns:
210
+ Dict[str, GroupedVersions]: A dictionary of versions
211
+ Raises:
212
+ NotFoundError when the product is not found.
213
+ """
214
+ details = self.get_product_by_id(entity_id)
215
+ self._check_product_versions(details)
216
+
217
+ version_ids: Dict[str, GroupedVersions] = {}
218
+
219
+ for v in details.versions:
220
+ delivery_options_list = []
221
+ ami_id_list = []
222
+ for delivery_option in v.delivery_options:
223
+ delivery_options_list.append(delivery_option)
224
+ for source in v.sources:
225
+ if isinstance(source, ProductVersionsVirtualizationSource):
226
+ ami_id_list.append(source.image)
227
+ delivery_options: GroupedVersions = {
228
+ "delivery_options": delivery_options_list,
229
+ "created_date": v.creation_date, # type: ignore
230
+ "ami_ids": ami_id_list,
231
+ }
232
+ version_ids[v.version_title] = delivery_options # type: ignore
233
+ return version_ids
234
+
235
+ def get_product_version_by_name(self, entity_id: str, version_name: str) -> DeliveryOption:
236
+ """
237
+ Get a version detail by it's name.
238
+
239
+ Args:
240
+ entity_id (str)
241
+ The Id of the entity to get version by name from
242
+ version_name (str)
243
+ A version title to get details of
244
+ Returns:
245
+ DeliveryOption: The delivery options of a version
246
+ Raises:
247
+ NotFoundError when the product is not found.
248
+ """
249
+ details = self.get_product_by_id(entity_id)
250
+ self._check_product_versions(details)
251
+
252
+ for version in details.versions:
253
+ if version.version_title == version_name:
254
+ # ATM we're not batching Delivery options so
255
+ # the first one should be the one we want.
256
+ return version.delivery_options[0]
257
+
258
+ self._raise_error(NotFoundError, f"No such version with name \"{version_name}\"")
259
+
260
+ def get_product_active_changesets(self, entity_id: str) -> List[ListChangeSet]:
261
+ """
262
+ Get the active changesets for a product.
263
+
264
+ Args:
265
+ entity_id (str)
266
+ The Id of the entity to get active changesets from
267
+ Returns:
268
+ str: A change set id
269
+ """
270
+ filter_list = [
271
+ {"Name": "EntityId", "ValueList": [entity_id]},
272
+ {"Name": "Status", "ValueList": ["APPLYING", "PREPARING"]},
273
+ ]
274
+
275
+ changeset_list = ListChangeSetsResponse.from_json(
276
+ self.marketplace.list_change_sets(Catalog="AWSMarketplace", FilterList=filter_list)
277
+ )
278
+ return changeset_list.change_set_list
279
+
280
+ def wait_active_changesets(self, entity_id: str) -> None:
281
+ """
282
+ Get the first active changeset, if there is one, and wait for it to finish.
283
+
284
+ Args:
285
+ entity_id (str)
286
+ The Id of the entity to wait for active changesets
287
+ """
288
+
289
+ def changeset_not_complete(change_set_list: List[ListChangeSet]) -> bool:
290
+ if change_set_list:
291
+ self.wait_for_changeset(change_set_list[0].id)
292
+ return True
293
+ else:
294
+ return False
295
+
296
+ r = Retrying(
297
+ stop=stop_after_attempt(self.wait_for_changeset_attempts),
298
+ retry=retry_if_result(changeset_not_complete),
299
+ )
300
+
301
+ try:
302
+ r(self.get_product_active_changesets, entity_id)
303
+ except RetryError:
304
+ self._raise_error(Timeout, f"Timed out waiting for {entity_id} to be unlocked")
305
+
306
+ def set_restrict_versions(
307
+ self, entity_id: str, marketplace_entity_type: str, delivery_option_ids: List[str]
308
+ ) -> str:
309
+ """
310
+ Restrict version(s) of a product by their id.
311
+
312
+ Args:
313
+ entity_id (str)
314
+ The Id of the entity to edit
315
+ marketplace_entity_type (str)
316
+ Product type of the AWS product
317
+ Example: AmiProduct
318
+ delivery_option_ids (List)
319
+ A list of strs of delivery options to restrict. Normally version Ids.
320
+ Returns:
321
+ str: A change set id
322
+ """
323
+ change_details = {"DeliveryOptionIds": delivery_option_ids}
324
+
325
+ rsp: ChangeSetResponse = self.marketplace.start_change_set(
326
+ Catalog="AWSMarketplace",
327
+ ChangeSet=[
328
+ {
329
+ "ChangeType": "RestrictDeliveryOptions",
330
+ "Entity": {
331
+ "Type": marketplace_entity_type + "@1.0",
332
+ "Identifier": entity_id,
333
+ },
334
+ "Details": json.dumps(change_details),
335
+ },
336
+ ],
337
+ )
338
+
339
+ pprint_debug_logging(log, rsp, "The response from the restrict version was: ")
340
+
341
+ return rsp["ChangeSetId"]
342
+
343
+ def cancel_change_set(self, change_set_id: str) -> str:
344
+ """
345
+ Cancel the publish of a new version in progress.
346
+
347
+ Args:
348
+ change_set_id (str)
349
+ A change set id to cancel
350
+ Returns:
351
+ str: A change set id
352
+ """
353
+ rsp: ChangeSetResponse = self.marketplace.cancel_change_set(
354
+ Catalog="AWSMarketplace", ChangeSetId=change_set_id
355
+ )
356
+
357
+ pprint_debug_logging(log, rsp, "The response from cancelling a changeset was: ")
358
+
359
+ return rsp["ChangeSetId"]
360
+
361
+ def check_publish_status(self, change_set_id: str) -> str:
362
+ """
363
+ Check the status of a change set.
364
+
365
+ Args:
366
+ change_set_id (str)
367
+ A change set id to check the status of
368
+ Returns:
369
+ str: Status of the publish
370
+ Raises:
371
+ InvalidStateError if the job failed
372
+ """
373
+ rsp = DescribeChangeSetReponse.from_json(
374
+ self.marketplace.describe_change_set(
375
+ Catalog="AWSMarketplace", ChangeSetId=change_set_id
376
+ )
377
+ )
378
+
379
+ status = rsp.status
380
+
381
+ log.info("Current change status is %s.", status.lower())
382
+
383
+ if status.lower() == "failed":
384
+ failure_code = rsp.failure_code
385
+ # ATM we're not batching changesets so
386
+ # the first one should be the one we want.
387
+ failure_list = rsp.change_set[0].error_details
388
+ pprint_debug_logging(log, rsp, "The response from the status was: ")
389
+ error_message = (
390
+ f"Changeset {change_set_id} failed with code {failure_code}: \n {failure_list}"
391
+ )
392
+ self._raise_error(InvalidStateError, error_message)
393
+
394
+ return rsp.status
395
+
396
+ def wait_for_changeset(self, change_set_id: str) -> None:
397
+ """
398
+ Wait until ChangeSet is complete.
399
+
400
+ Args:
401
+ change_set_id (str)
402
+ Id for the change set to wait on
403
+ Raises:
404
+ Timeout when the status doesn't change to either
405
+ 'Succeeded' or 'Failed' within the set retry time.
406
+ """
407
+
408
+ def changeset_not_complete(status: str) -> bool:
409
+ if status.lower() == "succeeded":
410
+ return False
411
+ else:
412
+ return True
413
+
414
+ r = Retrying(
415
+ wait=wait_fixed(self.wait_for_changeset_interval),
416
+ stop=stop_after_attempt(self.wait_for_changeset_attempts),
417
+ retry=retry_if_result(changeset_not_complete),
418
+ )
419
+
420
+ try:
421
+ r(self.check_publish_status, change_set_id)
422
+ except RetryError:
423
+ self._raise_error(Timeout, f"Timed out waiting for {change_set_id} to finish")
424
+
425
+ def restrict_versions(
426
+ self,
427
+ entity_id: str,
428
+ marketplace_entity_type: str,
429
+ restrict_major: Optional[int] = None,
430
+ restrict_minor: Optional[int] = 1,
431
+ ) -> List[str]:
432
+ """
433
+ Restrict the old versions of a release.
434
+
435
+ Args:
436
+ entity_id (str)
437
+ The entity id to modifiy.
438
+ marketplace_entity_type (str)
439
+ Product type of the AWS product
440
+ Example: AmiProduct
441
+ restrict_major (optional int)
442
+ How many major versions are allowed
443
+ Example: 3
444
+ restrict_minor (optional int)
445
+ how many minor versions are allowed
446
+ Example: 3
447
+ Returns:
448
+ List[str]: List of AMI ids of restricted versions
449
+ """
450
+ versions = self.get_product_versions(entity_id)
451
+ version_tree = create_version_tree(versions)
452
+
453
+ restrict_delivery_ids = []
454
+ restrict_ami_ids = []
455
+
456
+ if restrict_major and len(version_tree) > restrict_major:
457
+ major_delivery_ids, major_ami_ids, version_tree = get_restricted_major_versions(
458
+ version_tree, restrict_major
459
+ )
460
+ restrict_delivery_ids.extend(major_delivery_ids)
461
+ restrict_ami_ids.extend(major_ami_ids)
462
+
463
+ if restrict_minor:
464
+ minor_delivery_ids, minor_ami_ids, version_tree = get_restricted_minor_versions(
465
+ version_tree, restrict_minor
466
+ )
467
+ restrict_delivery_ids.extend(minor_delivery_ids)
468
+ restrict_ami_ids.extend(minor_ami_ids)
469
+
470
+ patch_delivery_ids, patch_ami_ids = get_restricted_patch_versions(version_tree)
471
+ restrict_delivery_ids.extend(patch_delivery_ids)
472
+ restrict_ami_ids.extend(patch_ami_ids)
473
+
474
+ if restrict_delivery_ids:
475
+ log.debug(f"Restricting these minor version(s) with id(s): {restrict_delivery_ids}")
476
+ change_id = self.set_restrict_versions(
477
+ entity_id, marketplace_entity_type, restrict_delivery_ids
478
+ )
479
+ self.wait_for_changeset(change_id)
480
+
481
+ return restrict_ami_ids
482
+
483
+ def publish(self, metadata: AWSVersionMetadata) -> None:
484
+ """
485
+ Add new version to an existing product.
486
+
487
+ Args:
488
+ new_version_details (VersionMapping): A model of the version mapping
489
+ """
490
+ change_set = {
491
+ "ChangeType": "AddDeliveryOptions",
492
+ "Entity": {
493
+ "Type": f"{metadata.marketplace_entity_type}@1.0",
494
+ "Identifier": metadata.destination,
495
+ },
496
+ # AWS accepts 'Details' as a JSON string.
497
+ # So we convert it here.
498
+ "DetailsDocument": metadata.version_mapping.to_json(),
499
+ }
500
+
501
+ if metadata.overwrite:
502
+ # Make a copy of the original Version Mapping to avoid overwriting settings
503
+ json_mapping = deepcopy(metadata.version_mapping)
504
+ org_version_details = self.get_product_version_by_name(
505
+ metadata.destination, metadata.version_mapping.version.version_title
506
+ )
507
+ # ATM we're not batching Delivery options so
508
+ # the first one should be the one we want.
509
+ json_mapping.delivery_options[0].id = org_version_details.id
510
+
511
+ change_set["ChangeType"] = "UpdateDeliveryOptions"
512
+ change_set["DetailsDocument"] = json_mapping.to_json()
513
+
514
+ if metadata.keepdraft or metadata.preview_only:
515
+ log.info("Sending draft version to %s.", metadata.marketplace_entity_type)
516
+ rsp: ChangeSetResponse = self.marketplace.start_change_set(
517
+ Catalog="AWSMarketplace", ChangeSet=[change_set], Intent="VALIDATE"
518
+ )
519
+ else:
520
+ log.info("Publishing new version in %s.", metadata.marketplace_entity_type)
521
+ rsp = self.marketplace.start_change_set(
522
+ Catalog="AWSMarketplace", ChangeSet=[change_set], Intent="APPLY"
523
+ )
524
+ pprint_debug_logging(log, rsp, "The response from publishing was: ")
525
+
526
+ self.wait_for_changeset(rsp["ChangeSetId"])
cloudpub/aws/utils.py ADDED
@@ -0,0 +1,143 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ import logging
3
+ from pprint import pformat
4
+ from typing import Any, Dict, List, Mapping, Tuple
5
+
6
+ import dateutil.parser
7
+ from packaging.version import InvalidVersion, Version
8
+
9
+ from cloudpub.models.aws import GroupedVersions
10
+
11
+
12
+ def create_version_tree(versions: Dict[str, GroupedVersions]) -> Dict[str, Any]:
13
+ """
14
+ Create a version sorted tree.
15
+
16
+ Args:
17
+ versions (Dict[str, GroupedVersions])
18
+ The versions to create the tree from.
19
+ Returns:
20
+ Dict[str, Any]: Dict of the version tree
21
+ """
22
+ version_tree: Dict[str, Any] = {}
23
+ for version, info in versions.items():
24
+ try:
25
+ # Try to pull version from first split
26
+ # If we can't get the version then we just don't add it to dict
27
+ version_number = Version(version.split(" ")[0])
28
+ except InvalidVersion:
29
+ continue
30
+ major = str(version_number.major)
31
+ minor = str(version_number.minor)
32
+ if not version_tree.get(major):
33
+ version_tree[major] = {}
34
+ if not version_tree[major].get(minor):
35
+ version_tree[major][minor] = {}
36
+ if info["delivery_options"][0].visibility == "Public":
37
+ version_tree[major][minor][version] = info
38
+ return version_tree
39
+
40
+
41
+ def get_restricted_major_versions(
42
+ version_tree: Dict[str, Any], restrict_major: int
43
+ ) -> Tuple[List[str], List[str], Dict[str, Any]]:
44
+ """
45
+ Get all the restricted major versions.
46
+
47
+ Args:
48
+ version_tree (Dict[str, Any])
49
+ The dict tree to pull major versions from.
50
+ restrict_major (int)
51
+ How many major versions to restrict to.
52
+ Returns:
53
+ Tuple[List[str], List[str], Dict[str, Any]]
54
+ Tuple of restricted delivery ids, restricted ami ids,
55
+ and updated version_tree
56
+ """
57
+ restrict_delivery_ids = []
58
+ restrict_ami_ids = []
59
+ version_list = sorted(version_tree.keys(), reverse=True, key=lambda x: int(x))
60
+ for v in version_list[restrict_major:]:
61
+ for s in version_tree[v].values():
62
+ for x in s.values():
63
+ delivery_options = x["delivery_options"]
64
+ restrict_delivery_ids.append(delivery_options[0].id)
65
+ restrict_ami_ids.extend(x["ami_ids"])
66
+ del version_tree[v]
67
+ return restrict_delivery_ids, restrict_ami_ids, version_tree
68
+
69
+
70
+ def get_restricted_minor_versions(
71
+ version_tree: Dict[str, Any], restrict_minor: int
72
+ ) -> Tuple[List[str], List[str], Dict[str, Any]]:
73
+ """
74
+ Get all the restricted major versions.
75
+
76
+ Args:
77
+ version_tree (Dict[str, Any])
78
+ The dict tree to pull major versions from.
79
+ restrict_minor (int)
80
+ How many minor versions to restrict to.
81
+ Returns:
82
+ Tuple[List[str], List[str], Dict[str, Any]]
83
+ Tuple of restricted delivery ids, restricted ami ids,
84
+ and updated version_tree
85
+ """
86
+ restrict_delivery_ids = []
87
+ restrict_ami_ids = []
88
+ version_major_list = list(version_tree.keys())
89
+ for major_version in version_major_list:
90
+ version_list = sorted(
91
+ version_tree[major_version].keys(), reverse=True, key=lambda x: int(x)
92
+ )
93
+ for v in version_list[restrict_minor:]:
94
+ for s in version_tree[major_version][v].values():
95
+ delivery_options = s["delivery_options"]
96
+ restrict_delivery_ids.append(delivery_options[0].id)
97
+ restrict_ami_ids.extend(s["ami_ids"])
98
+ del version_tree[major_version][v]
99
+ return restrict_delivery_ids, restrict_ami_ids, version_tree
100
+
101
+
102
+ def get_restricted_patch_versions(version_tree: Dict[str, Any]) -> Tuple[List[str], List[str]]:
103
+ """
104
+ Get all the patch versions to latest.
105
+
106
+ Args:
107
+ version_tree (Dict[str, Any])
108
+ The dict tree to pull major versions from.
109
+ Returns:
110
+ Tuple[List[str], List[str]]: Tuple of restricted delivery ids and restricted ami ids
111
+ """
112
+ restrict_delivery_ids = []
113
+ restrict_ami_ids = []
114
+ for major in version_tree.values():
115
+ for minor in major.values():
116
+ ordered_versions = sorted(
117
+ minor.values(),
118
+ key=lambda x: dateutil.parser.isoparse(x["created_date"]),
119
+ reverse=True,
120
+ )
121
+ for x in ordered_versions[1:]:
122
+ delivery_options = x["delivery_options"]
123
+ restrict_delivery_ids.append(delivery_options[0].id)
124
+ restrict_ami_ids.extend(x["ami_ids"])
125
+ return restrict_delivery_ids, restrict_ami_ids
126
+
127
+
128
+ def pprint_debug_logging(
129
+ log: logging.Logger, rsp_log: Mapping[str, Any], log_tag: str = "Response: "
130
+ ) -> None:
131
+ """
132
+ Pprint a dict into the appropriate logger.
133
+
134
+ Args:
135
+ log (Logger)
136
+ The log to report to.
137
+ rsp_log (Dict[str, Any])
138
+ The dict to add to logging.
139
+ Returns:
140
+ None
141
+ """
142
+ if log.isEnabledFor(logging.DEBUG):
143
+ log.debug("%s\n%s", log_tag, pformat(rsp_log))