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

[IMP] Cookie Validation and Retry Mechanism on 401 Error #384

Merged
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
4 changes: 1 addition & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,8 @@ jobs:
fail-fast: false
matrix:
# A list of Python versions to run the tests on
python-version: ['3.5', '3.6', '3.7', '3.8', '3.9']
python-version: ['3.6', '3.7', '3.8', '3.9']
include:
- python-version: '3.5'
TOXENV: 'py35'
- python-version: '3.6'
TOXENV: 'py36'
- python-version: '3.7'
Expand Down
156 changes: 138 additions & 18 deletions infoblox_client/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import requests
import six
import urllib3
import time
from requests import exceptions as req_exc

try:
Expand Down Expand Up @@ -49,6 +50,7 @@ def reraise_neutron_exception(func):

@functools.wraps(func)
def callee(*args, **kwargs):
"""Catches third-party exceptions and raises Infoblox exceptions"""
try:
return func(*args, **kwargs)
except req_exc.Timeout as e:
Expand All @@ -59,6 +61,47 @@ def callee(*args, **kwargs):
return callee


def retry_on_expired_cookie(func):
"""
Decorator to handle expired cookies by re-authenticating
and retrying the request.
"""
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
"""
Wrapper function to handle expired cookies by re-authenticating
and retrying the request.
"""
try:
return func(self, *args, **kwargs)
except (req_exc.HTTPError, ib_ex.InfobloxBadWAPICredential,
ib_ex.InfobloxFileDownloadFailed,
ib_ex.InfobloxFileUploadFailed) as e:
if isinstance(e, (req_exc.HTTPError,
ib_ex.InfobloxBadWAPICredential,
ib_ex.InfobloxFileDownloadFailed,
ib_ex.InfobloxFileUploadFailed)):
LOG.warning("Bad WAPI credentials or Cookie timeout.\
Re-authenticating and retrying the request.")
else:
raise

LOG.warning("Clearing cookies and re-authenticating.")
self.session.cookies.clear()
if hasattr(self, 'username') and hasattr(self, 'password'):
LOG.warning("Re-authenticating with username and password.")
self.session.auth = (self.username, self.password)
elif hasattr(self, 'cert') and hasattr(self, 'key'):
LOG.warning("Re-authenticating with client certificate.")
self.session.cert = (self.cert, self.key)
else:
LOG.warning("No valid credentials found to re-authenticate.")
raise ib_ex.InfobloxConfigException(
msg="No valid credentials found to re-authenticate.")
return func(self, *args, **kwargs)
return wrapper


class Connector(object):
"""Connector stands for interacting with Infoblox NIOS

Expand Down Expand Up @@ -92,7 +135,7 @@ def __init__(self, options):
self._urljoin = urlparse.urljoin

def _parse_options(self, options):
"""Copy needed options to self"""
"""Copy necessary options to self"""
attributes = ('host', 'wapi_version', 'username', 'password',
'cert', 'key', 'ssl_verify', 'http_request_timeout',
'max_retries', 'http_pool_connections',
Expand Down Expand Up @@ -207,6 +250,7 @@ def _validate_obj_type_or_die(obj_type, obj_type_expected=True):
@staticmethod
def _validate_authorized(response):
if response.status_code == requests.codes.UNAUTHORIZED:
LOG.debug("WAPI Response: %s", response.content)
raise ib_ex.InfobloxBadWAPICredential(response='')

@staticmethod
Expand All @@ -233,6 +277,37 @@ def _build_query_params(payload=None, return_fields=None,

return query_params

def is_cookie_expired(self):
"""
Check if the cookie is expired by comparing the expiration time
with the current time.
"""

cookie_jar = self.session.cookies
for cookie in cookie_jar:
if cookie.name == 'ibapauth':
# Parse the cookie value
cookie_value = cookie.value.strip('"')
cookie_parts = {}
for part in cookie_value.split(','):
if '=' in part:
key, value = part.split('=', 1)
cookie_parts[key] = value

# Extract ctime and timeout
ctime = int(cookie_parts.get('ctime', 0))
timeout = int(cookie_parts.get('timeout', 0))

# Calculate expiration time
expiration_time = ctime + timeout

# Get current time
current_time = int(time.time())

# Check if the cookie is expired
return current_time > expiration_time
return True # If the cookie is not found, consider it expired

def _get_request_options(self, data=None):
opts = dict(timeout=self.http_request_timeout,
headers=self.DEFAULT_HEADER,
Expand All @@ -241,15 +316,36 @@ def _get_request_options(self, data=None):
opts['data'] = jsonutils.dumps(data)
return opts

def _validate_cookie(self):
if self.is_cookie_expired():
LOG.info("Cookie expired. \
Clearing cookies and re-authenticating on the next request.")
self.session.cookies.clear()
if hasattr(self, 'username') and hasattr(self, 'password'):
LOG.info("Re-authenticating with username and password.")
self.session.auth = (self.username, self.password)
elif hasattr(self, 'cert') and hasattr(self, 'key'):
LOG.info("Re-authenticating with client certificate.")
self.session.cert = (self.cert, self.key)
else:
LOG.warning("No valid credentials found to re-authenticate.")
raise ib_ex.InfobloxConfigException(
msg="No valid credentials found to re-authenticate.")
else:
LOG.info("Using existing cookie for authentication")
self.session.auth = None # Do not re-authenticate

@staticmethod
def _parse_reply(request):
"""Tries to parse reply from NIOS.

Raises exception with content if reply is not in json format
"""
try:
LOG.debug("WAPI Response: %s", request.content)
return jsonutils.loads(request.content)
except ValueError:
LOG.error("Failed to parse reply from NIOS: %s", request.content)
raise ib_ex.InfobloxConnectionError(reason=request.content)

def _log_request(self, verb, url, opts):
Expand All @@ -261,6 +357,7 @@ def _log_request(self, verb, url, opts):
LOG.debug(*message)

@reraise_neutron_exception
@retry_on_expired_cookie
def get_object(self, obj_type, payload=None, return_fields=None,
extattrs=None, force_proxy=False, max_results=None,
paging=None):
Expand All @@ -269,7 +366,7 @@ def get_object(self, obj_type, payload=None, return_fields=None,
Some get requests like 'ipv4address' should be always
proxied to GM on Hellfire
If request is cloud and proxy is not forced yet,
then plan to do 2 request:
then plan to do two requests:
- the first one is not proxied to GM
- the second is proxied to GM

Expand All @@ -282,13 +379,13 @@ def get_object(self, obj_type, payload=None, return_fields=None,
force_proxy (bool): Set _proxy_search flag
to process requests on GM
max_results (int): Maximum number of objects to be returned.
If set to a negative number the appliance will return an error
If set to a negative number, the appliance will return an error
when the number of returned objects would exceed the setting.
The default is -1000. If this is set to a positive number,
the results will be truncated when necessary.
paging (bool): Enables paging to wapi calls if paging = True,
Paging (bool): Enables paging to wapi calls if paging = True,
it uses _max_results to set paging size of the wapi calls.
If _max_results is negative it will take paging size as 1000.
If _max_results is negative, it will take paging size as 1000.

Returns:
A list of the Infoblox objects requested
Expand Down Expand Up @@ -359,7 +456,7 @@ def _get_object(self, obj_type, url):
if self.session.cookies:
# the first 'get' or 'post' action will generate a cookie
# after that, we don't need to re-authenticate
self.session.auth = None
self._validate_cookie()
r = self.session.get(url, **opts)

self._validate_authorized(r)
Expand All @@ -371,11 +468,18 @@ def _get_object(self, obj_type, url):
return self._parse_reply(r)

@reraise_neutron_exception
@retry_on_expired_cookie
def download_file(self, url):
req_cookies = None
if self.session.cookies:
self.session.auth = None
ibapauth_cookie = self.session.cookies.get('ibapauth')
req_cookies = {'ibapauth': ibapauth_cookie}
self._validate_cookie()
cookies_dict = self.session.cookies.get_dict()
ibapauth_cookie = cookies_dict.get('ibapauth', None)
if ibapauth_cookie:
req_cookies = {'ibapauth': ibapauth_cookie}
else:
req_cookies = None

headers = {'content-type': 'application/force-download'}
r = self.session.get(url, headers=headers, cookies=req_cookies)
if r.status_code != requests.codes.ok:
Expand All @@ -384,10 +488,10 @@ def download_file(self, url):
response=response,
url=url
)

return r

@reraise_neutron_exception
@retry_on_expired_cookie
def upload_file(self, url, files):
"""Upload file to fully-qualified upload url

Expand All @@ -399,9 +503,16 @@ def upload_file(self, url, files):
Raises:
InfobloxException
"""
req_cookies = None
if self.session.cookies:
self.session.auth = None
r = self.session.post(url, files=files)
self._validate_cookie()
cookies_dict = self.session.cookies.get_dict()
ibapauth_cookie = cookies_dict.get('ibapauth', None)
if ibapauth_cookie:
req_cookies = {'ibapauth': ibapauth_cookie}
else:
req_cookies = None
r = self.session.post(url, files=files, cookies=req_cookies)
if r.status_code != requests.codes.ok:
response = utils.safe_json_load(r.content)
raise ib_ex.InfobloxFileUploadFailed(
Expand All @@ -410,20 +521,20 @@ def upload_file(self, url, files):
content=response,
code=r.status_code,
)

return r

@reraise_neutron_exception
@retry_on_expired_cookie
def create_object(self, obj_type, payload, return_fields=None):
"""Create an Infoblox object of type 'obj_type'

Args:
obj_type (str): Infoblox object type,
e.g. 'network', 'range', etc.
payload (dict): Payload with data to send
obj_type (str): Infoblox object type,
e.g. 'network', 'range', etc.
payload (dict): Payload with data to send
return_fields (list): List of fields to be returned
Returns:
The object reference of the newly create object
The object reference of the new creation object
Raises:
InfobloxException
"""
Expand All @@ -437,7 +548,7 @@ def create_object(self, obj_type, payload, return_fields=None):
if self.session.cookies:
# the first 'get' or 'post' action will generate a cookie
# after that, we don't need to re-authenticate
self.session.auth = None
self._validate_cookie()
r = self.session.post(url, **opts)

self._validate_authorized(r)
Expand Down Expand Up @@ -468,7 +579,10 @@ def _check_service_availability(self, operation, resp, ref):
code=resp.status_code)

@reraise_neutron_exception
@retry_on_expired_cookie
def call_func(self, func_name, ref, payload, return_fields=None):
if self.session.cookies:
self._validate_cookie()
query_params = self._build_query_params(return_fields=return_fields)
query_params['_function'] = func_name

Expand All @@ -493,6 +607,7 @@ def call_func(self, func_name, ref, payload, return_fields=None):
return self._parse_reply(r)

@reraise_neutron_exception
@retry_on_expired_cookie
def update_object(self, ref, payload, return_fields=None):
"""Update an Infoblox object

Expand All @@ -510,6 +625,8 @@ def update_object(self, ref, payload, return_fields=None):
opts = self._get_request_options(data=payload)
url = self._construct_url(ref, query_params)
self._log_request('put', url, opts)
if self.session.cookies:
self._validate_cookie()
r = self.session.put(url, **opts)

self._validate_authorized(r)
Expand All @@ -526,6 +643,7 @@ def update_object(self, ref, payload, return_fields=None):
return self._parse_reply(r)

@reraise_neutron_exception
@retry_on_expired_cookie
def delete_object(self, ref, delete_arguments=None):
"""Remove an Infoblox object

Expand All @@ -542,6 +660,8 @@ def delete_object(self, ref, delete_arguments=None):
delete_arguments = {}
url = self._construct_url(ref, query_params=delete_arguments)
self._log_request('delete', url, opts)
if self.session.cookies:
self._validate_cookie()
r = self.session.delete(url, **opts)

self._validate_authorized(r)
Expand Down
5 changes: 5 additions & 0 deletions infoblox_client/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,14 @@


def is_valid_ip(ip):
"""Check if the given IP address is valid."""
if not ip:
LOG.info("IP address is not provided")
return False
try:
netaddr.IPAddress(ip)
except netaddr.core.AddrFormatError:
LOG.info("Invalid IP address provided: %s", ip)
return False
return True

Expand Down
Loading
Loading