mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
[cloud][docs] Improve exception handling guidelines for AWS modules(#30745)
Better document what exceptions to handle, when and why. Describe how to handle client auth exceptions, and that AWSRetry retries on `XYZNotFound` exceptions.
This commit is contained in:
parent
8ecc7bc4a1
commit
6b9faaf90e
1 changed files with 79 additions and 26 deletions
|
@ -57,11 +57,12 @@ else:
|
|||
|
||||
The `ansible.module_utils.ec2` module and `ansible.module_utils.core.aws` modules will both
|
||||
automatically import boto3 and botocore. If boto3 is missing from the system then the variable
|
||||
HAS_BOTO3 will be set to false. Normally, this means that modules don't need to import either
|
||||
botocore or boto3 directly.
|
||||
`HAS_BOTO3` will be set to false. Normally, this means that modules don't need to import either
|
||||
botocore or boto3 directly. There is no need to check `HAS_BOTO3` when using AnsibleAWSModule
|
||||
as the module does that check.
|
||||
|
||||
If you want to import the modules anyway (for example `from botocore.exception import
|
||||
ClientError`) Wrap import statements in a try block and fail the module later using HAS_BOTO3 if
|
||||
ClientError`) Wrap import statements in a try block and fail the module later using `HAS_BOTO3` if
|
||||
the import fails.
|
||||
|
||||
#### boto
|
||||
|
@ -83,7 +84,15 @@ def main():
|
|||
#### boto3
|
||||
|
||||
```python
|
||||
from ansible.module_utils.aws.core import HAS_BOTO3
|
||||
from ansible.module_utils.aws.core import AnsibleAWSModule
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```python
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.ec2 import HAS_BOTO3
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
|
@ -94,7 +103,7 @@ def main():
|
|||
#### boto and boto3 combined
|
||||
|
||||
Ensure that you clearly document if a new parameter requires requires a specific version. Import
|
||||
boto3 at the top of the module as normal and then use the HAS_BOTO3 bool when necessary, before the
|
||||
boto3 at the top of the module as normal and then use the `HAS_BOTO3` bool when necessary, before the
|
||||
new feature.
|
||||
|
||||
```python
|
||||
|
@ -142,7 +151,8 @@ else:
|
|||
|
||||
An example of connecting to ec2 is shown below. Note that there is no `NoAuthHandlerFound`
|
||||
exception handling like in boto. Instead, an `AuthFailure` exception will be thrown when you use
|
||||
'connection'. See exception handling.
|
||||
'connection'. To ensure that authorization, parameter validation and permissions errors are all
|
||||
caught, you should catch `ClientError` and `BotoCoreError` exceptions with every boto3 connection call.
|
||||
|
||||
```python
|
||||
region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True)
|
||||
|
@ -203,7 +213,7 @@ exceptions. Call this on your exception and it will report the error together w
|
|||
use in Ansible verbose mode.
|
||||
|
||||
```python
|
||||
from ansible.module_utils.aws.core import HAS_BOTO3, AnsibleAWSModule
|
||||
from ansible.module_utils.aws.core AnsibleAWSModule
|
||||
|
||||
# Set up module parameters
|
||||
...
|
||||
|
@ -212,10 +222,11 @@ from ansible.module_utils.aws.core import HAS_BOTO3, AnsibleAWSModule
|
|||
...
|
||||
|
||||
# Make a call to AWS
|
||||
name = module.params.get['name']
|
||||
try:
|
||||
result = connection.aws_call()
|
||||
except Exception as e:
|
||||
module.fail_json_aws(e, msg="trying to do aws_call")
|
||||
result = connection.describe_frooble(FroobleName=name)
|
||||
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
|
||||
module.fail_json_aws(e, msg="Couldn't obtain frooble %s" % name)
|
||||
```
|
||||
|
||||
Note that it should normally be acceptable to catch all normal exceptions here, however if you
|
||||
|
@ -225,13 +236,16 @@ If you need to perform an action based on the error boto3 returned, use the erro
|
|||
|
||||
```python
|
||||
# Make a call to AWS
|
||||
name = module.params.get['name']
|
||||
try:
|
||||
result = connection.aws_call()
|
||||
except ClientError, e:
|
||||
if e.response['Error']['Code'] == 'NoSuchEntity':
|
||||
result = connection.describe_frooble(FroobleName=name)
|
||||
except botocore.exceptions.ClientError as e:
|
||||
if e.response['Error']['Code'] == 'FroobleNotFound':
|
||||
return None
|
||||
else:
|
||||
module.fail_json_aws(e, msg="trying to do aws_call")
|
||||
module.fail_json_aws(e, msg="Couldn't obtain frooble %s" % name)
|
||||
except botocore.exceptions.BotoCoreError as e:
|
||||
module.fail_json_aws(e, msg="Couldn't obtain frooble %s" % name)
|
||||
```
|
||||
|
||||
#### using fail_json() and avoiding ansible.module_utils.aws.core
|
||||
|
@ -240,36 +254,45 @@ Boto3 provides lots of useful information when an exception is thrown so pass th
|
|||
along with the message.
|
||||
|
||||
```python
|
||||
# Import ClientError from botocore
|
||||
from ansible.module_utils.ec2 import HAS_BOTO3
|
||||
|
||||
try:
|
||||
from botocore.exceptions import ClientError
|
||||
HAS_BOTO3 = True
|
||||
import botocore
|
||||
except ImportError:
|
||||
HAS_BOTO3 = False
|
||||
pass # caught by imported HAS_BOTO3
|
||||
|
||||
# Connect to AWS
|
||||
...
|
||||
|
||||
# Make a call to AWS
|
||||
name = module.params.get['name']
|
||||
try:
|
||||
result = connection.aws_call()
|
||||
except ClientError as e:
|
||||
module.fail_json(msg=e.message, exception=traceback.format_exc(),
|
||||
result = connection.describe_frooble(FroobleName=name)
|
||||
except botocore.exceptions.ClientError as e:
|
||||
module.fail_json(msg="Couldn't obtain frooble %s: %s" % (name, str(e)),
|
||||
exception=traceback.format_exc(),
|
||||
**camel_dict_to_snake_dict(e.response))
|
||||
```
|
||||
|
||||
Note: we use `str(e)` rather than `e.message` as the latter doesn't
|
||||
work with python3
|
||||
|
||||
If you need to perform an action based on the error boto3 returned, use the error code.
|
||||
|
||||
```python
|
||||
# Make a call to AWS
|
||||
name = module.params.get['name']
|
||||
try:
|
||||
result = connection.aws_call()
|
||||
except ClientError as e:
|
||||
if e.response['Error']['Code'] == 'NoSuchEntity':
|
||||
result = connection.describe_frooble(FroobleName=name)
|
||||
except botocore.exceptions.ClientError as e:
|
||||
if e.response['Error']['Code'] == 'FroobleNotFound':
|
||||
return None
|
||||
else:
|
||||
module.fail_json(msg=e.message, exception=traceback.format_exc(),
|
||||
module.fail_json(msg="Couldn't obtain frooble %s: %s" % (name, str(e)),
|
||||
exception=traceback.format_exc(),
|
||||
**camel_dict_to_snake_dict(e.response))
|
||||
except botocore.exceptions.BotoCoreError as e:
|
||||
module.fail_json_aws(e, msg="Couldn't obtain frooble %s" % name)
|
||||
```
|
||||
|
||||
### API throttling and pagination
|
||||
|
@ -295,7 +318,7 @@ for more details.
|
|||
The combination of these two approaches is then
|
||||
|
||||
```
|
||||
@AWSRetry.exponential_backoff(tries=5, delay=5)
|
||||
@AWSRetry.exponential_backoff(retries=5, delay=5)
|
||||
def describe_some_resource_with_backoff(client, **kwargs):
|
||||
paginator = client.get_paginator('describe_some_resource')
|
||||
return paginator.paginate(**kwargs).build_full_result()['SomeResource']
|
||||
|
@ -309,6 +332,36 @@ def describe_some_resource(client, module):
|
|||
module.fail_json_aws(e, msg="Could not describe some resource")
|
||||
```
|
||||
|
||||
If the underlying `describe_some_resources` API call throws a `ResourceNotFound`
|
||||
exception, `AWSRetry` takes this as a cue to retry until it's not thrown (this
|
||||
is so that when creating a resource, we can just retry until it exists).
|
||||
|
||||
To handle authorization failures or parameter validation errors in
|
||||
`describe_some_resource_with_backoff`, where we just want to return `None` if
|
||||
the resource doesn't exist and not retry, we need:
|
||||
|
||||
```
|
||||
@AWSRetry.exponential_backoff(retries=5, delay=5)
|
||||
def describe_some_resource_with_backoff(client, **kwargs):
|
||||
try:
|
||||
return client.describe_some_resource(ResourceName=kwargs['name'])['Resources']
|
||||
except botocore.exceptions.ClientError as e:
|
||||
if e.response['Error']['Code'] == 'ResourceNotFound':
|
||||
return None
|
||||
else:
|
||||
raise
|
||||
except BotoCoreError as e:
|
||||
raise
|
||||
|
||||
|
||||
def describe_some_resource(client, module):
|
||||
name = module.params.get['name']
|
||||
try:
|
||||
return describe_some_resource_with_backoff(client, name=name)
|
||||
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
|
||||
module.fail_json_aws(e, msg="Could not describe resource %s" % name)
|
||||
```
|
||||
|
||||
### Returning Values
|
||||
|
||||
When you make a call using boto3, you will probably get back some useful information that you
|
||||
|
|
Loading…
Reference in a new issue