Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exception message and Log Enhancement #387

Merged
merged 2 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 44 additions & 8 deletions infoblox_client/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def callee(*args, **kwargs):
try:
return func(*args, **kwargs)
except req_exc.Timeout as e:
raise ib_ex.InfobloxTimeoutError(e)
raise ib_ex.InfobloxTimeoutError(e, reason=e)
except req_exc.RequestException as e:
raise ib_ex.InfobloxConnectionError(reason=e)

Expand Down Expand Up @@ -133,6 +133,9 @@ def __init__(self, options):
self._urlencode = urlparse.urlencode
self._quote = urlparse.quote
self._urljoin = urlparse.urljoin
masked_options = utils.mask_sensitive_data(options)
LOG.debug(
"Connector initialized with options: {}".format(masked_options))

def _parse_options(self, options):
"""Copy necessary options to self"""
Expand All @@ -159,6 +162,7 @@ def _parse_options(self, options):
setattr(self, attr, self.DEFAULT_OPTIONS[attr])
elif attr not in creds:
msg = "WAPI config error. Option %s is not defined" % attr
LOG.error(msg)
raise ib_ex.InfobloxConfigException(msg=msg)

def check_creds(credentials):
Expand All @@ -174,6 +178,7 @@ def check_creds(credentials):
not check_creds(creds_cert_auth):
msg = "WAPI config error. Option either (username, password) " \
"or (cert, key) should be passed"
LOG.error(msg)
raise ib_ex.InfobloxConfigException(msg=msg)

self.wapi_url = "https://%s/wapi/v%s/" % (self.host,
Expand All @@ -182,6 +187,7 @@ def check_creds(credentials):
self.wapi_version)

def _configure_session(self):
LOG.debug("Configuring session")
self.session = requests.Session()
adapter = requests.adapters.HTTPAdapter(
pool_connections=self.http_pool_connections,
Expand All @@ -190,22 +196,31 @@ def _configure_session(self):
self.session.mount('http://', adapter)
self.session.mount('https://', adapter)
if hasattr(self, 'username') and hasattr(self, 'password'):
LOG.info("Authenticating with username and password.")
self.session.auth = (self.username, self.password)
else:
self.session.cert = (self.cert, self.key)
LOG.info("Authenticating with client certificate.")
self.session.verify = utils.try_value_to_bool(self.ssl_verify,
strict_mode=False)
LOG.debug("SSL verification is %s", self.session.verify)

if self.silent_ssl_warnings:
LOG.debug("Silencing SSL warnings")
urllib3.disable_warnings()

def _construct_url(self, relative_path, query_params=None,
extattrs=None, force_proxy=False):
"""
Construct URL for Infoblox WAPI request
"""
LOG.debug("Constructing URL for relative path: %s", relative_path)
if query_params is None:
query_params = {}
if extattrs is None:
extattrs = {}
if force_proxy:
LOG.debug("Forcing proxy search")
query_params['_proxy_search'] = 'GM'

if not relative_path or relative_path[0] == '/':
Expand All @@ -232,6 +247,7 @@ def _construct_url(self, relative_path, query_params=None,

base_url = self._urljoin(self.wapi_url,
self._quote(relative_path))
LOG.debug("Constructed URL: %s", base_url + query)
return base_url + query

@staticmethod
Expand All @@ -245,13 +261,18 @@ def _validate_obj_type_or_die(obj_type, obj_type_expected=True):
if not obj_type:
raise ValueError('NIOS object type cannot be empty.')
if obj_type_expected and '/' in obj_type:
LOG.error(
"NIOS object type cannot contain slash: {}".format(obj_type))
raise ValueError('NIOS object type cannot contain slash.')

@staticmethod
def _validate_authorized(response):
if response.status_code == requests.codes.UNAUTHORIZED:
LOG.debug("WAPI Response: %s", response.content)
raise ib_ex.InfobloxBadWAPICredential(response='')
raise ib_ex.InfobloxBadWAPICredential(
response=response.content,
content=utils.format_html(response.content),
code=response.status_code
)

@staticmethod
def _build_query_params(payload=None, return_fields=None,
Expand Down Expand Up @@ -282,7 +303,7 @@ def is_cookie_expired(self):
Check if the cookie is expired by comparing the expiration time
with the current time.
"""

LOG.debug("Validating cookie expiration")
cookie_jar = self.session.cookies
for cookie in cookie_jar:
if cookie.name == 'ibapauth':
Expand Down Expand Up @@ -411,9 +432,11 @@ def get_object(self, obj_type, payload=None, return_fields=None,
proxy_flag = self.cloud_api_enabled and force_proxy

try:
return self._handle_get_object(obj_type, query_params, extattrs,
proxy_flag)
return self._handle_get_object(
obj_type, query_params, extattrs, proxy_flag)
except req_exc.HTTPError:
LOG.warning(
"Failed on object search with proxy flag %s", proxy_flag)
# Do second get call with force_proxy if not done yet
return self._handle_get_object(obj_type, query_params,
extattrs, proxy_flag=True)
Expand Down Expand Up @@ -470,6 +493,7 @@ def _get_object(self, obj_type, url):
@reraise_neutron_exception
@retry_on_expired_cookie
def download_file(self, url):
LOG.debug("Downloading file from %s", url)
req_cookies = None
if self.session.cookies:
self._validate_cookie()
Expand All @@ -483,10 +507,14 @@ def download_file(self, url):
headers = {'content-type': 'application/force-download'}
r = self.session.get(url, headers=headers, cookies=req_cookies)
if r.status_code != requests.codes.ok:
res_content = utils.format_html(r.content)
LOG.error("Failed to download file from %s: %s", url, res_content)
response = utils.safe_json_load(r.content)
raise ib_ex.InfobloxFileDownloadFailed(
response=response,
url=url
url=url,
content=res_content,
code=r.status_code
)
return r

Expand All @@ -503,6 +531,7 @@ def upload_file(self, url, files):
Raises:
InfobloxException
"""
LOG.debug("Uploading file to %s", url)
req_cookies = None
if self.session.cookies:
self._validate_cookie()
Expand All @@ -515,10 +544,12 @@ def upload_file(self, url, files):
r = self.session.post(url, files=files, cookies=req_cookies)
if r.status_code != requests.codes.ok:
response = utils.safe_json_load(r.content)
res_content = utils.format_html(r.content)
LOG.error("Failed to upload file to %s: %s", url, res_content)
raise ib_ex.InfobloxFileUploadFailed(
response=response,
url=url,
content=response,
content=res_content,
code=r.status_code,
)
return r
Expand Down Expand Up @@ -554,6 +585,8 @@ def create_object(self, obj_type, payload, return_fields=None):
self._validate_authorized(r)

if r.status_code != requests.codes.CREATED:
LOG.error(
"Failed to create object with url %s: %s", url, r.content)
response = utils.safe_json_load(r.content)
already_assigned = 'is assigned to another network view'
if response and already_assigned in response.get('text'):
Expand Down Expand Up @@ -581,6 +614,7 @@ def _check_service_availability(self, operation, resp, ref):
@reraise_neutron_exception
@retry_on_expired_cookie
def call_func(self, func_name, ref, payload, return_fields=None):
LOG.debug("Calling function %s on object %s", func_name, ref)
if self.session.cookies:
self._validate_cookie()
query_params = self._build_query_params(return_fields=return_fields)
Expand Down Expand Up @@ -697,5 +731,7 @@ def is_cloud_wapi(wapi_version):
version_match = re.search(r'(\d+)\.(\d+)', wapi_version)
if version_match:
if int(version_match.group(1)) >= CLOUD_WAPI_MAJOR_VERSION:
LOG.debug("Cloud WAPI version detected: %s", wapi_version)
return True
LOG.debug("Non-cloud WAPI version detected: %s", wapi_version)
return False
10 changes: 6 additions & 4 deletions infoblox_client/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,12 @@ class InfobloxSearchError(InfobloxException):


class InfobloxFileDownloadFailed(InfobloxException):
message = "Unable to download file from '%(url)s'"
message = "Unable to download file from '%(url)s'" \
"\n '%(content)s' [code %(code)s]"


class InfobloxFileUploadFailed(InfobloxException):
message = "Unable to upload file to '%(url)s '%(content)s' '%(code)s'"
message = "Unable to upload file to '%(url)s '\n %(content)s' '%(code)s'"


class InfobloxCannotCreateObject(InfobloxException):
Expand Down Expand Up @@ -115,11 +116,12 @@ class InfobloxConfigException(BaseExc):

class InfobloxBadWAPICredential(InfobloxException):
message = "Infoblox IPAM is misconfigured: " \
"infoblox_username and infoblox_password are incorrect."
"infoblox_username and infoblox_password are incorrect." \
"\n %(content)s [code %(code)s]"


class InfobloxTimeoutError(InfobloxException):
message = "Connection to NIOS timed out"
message = "Connection to NIOS timed out: %(reason)s"


class InfobloxGridTemporaryUnavailable(InfobloxException):
Expand Down
6 changes: 0 additions & 6 deletions infoblox_client/object_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,12 +469,6 @@ def delete_objects_associated_with_a_record(self, name, view, delete_list):
for ib_obj in ib_objs:
self.delete_object_by_ref(ib_obj['_ref'])

def delete_all_associated_objects(self, network_view, ip, delete_list):
LOG.warning(
"DEPRECATION WARNING! Using delete_all_associated_objects() "
"is deprecated and to be removed in next releases. "
"Use unbind_name_from_record_a() instead.")

def delete_object_by_ref(self, ref):
try:
self.connector.delete_object(ref)
Expand Down
33 changes: 33 additions & 0 deletions infoblox_client/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import netaddr
import six
import re

try:
from oslo_log import log as logging
Expand Down Expand Up @@ -136,3 +137,35 @@ def paging(response, max_results):
while i < len(response):
yield response[i:i + max_results]
i = i + max_results


def mask_sensitive_data(data, sensitive_keys=None):
if sensitive_keys is None:
sensitive_keys = ['username', 'password', 'cert', 'key']

if isinstance(data, dict):
# Mask sensitive data in dictionary
return {
k: ('****' if k in sensitive_keys else v) for k, v in data.items()
}
elif isinstance(data, str):
# Mask sensitive data in string
patterns = {key: rf'(?<={key}=)[^&]*' for key in sensitive_keys}
for key, pattern in patterns.items():
data = re.sub(pattern, '****', data)
return data
return data


def format_html(html_content):
if not html_content:
return ''
# Replace HTML tags with newlines and indentation for readability
# Decode if the content is in bytes
if isinstance(html_content, bytes):
html_content = html_content.decode('utf-8')
formatted_content = html_content.replace('<', '\n<')
formatted_content = formatted_content.replace('>', '>\n')
formatted_content = '\n'.join(
line.strip() for line in formatted_content.split('\n') if line.strip())
return formatted_content
9 changes: 9 additions & 0 deletions tests/test_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,15 @@ def test_neutron_exception_is_raised_on_any_request_error(self):
def test_exception_raised_for_non_authorized(self):
response = mock.Mock()
response.status_code = requests.codes.UNAUTHORIZED
response.content = (b'<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">'
b'\n<html><head>\n<title>401 Unauthorized</title>\n'
b'</head><body>\n<h1>Unauthorized</h1>'
b'\n<p>This server could not verify that you'
b'\nare authorized to access the document'
b'\nrequested. Either you supplied the wrong'
b'\ncredentials (e.g., bad password), or your'
b'\nbrowser doesn\'t understand how to supply'
b'\nthe credentials required.</p>\n</body></html>\n')
self.assertRaises(exceptions.InfobloxBadWAPICredential,
connector.Connector._validate_authorized,
response)
Expand Down
Loading