2020-01-17 18:21:42 +03:00
from __future__ import absolute_import , division , print_function
__metaclass__ = type
2020-02-05 21:24:46 +03:00
from ansible . module_utils . basic import AnsibleModule , env_fallback
2020-01-17 18:21:42 +03:00
from ansible . module_utils . urls import Request , SSLValidationError , ConnectionError
2020-02-04 19:50:20 +03:00
from ansible . module_utils . six import PY2
2020-02-07 18:31:43 +03:00
from ansible . module_utils . six . moves import StringIO
2020-01-17 18:21:42 +03:00
from ansible . module_utils . six . moves . urllib . parse import urlparse , urlencode
from ansible . module_utils . six . moves . urllib . error import HTTPError
from ansible . module_utils . six . moves . http_cookiejar import CookieJar
2020-02-08 15:24:40 +03:00
from ansible . module_utils . six . moves . configparser import ConfigParser , NoOptionError
2020-01-28 05:14:55 +03:00
from socket import gethostbyname
2020-01-17 18:21:42 +03:00
import re
from json import loads , dumps
2020-02-04 19:50:20 +03:00
from os . path import isfile , expanduser , split , join , exists , isdir
2020-01-28 05:14:55 +03:00
from os import access , R_OK , getcwd
2020-02-07 23:35:23 +03:00
from distutils . util import strtobool
2020-02-05 21:24:46 +03:00
try :
import yaml
HAS_YAML = True
except ImportError :
HAS_YAML = False
2020-01-28 05:14:55 +03:00
class ConfigFileException ( Exception ) :
pass
2020-01-17 18:21:42 +03:00
2020-01-29 00:56:37 +03:00
class ItemNotDefined ( Exception ) :
pass
2020-01-17 18:21:42 +03:00
class TowerModule ( AnsibleModule ) :
url = None
honorred_settings = [ ' host ' , ' username ' , ' password ' , ' verify_ssl ' , ' oauth_token ' ]
host = ' 127.0.0.1 '
username = None
password = None
verify_ssl = True
oauth_token = None
oauth_token_id = None
session = None
cookie_jar = CookieJar ( )
authenticated = False
json_output = { ' changed ' : False }
2020-01-28 05:14:55 +03:00
config_name = ' tower_cli.cfg '
2020-01-17 18:21:42 +03:00
def __init__ ( self , argument_spec , * * kwargs ) :
args = dict (
tower_host = dict ( required = False , fallback = ( env_fallback , [ ' TOWER_HOST ' ] ) ) ,
tower_username = dict ( required = False , fallback = ( env_fallback , [ ' TOWER_USERNAME ' ] ) ) ,
tower_password = dict ( no_log = True , required = False , fallback = ( env_fallback , [ ' TOWER_PASSWORD ' ] ) ) ,
validate_certs = dict ( type = ' bool ' , aliases = [ ' tower_verify_ssl ' ] , required = False , fallback = ( env_fallback , [ ' TOWER_VERIFY_SSL ' ] ) ) ,
2020-01-21 22:44:19 +03:00
tower_oauthtoken = dict ( type = ' str ' , no_log = True , required = False , fallback = ( env_fallback , [ ' TOWER_OAUTH_TOKEN ' ] ) ) ,
2020-01-17 18:21:42 +03:00
tower_config_file = dict ( type = ' path ' , required = False , default = None ) ,
)
args . update ( argument_spec )
kwargs [ ' supports_check_mode ' ] = True
2020-02-05 20:46:30 +03:00
# We have to take off mutually_exclusive_if in order to init with Ansible
mutually_exclusive_if = kwargs . pop ( ' mutually_exclusive_if ' , None )
2020-01-17 18:21:42 +03:00
super ( TowerModule , self ) . __init__ ( argument_spec = args , * * kwargs )
2020-02-05 21:24:46 +03:00
2020-02-05 20:46:30 +03:00
# Eventually, we would like to push this as a feature to Ansible core for others to use...
# Test mutually_exclusive if
if mutually_exclusive_if :
for ( var_name , var_value , exclusive_names ) in mutually_exclusive_if :
if self . params . get ( var_name ) == var_value :
for excluded_param_name in exclusive_names :
2020-02-05 21:24:46 +03:00
if self . params . get ( excluded_param_name ) is not None :
self . fail_json ( msg = ' Arguments {0} can not be set if source is {1} ' . format ( ' , ' . join ( exclusive_names ) , var_value ) )
2020-01-17 18:21:42 +03:00
2020-01-28 05:14:55 +03:00
self . load_config_files ( )
2020-01-17 18:21:42 +03:00
2020-01-28 05:14:55 +03:00
# Parameters specified on command line will override settings in any config
2020-01-17 18:21:42 +03:00
if self . params . get ( ' tower_host ' ) :
self . host = self . params . get ( ' tower_host ' )
if self . params . get ( ' tower_username ' ) :
self . username = self . params . get ( ' tower_username ' )
if self . params . get ( ' tower_password ' ) :
self . password = self . params . get ( ' tower_password ' )
if self . params . get ( ' validate_certs ' ) is not None :
self . verify_ssl = self . params . get ( ' validate_certs ' )
if self . params . get ( ' tower_oauthtoken ' ) :
self . oauth_token = self . params . get ( ' tower_oauthtoken ' )
# Perform some basic validation
if not re . match ( ' ^https { 0,1}:// ' , self . host ) :
self . host = " https:// {0} " . format ( self . host )
# Try to parse the hostname as a url
try :
self . url = urlparse ( self . host )
except Exception as e :
self . fail_json ( msg = " Unable to parse tower_host as a URL ( {1} ): {0} " . format ( self . host , e ) )
# Try to resolve the hostname
hostname = self . url . netloc . split ( ' : ' ) [ 0 ]
try :
gethostbyname ( hostname )
except Exception as e :
self . fail_json ( msg = " Unable to resolve tower_host ( {1} ): {0} " . format ( hostname , e ) )
2020-02-07 20:38:28 +03:00
self . session = Request ( cookies = CookieJar ( ) , validate_certs = self . verify_ssl )
2020-01-17 18:21:42 +03:00
2020-01-28 05:14:55 +03:00
def load_config_files ( self ) :
# Load configs like TowerCLI would have from least import to most
2020-02-04 19:50:20 +03:00
config_files = [ ' /etc/tower/tower_cli.cfg ' , join ( expanduser ( " ~ " ) , " . {0} " . format ( self . config_name ) ) ]
2020-01-28 05:14:55 +03:00
local_dir = getcwd ( )
config_files . append ( join ( local_dir , self . config_name ) )
while split ( local_dir ) [ 1 ] :
local_dir = split ( local_dir ) [ 0 ]
2020-02-04 19:50:20 +03:00
config_files . insert ( 2 , join ( local_dir , " . {0} " . format ( self . config_name ) ) )
2020-01-28 05:14:55 +03:00
for config_file in config_files :
2020-02-04 19:50:20 +03:00
if exists ( config_file ) and not isdir ( config_file ) :
# Only throw a formatting error if the file exists and is not a directory
try :
self . load_config ( config_file )
except ConfigFileException :
self . fail_json ( ' The config file {0} is not properly formatted ' . format ( config_file ) )
2020-01-28 05:14:55 +03:00
# If we have a specified tower config, load it
if self . params . get ( ' tower_config_file ' ) :
try :
self . load_config ( self . params . get ( ' tower_config_file ' ) )
except ConfigFileException as cfe :
# Since we were told specifically to load this we want to fail if we have an error
self . fail_json ( msg = cfe )
2020-01-17 18:21:42 +03:00
def load_config ( self , config_path ) :
# Validate the config file is an actual file
if not isfile ( config_path ) :
2020-01-28 05:14:55 +03:00
raise ConfigFileException ( ' The specified config file does not exist ' )
2020-01-17 18:21:42 +03:00
if not access ( config_path , R_OK ) :
2020-01-28 05:14:55 +03:00
raise ConfigFileException ( " The specified config file can not be read " )
2020-01-17 18:21:42 +03:00
2020-02-08 15:24:40 +03:00
# Read in the file contents:
with open ( config_path , ' r ' ) as f :
config_string = f . read ( )
# First try to yaml load the content (which will also load json)
2020-02-06 19:07:19 +03:00
try :
2020-02-08 15:24:40 +03:00
config_data = yaml . load ( config_string , Loader = yaml . SafeLoader )
# If this is an actual ini file, yaml will return the whole thing as a string instead of a dict
if type ( config_data ) is not dict :
raise AssertionError ( " The yaml config file is not properly formatted as a dict. " )
except ( AttributeError , yaml . YAMLError , AssertionError ) :
# TowerCLI used to support a config file with a missing [general] section by prepending it if missing
if ' [general] ' not in config_string :
config_string = ' [general] {0} ' . format ( config_string )
config = ConfigParser ( )
2020-01-17 18:21:42 +03:00
try :
2020-02-08 15:24:40 +03:00
placeholder_file = StringIO ( config_string )
# py2 ConfigParser has readfp, that has been deprecated in favor of read_file in py3
# This "if" removes the deprecation warning
if hasattr ( config , ' read_file ' ) :
config . read_file ( placeholder_file )
else :
config . readfp ( placeholder_file )
# If we made it here then we have values from reading the ini file, so lets pull them out into a dict
config_data = { }
for honorred_setting in self . honorred_settings :
try :
config_data [ honorred_setting ] = config . get ( ' general ' , honorred_setting )
except ( NoOptionError ) :
pass
except Exception as e :
raise ConfigFileException ( " An unknown exception occured trying to ini load config file: {0} " . format ( e ) )
except Exception as e :
raise ConfigFileException ( " An unknown exception occured trying to load config file: {0} " . format ( e ) )
# If we made it here, we have a dict which has values in it from our config, any final settings logic can be performed here
for honorred_setting in self . honorred_settings :
if honorred_setting in config_data :
# Veriffy SSL must be a boolean
2020-02-07 23:35:23 +03:00
if honorred_setting == ' verify_ssl ' :
2020-02-08 15:24:40 +03:00
setattr ( self , honorred_setting , strtobool ( config_data [ honorred_setting ] ) )
2020-02-07 23:35:23 +03:00
else :
2020-02-08 15:24:40 +03:00
setattr ( self , honorred_setting , config_data [ honorred_setting ] )
2020-01-17 18:21:42 +03:00
2020-01-24 19:30:16 +03:00
def head_endpoint ( self , endpoint , * args , * * kwargs ) :
return self . make_request ( ' HEAD ' , endpoint , * * kwargs )
2020-01-17 18:21:42 +03:00
def get_endpoint ( self , endpoint , * args , * * kwargs ) :
return self . make_request ( ' GET ' , endpoint , * * kwargs )
def patch_endpoint ( self , endpoint , * args , * * kwargs ) :
2020-01-17 23:44:07 +03:00
# Handle check mode
if self . check_mode :
self . json_output [ ' changed ' ] = True
self . exit_json ( * * self . json_output )
2020-01-17 18:21:42 +03:00
return self . make_request ( ' PATCH ' , endpoint , * * kwargs )
2020-01-29 00:56:37 +03:00
def post_endpoint ( self , endpoint , * args , * * kwargs ) :
2020-01-17 23:44:07 +03:00
# Handle check mode
if self . check_mode :
self . json_output [ ' changed ' ] = True
self . exit_json ( * * self . json_output )
2020-01-21 22:44:19 +03:00
2020-01-29 00:56:37 +03:00
return self . make_request ( ' POST ' , endpoint , * * kwargs )
2020-01-17 18:21:42 +03:00
2020-01-29 00:56:37 +03:00
def delete_endpoint ( self , endpoint , * args , * * kwargs ) :
2020-01-17 23:44:07 +03:00
# Handle check mode
if self . check_mode :
self . json_output [ ' changed ' ] = True
self . exit_json ( * * self . json_output )
2020-01-29 00:56:37 +03:00
return self . make_request ( ' DELETE ' , endpoint , * * kwargs )
2020-01-17 18:21:42 +03:00
def get_all_endpoint ( self , endpoint , * args , * * kwargs ) :
2020-01-21 22:44:19 +03:00
response = self . get_endpoint ( endpoint , * args , * * kwargs )
next_page = response [ ' json ' ] [ ' next ' ]
if response [ ' json ' ] [ ' count ' ] > 10000 :
self . fail_json ( msg = ' The number of items being queried for is higher than 10,000. ' )
while next_page is not None :
next_response = self . get_endpoint ( next_page )
response [ ' json ' ] [ ' results ' ] = response [ ' json ' ] [ ' results ' ] + next_response [ ' json ' ] [ ' results ' ]
next_page = next_response [ ' json ' ] [ ' next ' ]
return response
2020-01-17 18:21:42 +03:00
def get_one ( self , endpoint , * args , * * kwargs ) :
response = self . get_endpoint ( endpoint , * args , * * kwargs )
if response [ ' status_code ' ] != 200 :
self . fail_json ( msg = " Got a {0} response when trying to get one from {1} " . format ( response [ ' status_code ' ] , endpoint ) )
if ' count ' not in response [ ' json ' ] or ' results ' not in response [ ' json ' ] :
self . fail_json ( msg = " The endpoint did not provide count and results " )
if response [ ' json ' ] [ ' count ' ] == 0 :
return None
elif response [ ' json ' ] [ ' count ' ] > 1 :
self . fail_json ( msg = " An unexpected number of items was returned from the API ( {0} ) " . format ( response [ ' json ' ] [ ' count ' ] ) )
return response [ ' json ' ] [ ' results ' ] [ 0 ]
def resolve_name_to_id ( self , endpoint , name_or_id ) :
# Try to resolve the object by name
response = self . get_endpoint ( endpoint , * * { ' data ' : { ' name ' : name_or_id } } )
if response [ ' json ' ] [ ' count ' ] == 1 :
return response [ ' json ' ] [ ' results ' ] [ 0 ] [ ' id ' ]
elif response [ ' json ' ] [ ' count ' ] == 0 :
2020-01-24 19:30:16 +03:00
# If we got 0 items by name, maybe they gave us an ID, lets try looking it by by ID
2020-01-28 05:14:55 +03:00
response = self . head_endpoint ( " {0} / {1} " . format ( endpoint , name_or_id ) , * * { ' return_none_on_404 ' : True } )
2020-01-24 19:30:16 +03:00
if response is not None :
return name_or_id
2020-01-21 22:44:19 +03:00
self . fail_json ( msg = " The {0} {1} was not found on the Tower server " . format ( endpoint , name_or_id ) )
2020-01-17 18:21:42 +03:00
else :
2020-01-24 19:30:16 +03:00
self . fail_json ( msg = " Found too many names {0} at endpoint {1} try using an ID instead of a name " . format ( name_or_id , endpoint ) )
2020-01-17 18:21:42 +03:00
def make_request ( self , method , endpoint , * args , * * kwargs ) :
2020-01-29 21:33:04 +03:00
# In case someone is calling us directly; make sure we were given a method, lets not just assume a GET
2020-01-17 18:21:42 +03:00
if not method :
raise Exception ( " The HTTP method must be defined " )
# Make sure we start with /api/vX
if not endpoint . startswith ( " / " ) :
endpoint = " / {0} " . format ( endpoint )
if not endpoint . startswith ( " /api/ " ) :
endpoint = " /api/v2 {0} " . format ( endpoint )
2020-01-21 22:44:19 +03:00
if not endpoint . endswith ( ' / ' ) and ' ? ' not in endpoint :
endpoint = " {0} / " . format ( endpoint )
2020-01-17 18:21:42 +03:00
# Extract the headers, this will be used in a couple of places
headers = kwargs . get ( ' headers ' , { } )
# Authenticate to Tower (if we've not already done so)
if not self . authenticated :
# This method will set a cookie in the cookie jar for us
self . authenticate ( * * kwargs )
if self . oauth_token :
# If we have a oauth toekn we just use a bearer header
2020-01-21 22:44:19 +03:00
headers [ ' Authorization ' ] = ' Bearer {0} ' . format ( self . oauth_token )
2020-01-17 18:21:42 +03:00
# Update the URL path with the endpoint
self . url = self . url . _replace ( path = endpoint )
if method in [ ' POST ' , ' PUT ' , ' PATCH ' ] :
headers . setdefault ( ' Content-Type ' , ' application/json ' )
kwargs [ ' headers ' ] = headers
elif kwargs . get ( ' data ' ) :
self . url = self . url . _replace ( query = urlencode ( kwargs . get ( ' data ' ) ) )
data = { }
if headers . get ( ' Content-Type ' , ' ' ) == ' application/json ' :
data = dumps ( kwargs . get ( ' data ' , { } ) )
try :
response = self . session . open ( method , self . url . geturl ( ) , headers = headers , validate_certs = self . verify_ssl , follow_redirects = True , data = data )
self . url = self . url . _replace ( query = None )
except ( SSLValidationError ) as ssl_err :
self . fail_json ( msg = " Could not establish a secure connection to your host ( {1} ): {0} . " . format ( self . url . netloc , ssl_err ) )
except ( ConnectionError ) as con_err :
self . fail_json ( msg = " There was a network error of some kind trying to connect to your host ( {1} ): {0} . " . format ( self . url . netloc , con_err ) )
except ( HTTPError ) as he :
# Sanity check: Did the server send back some kind of internal error?
if he . code > = 500 :
self . fail_json ( msg = ' The host sent back a server error ( {1} ): {0} . Please check the logs and try again later ' . format ( self . url . path , he ) )
# Sanity check: Did we fail to authenticate properly? If so, fail out now; this is always a failure.
elif he . code == 401 :
self . fail_json ( msg = ' Invalid Tower authentication credentials for {0} (HTTP 401). ' . format ( self . url . path ) )
# Sanity check: Did we get a forbidden response, which means that the user isn't allowed to do this? Report that.
elif he . code == 403 :
self . fail_json ( msg = " You don ' t have permission to {1} to {0} (HTTP 403). " . format ( self . url . path , method ) )
# Sanity check: Did we get a 404 response?
# Requests with primary keys will return a 404 if there is no response, and we want to consistently trap these.
elif he . code == 404 :
2020-01-24 19:30:16 +03:00
if kwargs . get ( ' return_none_on_404 ' , False ) :
return None
2020-01-17 18:21:42 +03:00
self . fail_json ( msg = ' The requested object could not be found at {0} . ' . format ( self . url . path ) )
# Sanity check: Did we get a 405 response?
# A 405 means we used a method that isn't allowed. Usually this is a bad request, but it requires special treatment because the
# API sends it as a logic error in a few situations (e.g. trying to cancel a job that isn't running).
elif he . code == 405 :
self . fail_json ( msg = " The Tower server says you can ' t make a request with the {0} method to this endpoing {1} " . format ( method , self . url . path ) )
# Sanity check: Did we get some other kind of error? If so, write an appropriate error message.
elif he . code > = 400 :
# We are going to return a 400 so the module can decide what to do with it
page_data = he . read ( )
try :
return { ' status_code ' : he . code , ' json ' : loads ( page_data ) }
# JSONDecodeError only available on Python 3.5+
except ValueError :
return { ' status_code ' : he . code , ' text ' : page_data }
# self.fail_json(msg='The Tower server claims it was sent a bad request.\n{0} {1}\nstatus code: {2}\n\nResponse: {3}'.format(
# method, self.url.path, he.code, he.read()))
elif he . code == 204 and method == ' DELETE ' :
# a 204 is a normal response for a delete function
pass
else :
self . fail_json ( msg = " Unexpected return code when calling {0} : {1} " . format ( self . url . geturl ( ) , he ) )
except ( Exception ) as e :
self . fail_json ( msg = " There was an unknown error when trying to connect to {2} : {0} {1} " . format ( type ( e ) . __name__ , e , self . url . geturl ( ) ) )
response_body = ' '
try :
response_body = response . read ( )
except ( Exception ) as e :
self . fail_json ( msg = " Failed to read response body: {0} " . format ( e ) )
response_json = { }
if response_body and response_body != ' ' :
try :
response_json = loads ( response_body )
except ( Exception ) as e :
self . fail_json ( msg = " Failed to parse the response json: {0} " . format ( e ) )
2020-02-04 19:50:20 +03:00
if PY2 :
status_code = response . getcode ( )
else :
status_code = response . status
return { ' status_code ' : status_code , ' json ' : response_json }
2020-01-17 18:21:42 +03:00
def authenticate ( self , * * kwargs ) :
if self . username and self . password :
# Attempt to get a token from /api/v2/tokens/ by giving it our username/password combo
# If we have a username and password we need to get a session cookie
login_data = {
" description " : " Ansible Tower Module Token " ,
" application " : None ,
" scope " : " write " ,
}
# Post to the tokens endpoint with baisc auth to try and get a token
api_token_url = ( self . url . _replace ( path = ' /api/v2/tokens/ ' ) ) . geturl ( )
try :
response = self . session . open (
' POST ' , api_token_url ,
validate_certs = self . verify_ssl , follow_redirects = True ,
force_basic_auth = True , url_username = self . username , url_password = self . password ,
data = dumps ( login_data ) , headers = { ' Content-Type ' : ' application/json ' }
)
except ( Exception ) as e :
# Sanity check: Did the server send back some kind of internal error?
self . fail_json ( msg = ' Failed to get token: {0} ' . format ( e ) )
2020-01-24 19:30:16 +03:00
token_response = None
2020-01-17 18:21:42 +03:00
try :
2020-01-24 19:30:16 +03:00
token_response = response . read ( )
response_json = loads ( token_response )
2020-01-17 18:21:42 +03:00
self . oauth_token_id = response_json [ ' id ' ]
self . oauth_token = response_json [ ' token ' ]
except ( Exception ) as e :
2020-01-24 19:30:16 +03:00
self . fail_json ( msg = " Failed to extract token information from login response: {0} " . format ( e ) , * * { ' response ' : token_response } )
2020-01-17 18:21:42 +03:00
# If we have neiter of these then we can try un-authenticated access
self . authenticated = True
def default_check_mode ( self ) :
''' Execute check mode logic for Ansible Tower modules '''
if self . check_mode :
try :
result = self . get_endpoint ( ' ping ' )
self . exit_json ( * * { ' changed ' : True , ' tower_version ' : ' {0} ' . format ( result [ ' json ' ] [ ' version ' ] ) } )
except ( Exception ) as excinfo :
self . fail_json ( changed = False , msg = ' Failed check mode: {0} ' . format ( excinfo ) )
2020-01-29 00:56:37 +03:00
def delete_if_needed ( self , existing_item , handle_response = True , on_delete = None ) :
# This will exit from the module on its own unless handle_response is False.
2020-01-29 21:33:04 +03:00
# If handle response is True and the method successfully deletes an item and on_delete param is defined
2020-01-29 00:56:37 +03:00
# the on_delete parameter will be called as a method pasing in this object and the json from the response
# If you pass handle_response=False it will return one of two things:
# None if the existing_item is not defined (so no delete needs to happen)
# The response from Tower from calling the delete on the endpont. Its up to you to process the response and exit from the module
# Note: common error codes from the Tower API can cause the module to fail even if handle_response is set to False
if existing_item :
# If we have an item, we can try to delete it
try :
item_url = existing_item [ ' url ' ]
2020-01-29 19:50:56 +03:00
item_type = existing_item [ ' type ' ]
2020-01-29 00:56:37 +03:00
item_id = existing_item [ ' id ' ]
except KeyError as ke :
self . fail_json ( msg = " Unable to process delete of item due to missing data {0} " . format ( ke ) )
2020-01-29 19:50:56 +03:00
if ' name ' in existing_item :
item_name = existing_item [ ' name ' ]
elif ' username ' in existing_item :
item_name = existing_item [ ' username ' ]
else :
2020-01-29 21:33:04 +03:00
self . fail_json ( msg = " Unable to process delete of {0} due to missing name " . format ( item_type ) )
2020-01-29 19:50:56 +03:00
2020-01-29 00:56:37 +03:00
response = self . delete_endpoint ( item_url )
if not handle_response :
return response
elif response [ ' status_code ' ] in [ 202 , 204 ] :
if on_delete :
on_delete ( self , response [ ' json ' ] )
self . json_output [ ' changed ' ] = True
self . json_output [ ' id ' ] = item_id
self . exit_json ( * * self . json_output )
else :
if ' json ' in response and ' __all__ ' in response [ ' json ' ] :
self . fail_json ( msg = " Unable to delete {0} {1} : {2} " . format ( item_type , item_name , response [ ' json ' ] [ ' __all__ ' ] [ 0 ] ) )
elif ' json ' in response :
# This is from a project delete if there is an active job against it
if ' error ' in response [ ' json ' ] :
self . fail_json ( msg = " Unable to delete {0} {1} : {2} " . format ( item_type , item_name , response [ ' json ' ] [ ' error ' ] ) )
else :
self . fail_json ( msg = " Unable to delete {0} {1} : {2} " . format ( item_type , item_name , response [ ' json ' ] ) )
else :
self . fail_json ( msg = " Unable to delete {0} {1} : {2} " . format ( item_type , item_name , response [ ' status_code ' ] ) )
else :
if not handle_response :
return None
else :
self . exit_json ( * * self . json_output )
2020-01-17 23:44:07 +03:00
2020-01-29 00:56:37 +03:00
def create_if_needed ( self , existing_item , new_item , endpoint , handle_response = True , on_create = None , item_type = ' unknown ' ) :
#
# This will exit from the module on its own unless handle_response is False.
2020-01-29 21:33:04 +03:00
# If handle response is True and the method successfully creates an item and on_create param is defined
2020-01-29 00:56:37 +03:00
# the on_create parameter will be called as a method pasing in this object and the json from the response
# If you pass handle_response=False it will return one of two things:
# None if the existing_item is already defined (so no create needs to happen)
# The response from Tower from calling the patch on the endpont. Its up to you to process the response and exit from the module
# Note: common error codes from the Tower API can cause the module to fail even if handle_response is set to False
#
if not endpoint :
2020-01-29 21:33:04 +03:00
self . fail_json ( msg = " Unable to create new {0} due to missing endpoint " . format ( item_type ) )
2020-01-29 00:56:37 +03:00
if existing_item :
try :
2020-01-29 21:33:04 +03:00
existing_item [ ' url ' ]
2020-01-29 00:56:37 +03:00
except KeyError as ke :
self . fail_json ( msg = " Unable to process delete of item due to missing data {0} " . format ( ke ) )
if not handle_response :
return None
else :
self . exit_json ( * * self . json_output )
else :
# If we dont have an exisitng_item, we can try to create it
# We have to rely on item_type being passed in since we don't have an existing item that declares its type
# The item_name we will pull out from the new_item (if it exists)
item_name = new_item . get ( ' name ' , ' unknown ' )
response = self . post_endpoint ( endpoint , * * { ' data ' : new_item } )
if not handle_response :
return response
elif response [ ' status_code ' ] == 201 :
2020-01-29 19:50:56 +03:00
self . json_output [ ' name ' ] = ' unknown '
if ' name ' in response [ ' json ' ] :
self . json_output [ ' name ' ] = response [ ' json ' ] [ ' name ' ]
elif ' username ' in response [ ' json ' ] :
# User objects return username instead of name
self . json_output [ ' name ' ] = response [ ' json ' ] [ ' username ' ]
2020-01-29 00:56:37 +03:00
self . json_output [ ' id ' ] = response [ ' json ' ] [ ' id ' ]
self . json_output [ ' changed ' ] = True
if on_create is None :
self . exit_json ( * * self . json_output )
else :
on_create ( self , response [ ' json ' ] )
else :
if ' json ' in response and ' __all__ ' in response [ ' json ' ] :
self . fail_json ( msg = " Unable to create {0} {1} : {2} " . format ( item_type , item_name , response [ ' json ' ] [ ' __all__ ' ] [ 0 ] ) )
elif ' json ' in response :
self . fail_json ( msg = " Unable to create {0} {1} : {2} " . format ( item_type , item_name , response [ ' json ' ] ) )
else :
2020-01-29 21:33:04 +03:00
self . fail_json ( msg = " Unable to create {0} {1} : {2} " . format ( item_type , item_name , response [ ' status_code ' ] ) )
2020-01-29 00:56:37 +03:00
def update_if_needed ( self , existing_item , new_item , handle_response = True , on_update = None ) :
# This will exit from the module on its own unless handle_response is False.
2020-01-29 21:33:04 +03:00
# If handle response is True and the method successfully updates an item and on_update param is defined
2020-01-29 00:56:37 +03:00
# the on_update parameter will be called as a method pasing in this object and the json from the response
# If you pass handle_response=False it will return one of three things:
# None if the existing_item does not need to be updated
# The response from Tower from patching to the endpoint. Its up to you to process the response and exit from the module.
# an ItemNotDefined exception if the existing_item does not exist
# Note: common error codes from the Tower API can cause the module to fail even if handle_response is set to False
if existing_item :
# If we have an item, we can see if needs an update
try :
item_url = existing_item [ ' url ' ]
item_name = existing_item [ ' name ' ]
item_type = existing_item [ ' url ' ]
item_id = existing_item [ ' id ' ]
except KeyError as ke :
self . fail_json ( msg = " Unable to process update of item due to missing data {0} " . format ( ke ) )
needs_update = False
for field in new_item :
existing_field = existing_item . get ( field , None )
new_field = new_item . get ( field , None )
# If the two items don't match and we are not comparing '' to None
if existing_field != new_field and not ( existing_field in ( None , ' ' ) and new_field == ' ' ) :
# something dosent match so lets do it
2020-01-29 19:52:09 +03:00
needs_update = True
2020-01-29 00:56:37 +03:00
break
if needs_update :
response = self . patch_endpoint ( item_url , * * { ' data ' : new_item } )
2020-01-17 18:21:42 +03:00
if not handle_response :
return response
elif response [ ' status_code ' ] == 200 :
2020-01-29 00:56:37 +03:00
self . json_output [ ' changed ' ] = True
self . json_output [ ' id ' ] = item_id
if on_update is None :
self . exit_json ( * * self . json_output )
2020-01-24 19:30:16 +03:00
else :
2020-01-29 00:56:37 +03:00
on_update ( self , response [ ' json ' ] )
2020-01-17 18:21:42 +03:00
elif ' json ' in response and ' __all__ ' in response [ ' json ' ] :
self . fail_json ( msg = response [ ' json ' ] [ ' __all__ ' ] )
else :
2020-01-29 00:56:37 +03:00
self . fail_json ( * * { ' msg ' : " Unable to update {0} {1} , see response " . format ( item_type , item_name ) , ' response ' : response } )
else :
if not handle_response :
return None
2020-01-17 18:21:42 +03:00
2020-01-29 00:56:37 +03:00
# Since we made it here, we don't need to update, status ok
self . json_output [ ' changed ' ] = False
self . json_output [ ' id ' ] = item_id
self . exit_json ( * * self . json_output )
else :
if handle_response :
self . fail_json ( msg = " The exstiing item is not defined and thus cannot be updated " )
else :
raise ItemNotDefined ( " Not given an existing item to update " )
def create_or_update_if_needed ( self , existing_item , new_item , endpoint = None , handle_response = True , item_type = ' unknown ' , on_create = None , on_update = None ) :
if existing_item :
return self . update_if_needed ( existing_item , new_item , handle_response = handle_response , on_update = on_update )
else :
return self . create_if_needed ( existing_item , new_item , endpoint , handle_response = handle_response , on_create = on_create , item_type = item_type )
2020-01-17 18:21:42 +03:00
def logout ( self ) :
2020-01-21 22:44:19 +03:00
if self . oauth_token_id is not None and self . username and self . password :
2020-01-17 18:45:44 +03:00
# Attempt to delete our current token from /api/v2/tokens/
# Post to the tokens endpoint with baisc auth to try and get a token
api_token_url = ( self . url . _replace ( path = ' /api/v2/tokens/ {0} / ' . format ( self . oauth_token_id ) ) ) . geturl ( )
2020-01-17 18:21:42 +03:00
try :
2020-01-28 05:14:55 +03:00
self . session . open (
2020-01-29 00:56:37 +03:00
' DELETE ' ,
api_token_url ,
validate_certs = self . verify_ssl ,
follow_redirects = True ,
force_basic_auth = True ,
url_username = self . username ,
url_password = self . password
2020-01-17 18:45:44 +03:00
)
self . oauth_token_id = None
2020-01-17 18:21:42 +03:00
self . authenticated = False
2020-01-17 18:45:44 +03:00
except ( Exception ) as e :
# Sanity check: Did the server send back some kind of internal error?
2020-01-17 19:07:39 +03:00
self . warn ( ' Failed to release tower token {0} : {1} ' . format ( self . oauth_token_id , e ) )
2020-01-17 18:21:42 +03:00
def fail_json ( self , * * kwargs ) :
# Try to logout if we are authenticated
self . logout ( )
2020-01-28 23:39:53 +03:00
super ( TowerModule , self ) . fail_json ( * * kwargs )
2020-01-17 18:21:42 +03:00
def exit_json ( self , * * kwargs ) :
# Try to logout if we are authenticated
self . logout ( )
2020-01-28 23:39:53 +03:00
super ( TowerModule , self ) . exit_json ( * * kwargs )
2020-01-24 19:30:16 +03:00
def is_job_done ( self , job_status ) :
2020-01-28 05:14:55 +03:00
if job_status in [ ' new ' , ' pending ' , ' waiting ' , ' running ' ] :
2020-01-24 19:30:16 +03:00
return False
2020-01-29 21:33:04 +03:00
else :
return True
2020-02-04 21:00:01 +03:00
def load_variables_if_file_specified ( self , vars_value , var_name ) :
if not vars_value . startswith ( ' @ ' ) :
return vars_value
2020-02-05 21:24:46 +03:00
if not HAS_YAML :
self . fail_json ( msg = self . missing_required_lib ( ' yaml ' ) )
2020-02-04 21:00:01 +03:00
file_name = None
file_content = None
try :
file_name = expanduser ( vars_value [ 1 : ] )
with open ( file_name , ' r ' ) as f :
file_content = f . read ( )
except Exception as e :
self . fail_json ( msg = " Failed to load file {0} for {1} : {2} " . format ( file_name , var_name , e ) )
try :
vars_value = yaml . safe_load ( file_content )
except yaml . YAMLError :
# Maybe it wasn't a YAML structure... lets try JSON
try :
vars_value = loads ( file_content )
except ValueError :
self . fail_json ( msg = " Failed to load file {0} specifed by {1} as yaml or json " . format ( file_name , var_name ) )
return dumps ( vars_value )