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 +1 -0
- cloudpub/aws/__init__.py +2 -0
- cloudpub/aws/service.py +526 -0
- cloudpub/aws/utils.py +143 -0
- cloudpub/common.py +129 -0
- cloudpub/error.py +25 -0
- cloudpub/models/__init__.py +1 -0
- cloudpub/models/aws.py +1188 -0
- cloudpub/models/common.py +146 -0
- cloudpub/models/ms_azure.py +1775 -0
- cloudpub/ms_azure/__init__.py +2 -0
- cloudpub/ms_azure/service.py +745 -0
- cloudpub/ms_azure/session.py +176 -0
- cloudpub/ms_azure/utils.py +317 -0
- cloudpub/utils.py +40 -0
- cloudpub-0.9.2.dist-info/LICENSE +674 -0
- cloudpub-0.9.2.dist-info/METADATA +24 -0
- cloudpub-0.9.2.dist-info/RECORD +20 -0
- cloudpub-0.9.2.dist-info/WHEEL +5 -0
- cloudpub-0.9.2.dist-info/top_level.txt +1 -0
cloudpub/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
cloudpub/aws/__init__.py
ADDED
cloudpub/aws/service.py
ADDED
|
@@ -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))
|