From e028ed878e4892112a184e5dacca10a187b98f57 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Mon, 27 Jan 2020 21:14:55 -0500 Subject: [PATCH] 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 --- .../plugins/module_utils/tower_api.py | 67 +++++++++++++------ .../plugins/modules/tower_project.py | 19 ++++-- awx_collection/test/awx/test_project.py | 9 +-- 3 files changed, 66 insertions(+), 29 deletions(-) diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index ae5e884159..ff9dc8744f 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -5,15 +5,18 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import env_fallback from ansible.module_utils.urls import Request, SSLValidationError, ConnectionError 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.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 from json import loads, dumps -from os.path import isfile -from os import access, R_OK +from os.path import isfile, expanduser, split, join +from os import access, R_OK, getcwd + + +class ConfigFileException(Exception): + pass class TowerModule(AnsibleModule): @@ -30,6 +33,7 @@ class TowerModule(AnsibleModule): authenticated = False json_output = {'changed': False} on_change = None + config_name = 'tower_cli.cfg' def __init__(self, argument_spec, **kwargs): args = dict( @@ -45,11 +49,9 @@ class TowerModule(AnsibleModule): super(TowerModule, self).__init__(argument_spec=args, **kwargs) - # If we have a tower config, load it - if self.params.get('tower_config_file'): - self.load_config(self.params.get('tower_config_file')) + self.load_config_files() - # 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'): self.host = self.params.get('tower_host') if self.params.get('tower_username'): @@ -80,22 +82,48 @@ class TowerModule(AnsibleModule): 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): config = ConfigParser() # Validate the config file is an actual file 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): - 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) + 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: try: 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): pass @@ -124,7 +152,7 @@ class TowerModule(AnsibleModule): self.json_output['name'] = response['json']['name'] self.json_output['id'] = response['json']['id'] self.json_output['changed'] = True - if self.on_change == None: + if self.on_change is None: self.exit_json(**self.json_output) else: self.on_change(self, response['json']) @@ -134,7 +162,7 @@ class TowerModule(AnsibleModule): elif 'json' in response: self.fail_json(msg="Unable to create {0} {1}: {2}".format(item_type, item_name, response['json'])) 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): # Handle check mode @@ -195,7 +223,7 @@ class TowerModule(AnsibleModule): return response['json']['results'][0]['id'] 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 - 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: return 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: existing_return['changed'] = True existing_return['id'] = response['json'].get('id') - if self.on_change == None: + if self.on_change is None: self.exit_json(**existing_return) else: 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() try: - response = self.session.open( + self.session.open( 'DELETE', api_token_url, validate_certs=self.verify_ssl, follow_redirects=True, force_basic_auth=True, url_username=self.username, url_password=self.password @@ -402,7 +430,6 @@ class TowerModule(AnsibleModule): super().exit_json(**kwargs) 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 True - diff --git a/awx_collection/plugins/modules/tower_project.py b/awx_collection/plugins/modules/tower_project.py index 8f637275a4..00c614aee6 100644 --- a/awx_collection/plugins/modules/tower_project.py +++ b/awx_collection/plugins/modules/tower_project.py @@ -118,6 +118,11 @@ options: on the project may be successfully created type: bool default: True + tower_oauthtoken: + description: + - The Tower OAuth token to use. + required: False + type: str extends_documentation_fragment: awx.awx.auth ''' @@ -143,6 +148,8 @@ EXAMPLES = ''' tower_config_file: "~/tower_cli.cfg" ''' +import time + 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']: running = True 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']): + time.sleep(1) running = False if result['status'] != 'successful': @@ -162,6 +170,7 @@ def wait_for_project_update(module, last_request): module.exit_json(**module.json_output) + def main(): # Any additional arguments that are not fields of the item can be added here 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) 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) # 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, 'custom_virtualenv': custom_virtualenv, } - if scm_credential != None: + if scm_credential is not None: 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 if scm_type == '': 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, **{}) 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 - 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: # 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 diff --git a/awx_collection/test/awx/test_project.py b/awx_collection/test/awx/test_project.py index 3c858f5a04..f07dc5b29d 100644 --- a/awx_collection/test/awx/test_project.py +++ b/awx_collection/test/awx/test_project.py @@ -7,14 +7,15 @@ from awx.main.models import Project @pytest.mark.django_db -def test_create_project(run_module, admin_user, organization): - result = run_module('tower_project', dict( +def test_create_project(run_converted_module, admin_user, organization): + result = run_converted_module('tower_project', dict( name='foo', organization=organization.name, scm_type='git', scm_url='https://foo.invalid', wait=False ), 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 proj = Project.objects.get(name='foo') @@ -23,7 +24,7 @@ def test_create_project(run_module, admin_user, organization): result.pop('invocation') assert result == { + 'name': 'foo', 'id': proj.id, - 'project': 'foo', - 'state': 'present' + 'warnings': warning }