toil 6.1.0a1__py3-none-any.whl → 8.0.0__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.
Files changed (193) hide show
  1. toil/__init__.py +122 -315
  2. toil/batchSystems/__init__.py +1 -0
  3. toil/batchSystems/abstractBatchSystem.py +173 -89
  4. toil/batchSystems/abstractGridEngineBatchSystem.py +272 -148
  5. toil/batchSystems/awsBatch.py +244 -135
  6. toil/batchSystems/cleanup_support.py +26 -16
  7. toil/batchSystems/contained_executor.py +31 -28
  8. toil/batchSystems/gridengine.py +86 -50
  9. toil/batchSystems/htcondor.py +166 -89
  10. toil/batchSystems/kubernetes.py +632 -382
  11. toil/batchSystems/local_support.py +20 -15
  12. toil/batchSystems/lsf.py +134 -81
  13. toil/batchSystems/lsfHelper.py +13 -11
  14. toil/batchSystems/mesos/__init__.py +41 -29
  15. toil/batchSystems/mesos/batchSystem.py +290 -151
  16. toil/batchSystems/mesos/executor.py +79 -50
  17. toil/batchSystems/mesos/test/__init__.py +31 -23
  18. toil/batchSystems/options.py +46 -28
  19. toil/batchSystems/registry.py +53 -19
  20. toil/batchSystems/singleMachine.py +296 -125
  21. toil/batchSystems/slurm.py +603 -138
  22. toil/batchSystems/torque.py +47 -33
  23. toil/bus.py +186 -76
  24. toil/common.py +664 -368
  25. toil/cwl/__init__.py +1 -1
  26. toil/cwl/cwltoil.py +1136 -483
  27. toil/cwl/utils.py +17 -22
  28. toil/deferred.py +63 -42
  29. toil/exceptions.py +5 -3
  30. toil/fileStores/__init__.py +5 -5
  31. toil/fileStores/abstractFileStore.py +140 -60
  32. toil/fileStores/cachingFileStore.py +717 -269
  33. toil/fileStores/nonCachingFileStore.py +116 -87
  34. toil/job.py +1225 -368
  35. toil/jobStores/abstractJobStore.py +416 -266
  36. toil/jobStores/aws/jobStore.py +863 -477
  37. toil/jobStores/aws/utils.py +201 -120
  38. toil/jobStores/conftest.py +3 -2
  39. toil/jobStores/fileJobStore.py +292 -154
  40. toil/jobStores/googleJobStore.py +140 -74
  41. toil/jobStores/utils.py +36 -15
  42. toil/leader.py +668 -272
  43. toil/lib/accelerators.py +115 -18
  44. toil/lib/aws/__init__.py +74 -31
  45. toil/lib/aws/ami.py +122 -87
  46. toil/lib/aws/iam.py +284 -108
  47. toil/lib/aws/s3.py +31 -0
  48. toil/lib/aws/session.py +214 -39
  49. toil/lib/aws/utils.py +287 -231
  50. toil/lib/bioio.py +13 -5
  51. toil/lib/compatibility.py +11 -6
  52. toil/lib/conversions.py +104 -47
  53. toil/lib/docker.py +131 -103
  54. toil/lib/ec2.py +361 -199
  55. toil/lib/ec2nodes.py +174 -106
  56. toil/lib/encryption/_dummy.py +5 -3
  57. toil/lib/encryption/_nacl.py +10 -6
  58. toil/lib/encryption/conftest.py +1 -0
  59. toil/lib/exceptions.py +26 -7
  60. toil/lib/expando.py +5 -3
  61. toil/lib/ftp_utils.py +217 -0
  62. toil/lib/generatedEC2Lists.py +127 -19
  63. toil/lib/humanize.py +6 -2
  64. toil/lib/integration.py +341 -0
  65. toil/lib/io.py +141 -15
  66. toil/lib/iterables.py +4 -2
  67. toil/lib/memoize.py +12 -8
  68. toil/lib/misc.py +66 -21
  69. toil/lib/objects.py +2 -2
  70. toil/lib/resources.py +68 -15
  71. toil/lib/retry.py +126 -81
  72. toil/lib/threading.py +299 -82
  73. toil/lib/throttle.py +16 -15
  74. toil/options/common.py +843 -409
  75. toil/options/cwl.py +175 -90
  76. toil/options/runner.py +50 -0
  77. toil/options/wdl.py +73 -17
  78. toil/provisioners/__init__.py +117 -46
  79. toil/provisioners/abstractProvisioner.py +332 -157
  80. toil/provisioners/aws/__init__.py +70 -33
  81. toil/provisioners/aws/awsProvisioner.py +1145 -715
  82. toil/provisioners/clusterScaler.py +541 -279
  83. toil/provisioners/gceProvisioner.py +282 -179
  84. toil/provisioners/node.py +155 -79
  85. toil/realtimeLogger.py +34 -22
  86. toil/resource.py +137 -75
  87. toil/server/app.py +128 -62
  88. toil/server/celery_app.py +3 -1
  89. toil/server/cli/wes_cwl_runner.py +82 -53
  90. toil/server/utils.py +54 -28
  91. toil/server/wes/abstract_backend.py +64 -26
  92. toil/server/wes/amazon_wes_utils.py +21 -15
  93. toil/server/wes/tasks.py +121 -63
  94. toil/server/wes/toil_backend.py +142 -107
  95. toil/server/wsgi_app.py +4 -3
  96. toil/serviceManager.py +58 -22
  97. toil/statsAndLogging.py +224 -70
  98. toil/test/__init__.py +282 -183
  99. toil/test/batchSystems/batchSystemTest.py +460 -210
  100. toil/test/batchSystems/batch_system_plugin_test.py +90 -0
  101. toil/test/batchSystems/test_gridengine.py +173 -0
  102. toil/test/batchSystems/test_lsf_helper.py +67 -58
  103. toil/test/batchSystems/test_slurm.py +110 -49
  104. toil/test/cactus/__init__.py +0 -0
  105. toil/test/cactus/test_cactus_integration.py +56 -0
  106. toil/test/cwl/cwlTest.py +496 -287
  107. toil/test/cwl/measure_default_memory.cwl +12 -0
  108. toil/test/cwl/not_run_required_input.cwl +29 -0
  109. toil/test/cwl/scatter_duplicate_outputs.cwl +40 -0
  110. toil/test/cwl/seqtk_seq.cwl +1 -1
  111. toil/test/docs/scriptsTest.py +69 -46
  112. toil/test/jobStores/jobStoreTest.py +427 -264
  113. toil/test/lib/aws/test_iam.py +118 -50
  114. toil/test/lib/aws/test_s3.py +16 -9
  115. toil/test/lib/aws/test_utils.py +5 -6
  116. toil/test/lib/dockerTest.py +118 -141
  117. toil/test/lib/test_conversions.py +113 -115
  118. toil/test/lib/test_ec2.py +58 -50
  119. toil/test/lib/test_integration.py +104 -0
  120. toil/test/lib/test_misc.py +12 -5
  121. toil/test/mesos/MesosDataStructuresTest.py +23 -10
  122. toil/test/mesos/helloWorld.py +7 -6
  123. toil/test/mesos/stress.py +25 -20
  124. toil/test/options/__init__.py +13 -0
  125. toil/test/options/options.py +42 -0
  126. toil/test/provisioners/aws/awsProvisionerTest.py +320 -150
  127. toil/test/provisioners/clusterScalerTest.py +440 -250
  128. toil/test/provisioners/clusterTest.py +166 -44
  129. toil/test/provisioners/gceProvisionerTest.py +174 -100
  130. toil/test/provisioners/provisionerTest.py +25 -13
  131. toil/test/provisioners/restartScript.py +5 -4
  132. toil/test/server/serverTest.py +188 -141
  133. toil/test/sort/restart_sort.py +137 -68
  134. toil/test/sort/sort.py +134 -66
  135. toil/test/sort/sortTest.py +91 -49
  136. toil/test/src/autoDeploymentTest.py +141 -101
  137. toil/test/src/busTest.py +20 -18
  138. toil/test/src/checkpointTest.py +8 -2
  139. toil/test/src/deferredFunctionTest.py +49 -35
  140. toil/test/src/dockerCheckTest.py +32 -24
  141. toil/test/src/environmentTest.py +135 -0
  142. toil/test/src/fileStoreTest.py +539 -272
  143. toil/test/src/helloWorldTest.py +7 -4
  144. toil/test/src/importExportFileTest.py +61 -31
  145. toil/test/src/jobDescriptionTest.py +46 -21
  146. toil/test/src/jobEncapsulationTest.py +2 -0
  147. toil/test/src/jobFileStoreTest.py +74 -50
  148. toil/test/src/jobServiceTest.py +187 -73
  149. toil/test/src/jobTest.py +121 -71
  150. toil/test/src/miscTests.py +19 -18
  151. toil/test/src/promisedRequirementTest.py +82 -36
  152. toil/test/src/promisesTest.py +7 -6
  153. toil/test/src/realtimeLoggerTest.py +10 -6
  154. toil/test/src/regularLogTest.py +71 -37
  155. toil/test/src/resourceTest.py +80 -49
  156. toil/test/src/restartDAGTest.py +36 -22
  157. toil/test/src/resumabilityTest.py +9 -2
  158. toil/test/src/retainTempDirTest.py +45 -14
  159. toil/test/src/systemTest.py +12 -8
  160. toil/test/src/threadingTest.py +44 -25
  161. toil/test/src/toilContextManagerTest.py +10 -7
  162. toil/test/src/userDefinedJobArgTypeTest.py +8 -5
  163. toil/test/src/workerTest.py +73 -23
  164. toil/test/utils/toilDebugTest.py +103 -33
  165. toil/test/utils/toilKillTest.py +4 -5
  166. toil/test/utils/utilsTest.py +245 -106
  167. toil/test/wdl/wdltoil_test.py +818 -149
  168. toil/test/wdl/wdltoil_test_kubernetes.py +91 -0
  169. toil/toilState.py +120 -35
  170. toil/utils/toilConfig.py +13 -4
  171. toil/utils/toilDebugFile.py +44 -27
  172. toil/utils/toilDebugJob.py +214 -27
  173. toil/utils/toilDestroyCluster.py +11 -6
  174. toil/utils/toilKill.py +8 -3
  175. toil/utils/toilLaunchCluster.py +256 -140
  176. toil/utils/toilMain.py +37 -16
  177. toil/utils/toilRsyncCluster.py +32 -14
  178. toil/utils/toilSshCluster.py +49 -22
  179. toil/utils/toilStats.py +356 -273
  180. toil/utils/toilStatus.py +292 -139
  181. toil/utils/toilUpdateEC2Instances.py +3 -1
  182. toil/version.py +12 -12
  183. toil/wdl/utils.py +5 -5
  184. toil/wdl/wdltoil.py +3913 -1033
  185. toil/worker.py +367 -184
  186. {toil-6.1.0a1.dist-info → toil-8.0.0.dist-info}/LICENSE +25 -0
  187. toil-8.0.0.dist-info/METADATA +173 -0
  188. toil-8.0.0.dist-info/RECORD +253 -0
  189. {toil-6.1.0a1.dist-info → toil-8.0.0.dist-info}/WHEEL +1 -1
  190. toil-6.1.0a1.dist-info/METADATA +0 -125
  191. toil-6.1.0a1.dist-info/RECORD +0 -237
  192. {toil-6.1.0a1.dist-info → toil-8.0.0.dist-info}/entry_points.txt +0 -0
  193. {toil-6.1.0a1.dist-info → toil-8.0.0.dist-info}/top_level.txt +0 -0
toil/lib/aws/ami.py CHANGED
@@ -1,19 +1,25 @@
1
1
  import json
2
2
  import logging
3
3
  import os
4
+ import time
4
5
  import urllib.request
5
- from typing import Dict, Iterator, Optional, cast
6
+ from collections.abc import Iterator
7
+ from typing import Optional, cast
6
8
  from urllib.error import HTTPError, URLError
7
9
 
8
10
  from botocore.client import BaseClient
9
- from botocore.exceptions import ClientError
11
+ from botocore.exceptions import ClientError, EndpointConnectionError
10
12
 
11
13
  from toil.lib.retry import retry
12
14
 
13
15
  logger = logging.getLogger(__name__)
14
16
 
17
+ class ReleaseFeedUnavailableError(RuntimeError):
18
+ """Raised when a Flatcar releases can't be located."""
19
+ pass
15
20
 
16
- def get_flatcar_ami(ec2_client: BaseClient, architecture: str = 'amd64') -> str:
21
+ @retry(errors=[ReleaseFeedUnavailableError])
22
+ def get_flatcar_ami(ec2_client: BaseClient, architecture: str = "amd64") -> str:
17
23
  """
18
24
  Retrieve the flatcar AMI image to use as the base for all Toil autoscaling instances.
19
25
 
@@ -24,124 +30,139 @@ def get_flatcar_ami(ec2_client: BaseClient, architecture: str = 'amd64') -> str:
24
30
  2. Official AMI from stable.release.flatcar-linux.net
25
31
  3. Search the AWS Marketplace
26
32
 
27
- If all of these sources fail, we raise an error to complain.
33
+ :raises ReleaseFeedUnavailableError: if all of these sources fail.
28
34
 
29
35
  :param ec2_client: Boto3 EC2 Client
30
36
  :param architecture: The architecture type for the new AWS machine. Can be either amd64 or arm64
31
37
  """
32
-
33
38
  # Take a user override
34
- ami = os.environ.get('TOIL_AWS_AMI')
35
- try_number = 0
36
- if not ami:
37
- logger.debug('No AMI found in TOIL_AWS_AMI; checking stable Flatcar release feed')
38
- ami = feed_flatcar_ami_release(ec2_client=ec2_client, architecture=architecture, source='stable')
39
+ ami = os.environ.get("TOIL_AWS_AMI")
39
40
  if not ami:
40
- logger.warning('No available AMI found in Flatcar release feed; checking marketplace')
41
- ami = aws_marketplace_flatcar_ami_search(ec2_client=ec2_client, architecture=architecture)
41
+ logger.debug(
42
+ "No AMI found in TOIL_AWS_AMI; checking stable Flatcar release feed"
43
+ )
44
+ ami = feed_flatcar_ami_release(
45
+ ec2_client=ec2_client, architecture=architecture, source="stable"
46
+ )
42
47
  if not ami:
43
- logger.debug('No AMI found in marketplace; checking Toil Flatcar release feed')
44
- ami = feed_flatcar_ami_release(ec2_client=ec2_client, architecture=architecture, source='toil')
48
+ logger.warning(
49
+ "No available AMI found in Flatcar release feed; checking marketplace"
50
+ )
51
+ ami = aws_marketplace_flatcar_ami_search(
52
+ ec2_client=ec2_client, architecture=architecture
53
+ )
45
54
  if not ami:
46
- logger.debug('No AMI found in Toil project feed; checking beta Flatcar release feed')
47
- ami = feed_flatcar_ami_release(ec2_client=ec2_client, architecture=architecture, source='beta')
55
+ logger.debug(
56
+ "No AMI found in Toil project feed; checking beta Flatcar release feed"
57
+ )
58
+ ami = feed_flatcar_ami_release(
59
+ ec2_client=ec2_client, architecture=architecture, source="beta"
60
+ )
48
61
  if not ami:
49
- logger.debug('No AMI found in beta Flatcar release feed; checking archived Flatcar release feed')
50
- ami = feed_flatcar_ami_release(ec2_client=ec2_client, architecture=architecture, source='archive')
62
+ logger.debug(
63
+ "No AMI found in beta Flatcar release feed; checking archived Flatcar release feed"
64
+ )
65
+ ami = feed_flatcar_ami_release(
66
+ ec2_client=ec2_client, architecture=architecture, source="archive"
67
+ )
51
68
  if not ami:
52
- logger.critical('No available Flatcar AMI in any source!')
53
- raise RuntimeError(f'Unable to fetch the latest flatcar image. Upload '
54
- f'https://stable.release.flatcar-linux.net/{architecture}-usr/current/flatcar_production_ami_image.bin.bz2 '
55
- f'to AWS as am AMI and set TOIL_AWS_AMI in the environment to its AMI ID.')
56
- logger.info('Selected Flatcar AMI: %s', ami)
69
+ logger.critical("No available Flatcar AMI in any source!")
70
+ raise ReleaseFeedUnavailableError(
71
+ f"Unable to fetch the latest flatcar image. Upload "
72
+ f"https://stable.release.flatcar-linux.net/{architecture}-usr/current/flatcar_production_ami_image.bin.bz2 "
73
+ f"to AWS as am AMI and set TOIL_AWS_AMI in the environment to its AMI ID."
74
+ )
75
+ logger.info("Selected Flatcar AMI: %s", ami)
57
76
  return ami
58
77
 
59
- @retry(errors=[HTTPError])
60
- def _fetch_flatcar_feed(architecture: str = 'amd64', source: str = 'stable') -> bytes:
78
+
79
+ def _fetch_flatcar_feed(architecture: str = "amd64", source: str = "stable") -> bytes:
61
80
  """
62
81
  Get the binary data of the Flatcar release feed for the given architecture.
63
-
82
+
64
83
  :param source: can be set to a Flatcar release channel ('stable', 'beta',
65
84
  or 'alpha'), 'archive' to check the Internet Archive for a feed,
66
85
  and 'toil' to check if the Toil project has put up a feed.
67
-
86
+
68
87
  :raises HTTPError: if the feed cannot be fetched.
69
88
  """
70
-
89
+
71
90
  # We have a few places we know to get the feed from.
72
91
  JSON_FEED_URL = {
73
- 'stable': f'https://stable.release.flatcar-linux.net/{architecture}-usr/current/flatcar_production_ami_all.json',
74
- 'beta': f'https://beta.release.flatcar-linux.net/{architecture}-usr/current/flatcar_production_ami_all.json',
75
- 'alpha': f'https://alpha.release.flatcar-linux.net/{architecture}-usr/current/flatcar_production_ami_all.json',
76
- 'archive': f'https://web.archive.org/web/20220625112618if_/https://stable.release.flatcar-linux.net/{architecture}-usr/current/flatcar_production_ami_all.json',
77
- 'toil': f'https://raw.githubusercontent.com/DataBiosphere/toil/master/contrib/flatcar/{architecture}-usr/current/flatcar_production_ami_all.json'
92
+ "stable": f"https://stable.release.flatcar-linux.net/{architecture}-usr/current/flatcar_production_ami_all.json",
93
+ "beta": f"https://beta.release.flatcar-linux.net/{architecture}-usr/current/flatcar_production_ami_all.json",
94
+ # "alpha": f"https://alpha.release.flatcar-linux.net/{architecture}-usr/current/flatcar_production_ami_all.json",
95
+ "archive": f"https://web.archive.org/web/20220625112618if_/https://stable.release.flatcar-linux.net/{architecture}-usr/current/flatcar_production_ami_all.json"
78
96
  }[source]
79
97
  return cast(bytes, urllib.request.urlopen(JSON_FEED_URL).read())
80
98
 
81
- def flatcar_release_feed_amis(region: str, architecture: str = 'amd64', source: str = 'stable') -> Iterator[str]:
99
+
100
+ def flatcar_release_feed_ami(
101
+ region: str, architecture: str = "amd64", source: str = "stable"
102
+ ) -> Optional[str]:
82
103
  """
83
104
  Yield AMI IDs for the given architecture from the Flatcar release feed.
84
-
105
+
85
106
  :param source: can be set to a Flatcar release channel ('stable', 'beta',
86
107
  or 'alpha'), 'archive' to check the Internet Archive for a feed,
87
108
  and 'toil' to check if the Toil project has put up a feed.
88
-
109
+
89
110
  Retries if the release feed cannot be fetched. If the release feed has a
90
111
  permanent error, yields nothing. If some entries in the release feed are
91
112
  unparseable, yields the others.
92
113
  """
93
-
114
+
94
115
  # If we get non-JSON content we want to retry.
95
116
  MAX_TRIES = 3
96
-
117
+
97
118
  try_number = 0
98
119
  while try_number < MAX_TRIES:
120
+ if try_number != 0:
121
+ time.sleep(1)
99
122
  try:
100
123
  feed = json.loads(_fetch_flatcar_feed(architecture, source))
101
124
  break
102
125
  except HTTPError:
103
126
  # Flatcar servers did not return the feed
104
- logger.exception(f'Could not retrieve {source} Flatcar release feed JSON')
105
- # Don't retry
106
- return
127
+ logger.exception(f"Could not retrieve {source} Flatcar release feed JSON")
128
+ # This is probably a permanent error, or at least unlikely to go away immediately.
129
+ return None
107
130
  except json.JSONDecodeError:
108
131
  # Feed is not JSON
109
- logger.exception(f'Could not decode {source} Flatcar release feed JSON')
132
+ logger.exception(f"Could not decode {source} Flatcar release feed JSON")
110
133
  # Try again
111
134
  try_number += 1
112
135
  continue
113
136
  except URLError:
114
137
  # Could be a connection timeout
115
- logger.exception(f'Failed to retrieve {source} Flatcar release feed JSON')
138
+ logger.exception(f"Failed to retrieve {source} Flatcar release feed JSON")
116
139
  # Try again
117
140
  try_number += 1
118
141
  continue
119
142
  if try_number == MAX_TRIES:
120
143
  # We could not get the JSON
121
- logger.error(f'Could not get a readable {source} Flatcar release feed JSON')
144
+ logger.error(f"Could not get a readable {source} Flatcar release feed JSON")
122
145
  # Bail on this method
123
- return
146
+ return None
124
147
 
125
- for ami_record in feed.get('amis', []):
148
+ for ami_record in feed.get("amis", []):
126
149
  # Scan the list of regions
127
- if ami_record.get('name', None) == region:
128
- # When we find ours, return the AMI ID
129
- if 'hvm' in ami_record:
130
- yield ami_record['hvm']
131
- # And stop, there should be one per region.
132
- return
150
+ if ami_record.get("name") == region:
151
+ return str(ami_record.get("hvm")) if ami_record.get("hvm") else None
133
152
  # We didn't find our region
134
- logger.warning(f'Flatcar {source} release feed does not have an image for region {region}')
135
-
136
-
153
+ logger.warning(f"Flatcar {source} release feed does not have an image for region {region}")
154
+
137
155
 
138
- @retry() # TODO: What errors do we get for timeout, JSON parse failure, etc?
139
- def feed_flatcar_ami_release(ec2_client: BaseClient, architecture: str = 'amd64', source: str = 'stable') -> Optional[str]:
156
+ def feed_flatcar_ami_release(
157
+ ec2_client: BaseClient, architecture: str = "amd64", source: str = "stable"
158
+ ) -> Optional[str]:
140
159
  """
141
160
  Check a Flatcar release feed for the latest flatcar AMI.
142
-
161
+
143
162
  Verify it's on AWS.
144
163
 
164
+ Does not raise exceptions.
165
+
145
166
  :param ec2_client: Boto3 EC2 Client
146
167
  :param architecture: The architecture type for the new AWS machine. Can be either amd64 or arm64
147
168
  :param source: can be set to a Flatcar release channel ('stable', 'beta',
@@ -152,40 +173,54 @@ def feed_flatcar_ami_release(ec2_client: BaseClient, architecture: str = 'amd64'
152
173
  # Rather than hardcode a list of AMIs by region that will die, we use
153
174
  # their JSON feed of the current ones.
154
175
 
155
- region = ec2_client._client_config.region_name # type: ignore
156
-
157
- for ami in flatcar_release_feed_amis(region, architecture, source):
158
- # verify it exists on AWS
159
- try:
160
- response = ec2_client.describe_images(Filters=[{'Name': 'image-id', 'Values': [ami]}]) # type: ignore
161
- if len(response['Images']) == 1 and response['Images'][0]['State'] == 'available':
162
- return ami
163
- else:
164
- logger.warning(f'Flatcar release feed suggests image {ami} which does not exist on AWS in {region}')
165
- except ClientError:
166
- # Sometimes we get back nonsense like:
167
- # botocore.exceptions.ClientError: An error occurred (AuthFailure) when calling the DescribeImages operation: AWS was not able to validate the provided access credentials
168
- # Don't hold that against the AMI.
169
- logger.exception(f'Unable to check if AMI {ami} exists on AWS in {region}; assuming it does')
176
+ region = ec2_client._client_config.region_name # type: ignore
177
+
178
+ ami = flatcar_release_feed_ami(region, architecture, source)
179
+ # verify it exists on AWS
180
+ try:
181
+ response = ec2_client.describe_images(Filters=[{"Name": "image-id", "Values": [ami]}]) # type: ignore
182
+ if (len(response["Images"]) == 1 and response["Images"][0]["State"] == "available"):
170
183
  return ami
184
+ else:
185
+ logger.warning(f"Flatcar release feed suggests image {ami} which does not exist on AWS in {region}")
186
+ except (ClientError, EndpointConnectionError):
187
+ # Sometimes we get back nonsense like:
188
+ # botocore.exceptions.ClientError: An error occurred (AuthFailure) when calling the DescribeImages operation: AWS was not able to validate the provided access credentials
189
+ # Don't hold that against the AMI.
190
+ logger.exception(f"Unable to check if AMI {ami} exists on AWS in {region}; assuming it does")
191
+ return ami
171
192
  # We didn't find it
172
- logger.warning(f'Flatcar release feed does not have an image for region {region} that exists on AWS')
173
- return None
193
+ logger.warning(f"Flatcar release feed does not have an image for region {region} that exists on AWS")
194
+
174
195
 
196
+ def aws_marketplace_flatcar_ami_search(
197
+ ec2_client: BaseClient, architecture: str = "amd64"
198
+ ) -> Optional[str]:
199
+ """
200
+ Query AWS for all AMI names matching ``Flatcar-stable-*`` and return the most recent one.
175
201
 
176
- @retry() # TODO: What errors do we get for timeout, JSON parse failure, etc?
177
- def aws_marketplace_flatcar_ami_search(ec2_client: BaseClient, architecture: str = 'amd64') -> Optional[str]:
178
- """Query AWS for all AMI names matching ``Flatcar-stable-*`` and return the most recent one."""
202
+ Does not raise exceptions.
203
+
204
+ :returns: An AMI name, or None if no matching AMI was found or we could not talk to AWS.
205
+ """
179
206
 
180
207
  # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#EC2.Client.describe_images
181
208
  # Possible arch choices on AWS: 'i386'|'x86_64'|'arm64'|'x86_64_mac'
182
- architecture_mapping = {'amd64': 'x86_64',
183
- 'arm64': 'arm64'}
184
- response: dict = ec2_client.describe_images(Owners=['aws-marketplace'], # type: ignore
185
- Filters=[{'Name': 'name', 'Values': ['Flatcar-stable-*']}])
186
- latest: Dict[str, str] = {'CreationDate': '0lder than atoms.'}
187
- for image in response['Images']:
188
- if image['Architecture'] == architecture_mapping[architecture] and image['State'] == 'available':
189
- if image['CreationDate'] > latest['CreationDate']:
209
+ architecture_mapping = {"amd64": "x86_64", "arm64": "arm64"}
210
+ try:
211
+ response = ec2_client.describe_images( # type: ignore[attr-defined]
212
+ Owners=["aws-marketplace"],
213
+ Filters=[{"Name": "name", "Values": ["Flatcar-stable-*"]}],
214
+ )
215
+ except (ClientError, EndpointConnectionError):
216
+ logger.exception("Unable to search AWS marketplace")
217
+ return None
218
+ latest: dict[str, str] = {"CreationDate": "0lder than atoms."}
219
+ for image in response["Images"]:
220
+ if (
221
+ image["Architecture"] == architecture_mapping[architecture]
222
+ and image["State"] == "available"
223
+ ):
224
+ if image["CreationDate"] > latest["CreationDate"]:
190
225
  latest = image
191
- return latest.get('ImageId', None)
226
+ return latest.get("ImageId", None)