mirror of
https://github.com/ansible/awx.git
synced 2026-05-19 14:57:39 -02:30
Merge pull request #6295 from beeankha/module_utils_updates
Update module_utils Functionality Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
@@ -222,6 +222,8 @@ class TowerModule(AnsibleModule):
|
|||||||
|
|
||||||
def get_all_endpoint(self, endpoint, *args, **kwargs):
|
def get_all_endpoint(self, endpoint, *args, **kwargs):
|
||||||
response = self.get_endpoint(endpoint, *args, **kwargs)
|
response = self.get_endpoint(endpoint, *args, **kwargs)
|
||||||
|
if 'next' not in response['json']:
|
||||||
|
raise RuntimeError('Expected list from API at {0}, got: {1}'.format(endpoint, response))
|
||||||
next_page = response['json']['next']
|
next_page = response['json']['next']
|
||||||
|
|
||||||
if response['json']['count'] > 10000:
|
if response['json']['count'] > 10000:
|
||||||
@@ -414,14 +416,14 @@ class TowerModule(AnsibleModule):
|
|||||||
except(Exception) as excinfo:
|
except(Exception) as excinfo:
|
||||||
self.fail_json(changed=False, msg='Failed check mode: {0}'.format(excinfo))
|
self.fail_json(changed=False, msg='Failed check mode: {0}'.format(excinfo))
|
||||||
|
|
||||||
def delete_if_needed(self, existing_item, handle_response=True, on_delete=None):
|
def delete_if_needed(self, existing_item, on_delete=None):
|
||||||
# This will exit from the module on its own unless handle_response is False.
|
# This will exit from the module on its own.
|
||||||
# If handle_response is True and the method successfully deletes an item and on_delete param is defined,
|
# If the method successfully deletes an item and on_delete param is defined,
|
||||||
# the on_delete parameter will be called as a method pasing in this object and the json from the response
|
# 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:
|
# This will return one of two things:
|
||||||
# 1. None if the existing_item is not defined (so no delete needs to happen)
|
# 1. None if the existing_item is not defined (so no delete needs to happen)
|
||||||
# 2. The response from Tower from calling the delete on the endpont. It's up to you to process the response and exit from the module
|
# 2. The response from Tower from calling the delete on the endpont. It's 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
|
# Note: common error codes from the Tower API can cause the module to fail
|
||||||
if existing_item:
|
if existing_item:
|
||||||
# If we have an item, we can try to delete it
|
# If we have an item, we can try to delete it
|
||||||
try:
|
try:
|
||||||
@@ -440,9 +442,7 @@ class TowerModule(AnsibleModule):
|
|||||||
|
|
||||||
response = self.delete_endpoint(item_url)
|
response = self.delete_endpoint(item_url)
|
||||||
|
|
||||||
if not handle_response:
|
if response['status_code'] in [202, 204]:
|
||||||
return response
|
|
||||||
elif response['status_code'] in [202, 204]:
|
|
||||||
if on_delete:
|
if on_delete:
|
||||||
on_delete(self, response['json'])
|
on_delete(self, response['json'])
|
||||||
self.json_output['changed'] = True
|
self.json_output['changed'] = True
|
||||||
@@ -460,21 +460,40 @@ class TowerModule(AnsibleModule):
|
|||||||
else:
|
else:
|
||||||
self.fail_json(msg="Unable to delete {0} {1}: {2}".format(item_type, item_name, response['status_code']))
|
self.fail_json(msg="Unable to delete {0} {1}: {2}".format(item_type, item_name, response['status_code']))
|
||||||
else:
|
else:
|
||||||
if not handle_response:
|
self.exit_json(**self.json_output)
|
||||||
return None
|
|
||||||
else:
|
|
||||||
self.exit_json(**self.json_output)
|
|
||||||
|
|
||||||
def create_if_needed(self, existing_item, new_item, endpoint, handle_response=True, on_create=None, item_type='unknown'):
|
def modify_associations(self, association_endpoint, new_association_list):
|
||||||
#
|
# First get the existing associations
|
||||||
# This will exit from the module on its own unless handle_response is False.
|
response = self.get_all_endpoint(association_endpoint)
|
||||||
# If handle_response is True and the method successfully creates an item and on_create param is defined,
|
existing_associated_ids = [association['id'] for association in response['json']['results']]
|
||||||
|
|
||||||
|
# Disassociate anything that is in existing_associated_ids but not in new_association_list
|
||||||
|
ids_to_remove = list(set(existing_associated_ids) - set(new_association_list))
|
||||||
|
for an_id in ids_to_remove:
|
||||||
|
response = self.post_endpoint(association_endpoint, **{'data': {'id': int(an_id), 'disassociate': True}})
|
||||||
|
if response['status_code'] == 204:
|
||||||
|
self.json_output['changed'] = True
|
||||||
|
else:
|
||||||
|
self.fail_json(msg="Failed to disassociate item {0}".format(response['json']['detail']))
|
||||||
|
|
||||||
|
# Associate anything that is in new_association_list but not in `association`
|
||||||
|
for an_id in list(set(new_association_list) - set(existing_associated_ids)):
|
||||||
|
response = self.post_endpoint(association_endpoint, **{'data': {'id': int(an_id)}})
|
||||||
|
if response['status_code'] == 204:
|
||||||
|
self.json_output['changed'] = True
|
||||||
|
else:
|
||||||
|
self.fail_json(msg="Failed to associate item {0}".format(response['json']['detail']))
|
||||||
|
|
||||||
|
def create_if_needed(self, existing_item, new_item, endpoint, on_create=None, item_type='unknown', associations=None):
|
||||||
|
|
||||||
|
# This will exit from the module on its own
|
||||||
|
# If the method successfully creates an item and on_create param is defined,
|
||||||
# the on_create parameter will be called as a method pasing in this object and the json from the response
|
# 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:
|
# This will return one of two things:
|
||||||
# 1. None if the existing_item is already defined (so no create needs to happen)
|
# 1. None if the existing_item is already defined (so no create needs to happen)
|
||||||
# 2. The response from Tower from calling the patch on the endpont. It's up to you to process the response and exit from the module
|
# 2. The response from Tower from calling the patch on the endpont. It's 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
|
# Note: common error codes from the Tower API can cause the module to fail
|
||||||
#
|
|
||||||
if not endpoint:
|
if not endpoint:
|
||||||
self.fail_json(msg="Unable to create new {0} due to missing endpoint".format(item_type))
|
self.fail_json(msg="Unable to create new {0} due to missing endpoint".format(item_type))
|
||||||
|
|
||||||
@@ -482,11 +501,7 @@ class TowerModule(AnsibleModule):
|
|||||||
try:
|
try:
|
||||||
existing_item['url']
|
existing_item['url']
|
||||||
except KeyError as ke:
|
except KeyError as ke:
|
||||||
self.fail_json(msg="Unable to process delete of item due to missing data {0}".format(ke))
|
self.fail_json(msg="Unable to process create of item due to missing data {0}".format(ke))
|
||||||
if not handle_response:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
self.exit_json(**self.json_output)
|
|
||||||
else:
|
else:
|
||||||
# If we don't have an exisitng_item, we can try to create it
|
# If we don't have an exisitng_item, we can try to create it
|
||||||
|
|
||||||
@@ -495,9 +510,7 @@ class TowerModule(AnsibleModule):
|
|||||||
item_name = new_item.get('name', 'unknown')
|
item_name = new_item.get('name', 'unknown')
|
||||||
|
|
||||||
response = self.post_endpoint(endpoint, **{'data': new_item})
|
response = self.post_endpoint(endpoint, **{'data': new_item})
|
||||||
if not handle_response:
|
if response['status_code'] == 201:
|
||||||
return response
|
|
||||||
elif response['status_code'] == 201:
|
|
||||||
self.json_output['name'] = 'unknown'
|
self.json_output['name'] = 'unknown'
|
||||||
if 'name' in response['json']:
|
if 'name' in response['json']:
|
||||||
self.json_output['name'] = response['json']['name']
|
self.json_output['name'] = response['json']['name']
|
||||||
@@ -506,10 +519,6 @@ class TowerModule(AnsibleModule):
|
|||||||
self.json_output['name'] = response['json']['username']
|
self.json_output['name'] = response['json']['username']
|
||||||
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 on_create is None:
|
|
||||||
self.exit_json(**self.json_output)
|
|
||||||
else:
|
|
||||||
on_create(self, response['json'])
|
|
||||||
else:
|
else:
|
||||||
if 'json' in response and '__all__' in response['json']:
|
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]))
|
self.fail_json(msg="Unable to create {0} {1}: {2}".format(item_type, item_name, response['json']['__all__'][0]))
|
||||||
@@ -518,16 +527,28 @@ class TowerModule(AnsibleModule):
|
|||||||
else:
|
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']))
|
||||||
|
|
||||||
def update_if_needed(self, existing_item, new_item, handle_response=True, on_update=None):
|
# Process any associations with this item
|
||||||
# This will exit from the module on its own unless handle_response is False.
|
if associations is not None:
|
||||||
# If handle_response is True and the method successfully updates an item and on_update param is defined,
|
for association_type in associations:
|
||||||
|
self.modify_associations(response, associations[association_type])
|
||||||
|
|
||||||
|
# If we have an on_create method and we actually changed something we can call on_create
|
||||||
|
if on_create is not None and self.json_output['changed']:
|
||||||
|
on_create(self, response['json'])
|
||||||
|
else:
|
||||||
|
self.exit_json(**self.json_output)
|
||||||
|
|
||||||
|
def update_if_needed(self, existing_item, new_item, on_update=None, associations=None):
|
||||||
|
# This will exit from the module on its own
|
||||||
|
# If the method successfully updates an item and on_update param is defined,
|
||||||
# the on_update parameter will be called as a method pasing in this object and the json from the response
|
# 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:
|
# This will return one of three things:
|
||||||
# 1. None if the existing_item does not need to be updated
|
# 1. None if the existing_item does not need to be updated
|
||||||
# 2. The response from Tower from patching to the endpoint. It's up to you to process the response and exit from the module.
|
# 2. The response from Tower from patching to the endpoint. It's up to you to process the response and exit from the module.
|
||||||
# 3. An ItemNotDefined exception, if the existing_item does not exist
|
# 3. 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
|
# Note: common error codes from the Tower API can cause the module to fail
|
||||||
if existing_item:
|
if existing_item:
|
||||||
|
|
||||||
# If we have an item, we can see if it needs an update
|
# If we have an item, we can see if it needs an update
|
||||||
try:
|
try:
|
||||||
item_url = existing_item['url']
|
item_url = existing_item['url']
|
||||||
@@ -540,6 +561,7 @@ class TowerModule(AnsibleModule):
|
|||||||
except KeyError as ke:
|
except KeyError as ke:
|
||||||
self.fail_json(msg="Unable to process update of item due to missing data {0}".format(ke))
|
self.fail_json(msg="Unable to process update of item due to missing data {0}".format(ke))
|
||||||
|
|
||||||
|
# Check to see if anything within the item requires the item to be updated
|
||||||
needs_update = False
|
needs_update = False
|
||||||
for field in new_item:
|
for field in new_item:
|
||||||
existing_field = existing_item.get(field, None)
|
existing_field = existing_item.get(field, None)
|
||||||
@@ -550,40 +572,37 @@ class TowerModule(AnsibleModule):
|
|||||||
needs_update = True
|
needs_update = True
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# If we decided the item needs to be updated, update it
|
||||||
|
self.json_output['id'] = item_id
|
||||||
if needs_update:
|
if needs_update:
|
||||||
response = self.patch_endpoint(item_url, **{'data': new_item})
|
response = self.patch_endpoint(item_url, **{'data': new_item})
|
||||||
if not handle_response:
|
if response['status_code'] == 200:
|
||||||
return response
|
|
||||||
elif response['status_code'] == 200:
|
|
||||||
self.json_output['changed'] = True
|
self.json_output['changed'] = True
|
||||||
self.json_output['id'] = item_id
|
|
||||||
if on_update is None:
|
|
||||||
self.exit_json(**self.json_output)
|
|
||||||
else:
|
|
||||||
on_update(self, response['json'])
|
|
||||||
elif 'json' in response and '__all__' in response['json']:
|
elif 'json' in response and '__all__' in response['json']:
|
||||||
self.fail_json(msg=response['json']['__all__'])
|
self.fail_json(msg=response['json']['__all__'])
|
||||||
else:
|
else:
|
||||||
self.fail_json(**{'msg': "Unable to update {0} {1}, see response".format(item_type, item_name), 'response': response})
|
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
|
|
||||||
|
|
||||||
# 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:
|
else:
|
||||||
if handle_response:
|
raise RuntimeError('update_if_needed called incorrectly without existing_item')
|
||||||
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):
|
# Process any associations with this item
|
||||||
|
if associations is not None:
|
||||||
|
for association_type, id_list in associations.items():
|
||||||
|
endpoint = '{0}{1}/'.format(item_url, association_type)
|
||||||
|
self.modify_associations(endpoint, id_list)
|
||||||
|
|
||||||
|
# If we change something and have an on_change call it
|
||||||
|
if on_update is not None and self.json_output['changed']:
|
||||||
|
on_update(self, response['json'])
|
||||||
|
else:
|
||||||
|
self.exit_json(**self.json_output)
|
||||||
|
|
||||||
|
def create_or_update_if_needed(self, existing_item, new_item, endpoint=None, item_type='unknown', on_create=None, on_update=None, associations=None):
|
||||||
if existing_item:
|
if existing_item:
|
||||||
return self.update_if_needed(existing_item, new_item, handle_response=handle_response, on_update=on_update)
|
return self.update_if_needed(existing_item, new_item, on_update=on_update, associations=associations)
|
||||||
else:
|
else:
|
||||||
return self.create_if_needed(existing_item, new_item, endpoint, handle_response=handle_response, on_create=on_create, item_type=item_type)
|
return self.create_if_needed(existing_item, new_item, endpoint, on_create=on_create, item_type=item_type, associations=associations)
|
||||||
|
|
||||||
def logout(self):
|
def logout(self):
|
||||||
if self.oauth_token_id is not None and self.username and self.password:
|
if self.oauth_token_id is not None and self.username and self.password:
|
||||||
|
|||||||
@@ -41,6 +41,18 @@ options:
|
|||||||
description:
|
description:
|
||||||
- Variables to use for the group.
|
- Variables to use for the group.
|
||||||
type: dict
|
type: dict
|
||||||
|
hosts:
|
||||||
|
description:
|
||||||
|
- List of hosts that should be put in this group.
|
||||||
|
required: False
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
groups:
|
||||||
|
description:
|
||||||
|
- List of groups that should be nested inside in this group.
|
||||||
|
required: False
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
state:
|
state:
|
||||||
description:
|
description:
|
||||||
- Desired state of the resource.
|
- Desired state of the resource.
|
||||||
@@ -85,6 +97,8 @@ def main():
|
|||||||
description=dict(required=False),
|
description=dict(required=False),
|
||||||
inventory=dict(required=True),
|
inventory=dict(required=True),
|
||||||
variables=dict(type='dict', required=False),
|
variables=dict(type='dict', required=False),
|
||||||
|
hosts=dict(type='list', elements='str'),
|
||||||
|
groups=dict(type='list', elements='str'),
|
||||||
state=dict(choices=['present', 'absent'], default='present'),
|
state=dict(choices=['present', 'absent'], default='present'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -120,12 +134,31 @@ def main():
|
|||||||
if variables is not None:
|
if variables is not None:
|
||||||
group_fields['variables'] = json.dumps(variables)
|
group_fields['variables'] = json.dumps(variables)
|
||||||
|
|
||||||
|
association_fields = {}
|
||||||
|
for resource, relationship in (('hosts', 'hosts'), ('groups', 'children')):
|
||||||
|
name_list = module.params.get(resource)
|
||||||
|
if name_list is None:
|
||||||
|
continue
|
||||||
|
id_list = []
|
||||||
|
for sub_name in name_list:
|
||||||
|
sub_obj = module.get_one(resource, **{
|
||||||
|
'data': {'inventory': inventory_id, 'name': sub_name}
|
||||||
|
})
|
||||||
|
if sub_obj is None:
|
||||||
|
module.fail_json(msg='Could not find {0} with name {1}'.format(resource, sub_name))
|
||||||
|
id_list.append(sub_obj['id'])
|
||||||
|
if id_list:
|
||||||
|
association_fields[relationship] = id_list
|
||||||
|
|
||||||
if state == 'absent':
|
if state == 'absent':
|
||||||
# If the state was absent we can let the module delete it if needed, the module will handle exiting from this
|
# If the state was absent we can let the module delete it if needed, the module will handle exiting from this
|
||||||
module.delete_if_needed(group)
|
module.delete_if_needed(group)
|
||||||
elif state == 'present':
|
elif state == 'present':
|
||||||
# If the state was present we can let the module build or update the existing group, this will return on its own
|
# If the state was present we can let the module build or update the existing group, this will return on its own
|
||||||
module.create_or_update_if_needed(group, group_fields, endpoint='groups', item_type='group')
|
module.create_or_update_if_needed(
|
||||||
|
group, group_fields, endpoint='groups', item_type='group',
|
||||||
|
associations=association_fields
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ __metaclass__ = type
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from awx.main.models import Organization, Inventory, Group
|
from awx.main.models import Organization, Inventory, Group, Host
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -32,6 +32,30 @@ def test_create_group(run_module, admin_user):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_associate_hosts_and_groups(run_module, admin_user, organization):
|
||||||
|
inv = Inventory.objects.create(name='test-inv', organization=organization)
|
||||||
|
group = Group.objects.create(name='Test Group', inventory=inv)
|
||||||
|
|
||||||
|
inv_hosts = [Host.objects.create(inventory=inv, name='foo{0}'.format(i)) for i in range(3)]
|
||||||
|
group.hosts.add(inv_hosts[0], inv_hosts[1])
|
||||||
|
|
||||||
|
child = Group.objects.create(inventory=inv, name='child_group')
|
||||||
|
|
||||||
|
result = run_module('tower_group', dict(
|
||||||
|
name='Test Group',
|
||||||
|
inventory='test-inv',
|
||||||
|
hosts=[inv_hosts[1].name, inv_hosts[2].name],
|
||||||
|
groups=[child.name],
|
||||||
|
state='present'
|
||||||
|
), admin_user)
|
||||||
|
assert not result.get('failed', False), result.get('msg', result)
|
||||||
|
assert result['changed'] is True
|
||||||
|
|
||||||
|
assert set(group.hosts.all()) == set([inv_hosts[1], inv_hosts[2]])
|
||||||
|
assert set(group.children.all()) == set([child])
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_tower_group_idempotent(run_module, admin_user):
|
def test_tower_group_idempotent(run_module, admin_user):
|
||||||
# https://github.com/ansible/ansible/issues/46803
|
# https://github.com/ansible/ansible/issues/46803
|
||||||
|
|||||||
Reference in New Issue
Block a user