More tower-cli-ish parsing of config files

Clear up test failures/linting errors, update unit test

Update module_utils for linter, add wait time to project module
This commit is contained in:
John Westcott IV
2020-01-27 21:14:55 -05:00
committed by beeankha
parent 838b2b7d1e
commit e028ed878e
3 changed files with 66 additions and 29 deletions

View File

@@ -5,15 +5,18 @@ from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.basic import env_fallback from ansible.module_utils.basic import env_fallback
from ansible.module_utils.urls import Request, SSLValidationError, ConnectionError from ansible.module_utils.urls import Request, SSLValidationError, ConnectionError
from ansible.module_utils.six.moves.urllib.parse import urlparse, urlencode from ansible.module_utils.six.moves.urllib.parse import urlparse, urlencode
from socket import gethostbyname
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.http_cookiejar import CookieJar from ansible.module_utils.six.moves.http_cookiejar import CookieJar
from ansible.module_utils.six.moves.configparser import ConfigParser, NoOptionError, NoSectionError from ansible.module_utils.six.moves.configparser import ConfigParser, NoOptionError
from socket import gethostbyname
import re import re
from json import loads, dumps from json import loads, dumps
from os.path import isfile from os.path import isfile, expanduser, split, join
from os import access, R_OK from os import access, R_OK, getcwd
class ConfigFileException(Exception):
pass
class TowerModule(AnsibleModule): class TowerModule(AnsibleModule):
@@ -30,6 +33,7 @@ class TowerModule(AnsibleModule):
authenticated = False authenticated = False
json_output = {'changed': False} json_output = {'changed': False}
on_change = None on_change = None
config_name = 'tower_cli.cfg'
def __init__(self, argument_spec, **kwargs): def __init__(self, argument_spec, **kwargs):
args = dict( args = dict(
@@ -45,11 +49,9 @@ class TowerModule(AnsibleModule):
super(TowerModule, self).__init__(argument_spec=args, **kwargs) super(TowerModule, self).__init__(argument_spec=args, **kwargs)
# If we have a tower config, load it self.load_config_files()
if self.params.get('tower_config_file'):
self.load_config(self.params.get('tower_config_file'))
# Parameters specified on command line will override settings in config # Parameters specified on command line will override settings in any config
if self.params.get('tower_host'): if self.params.get('tower_host'):
self.host = self.params.get('tower_host') self.host = self.params.get('tower_host')
if self.params.get('tower_username'): if self.params.get('tower_username'):
@@ -80,22 +82,48 @@ class TowerModule(AnsibleModule):
self.session = Request(cookies=self.cookie_jar) self.session = Request(cookies=self.cookie_jar)
def load_config_files(self):
# Load configs like TowerCLI would have from least import to most
config_files = [join('/etc/tower/', self.config_name), join(expanduser("~"), ".{0}".format(self.config_name))]
local_dir = getcwd()
config_files.append(join(local_dir, self.config_name))
while split(local_dir)[1]:
local_dir = split(local_dir)[0]
config_files.insert(2, join(local_dir, self.config_name))
for config_file in config_files:
try:
self.load_config(config_file)
except ConfigFileException:
# Since some of these may not exist or can't be read, we really don't care
pass
# 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)
def load_config(self, config_path): def load_config(self, config_path):
config = ConfigParser() config = ConfigParser()
# Validate the config file is an actual file # Validate the config file is an actual file
if not isfile(config_path): if not isfile(config_path):
self.fail_json(msg='The specified config file does not exist') raise ConfigFileException('The specified config file does not exist')
if not access(config_path, R_OK): if not access(config_path, R_OK):
self.fail_json(msg="The specified config file can not be read") raise ConfigFileException("The specified config file can not be read")
config.read(config_path) config.read(config_path)
if not config.has_section('general'):
self.warn("No general section in file, auto-appending")
with open(config_path, 'r') as f:
config.read_string('[general]\n%s' % f.read())
for honorred_setting in self.honorred_settings: for honorred_setting in self.honorred_settings:
try: try:
setattr(self, honorred_setting, config.get('general', honorred_setting)) setattr(self, honorred_setting, config.get('general', honorred_setting))
except (NoSectionError) as nse:
self.fail_json(msg="The specified config file does not contain a general section ({0})".format(nse))
except (NoOptionError): except (NoOptionError):
pass pass
@@ -124,7 +152,7 @@ class TowerModule(AnsibleModule):
self.json_output['name'] = response['json']['name'] self.json_output['name'] = response['json']['name']
self.json_output['id'] = response['json']['id'] self.json_output['id'] = response['json']['id']
self.json_output['changed'] = True self.json_output['changed'] = True
if self.on_change == None: if self.on_change is None:
self.exit_json(**self.json_output) self.exit_json(**self.json_output)
else: else:
self.on_change(self, response['json']) self.on_change(self, response['json'])
@@ -134,7 +162,7 @@ class TowerModule(AnsibleModule):
elif 'json' in response: elif 'json' in response:
self.fail_json(msg="Unable to create {0} {1}: {2}".format(item_type, item_name, response['json'])) self.fail_json(msg="Unable to create {0} {1}: {2}".format(item_type, item_name, response['json']))
else: else:
self.fail_json(msg="Unable to create {0} {1}: {2}".format(item_type, item_name, response['status_code']), **{ 'payload': kwargs['data'] }) self.fail_json(msg="Unable to create {0} {1}: {2}".format(item_type, item_name, response['status_code']), **{'payload': kwargs['data']})
def delete_endpoint(self, endpoint, handle_return=True, item_type='item', item_name='', *args, **kwargs): def delete_endpoint(self, endpoint, handle_return=True, item_type='item', item_name='', *args, **kwargs):
# Handle check mode # Handle check mode
@@ -195,7 +223,7 @@ class TowerModule(AnsibleModule):
return response['json']['results'][0]['id'] return response['json']['results'][0]['id']
elif response['json']['count'] == 0: elif response['json']['count'] == 0:
# If we got 0 items by name, maybe they gave us an ID, lets try looking it by by ID # If we got 0 items by name, maybe they gave us an ID, lets try looking it by by ID
response = self.head_endpoint("{}/{}".format(endpoint, name_or_id), **{'return_none_on_404': True}) response = self.head_endpoint("{0}/{1}".format(endpoint, name_or_id), **{'return_none_on_404': True})
if response is not None: if response is not None:
return name_or_id return name_or_id
self.fail_json(msg="The {0} {1} was not found on the Tower server".format(endpoint, name_or_id)) self.fail_json(msg="The {0} {1} was not found on the Tower server".format(endpoint, name_or_id))
@@ -359,7 +387,7 @@ class TowerModule(AnsibleModule):
elif response['status_code'] == 200: elif response['status_code'] == 200:
existing_return['changed'] = True existing_return['changed'] = True
existing_return['id'] = response['json'].get('id') existing_return['id'] = response['json'].get('id')
if self.on_change == None: if self.on_change is None:
self.exit_json(**existing_return) self.exit_json(**existing_return)
else: else:
self.on_change(self, response['json']) self.on_change(self, response['json'])
@@ -380,7 +408,7 @@ class TowerModule(AnsibleModule):
api_token_url = (self.url._replace(path='/api/v2/tokens/{0}/'.format(self.oauth_token_id))).geturl() api_token_url = (self.url._replace(path='/api/v2/tokens/{0}/'.format(self.oauth_token_id))).geturl()
try: try:
response = self.session.open( self.session.open(
'DELETE', api_token_url, 'DELETE', api_token_url,
validate_certs=self.verify_ssl, follow_redirects=True, validate_certs=self.verify_ssl, follow_redirects=True,
force_basic_auth=True, url_username=self.username, url_password=self.password force_basic_auth=True, url_username=self.username, url_password=self.password
@@ -402,7 +430,6 @@ class TowerModule(AnsibleModule):
super().exit_json(**kwargs) super().exit_json(**kwargs)
def is_job_done(self, job_status): def is_job_done(self, job_status):
if job_status in [ 'new', 'pending', 'waiting', 'running', ]: if job_status in ['new', 'pending', 'waiting', 'running']:
return False return False
return True return True

View File

@@ -118,6 +118,11 @@ options:
on the project may be successfully created on the project may be successfully created
type: bool type: bool
default: True default: True
tower_oauthtoken:
description:
- The Tower OAuth token to use.
required: False
type: str
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
''' '''
@@ -143,6 +148,8 @@ EXAMPLES = '''
tower_config_file: "~/tower_cli.cfg" tower_config_file: "~/tower_cli.cfg"
''' '''
import time
from ..module_utils.tower_api import TowerModule from ..module_utils.tower_api import TowerModule
@@ -152,9 +159,10 @@ def wait_for_project_update(module, last_request):
if 'current_update' in last_request['summary_fields']: if 'current_update' in last_request['summary_fields']:
running = True running = True
while running: while running:
result = module.get_endpoint('/project_updates/{}/'.format(last_request['summary_fields']['current_update']['id']))['json'] result = module.get_endpoint('/project_updates/{0}/'.format(last_request['summary_fields']['current_update']['id']))['json']
if module.is_job_done(result['status']): if module.is_job_done(result['status']):
time.sleep(1)
running = False running = False
if result['status'] != 'successful': if result['status'] != 'successful':
@@ -162,6 +170,7 @@ def wait_for_project_update(module, last_request):
module.exit_json(**module.json_output) module.exit_json(**module.json_output)
def main(): def main():
# Any additional arguments that are not fields of the item can be added here # Any additional arguments that are not fields of the item can be added here
argument_spec = dict( argument_spec = dict(
@@ -212,7 +221,7 @@ def main():
# Attempt to lookup the related items the user specified (these will fail the module if not found) # Attempt to lookup the related items the user specified (these will fail the module if not found)
org_id = module.resolve_name_to_id('organizations', organization) org_id = module.resolve_name_to_id('organizations', organization)
if scm_credential != None: if scm_credential is not None:
scm_credential_id = module.resolve_name_to_id('credentials', scm_credential) scm_credential_id = module.resolve_name_to_id('credentials', scm_credential)
# Attempt to lookup project based on the provided name and org ID # Attempt to lookup project based on the provided name and org ID
@@ -238,9 +247,9 @@ def main():
'scm_update_cache_timeout': scm_update_cache_timeout, 'scm_update_cache_timeout': scm_update_cache_timeout,
'custom_virtualenv': custom_virtualenv, 'custom_virtualenv': custom_virtualenv,
} }
if scm_credential != None: if scm_credential is not None:
project_fields['credential'] = scm_credential_id project_fields['credential'] = scm_credential_id
if scm_allow_override != None: if scm_allow_override is not None:
project_fields['scm_allow_override'] = scm_allow_override project_fields['scm_allow_override'] = scm_allow_override
if scm_type == '': if scm_type == '':
project_fields['local_path'] = local_path project_fields['local_path'] = local_path
@@ -261,7 +270,7 @@ def main():
module.delete_endpoint('projects/{0}'.format(project['id']), item_type='project', item_name=name, **{}) module.delete_endpoint('projects/{0}'.format(project['id']), item_type='project', item_name=name, **{})
elif state == 'present' and not project: elif state == 'present' and not project:
# if the state was present and we couldn't find a project we can build one, the module wikl handle exiting from this # if the state was present and we couldn't find a project we can build one, the module wikl handle exiting from this
response = module.post_endpoint('projects', handle_return=False,item_type='project', item_name=name, **{'data': project_fields }) module.post_endpoint('projects', handle_return=False, item_type='project', item_name=name, **{'data': project_fields})
else: else:
# If the state was present and we had a project we can see if we need to update it # If the state was present and we had a project we can see if we need to update it
# This will return on its own # This will return on its own

View File

@@ -7,14 +7,15 @@ from awx.main.models import Project
@pytest.mark.django_db @pytest.mark.django_db
def test_create_project(run_module, admin_user, organization): def test_create_project(run_converted_module, admin_user, organization):
result = run_module('tower_project', dict( result = run_converted_module('tower_project', dict(
name='foo', name='foo',
organization=organization.name, organization=organization.name,
scm_type='git', scm_type='git',
scm_url='https://foo.invalid', scm_url='https://foo.invalid',
wait=False wait=False
), admin_user) ), admin_user)
warning = ['scm_update_cache_timeout will be ignored since scm_update_on_launch was not set to true']
assert result.pop('changed', None), result assert result.pop('changed', None), result
proj = Project.objects.get(name='foo') proj = Project.objects.get(name='foo')
@@ -23,7 +24,7 @@ def test_create_project(run_module, admin_user, organization):
result.pop('invocation') result.pop('invocation')
assert result == { assert result == {
'name': 'foo',
'id': proj.id, 'id': proj.id,
'project': 'foo', 'warnings': warning
'state': 'present'
} }