Further module conversion changes, unit test changes

Multiple module changes

Added on_change callback

Added head_endpoint

Added additional error returns

Respond with a try an ID message if multiple assets found by name via return_none_on_404 kwarg

Diferentiated between login and logout token errors

Added is_job_done method
This commit is contained in:
beeankha 2020-01-24 11:30:16 -05:00
parent 68926dad27
commit 7c0ad461a5
10 changed files with 224 additions and 47 deletions

View File

@ -29,6 +29,7 @@ class TowerModule(AnsibleModule):
cookie_jar = CookieJar()
authenticated = False
json_output = {'changed': False}
on_change = None
def __init__(self, argument_spec, **kwargs):
args = dict(
@ -98,6 +99,9 @@ class TowerModule(AnsibleModule):
except (NoOptionError):
pass
def head_endpoint(self, endpoint, *args, **kwargs):
return self.make_request('HEAD', endpoint, **kwargs)
def get_endpoint(self, endpoint, *args, **kwargs):
return self.make_request('GET', endpoint, **kwargs)
@ -117,14 +121,20 @@ class TowerModule(AnsibleModule):
response = self.make_request('POST', endpoint, **kwargs)
if response['status_code'] == 201:
self.json_output['changed'] = True
self.json_output['name'] = response['json']['name']
self.json_output['id'] = response['json']['id']
self.exit_json(**self.json_output)
self.json_output['changed'] = True
if self.on_change == None:
self.exit_json(**self.json_output)
else:
self.on_change(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:
self.fail_json(msg="Unable to create {0} {1}: {2}".format(item_type, item_name, response['status_code']))
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
@ -139,7 +149,16 @@ class TowerModule(AnsibleModule):
self.json_output['changed'] = True
self.exit_json(**self.json_output)
else:
self.fail_json(msg="Unable to delete {0} {1}: {2}".format(item_type, item_name, response['status_code']))
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']))
def get_all_endpoint(self, endpoint, *args, **kwargs):
response = self.get_endpoint(endpoint, *args, **kwargs)
@ -175,9 +194,13 @@ class TowerModule(AnsibleModule):
if response['json']['count'] == 1:
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})
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))
else:
self.fail_json(msg="Found too many names {0} at endpoint {1}".format(name_or_id, endpoint))
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))
def make_request(self, method, endpoint, *args, **kwargs):
# Incase someone is calling us directly; make sure we were given a method, lets not just assume a GET
@ -236,6 +259,8 @@ class TowerModule(AnsibleModule):
# 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:
if kwargs.get('return_none_on_404', False):
return None
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
@ -299,12 +324,14 @@ class TowerModule(AnsibleModule):
# Sanity check: Did the server send back some kind of internal error?
self.fail_json(msg='Failed to get token: {0}'.format(e))
token_response = None
try:
response_json = loads(response.read())
token_response = response.read()
response_json = loads(token_response)
self.oauth_token_id = response_json['id']
self.oauth_token = response_json['token']
except(Exception) as e:
self.fail_json(msg="Failed to extract token information from response: {0}".format(e))
self.fail_json(msg="Failed to extract token information from login response: {0}".format(e), **{'response': token_response})
# If we have neiter of these then we can try un-authenticated access
self.authenticated = True
@ -332,7 +359,10 @@ class TowerModule(AnsibleModule):
elif response['status_code'] == 200:
existing_return['changed'] = True
existing_return['id'] = response['json'].get('id')
self.exit_json(**existing_return)
if self.on_change == None:
self.exit_json(**existing_return)
else:
self.on_change(self, response['json'])
elif 'json' in response and '__all__' in response['json']:
self.fail_json(msg=response['json']['__all__'])
else:
@ -370,3 +400,9 @@ class TowerModule(AnsibleModule):
# Try to logout if we are authenticated
self.logout()
super().exit_json(**kwargs)
def is_job_done(self, job_status):
if job_status in [ 'new', 'pending', 'waiting', 'running', ]:
return False
return True

View File

@ -130,17 +130,17 @@ def main():
module.default_check_mode()
# These will be passed into the create/updates
credental_type_params = {
credential_type_params = {
'name': new_name if new_name else name,
'kind': kind,
'managed_by_tower': False,
}
if module.params.get('description'):
credental_type_params['description'] = module.params.get('description')
credential_type_params['description'] = module.params.get('description')
if module.params.get('inputs'):
credental_type_params['inputs'] = module.params.get('inputs')
credential_type_params['inputs'] = module.params.get('inputs')
if module.params.get('injectors'):
credental_type_params['injectors'] = module.params.get('injectors')
credential_type_params['injectors'] = module.params.get('injectors')
# Attempt to lookup credential_type based on the provided name and org ID
credential_type = module.get_one('credential_types', **{
@ -159,12 +159,12 @@ def main():
elif state == 'present' and not credential_type:
# if the state was present and we couldn't find a credential_type we can build one, the module will handle exiting on its own
module.post_endpoint('credential_types', item_type='credential type', item_name=name, **{
'data': credental_type_params
'data': credential_type_params
})
else:
# If the state was present and we had a credential_type we can see if we need to update it
# This will handle existing on its own
module.update_if_needed(credential_type, credental_type_params)
module.update_if_needed(credential_type, credential_type_params)
if __name__ == '__main__':

View File

@ -1,7 +1,7 @@
#!/usr/bin/python
# coding: utf-8 -*-
# (c) 20189, John Westcott IV <john.westcott.iv@redhat.com>
# (c) 2019, John Westcott IV <john.westcott.iv@redhat.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
@ -65,7 +65,7 @@ def main():
json_output = {'changed': False}
if not module.params.get('eula_accepted'):
module.fail_json(msg='You must accept the EULA by passing in the param eula_acepte as True')
module.fail_json(msg='You must accept the EULA by passing in the param eula_accepted as True')
json_output['old_license'] = module.get_endpoint('settings/system/')['json']['LICENSE']
new_license = module.params.get('data')

View File

@ -90,18 +90,6 @@ def main():
state=dict(type='str', choices=['present', 'absent'], default='present', required=False),
)
# instance_groups=dict(type='list', required=False, default=[]),
# The above argument_spec fragment is being left in for reference since we may need
# it later when finalizing the changes.
# instance_groups:
# description:
# - The name of instance groups to tie to this organization
# default: []
# required: False
# The above docstring fragment is being left in for reference since we may need
# it later when finalizing the changes.
# Create a module for ourselves
module = TowerModule(argument_spec=argument_spec, supports_check_mode=True)
@ -134,11 +122,9 @@ def main():
int_max_hosts = 0
try:
int_max_hosts = int(max_hosts)
except Exception as e:
except Exception:
module.fail_json(msg="Unable to convert max_hosts to an integer")
new_org_data['max_hosts'] = int_max_hosts
# if instance_group_objects:
# new_org_data['instance_groups'] = instance_group_objects
if state == 'absent' and not organization:
# If the state was absent and we had no organization, we can just return

View File

@ -110,7 +110,7 @@ def main():
new_value = value
try:
new_value = loads(value)
except JSONDecodeError as e:
except JSONDecodeError:
# Attempt to deal with old tower_cli array types
if ',' in value:
new_value = re.split(r",\s+", new_value)

View File

@ -103,7 +103,7 @@ def main():
# Create data to sent to create and update
team_fields = {
'name': name,
'name': new_name if new_name else name,
'description': description,
'organization': org_id
}

View File

@ -111,6 +111,86 @@ def run_module(request):
return rf
@pytest.fixture
def run_converted_module(request):
# A placeholder to use while modules get converted
def rf(module_name, module_params, request_user):
def new_request(self, method, url, **kwargs):
kwargs_copy = kwargs.copy()
if 'data' in kwargs:
if not isinstance(kwargs['data'], dict):
kwargs_copy['data'] = json.loads(kwargs['data'])
else:
kwargs_copy['data'] = kwargs['data']
if 'params' in kwargs and method == 'GET':
# query params for GET are handled a bit differently by
# tower-cli and python requests as opposed to REST framework APIRequestFactory
kwargs_copy.setdefault('data', {})
if isinstance(kwargs['params'], dict):
kwargs_copy['data'].update(kwargs['params'])
elif isinstance(kwargs['params'], list):
for k, v in kwargs['params']:
kwargs_copy['data'][k] = v
# make request
rf = _request(method.lower())
django_response = rf(url, user=request_user, expect=None, **kwargs_copy)
# requests library response object is different from the Django response, but they are the same concept
# this converts the Django response object into a requests response object for consumption
resp = Response()
py_data = django_response.data
sanitize_dict(py_data)
resp._content = bytes(json.dumps(django_response.data), encoding='utf8')
resp.status_code = django_response.status_code
if request.config.getoption('verbose') > 0:
logger.info(
'%s %s by %s, code:%s',
method, '/api/' + url.split('/api/')[1],
request_user.username, resp.status_code
)
return resp
def new_open(self, method, url, **kwargs):
r = new_request(self, method, url, **kwargs)
return mock.MagicMock(read=mock.MagicMock(return_value=r._content), status=r.status_code)
stdout_buffer = io.StringIO()
# Requies specific PYTHONPATH, see docs
# Note that a proper Ansiballz explosion of the modules will have an import path like:
# ansible_collections.awx.awx.plugins.modules.{}
# We should consider supporting that in the future
resource_module = importlib.import_module('plugins.modules.{0}'.format(module_name))
if not isinstance(module_params, dict):
raise RuntimeError('Module params must be dict, got {0}'.format(type(module_params)))
# Ansible params can be passed as an invocation argument or over stdin
# this short circuits within the AnsibleModule interface
def mock_load_params(self):
self.params = module_params
with mock.patch.object(resource_module.TowerModule, '_load_params', new=mock_load_params):
# Call the test utility (like a mock server) instead of issuing HTTP requests
with mock.patch('ansible.module_utils.urls.Request.open', new=new_open):
with mock.patch('tower_cli.api.Session.request', new=new_request):
# Ansible modules return data to the mothership over stdout
with redirect_stdout(stdout_buffer):
try:
resource_module.main()
except SystemExit:
pass # A system exit indicates successful execution
module_stdout = stdout_buffer.getvalue().strip()
result = json.loads(module_stdout)
return result
return rf
@pytest.fixture
def organization():
return Organization.objects.create(name='Default')

View File

@ -63,9 +63,9 @@ def test_create_vault_credential(run_module, admin_user):
@pytest.mark.django_db
def test_create_custom_credential_type(run_module, admin_user):
def test_create_custom_credential_type(run_converted_module, admin_user):
# Example from docs
result = run_module('tower_credential_type', dict(
result = run_converted_module('tower_credential_type', dict(
name='Nexus',
description='Credentials type for Nexus',
kind='cloud',
@ -79,8 +79,7 @@ def test_create_custom_credential_type(run_module, admin_user):
ct = CredentialType.objects.get(name='Nexus')
result.pop('invocation')
assert result == {
"credential_type": "Nexus",
"state": "present",
"name": "Nexus",
"id": ct.pk,
"changed": True
}

View File

@ -7,20 +7,31 @@ from awx.main.models import Organization
@pytest.mark.django_db
def test_create_organization(run_module, admin_user):
def test_create_organization(run_converted_module, admin_user):
module_args = {'name': 'foo', 'description': 'barfoo', 'state': 'present'}
module_args = {
'name': 'foo',
'description': 'barfoo',
'state': 'present',
'max_hosts': '0',
'tower_host': None,
'tower_username': None,
'tower_password': None,
'validate_certs': None,
'tower_oauthtoken': None,
'tower_config_file': None,
'custom_virtualenv': None
}
result = run_module('tower_organization', module_args, admin_user)
result = run_converted_module('tower_organization', module_args, admin_user)
assert result.get('changed'), result
org = Organization.objects.get(name='foo')
assert result == {
"organization": "foo",
"state": "present",
"id": org.id,
"changed": True,
"name": "foo",
"id": org.id,
"invocation": {
"module_args": module_args
}
@ -30,10 +41,10 @@ def test_create_organization(run_module, admin_user):
@pytest.mark.django_db
def test_create_organization_with_venv(run_module, admin_user, mocker):
def test_create_organization_with_venv(run_converted_module, admin_user, mocker):
path = '/var/lib/awx/venv/custom-venv/foobar13489435/'
with mocker.patch('awx.main.models.mixins.get_custom_venv_choices', return_value=[path]):
result = run_module('tower_organization', {
result = run_converted_module('tower_organization', {
'name': 'foo',
'custom_virtualenv': path,
'state': 'present'
@ -44,8 +55,7 @@ def test_create_organization_with_venv(run_module, admin_user, mocker):
result.pop('invocation')
assert result == {
"organization": "foo",
"state": "present",
"name": "foo",
"id": org.id
}

View File

@ -0,0 +1,66 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import pytest
from awx.main.models import Organization, Team
@pytest.mark.django_db
def test_create_team(run_converted_module, admin_user):
org = Organization.objects.create(name='foo')
result = run_converted_module('tower_team', {
'name': 'foo_team',
'description': 'fooin around',
'state': 'present',
'organization': 'foo'
}, admin_user)
team = Team.objects.filter(name='foo_team').first()
result.pop('invocation')
assert result == {
"changed": True,
"name": "foo_team",
"id": team.id if team else None,
}
team = Team.objects.get(name='foo_team')
assert team.description == 'fooin around'
assert team.organization_id == org.id
@pytest.mark.django_db
def test_modify_team(run_converted_module, admin_user):
org = Organization.objects.create(name='foo')
team = Team.objects.create(
name='foo_team',
organization=org,
description='flat foo'
)
assert team.description == 'flat foo'
result = run_converted_module('tower_team', {
'name': 'foo_team',
'description': 'fooin around',
'organization': 'foo'
}, admin_user)
team.refresh_from_db()
result.pop('invocation')
assert result == {
"id": team.id,
"changed": True
}
assert team.description == 'fooin around'
# 2nd modification, should cause no change
result = run_converted_module('tower_team', {
'name': 'foo_team',
'description': 'fooin around',
'organization': 'foo'
}, admin_user)
result.pop('invocation')
assert result == {
"id": team.id,
"changed": False
}