Skip to content

Commit

Permalink
Added detailed error message to module output if an API call fails (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
mwester117 authored Mar 8, 2023
1 parent 7ce3237 commit 121b90c
Show file tree
Hide file tree
Showing 13 changed files with 126 additions and 223 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ Sva.Sentinelone Release Notes
.. contents:: Topics


v1.0.2
======

Release Summary
---------------

Added detailed error message to module output if an API call fails

v1.0.1
======

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ See [Ansible Using collections](https://docs.ansible.com/ansible/devel/user_guid
The module documentation can be found [here](https://svalabs.github.io/ansible-collection-sva.sentinelone/branch/main/collections/index_module.html).

## Changelog
**v1.0.2**: Added detailed error message to module output if an API call fails

**v1.0.1**: Bugfix release

**v1.0.0**: Initial release
Expand Down
5 changes: 2 additions & 3 deletions changelogs/.plugin-cache.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
objects:
role: {}
objects: {}
plugins:
become: {}
cache: {}
Expand Down Expand Up @@ -49,4 +48,4 @@ plugins:
shell: {}
strategy: {}
vars: {}
version: 1.0.0
version: 1.0.2
15 changes: 11 additions & 4 deletions changelogs/changelog.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,17 @@ releases:
changes:
bugfixes:
- 'sentinelone_policies module: When a group policy inherited from the site
scope was updated with a custom setting, all other settings were reset to the
default values. Now the inherited settings are updated by the settings passed
to the module and the other inherited settings are retained.'
release_summary: 'This is a bugfix release'
scope was updated with a custom setting, all other settings were reset to
the default values. Now the inherited settings are updated by the settings
passed to the module and the other inherited settings are retained.'
release_summary: This is a bugfix release
fragments:
- v1.0.1.yaml
release_date: '2023-01-30'
1.0.2:
changes:
release_summary: Added detailed error message to module output if an API call
fails
fragments:
- v1.0.2.yml
release_date: '2023-03-08'
10 changes: 5 additions & 5 deletions galaxy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace: "sva"
name: "sentinelone"

# The version of the collection. Must be compatible with semantic versioning
version: "1.0.1"
version: "1.0.2"

# The path to the Markdown (.md) readme file. This path is relative to the root of the collection
readme: "README.md"
Expand Down Expand Up @@ -42,16 +42,16 @@ tags:
- sentinelone_upgrade_policies

# The URL of the originating SCM repository
repository: "https://github.com/svalabs/ansible-collection-sentinelone"
repository: "https://github.com/svalabs/sva.sentinelone"

# The URL to any online docs
documentation: "https://github.com/svalabs/ansible-collection-sentinelone/blob/main/README.md"
documentation: "https://github.com/svalabs/sva.sentinelone/blob/main/README.md"

# The URL to the homepage of the collection/project
homepage: "https://github.com/svalabs/ansible-collection-sentinelone"
homepage: "https://github.com/svalabs/sva.sentinelone"

# The URL to the collection issue tracker
issues: "https://github.com/svalabs/ansible-collection-sentinelone/issues"
issues: "https://github.com/svalabs/sva.sentinelone/issues"

# A list of file glob-like patterns used to filter any files or directories that should not be included in the build
# artifact. A pattern is matched from the relative path of the file or directory of the collection directory. This
Expand Down
107 changes: 49 additions & 58 deletions plugins/module_utils/sentinelone/sentinelone_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@
import copy
import time
from ansible.module_utils.six.moves.urllib.parse import quote_plus
from ansible.module_utils.six import wraps
import ansible.module_utils.six.moves.urllib.error as urllib_error

from ansible.module_utils.urls import open_url
from ansible.module_utils.urls import fetch_url

lib_imp_errors = {'lib_imp_err': None}
try:
Expand All @@ -33,33 +32,6 @@
lib_imp_errors['lib_imp_err'] = traceback.format_exc()


def api_retry(func):
"""Retry decorator"""

retries = 3
retry_pause = 3

@wraps(func)
def retried(*args, **kwargs):
retry_count = 0
ret = None
while True:
retry_count += 1
try:
ret = func(*args, **kwargs)
except Exception as err:
if retry_count == retries:
raise err
else:
pass
if ret:
break
time.sleep(retry_pause)
return ret

return retried


class SentineloneBase:
def __init__(self, module: AnsibleModule):
"""
Expand Down Expand Up @@ -113,7 +85,6 @@ def __init__(self, module: AnsibleModule):

self.module = module

@api_retry
def api_call(self, module: AnsibleModule, api_endpoint: str, http_method: str = "get", **kwargs):
"""
Queries api_endpoint. if no http_method is passed a get request is performed. api_endpoint is mandatory
Expand All @@ -124,16 +95,23 @@ def api_call(self, module: AnsibleModule, api_endpoint: str, http_method: str =
:type api_endpoint: str
:param http_method: HTTP query method. Default is GET but POST, PUT, DELETE, etc. is supported as well
:type http_method: str
:param kwargs: You can pass custom headers or custom body.
If custom headers is not set default vaules will apply and should be sufficient.
If body is not passed body is empty
:type kwargs: dict
:param kwargs: See below
:Keyword Arguments:
* *headers* (dict) --
You can pass custom headers or custom body.
If custom headers is not set default vaules will apply and should be sufficient.
* *body* (dict) --
If body is not passed body is empty
* *error_msg* (str) --
Start of error message in case of a failed API call
:return: Returnes parsed json response. Type of return value depends on the data returned by the API.
Usually dictionary
:rtype: dict
"""

validate_cert = True
retries = 3
retry_pause = 3

headers = {}

if not kwargs.get("headers", {}):
Expand All @@ -143,17 +121,38 @@ def api_call(self, module: AnsibleModule, api_endpoint: str, http_method: str =

body = kwargs.get("body", {})

if body:
body_json = json.dumps(body)
response_raw = open_url(api_endpoint, headers=headers, data=body_json, method=http_method,
validate_certs=validate_cert)
else:
response_raw = open_url(api_endpoint, headers=headers, method=http_method, validate_certs=validate_cert)
error_msg = kwargs.get("error_msg", "API call failed.")

retry_count = 0
try:
response = json.loads(response_raw.read().decode('utf-8'))
while True:
retry_count += 1
try:
if body:
body_json = json.dumps(body)
response_raw, response_info = fetch_url(module, api_endpoint, headers=headers, data=body_json,
method=http_method)
else:
response_raw, response_info = fetch_url(module, api_endpoint, headers=headers,
method=http_method)
status_code = response_info['status']
if status_code >= 400:
response_unparsed = response_info['body'].decode('utf-8')
response = json.loads(response_unparsed)
raise response_raw
else:
response = json.loads(response_raw.read().decode('utf-8'))
break
except Exception as err:
if retry_count == retries:
raise err

time.sleep(retry_pause)
except json.decoder.JSONDecodeError as err:
module.fail_json(msg=f"API response is no valid JSON. Error: {str(err)}")
except urllib_error.HTTPError as err:
module.fail_json(
msg=f"{error_msg} Status code: {err.code} {err.reason}. Response body: {response_unparsed}")

return response

Expand All @@ -168,10 +167,8 @@ def get_account_obj(self, module: AnsibleModule):
"""

api_url = f"{self.api_endpoint_accounts}?states=active"
try:
response = self.api_call(module, api_url)
except urllib_error.HTTPError as err:
module.fail_json(msg=f"Failed to get account id. API response was {str(err)}.")
error_msg = "Failed to get account"
response = self.api_call(module, api_url, error_msg=error_msg)

if response["pagination"]["totalItems"] == 1:
return response["data"][0]
Expand All @@ -194,10 +191,8 @@ def get_site(self, site_name: str, module: AnsibleModule):
"""

api_url = f"{self.api_endpoint_sites}?name={quote_plus(site_name)}&state=active"
try:
response = self.api_call(module, api_url)
except urllib_error.HTTPError as err:
module.fail_json(msg=f"Failed to get site. API response was {str(err)}.")
error_msg = "Failed to get site."
response = self.api_call(module, api_url, error_msg=error_msg)

if response["pagination"]["totalItems"] == 1:
site_obj = response["data"]["sites"][0]
Expand All @@ -222,10 +217,8 @@ def get_group_ids_names(self, group_names: list, module: AnsibleModule):
group_ids_names = []
for group_name in group_names:
api_url = f"{self.api_endpoint_groups}?name={quote_plus(group_name)}&siteIds={quote_plus(self.site_id)}"
try:
response = self.api_call(module, api_url)
except urllib_error.HTTPError as err:
module.fail_json(msg=f"Failed to get group id(s). API response was {str(err)}.")
error_msg = f"Failed to get group {group_name}."
response = self.api_call(module, api_url, error_msg=error_msg)

if response["pagination"]["totalItems"] == 1:
group_id = response["data"][0]["id"]
Expand All @@ -248,10 +241,8 @@ def get_current_filter(self, filter_name: str, module: AnsibleModule):
"""

api_url = f"{self.api_endpoint_filters}?siteIds={self.site_id}&query={quote_plus(filter_name)}"
try:
response = self.api_call(module, api_url)
except urllib_error.HTTPError as err:
module.fail_json(msg=f"Failed to get filters from API. API response was {str(err)}.")
error_msg = "Failed to get filters from API."
response = self.api_call(module, api_url, error_msg=error_msg)

# API parameter "query" also matches substring. Making sure only the exactly matching element is returned
filtered_response = list(filter(lambda filterobj: filterobj['name'] == filter_name, response['data']))
Expand Down
24 changes: 6 additions & 18 deletions plugins/modules/sentinelone_config_overrides.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,6 @@
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible_collections.sva.sentinelone.plugins.module_utils.sentinelone.sentinelone_base import SentineloneBase, lib_imp_errors
from ansible.module_utils.six.moves.urllib.parse import quote_plus
import ansible.module_utils.six.moves.urllib.error as urllib_error
import copy


Expand Down Expand Up @@ -289,10 +288,7 @@ def get_current_config_override(self, module: AnsibleModule):

query_uri = '&'.join(query_options)
api_url = f"{self.api_endpoint_config_overrides}?{query_uri}"
try:
response = self.api_call(module, api_url)
except urllib_error.HTTPError as err:
module.fail_json(msg=f"{error_msg} API response was {str(err)}.")
response = self.api_call(module, api_url, error_msg=error_msg)

response_data = response['data']
count_config_overrides = len(response_data)
Expand All @@ -318,12 +314,10 @@ def delete_config_override(self, current_config_override_id: str, module: Ansibl
"""

if current_config_override_id:
# API call to delete the current config override which is currently set. Can be used on site or group level
# API call to delete the config override which is currently set. Can be used on site or group level
api_url = f"{self.api_endpoint_config_overrides}/{current_config_override_id}"
try:
response = self.api_call(module, api_url, "DELETE")
except urllib_error.HTTPError as err:
module.fail_json(msg=f"Failed to delete config override. API response was {str(err)}.")
error_msg = "Failed to delete config override."
response = self.api_call(module, api_url, "DELETE", error_msg=error_msg)

if not response['data']['success']:
module.fail_json(msg=("Error in delete_config_override: Config override should have been deleted via "
Expand All @@ -346,14 +340,8 @@ def create_config_override(self, create_body: dict, module: AnsibleModule):
"""

api_url = self.api_endpoint_config_overrides
try:
response = self.api_call(module, api_url, "POST", body=create_body)
except urllib_error.HTTPError as err:
if err.msg == "BAD REQUEST":
module.fail_json(msg=(f"Failed to create config override. API response was {str(err)}. "
f"Check the config_override parameter you passed to the module."))
else:
module.fail_json(msg=f"Failed to create config override. API response was {str(err)}.")
error_msg = "Failed to create config override."
response = self.api_call(module, api_url, "POST", body=create_body, error_msg=error_msg)

if not response['data']:
module.fail_json(msg=("Error in create_config_override: config override should have been created via API "
Expand Down
27 changes: 6 additions & 21 deletions plugins/modules/sentinelone_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@

from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible_collections.sva.sentinelone.plugins.module_utils.sentinelone.sentinelone_base import SentineloneBase, lib_imp_errors
import ansible.module_utils.six.moves.urllib.error as urllib_error


class SentineloneFilter(SentineloneBase):
Expand Down Expand Up @@ -192,14 +191,8 @@ def create_filter(self, module: AnsibleModule):

api_url = self.api_endpoint_filters
create_body = self.get_create_body()
try:
response = self.api_call(module, api_url, "POST", body=create_body)
except urllib_error.HTTPError as err:
if err.msg == "BAD REQUEST":
module.fail_json(msg=(f"Failed to create filter. API response was {str(err)}. "
f"Check the filterFields you passed to the module."))
else:
module.fail_json(msg=f"Failed to create filter. API response was {str(err)}.")
error_msg = "Failed to create filter."
response = self.api_call(module, api_url, "POST", body=create_body, error_msg=error_msg)

if not response['data']:
module.fail_json(msg=("Error in create_filter: filter should have been created via API "
Expand All @@ -218,10 +211,8 @@ def delete_filter(self, module: AnsibleModule):
"""

api_url = f"{self.api_endpoint_filters}/{self.current_filter_id}"
try:
response = self.api_call(module, api_url, "DELETE")
except urllib_error.HTTPError as err:
module.fail_json(msg=f"Failed to delete filter. API response was {str(err)}.")
error_msg = "Failed to delete filter."
response = self.api_call(module, api_url, "DELETE", error_msg=error_msg)

if not response['data']['success']:
module.fail_json(msg=("Error in delete_filter: Filter should have been deleted via API "
Expand All @@ -241,14 +232,8 @@ def update_filter(self, module: AnsibleModule):

api_url = f"{self.api_endpoint_filters}/{self.current_filter_id}"
update_body = self.get_update_body()
try:
response = self.api_call(module, api_url, "PUT", body=update_body)
except urllib_error.HTTPError as err:
if err.msg == "BAD REQUEST":
module.fail_json(msg=(f"Failed to update filter. API response was {str(err)}. "
f"Check the filterFields you passed to the module."))
else:
module.fail_json(msg=f"Failed to update filter. API response was {str(err)}.")
error_msg = "Failed to update filter."
response = self.api_call(module, api_url, "PUT", body=update_body, error_msg=error_msg)

if not response['data']:
module.fail_json(msg=("Error in update_filter: Filter should have been updated via API "
Expand Down
Loading

0 comments on commit 121b90c

Please sign in to comment.