@ -19,9 +19,11 @@ import threading
from ansible import constants as C
from ansible import constants as C
from ansible . errors import AnsibleError
from ansible . errors import AnsibleError
from ansible . galaxy . user_agent import user_agent
from ansible . galaxy . user_agent import user_agent
from ansible . module_utils . api import retry_with_delays_and_condition
from ansible . module_utils . api import generate_jittered_backoff
from ansible . module_utils . six import string_types
from ansible . module_utils . six import string_types
from ansible . module_utils . six . moves . urllib . error import HTTPError
from ansible . module_utils . six . moves . urllib . error import HTTPError
from ansible . module_utils . six . moves . urllib . parse import quote as urlquote , urlencode , urlparse
from ansible . module_utils . six . moves . urllib . parse import quote as urlquote , urlencode , urlparse , parse_qs , urljoin
from ansible . module_utils . _text import to_bytes , to_native , to_text
from ansible . module_utils . _text import to_bytes , to_native , to_text
from ansible . module_utils . urls import open_url , prepare_multipart
from ansible . module_utils . urls import open_url , prepare_multipart
from ansible . utils . display import Display
from ansible . utils . display import Display
@ -36,6 +38,11 @@ except ImportError:
display = Display ( )
display = Display ( )
_CACHE_LOCK = threading . Lock ( )
_CACHE_LOCK = threading . Lock ( )
COLLECTION_PAGE_SIZE = 100
RETRY_HTTP_ERROR_CODES = [ # TODO: Allow user-configuration
429 , # Too Many Requests
520 , # Galaxy rate limit error code (Cloudflare unknown error)
]
def cache_lock ( func ) :
def cache_lock ( func ) :
@ -46,6 +53,13 @@ def cache_lock(func):
return wrapped
return wrapped
def is_rate_limit_exception ( exception ) :
# Note: cloud.redhat.com masks rate limit errors with 403 (Forbidden) error codes.
# Since 403 could reflect the actual problem (such as an expired token), we should
# not retry by default.
return isinstance ( exception , GalaxyError ) and exception . http_code in RETRY_HTTP_ERROR_CODES
def g_connect ( versions ) :
def g_connect ( versions ) :
"""
"""
Wrapper to lazily initialize connection info to Galaxy and verify the API versions required are available on the
Wrapper to lazily initialize connection info to Galaxy and verify the API versions required are available on the
@ -309,10 +323,15 @@ class GalaxyAPI:
# Calling g_connect will populate self._available_api_versions
# Calling g_connect will populate self._available_api_versions
return self . _available_api_versions
return self . _available_api_versions
@retry_with_delays_and_condition (
backoff_iterator = generate_jittered_backoff ( retries = 6 , delay_base = 2 , delay_threshold = 40 ) ,
should_retry_error = is_rate_limit_exception
)
def _call_galaxy ( self , url , args = None , headers = None , method = None , auth_required = False , error_context_msg = None ,
def _call_galaxy ( self , url , args = None , headers = None , method = None , auth_required = False , error_context_msg = None ,
cache = False ) :
cache = False ) :
url_info = urlparse ( url )
url_info = urlparse ( url )
cache_id = get_cache_id ( url )
cache_id = get_cache_id ( url )
query = parse_qs ( url_info . query )
if cache and self . _cache :
if cache and self . _cache :
server_cache = self . _cache . setdefault ( cache_id , { } )
server_cache = self . _cache . setdefault ( cache_id , { } )
iso_datetime_format = ' % Y- % m- %d T % H: % M: % SZ '
iso_datetime_format = ' % Y- % m- %d T % H: % M: % SZ '
@ -322,7 +341,8 @@ class GalaxyAPI:
expires = datetime . datetime . strptime ( server_cache [ url_info . path ] [ ' expires ' ] , iso_datetime_format )
expires = datetime . datetime . strptime ( server_cache [ url_info . path ] [ ' expires ' ] , iso_datetime_format )
valid = datetime . datetime . utcnow ( ) < expires
valid = datetime . datetime . utcnow ( ) < expires
if valid and not url_info . query :
is_paginated_url = ' page ' in query or ' offset ' in query
if valid and not is_paginated_url :
# Got a hit on the cache and we aren't getting a paginated response
# Got a hit on the cache and we aren't getting a paginated response
path_cache = server_cache [ url_info . path ]
path_cache = server_cache [ url_info . path ]
if path_cache . get ( ' paginated ' ) :
if path_cache . get ( ' paginated ' ) :
@ -342,7 +362,7 @@ class GalaxyAPI:
return res
return res
elif not url_info. query :
elif not is_paginated_url :
# The cache entry had expired or does not exist, start a new blank entry to be filled later.
# The cache entry had expired or does not exist, start a new blank entry to be filled later.
expires = datetime . datetime . utcnow ( )
expires = datetime . datetime . utcnow ( )
expires + = datetime . timedelta ( days = 1 )
expires + = datetime . timedelta ( days = 1 )
@ -781,7 +801,8 @@ class GalaxyAPI:
api_path = self . available_api_versions [ ' v2 ' ]
api_path = self . available_api_versions [ ' v2 ' ]
pagination_path = [ ' next ' ]
pagination_path = [ ' next ' ]
versions_url = _urljoin ( self . api_server , api_path , ' collections ' , namespace , name , ' versions ' , ' / ' )
page_size_name = ' limit ' if ' v3 ' in self . available_api_versions else ' page_size '
versions_url = _urljoin ( self . api_server , api_path , ' collections ' , namespace , name , ' versions ' , ' /? %s = %d ' % ( page_size_name , COLLECTION_PAGE_SIZE ) )
versions_url_info = urlparse ( versions_url )
versions_url_info = urlparse ( versions_url )
# We should only rely on the cache if the collection has not changed. This may slow things down but it ensures
# We should only rely on the cache if the collection has not changed. This may slow things down but it ensures
@ -838,6 +859,9 @@ class GalaxyAPI:
elif relative_link :
elif relative_link :
# TODO: This assumes the pagination result is relative to the root server. Will need to be verified
# TODO: This assumes the pagination result is relative to the root server. Will need to be verified
# with someone who knows the AH API.
# with someone who knows the AH API.
# Remove the query string from the versions_url to use the next_link's query
versions_url = urljoin ( versions_url , urlparse ( versions_url ) . path )
next_link = versions_url . replace ( versions_url_info . path , next_link )
next_link = versions_url . replace ( versions_url_info . path , next_link )
data = self . _call_galaxy ( to_native ( next_link , errors = ' surrogate_or_strict ' ) ,
data = self . _call_galaxy ( to_native ( next_link , errors = ' surrogate_or_strict ' ) ,