mirror of
https://github.com/ansible/awx.git
synced 2026-05-12 11:57:37 -02:30
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:
committed by
beeankha
parent
838b2b7d1e
commit
e028ed878e
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user