From 9bcb5ef0c96381e87ffecd5dbf5734a2a1402eba Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Sat, 10 Oct 2020 04:06:49 -0500 Subject: [PATCH 01/25] intial update for workflow approval nodes --- .../plugins/module_utils/tower_api.py | 46 +++++++++++++++---- .../tower_workflow_job_template_node.py | 39 ++++++++++++++-- 2 files changed, 70 insertions(+), 15 deletions(-) diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index d41c32b772..2e03abbb1f 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -336,8 +336,8 @@ class TowerAPIModule(TowerModule): # If we have neither of these, then we can try un-authenticated access self.authenticated = True - - def delete_if_needed(self, existing_item, on_delete=None): + + def delete_if_needed(self, existing_item, on_delete=None, on_continue=None): # This will exit from the module on its own. # 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 @@ -363,6 +363,10 @@ class TowerAPIModule(TowerModule): self.json_output['changed'] = True self.json_output['id'] = item_id self.exit_json(**self.json_output) + if on_continue is not None: + return self.json_output + else: + self.exit_json(**self.json_output) else: 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])) @@ -375,7 +379,10 @@ class TowerAPIModule(TowerModule): else: self.fail_json(msg="Unable to delete {0} {1}: {2}".format(item_type, item_name, response['status_code'])) else: - self.exit_json(**self.json_output) + if on_continue is not None: + return None + else: + self.exit_json(**self.json_output) def modify_associations(self, association_endpoint, new_association_list): # if we got None instead of [] we are not modifying the association_list @@ -403,8 +410,8 @@ class TowerAPIModule(TowerModule): 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): - + def create_if_needed(self, existing_item, new_item, endpoint, on_create=None, on_continue=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 @@ -430,6 +437,7 @@ class TowerAPIModule(TowerModule): item_name = self.get_item_name(new_item, allow_unknown=True) response = self.post_endpoint(endpoint, **{'data': new_item}) + if response['status_code'] == 201: self.json_output['name'] = 'unknown' for key in ('name', 'username', 'identifier', 'hostname'): @@ -438,6 +446,15 @@ class TowerAPIModule(TowerModule): self.json_output['id'] = response['json']['id'] self.json_output['changed'] = True item_url = response['json']['url'] + # 200 is response from approval node creation + elif response['status_code'] == 200 and item_type == 'workflow_job_template_approval_node': + self.json_output['name'] = 'unknown' + for key in ('name', 'username', 'identifier', 'hostname'): + if key in response['json']: + self.json_output['name'] = response['json'][key] + self.json_output['id'] = response['json']['id'] + self.json_output['changed'] = True + item_url = response['json']['url'] 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])) @@ -455,6 +472,9 @@ class TowerAPIModule(TowerModule): # 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']) + elif on_continue is not None: + last_data = response['json'] + return last_data else: self.exit_json(**self.json_output) @@ -518,7 +538,7 @@ class TowerAPIModule(TowerModule): return True return False - def update_if_needed(self, existing_item, new_item, on_update=None, associations=None): + def update_if_needed(self, existing_item, new_item, on_update=None, on_continue=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 @@ -578,14 +598,20 @@ class TowerAPIModule(TowerModule): else: last_data = response['json'] on_update(self, last_data) + elif on_continue is not None: + if response is None: + last_data = existing_item + else: + last_data = response['json'] + return last_data 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): + + def create_or_update_if_needed(self, existing_item, new_item, endpoint=None, item_type='unknown', on_create=None, on_update=None, on_continue=None, associations=None): if existing_item: - return self.update_if_needed(existing_item, new_item, on_update=on_update, associations=associations) + return self.update_if_needed(existing_item, new_item, on_update=on_update, on_continue=on_continue, associations=associations) else: - return self.create_if_needed(existing_item, new_item, endpoint, on_create=on_create, item_type=item_type, associations=associations) + return self.create_if_needed(existing_item, new_item, endpoint, on_create=on_create, item_type=item_type, on_continue=on_continue, associations=associations) def logout(self): if self.authenticated and self.oauth_token_id: diff --git a/awx_collection/plugins/modules/tower_workflow_job_template_node.py b/awx_collection/plugins/modules/tower_workflow_job_template_node.py index 0705de6785..f1e65b4d75 100644 --- a/awx_collection/plugins/modules/tower_workflow_job_template_node.py +++ b/awx_collection/plugins/modules/tower_workflow_job_template_node.py @@ -166,6 +166,10 @@ def main(): identifier=dict(required=True), workflow_job_template=dict(required=True, aliases=['workflow']), organization=dict(), + approval_node=dict(type='bool'), + name=dict(), + description=dict(), + timeout=dict(type='int'), extra_data=dict(type='dict'), inventory=dict(), scm_branch=dict(), @@ -180,6 +184,7 @@ def main(): success_nodes=dict(type='list', elements='str'), always_nodes=dict(type='list', elements='str'), failure_nodes=dict(type='list', elements='str'), + approval_nodes=dict(type='list', elements='str'), credentials=dict(type='list', elements='str'), state=dict(choices=['present', 'absent'], default='present'), ) @@ -190,7 +195,10 @@ def main(): # Extract our parameters identifier = module.params.get('identifier') state = module.params.get('state') - + approval_node = module.params.get('approval_node') + name = module.params.get('name') + description = module.params.get('description') + timeout = module.params.get('timeout') new_fields = {} search_fields = {'identifier': identifier} @@ -237,7 +245,7 @@ def main(): new_fields[field_name] = field_val association_fields = {} - for association in ('always_nodes', 'success_nodes', 'failure_nodes', 'credentials'): + for association in ('always_nodes', 'success_nodes', 'failure_nodes', 'approval_nodes', 'credentials'): name_list = module.params.get(association) if name_list is None: continue @@ -264,10 +272,31 @@ def main(): # If the state was present and we can let the module build or update the existing item, this will return on its own module.create_or_update_if_needed( existing_item, new_fields, - endpoint='workflow_job_template_nodes', item_type='workflow_job_template_node', + endpoint='workflow_job_template_nodes', item_type='workflow_job_template_node', on_continue=approval_node, associations=association_fields ) - - + if approval_node: + # Set Approval Fields + new_fields = {} + if name is not None: + new_fields['name'] = name + if description is not None: + new_fields['description'] = description + if timeout is not None: + new_fields['timeout'] = timeout + # Find created approval node ID + search_fields = {'identifier': identifier} + search_fields['workflow_job_template'] = workflow_job_template_id + workflow_job_template_node = module.get_one('workflow_job_template_nodes', **{'data': search_fields}) + workflow_job_template_node_id = workflow_job_template_node['id'] + # Due to not able to lookup workflow_approval_templates, none existing item + existing_item = {} + # module.fail_json(msg="workflow_job_template_nodes/{0}/create_approval_template/".format(workflow_job_template_node_id)) + module.create_or_update_if_needed( + existing_item, new_fields, + endpoint='workflow_job_template_nodes/' + str(workflow_job_template_node_id) + '/create_approval_template/', item_type='workflow_job_template_approval_node', on_continue=approval_node, + associations=association_fields + ) + module.exit_json(**module.json_output) if __name__ == '__main__': main() From a2c8e3d87e324c5f920c089a6cce72562ca87dae Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Sat, 10 Oct 2020 11:53:50 -0500 Subject: [PATCH 02/25] clean up and tests added --- .../tower_workflow_job_template_node.py | 57 ++++++++++++------- .../targets/tower_job_wait/tasks/main.yml | 2 +- .../tasks/main.yml | 31 ++++++++++ 3 files changed, 70 insertions(+), 20 deletions(-) diff --git a/awx_collection/plugins/modules/tower_workflow_job_template_node.py b/awx_collection/plugins/modules/tower_workflow_job_template_node.py index f1e65b4d75..6b3b90a151 100644 --- a/awx_collection/plugins/modules/tower_workflow_job_template_node.py +++ b/awx_collection/plugins/modules/tower_workflow_job_template_node.py @@ -93,6 +93,24 @@ options: - Can be a job template, project, inventory source, etc. - Omit if creating an approval node (not yet implemented). type: str + approval_node: + description: + - A dictionary of Name, description, and timeout values for the approval node. + type: dict + suboptions: + name: + description: + - Name of this workflow approval template. + type: str + required: True + description: + description: + - Optional description of this workflow approval template. + type: str + timeout: + description: + - The amount of time (in seconds) before the approval node expires and fails. + type: int all_parents_must_converge: description: - If enabled then the node will only run if all of the parent nodes have met the criteria to reach this node @@ -166,10 +184,6 @@ def main(): identifier=dict(required=True), workflow_job_template=dict(required=True, aliases=['workflow']), organization=dict(), - approval_node=dict(type='bool'), - name=dict(), - description=dict(), - timeout=dict(type='int'), extra_data=dict(type='dict'), inventory=dict(), scm_branch=dict(), @@ -180,11 +194,11 @@ def main(): diff_mode=dict(type='bool'), verbosity=dict(choices=['0', '1', '2', '3', '4', '5']), unified_job_template=dict(), + approval_node=dict(type='dict'), all_parents_must_converge=dict(type='bool'), success_nodes=dict(type='list', elements='str'), always_nodes=dict(type='list', elements='str'), failure_nodes=dict(type='list', elements='str'), - approval_nodes=dict(type='list', elements='str'), credentials=dict(type='list', elements='str'), state=dict(choices=['present', 'absent'], default='present'), ) @@ -196,9 +210,6 @@ def main(): identifier = module.params.get('identifier') state = module.params.get('state') approval_node = module.params.get('approval_node') - name = module.params.get('name') - description = module.params.get('description') - timeout = module.params.get('timeout') new_fields = {} search_fields = {'identifier': identifier} @@ -245,7 +256,7 @@ def main(): new_fields[field_name] = field_val association_fields = {} - for association in ('always_nodes', 'success_nodes', 'failure_nodes', 'approval_nodes', 'credentials'): + for association in ('always_nodes', 'success_nodes', 'failure_nodes', 'credentials'): name_list = module.params.get(association) if name_list is None: continue @@ -275,23 +286,31 @@ def main(): endpoint='workflow_job_template_nodes', item_type='workflow_job_template_node', on_continue=approval_node, associations=association_fields ) + + # Create approval node unified template or update existing if approval_node: # Set Approval Fields new_fields = {} - if name is not None: - new_fields['name'] = name - if description is not None: - new_fields['description'] = description - if timeout is not None: - new_fields['timeout'] = timeout - # Find created approval node ID + + # Extract Parameters + if approval_node.get('name') is None: + module.fail_json(msg="Approval node name is required to create approval node.") + if approval_node.get('name') is not None: + new_fields['name'] = approval_node['name'] + if approval_node.get('description') is not None: + new_fields['description'] = approval_node['description'] + if approval_node.get('timeout') is not None: + new_fields['timeout'] = approval_node['timeout'] + + # Find created workflow node ID search_fields = {'identifier': identifier} search_fields['workflow_job_template'] = workflow_job_template_id workflow_job_template_node = module.get_one('workflow_job_template_nodes', **{'data': search_fields}) workflow_job_template_node_id = workflow_job_template_node['id'] - # Due to not able to lookup workflow_approval_templates, none existing item - existing_item = {} - # module.fail_json(msg="workflow_job_template_nodes/{0}/create_approval_template/".format(workflow_job_template_node_id)) + module.json_output['workflow_node_id'] = workflow_job_template_node_id + # Due to not able to lookup workflow_approval_templates, find the existing item in another place + if workflow_job_template_node['related'].get('unified_job_template') is not None: + existing_item = module.get_endpoint(workflow_job_template_node['related']['unified_job_template'])['json'] module.create_or_update_if_needed( existing_item, new_fields, endpoint='workflow_job_template_nodes/' + str(workflow_job_template_node_id) + '/create_approval_template/', item_type='workflow_job_template_approval_node', on_continue=approval_node, diff --git a/awx_collection/tests/integration/targets/tower_job_wait/tasks/main.yml b/awx_collection/tests/integration/targets/tower_job_wait/tasks/main.yml index b04fa62ff8..e56856da95 100644 --- a/awx_collection/tests/integration/targets/tower_job_wait/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_job_wait/tasks/main.yml @@ -100,7 +100,7 @@ # Make sure that we failed and that we have some data in our results - assert: that: - - "wait_results.msg == 'Monitoring aborted due to timeout' or 'Timeout waiting for job to finish.'" + - "wait_results.msg == 'Approval node name is required to create approval node.'" - "'id' in wait_results" - name: Async cancel the long running job diff --git a/awx_collection/tests/integration/targets/tower_workflow_job_template/tasks/main.yml b/awx_collection/tests/integration/targets/tower_workflow_job_template/tasks/main.yml index 393fbe33da..10a7a6f5ff 100644 --- a/awx_collection/tests/integration/targets/tower_workflow_job_template/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_workflow_job_template/tasks/main.yml @@ -9,6 +9,7 @@ demo_project_name: "AWX-Collection-tests-tower_workflow_job_template-proj-{{ test_id }}" jt1_name: "AWX-Collection-tests-tower_workflow_job_template-jt1-{{ test_id }}" jt2_name: "AWX-Collection-tests-tower_workflow_job_template-jt2-{{ test_id }}" + approval_node_name: "AWX-Collection-tests-tower_workflow_approval_node-{{ test_id }}" lab1: "AWX-Collection-tests-tower_job_template-lab1-{{ test_id }}" wfjt_name: "AWX-Collection-tests-tower_workflow_job_template-wfjt-{{ test_id }}" email_not: "AWX-Collection-tests-tower_job_template-email-not-{{ test_id }}" @@ -145,6 +146,36 @@ unified_job_template: "{{ jt1_name }}" workflow: "{{ wfjt_name }}" +- name: Fail if no name is set for approval + tower_workflow_job_template_node: + identifier: approval_test + approval_node: + description: "{{ approval_node_name }}" + workflow: "{{ wfjt_name }}" + register: no_name_results + ignore_errors: true + +- assert: + that: + - "no_name_results.msg == 'Approval node name is required to create approval node.'" + +- name: Create approval node + awx.awx.tower_workflow_job_template_node: + identifier: approval_test + approval_node: + name: "{{ approval_node_name }}" + timeout: 900 + workflow: test + +- name: Create link for root node + tower_workflow_job_template_node: + identifier: root + workflow: "{{ wfjt_name }}" + success_nodes: + - approval_test + always_nodes: + - leaf + - name: Add started notifications to workflow job template tower_workflow_job_template: name: "{{ wfjt_name }}" From 5655f766f04dae0f755d0bdbb0cc7d511240ad24 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Sat, 10 Oct 2020 11:56:00 -0500 Subject: [PATCH 03/25] linting --- awx_collection/plugins/module_utils/tower_api.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index 2e03abbb1f..4c180cead7 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -336,7 +336,7 @@ class TowerAPIModule(TowerModule): # If we have neither of these, then we can try un-authenticated access self.authenticated = True - + def delete_if_needed(self, existing_item, on_delete=None, on_continue=None): # This will exit from the module on its own. # If the method successfully deletes an item and on_delete param is defined, @@ -411,7 +411,7 @@ class TowerAPIModule(TowerModule): 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, on_continue=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 @@ -437,7 +437,7 @@ class TowerAPIModule(TowerModule): item_name = self.get_item_name(new_item, allow_unknown=True) response = self.post_endpoint(endpoint, **{'data': new_item}) - + if response['status_code'] == 201: self.json_output['name'] = 'unknown' for key in ('name', 'username', 'identifier', 'hostname'): @@ -447,7 +447,7 @@ class TowerAPIModule(TowerModule): self.json_output['changed'] = True item_url = response['json']['url'] # 200 is response from approval node creation - elif response['status_code'] == 200 and item_type == 'workflow_job_template_approval_node': + elif response['status_code'] == 200 and item_type == 'workflow_job_template_approval_node': self.json_output['name'] = 'unknown' for key in ('name', 'username', 'identifier', 'hostname'): if key in response['json']: @@ -606,7 +606,7 @@ class TowerAPIModule(TowerModule): return last_data 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, on_continue=None, associations=None): if existing_item: return self.update_if_needed(existing_item, new_item, on_update=on_update, on_continue=on_continue, associations=associations) From 7ffa70422a90ba3f4ac43a85443a8cff662f39d1 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Sat, 10 Oct 2020 11:57:34 -0500 Subject: [PATCH 04/25] remove typo changes --- .../tests/integration/targets/tower_job_wait/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx_collection/tests/integration/targets/tower_job_wait/tasks/main.yml b/awx_collection/tests/integration/targets/tower_job_wait/tasks/main.yml index e56856da95..b04fa62ff8 100644 --- a/awx_collection/tests/integration/targets/tower_job_wait/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_job_wait/tasks/main.yml @@ -100,7 +100,7 @@ # Make sure that we failed and that we have some data in our results - assert: that: - - "wait_results.msg == 'Approval node name is required to create approval node.'" + - "wait_results.msg == 'Monitoring aborted due to timeout' or 'Timeout waiting for job to finish.'" - "'id' in wait_results" - name: Async cancel the long running job From c3045f6a2982be7fc22928a9b4bf65027cb049d8 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Sat, 10 Oct 2020 13:02:58 -0500 Subject: [PATCH 05/25] update delete --- .../plugins/modules/tower_workflow_job_template_node.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/awx_collection/plugins/modules/tower_workflow_job_template_node.py b/awx_collection/plugins/modules/tower_workflow_job_template_node.py index 6b3b90a151..bee8a84a81 100644 --- a/awx_collection/plugins/modules/tower_workflow_job_template_node.py +++ b/awx_collection/plugins/modules/tower_workflow_job_template_node.py @@ -237,7 +237,7 @@ def main(): if state == 'absent': # 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(existing_item) + module.delete_if_needed(existing_item, on_continue=True,) unified_job_template = module.params.get('unified_job_template') if unified_job_template: @@ -311,6 +311,9 @@ def main(): # Due to not able to lookup workflow_approval_templates, find the existing item in another place if workflow_job_template_node['related'].get('unified_job_template') is not None: existing_item = module.get_endpoint(workflow_job_template_node['related']['unified_job_template'])['json'] + if state == 'absent': + # 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(existing_item) module.create_or_update_if_needed( existing_item, new_fields, endpoint='workflow_job_template_nodes/' + str(workflow_job_template_node_id) + '/create_approval_template/', item_type='workflow_job_template_approval_node', on_continue=approval_node, From c57ec1ea79555146a89f5cec36ef12b9efa1e825 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Sat, 10 Oct 2020 13:25:09 -0500 Subject: [PATCH 06/25] update delete --- .../tower_workflow_job_template_node.py | 86 +++++++++---------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/awx_collection/plugins/modules/tower_workflow_job_template_node.py b/awx_collection/plugins/modules/tower_workflow_job_template_node.py index bee8a84a81..af235b958e 100644 --- a/awx_collection/plugins/modules/tower_workflow_job_template_node.py +++ b/awx_collection/plugins/modules/tower_workflow_job_template_node.py @@ -238,54 +238,54 @@ def main(): if state == 'absent': # 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(existing_item, on_continue=True,) + else: + unified_job_template = module.params.get('unified_job_template') + if unified_job_template: + new_fields['unified_job_template'] = module.resolve_name_to_id('unified_job_templates', unified_job_template) - unified_job_template = module.params.get('unified_job_template') - if unified_job_template: - new_fields['unified_job_template'] = module.resolve_name_to_id('unified_job_templates', unified_job_template) + inventory = module.params.get('inventory') + if inventory: + new_fields['inventory'] = module.resolve_name_to_id('inventories', inventory) - inventory = module.params.get('inventory') - if inventory: - new_fields['inventory'] = module.resolve_name_to_id('inventories', inventory) + # Create the data that gets sent for create and update + for field_name in ( + 'identifier', 'extra_data', 'scm_branch', 'job_type', 'job_tags', 'skip_tags', + 'limit', 'diff_mode', 'verbosity', 'all_parents_must_converge',): + field_val = module.params.get(field_name) + if field_val: + new_fields[field_name] = field_val - # Create the data that gets sent for create and update - for field_name in ( - 'identifier', 'extra_data', 'scm_branch', 'job_type', 'job_tags', 'skip_tags', - 'limit', 'diff_mode', 'verbosity', 'all_parents_must_converge',): - field_val = module.params.get(field_name) - if field_val: - new_fields[field_name] = field_val + association_fields = {} + for association in ('always_nodes', 'success_nodes', 'failure_nodes', 'credentials'): + name_list = module.params.get(association) + if name_list is None: + continue + id_list = [] + for sub_name in name_list: + if association == 'credentials': + endpoint = 'credentials' + lookup_data = {'name': sub_name} + else: + endpoint = 'workflow_job_template_nodes' + lookup_data = {'identifier': sub_name} + if workflow_job_template_id: + lookup_data['workflow_job_template'] = workflow_job_template_id + sub_obj = module.get_one(endpoint, **{'data': lookup_data}) + if sub_obj is None: + module.fail_json(msg='Could not find {0} entry with name {1}'.format(association, sub_name)) + id_list.append(sub_obj['id']) + if id_list: + association_fields[association] = id_list - association_fields = {} - for association in ('always_nodes', 'success_nodes', 'failure_nodes', 'credentials'): - name_list = module.params.get(association) - if name_list is None: - continue - id_list = [] - for sub_name in name_list: - if association == 'credentials': - endpoint = 'credentials' - lookup_data = {'name': sub_name} - else: - endpoint = 'workflow_job_template_nodes' - lookup_data = {'identifier': sub_name} - if workflow_job_template_id: - lookup_data['workflow_job_template'] = workflow_job_template_id - sub_obj = module.get_one(endpoint, **{'data': lookup_data}) - if sub_obj is None: - module.fail_json(msg='Could not find {0} entry with name {1}'.format(association, sub_name)) - id_list.append(sub_obj['id']) - if id_list: - association_fields[association] = id_list + # In the case of a new object, the utils need to know it is a node + new_fields['type'] = 'workflow_job_template_node' - # In the case of a new object, the utils need to know it is a node - new_fields['type'] = 'workflow_job_template_node' - - # If the state was present and we can let the module build or update the existing item, this will return on its own - module.create_or_update_if_needed( - existing_item, new_fields, - endpoint='workflow_job_template_nodes', item_type='workflow_job_template_node', on_continue=approval_node, - associations=association_fields - ) + # If the state was present and we can let the module build or update the existing item, this will return on its own + module.create_or_update_if_needed( + existing_item, new_fields, + endpoint='workflow_job_template_nodes', item_type='workflow_job_template_node', on_continue=approval_node, + associations=association_fields + ) # Create approval node unified template or update existing if approval_node: From c205ee81f0cb66e384b4f100b5078a2765f3345d Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Mon, 12 Oct 2020 00:17:18 -0500 Subject: [PATCH 07/25] update delete --- .../tower_workflow_job_template_node.py | 97 ++++++++++--------- 1 file changed, 50 insertions(+), 47 deletions(-) diff --git a/awx_collection/plugins/modules/tower_workflow_job_template_node.py b/awx_collection/plugins/modules/tower_workflow_job_template_node.py index af235b958e..173f3b1173 100644 --- a/awx_collection/plugins/modules/tower_workflow_job_template_node.py +++ b/awx_collection/plugins/modules/tower_workflow_job_template_node.py @@ -236,56 +236,62 @@ def main(): existing_item = module.get_one('workflow_job_template_nodes', **{'data': search_fields}) if state == 'absent': + # Look up existing approval node for deletion + if existing_item['related'].get('unified_job_template') is not None: + existing_approval_node = module.get_endpoint(existing_item['related']['unified_job_template'])['json'] # 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(existing_item, on_continue=True,) - else: - unified_job_template = module.params.get('unified_job_template') - if unified_job_template: - new_fields['unified_job_template'] = module.resolve_name_to_id('unified_job_templates', unified_job_template) + # Delete the Approval Node + module.delete_if_needed(existing_approval_node, on_continue=True,) + # Delete Workflow Node + module.delete_if_needed(existing_item) - inventory = module.params.get('inventory') - if inventory: - new_fields['inventory'] = module.resolve_name_to_id('inventories', inventory) + unified_job_template = module.params.get('unified_job_template') + if unified_job_template: + new_fields['unified_job_template'] = module.resolve_name_to_id('unified_job_templates', unified_job_template) - # Create the data that gets sent for create and update - for field_name in ( - 'identifier', 'extra_data', 'scm_branch', 'job_type', 'job_tags', 'skip_tags', - 'limit', 'diff_mode', 'verbosity', 'all_parents_must_converge',): - field_val = module.params.get(field_name) - if field_val: - new_fields[field_name] = field_val + inventory = module.params.get('inventory') + if inventory: + new_fields['inventory'] = module.resolve_name_to_id('inventories', inventory) - association_fields = {} - for association in ('always_nodes', 'success_nodes', 'failure_nodes', 'credentials'): - name_list = module.params.get(association) - if name_list is None: - continue - id_list = [] - for sub_name in name_list: - if association == 'credentials': - endpoint = 'credentials' - lookup_data = {'name': sub_name} - else: - endpoint = 'workflow_job_template_nodes' - lookup_data = {'identifier': sub_name} - if workflow_job_template_id: - lookup_data['workflow_job_template'] = workflow_job_template_id - sub_obj = module.get_one(endpoint, **{'data': lookup_data}) - if sub_obj is None: - module.fail_json(msg='Could not find {0} entry with name {1}'.format(association, sub_name)) - id_list.append(sub_obj['id']) - if id_list: - association_fields[association] = id_list + # Create the data that gets sent for create and update + for field_name in ( + 'identifier', 'extra_data', 'scm_branch', 'job_type', 'job_tags', 'skip_tags', + 'limit', 'diff_mode', 'verbosity', 'all_parents_must_converge',): + field_val = module.params.get(field_name) + if field_val: + new_fields[field_name] = field_val - # In the case of a new object, the utils need to know it is a node - new_fields['type'] = 'workflow_job_template_node' + association_fields = {} + for association in ('always_nodes', 'success_nodes', 'failure_nodes', 'credentials'): + name_list = module.params.get(association) + if name_list is None: + continue + id_list = [] + for sub_name in name_list: + if association == 'credentials': + endpoint = 'credentials' + lookup_data = {'name': sub_name} + else: + endpoint = 'workflow_job_template_nodes' + lookup_data = {'identifier': sub_name} + if workflow_job_template_id: + lookup_data['workflow_job_template'] = workflow_job_template_id + sub_obj = module.get_one(endpoint, **{'data': lookup_data}) + if sub_obj is None: + module.fail_json(msg='Could not find {0} entry with name {1}'.format(association, sub_name)) + id_list.append(sub_obj['id']) + if id_list: + association_fields[association] = id_list - # If the state was present and we can let the module build or update the existing item, this will return on its own - module.create_or_update_if_needed( - existing_item, new_fields, - endpoint='workflow_job_template_nodes', item_type='workflow_job_template_node', on_continue=approval_node, - associations=association_fields - ) + # In the case of a new object, the utils need to know it is a node + new_fields['type'] = 'workflow_job_template_node' + + # If the state was present and we can let the module build or update the existing item, this will return on its own + module.create_or_update_if_needed( + existing_item, new_fields, + endpoint='workflow_job_template_nodes', item_type='workflow_job_template_node', on_continue=approval_node, + associations=association_fields + ) # Create approval node unified template or update existing if approval_node: @@ -311,9 +317,6 @@ def main(): # Due to not able to lookup workflow_approval_templates, find the existing item in another place if workflow_job_template_node['related'].get('unified_job_template') is not None: existing_item = module.get_endpoint(workflow_job_template_node['related']['unified_job_template'])['json'] - if state == 'absent': - # 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(existing_item) module.create_or_update_if_needed( existing_item, new_fields, endpoint='workflow_job_template_nodes/' + str(workflow_job_template_node_id) + '/create_approval_template/', item_type='workflow_job_template_approval_node', on_continue=approval_node, From 226046dd16dd3455a64ead4d063c0bf58a752ee8 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Mon, 12 Oct 2020 12:03:37 -0500 Subject: [PATCH 08/25] update --- awx_collection/test/awx/test_completeness.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx_collection/test/awx/test_completeness.py b/awx_collection/test/awx/test_completeness.py index 3626c42239..08bbd8b0ad 100644 --- a/awx_collection/test/awx/test_completeness.py +++ b/awx_collection/test/awx/test_completeness.py @@ -44,8 +44,8 @@ no_api_parameter_ok = { # We take an organization here to help with the lookups only 'tower_job_template': ['survey_spec', 'organization'], 'tower_inventory_source': ['organization'], - # Organization is how we are looking up job templates - 'tower_workflow_job_template_node': ['organization'], + # Organization is how we are looking up job templates, Approval node is for workflow_approval_templates + 'tower_workflow_job_template_node': ['organization', 'approval_node'], # Survey is how we handle associations 'tower_workflow_job_template': ['survey'], # ad hoc commands support interval and timeout since its more like tower_job_launc From 6b27ee6a3cb5042dacd1d5547c71308bb4d52a04 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Tue, 13 Oct 2020 14:24:22 -0500 Subject: [PATCH 09/25] updated workflow name --- .../targets/tower_workflow_job_template/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx_collection/tests/integration/targets/tower_workflow_job_template/tasks/main.yml b/awx_collection/tests/integration/targets/tower_workflow_job_template/tasks/main.yml index 10a7a6f5ff..3338a68c12 100644 --- a/awx_collection/tests/integration/targets/tower_workflow_job_template/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_workflow_job_template/tasks/main.yml @@ -165,7 +165,7 @@ approval_node: name: "{{ approval_node_name }}" timeout: 900 - workflow: test + workflow: "{{ wfjt_name }}" - name: Create link for root node tower_workflow_job_template_node: From e16a910062290674e19e7c2bf550d5e5a0e4ec99 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Tue, 13 Oct 2020 14:24:56 -0500 Subject: [PATCH 10/25] updated workflow task name --- .../targets/tower_workflow_job_template/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx_collection/tests/integration/targets/tower_workflow_job_template/tasks/main.yml b/awx_collection/tests/integration/targets/tower_workflow_job_template/tasks/main.yml index 3338a68c12..7df132c139 100644 --- a/awx_collection/tests/integration/targets/tower_workflow_job_template/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_workflow_job_template/tasks/main.yml @@ -160,7 +160,7 @@ - "no_name_results.msg == 'Approval node name is required to create approval node.'" - name: Create approval node - awx.awx.tower_workflow_job_template_node: + tower_workflow_job_template_node: identifier: approval_test approval_node: name: "{{ approval_node_name }}" From 51eb4e6d6bff0a6e50357e518bed59ddb0c88001 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Tue, 13 Oct 2020 18:17:22 -0500 Subject: [PATCH 11/25] update to auto_exit, add tests, add mutual exclusive parameters --- .../plugins/module_utils/tower_api.py | 36 +++++++++---------- .../tower_workflow_job_template_node.py | 22 ++++++------ .../tasks/main.yml | 8 +++++ 3 files changed, 38 insertions(+), 28 deletions(-) diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index 4c180cead7..30879a332b 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -337,7 +337,7 @@ class TowerAPIModule(TowerModule): # If we have neither of these, then we can try un-authenticated access self.authenticated = True - def delete_if_needed(self, existing_item, on_delete=None, on_continue=None): + def delete_if_needed(self, existing_item, on_delete=None, auto_exit=True): # This will exit from the module on its own. # 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 @@ -363,10 +363,10 @@ class TowerAPIModule(TowerModule): self.json_output['changed'] = True self.json_output['id'] = item_id self.exit_json(**self.json_output) - if on_continue is not None: - return self.json_output - else: + if auto_exit: self.exit_json(**self.json_output) + else: + return self.json_output ######### else: 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])) @@ -379,10 +379,10 @@ class TowerAPIModule(TowerModule): else: self.fail_json(msg="Unable to delete {0} {1}: {2}".format(item_type, item_name, response['status_code'])) else: - if on_continue is not None: - return None - else: + if auto_exit: self.exit_json(**self.json_output) + else: + return None def modify_associations(self, association_endpoint, new_association_list): # if we got None instead of [] we are not modifying the association_list @@ -410,7 +410,7 @@ class TowerAPIModule(TowerModule): 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, on_continue=None, item_type='unknown', associations=None): + def create_if_needed(self, existing_item, new_item, endpoint, on_create=None, auto_exit=True, 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, @@ -472,11 +472,11 @@ class TowerAPIModule(TowerModule): # 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']) - elif on_continue is not None: + elif auto_exit: + self.exit_json(**self.json_output) + else: last_data = response['json'] return last_data - else: - self.exit_json(**self.json_output) def _encrypted_changed_warning(self, field, old, warning=False): if not warning: @@ -538,7 +538,7 @@ class TowerAPIModule(TowerModule): return True return False - def update_if_needed(self, existing_item, new_item, on_update=None, on_continue=None, associations=None): + def update_if_needed(self, existing_item, new_item, on_update=None, auto_exit=True, 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 @@ -598,20 +598,20 @@ class TowerAPIModule(TowerModule): else: last_data = response['json'] on_update(self, last_data) - elif on_continue is not None: + elif auto_exit: + self.exit_json(**self.json_output) + else: if response is None: last_data = existing_item else: last_data = response['json'] return last_data - 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, on_continue=None, associations=None): + def create_or_update_if_needed(self, existing_item, new_item, endpoint=None, item_type='unknown', on_create=None, on_update=None, auto_exit=True, associations=None): if existing_item: - return self.update_if_needed(existing_item, new_item, on_update=on_update, on_continue=on_continue, associations=associations) + return self.update_if_needed(existing_item, new_item, on_update=on_update, auto_exit=auto_exit, associations=associations) else: - return self.create_if_needed(existing_item, new_item, endpoint, on_create=on_create, item_type=item_type, on_continue=on_continue, associations=associations) + return self.create_if_needed(existing_item, new_item, endpoint, on_create=on_create, item_type=item_type, auto_exit=auto_exit, associations=associations) def logout(self): if self.authenticated and self.oauth_token_id: diff --git a/awx_collection/plugins/modules/tower_workflow_job_template_node.py b/awx_collection/plugins/modules/tower_workflow_job_template_node.py index 173f3b1173..dc3ac1cf8a 100644 --- a/awx_collection/plugins/modules/tower_workflow_job_template_node.py +++ b/awx_collection/plugins/modules/tower_workflow_job_template_node.py @@ -91,11 +91,13 @@ options: description: - Name of unified job template to run in the workflow. - Can be a job template, project, inventory source, etc. - - Omit if creating an approval node (not yet implemented). + - Omit if creating an approval node. + - This parameter is mutually exclusive with C(approval_node). type: str approval_node: description: - A dictionary of Name, description, and timeout values for the approval node. + - This parameter is mutually exclusive with C(unified_job_template). type: dict suboptions: name: @@ -202,9 +204,15 @@ def main(): credentials=dict(type='list', elements='str'), state=dict(choices=['present', 'absent'], default='present'), ) + mutually_exclusive = [("unified_job_template", "approval_node")] + required_one_of = [["unified_job_template", "approval_node", "success_nodes", "always_nodes", "failure_nodes"]] # Create a module for ourselves - module = TowerAPIModule(argument_spec=argument_spec) + module = TowerAPIModule( + argument_spec=argument_spec, + mutually_exclusive=mutually_exclusive, + required_one_of=required_one_of, + ) # Extract our parameters identifier = module.params.get('identifier') @@ -236,13 +244,7 @@ def main(): existing_item = module.get_one('workflow_job_template_nodes', **{'data': search_fields}) if state == 'absent': - # Look up existing approval node for deletion - if existing_item['related'].get('unified_job_template') is not None: - existing_approval_node = module.get_endpoint(existing_item['related']['unified_job_template'])['json'] # If the state was absent we can let the module delete it if needed, the module will handle exiting from this - # Delete the Approval Node - module.delete_if_needed(existing_approval_node, on_continue=True,) - # Delete Workflow Node module.delete_if_needed(existing_item) unified_job_template = module.params.get('unified_job_template') @@ -289,7 +291,7 @@ def main(): # If the state was present and we can let the module build or update the existing item, this will return on its own module.create_or_update_if_needed( existing_item, new_fields, - endpoint='workflow_job_template_nodes', item_type='workflow_job_template_node', on_continue=approval_node, + endpoint='workflow_job_template_nodes', item_type='workflow_job_template_node', auto_exit=not approval_node, associations=association_fields ) @@ -319,7 +321,7 @@ def main(): existing_item = module.get_endpoint(workflow_job_template_node['related']['unified_job_template'])['json'] module.create_or_update_if_needed( existing_item, new_fields, - endpoint='workflow_job_template_nodes/' + str(workflow_job_template_node_id) + '/create_approval_template/', item_type='workflow_job_template_approval_node', on_continue=approval_node, + endpoint='workflow_job_template_nodes/' + str(workflow_job_template_node_id) + '/create_approval_template/', item_type='workflow_job_template_approval_node', associations=association_fields ) module.exit_json(**module.json_output) diff --git a/awx_collection/tests/integration/targets/tower_workflow_job_template/tasks/main.yml b/awx_collection/tests/integration/targets/tower_workflow_job_template/tasks/main.yml index 7df132c139..5c6b4911be 100644 --- a/awx_collection/tests/integration/targets/tower_workflow_job_template/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_workflow_job_template/tasks/main.yml @@ -176,6 +176,14 @@ always_nodes: - leaf +- name: Delete approval node + awx.awx.tower_workflow_job_template_node: + identifier: approval_test + approval_node: + name: "{{ approval_node_name }}" + state: absent + workflow: "{{ wfjt_name }}" + - name: Add started notifications to workflow job template tower_workflow_job_template: name: "{{ wfjt_name }}" From c72c335b0ce600258e3c0e3459ea35f8e9d45da4 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Tue, 13 Oct 2020 18:27:27 -0500 Subject: [PATCH 12/25] fix pep8 issues --- awx_collection/plugins/module_utils/tower_api.py | 7 +++++-- .../plugins/modules/tower_workflow_job_template_node.py | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index 30879a332b..b609f76e67 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -607,11 +607,14 @@ class TowerAPIModule(TowerModule): last_data = response['json'] return last_data - def create_or_update_if_needed(self, existing_item, new_item, endpoint=None, item_type='unknown', on_create=None, on_update=None, auto_exit=True, associations=None): + def create_or_update_if_needed( + self, existing_item, new_item, endpoint=None, item_type='unknown', on_create=None, on_update=None, auto_exit=True, associations=None + ): if existing_item: return self.update_if_needed(existing_item, new_item, on_update=on_update, auto_exit=auto_exit, associations=associations) else: - return self.create_if_needed(existing_item, new_item, endpoint, on_create=on_create, item_type=item_type, auto_exit=auto_exit, associations=associations) + return self.create_if_needed( + existing_item, new_item, endpoint, on_create=on_create, item_type=item_type, auto_exit=auto_exit, associations=associations) def logout(self): if self.authenticated and self.oauth_token_id: diff --git a/awx_collection/plugins/modules/tower_workflow_job_template_node.py b/awx_collection/plugins/modules/tower_workflow_job_template_node.py index dc3ac1cf8a..71e312c625 100644 --- a/awx_collection/plugins/modules/tower_workflow_job_template_node.py +++ b/awx_collection/plugins/modules/tower_workflow_job_template_node.py @@ -319,11 +319,14 @@ def main(): # Due to not able to lookup workflow_approval_templates, find the existing item in another place if workflow_job_template_node['related'].get('unified_job_template') is not None: existing_item = module.get_endpoint(workflow_job_template_node['related']['unified_job_template'])['json'] + approval_endpoint = 'workflow_job_template_nodes/' + str(workflow_job_template_node_id) + '/create_approval_template/' module.create_or_update_if_needed( existing_item, new_fields, - endpoint='workflow_job_template_nodes/' + str(workflow_job_template_node_id) + '/create_approval_template/', item_type='workflow_job_template_approval_node', + endpoint=approval_endpoint, item_type='workflow_job_template_approval_node', associations=association_fields ) + + module.exit_json(**module.json_output) if __name__ == '__main__': main() From 3b903a7459ce586d1f0fe51bcf991e88c734d821 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Tue, 13 Oct 2020 18:41:52 -0500 Subject: [PATCH 13/25] fix typo --- .../targets/tower_workflow_job_template/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx_collection/tests/integration/targets/tower_workflow_job_template/tasks/main.yml b/awx_collection/tests/integration/targets/tower_workflow_job_template/tasks/main.yml index 5c6b4911be..1542b42029 100644 --- a/awx_collection/tests/integration/targets/tower_workflow_job_template/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_workflow_job_template/tasks/main.yml @@ -177,7 +177,7 @@ - leaf - name: Delete approval node - awx.awx.tower_workflow_job_template_node: + tower_workflow_job_template_node: identifier: approval_test approval_node: name: "{{ approval_node_name }}" From d9184e02f5139a07c8c4d34b6c6e45c50e14d164 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Tue, 13 Oct 2020 20:40:35 -0500 Subject: [PATCH 14/25] update pytest --- awx_collection/test/awx/test_workflow_job_template_node.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/awx_collection/test/awx/test_workflow_job_template_node.py b/awx_collection/test/awx/test_workflow_job_template_node.py index 935b01541a..33fdc5fb58 100644 --- a/awx_collection/test/awx/test_workflow_job_template_node.py +++ b/awx_collection/test/awx/test_workflow_job_template_node.py @@ -54,14 +54,13 @@ def test_create_workflow_job_template_node(run_module, admin_user, wfjt, job_tem @pytest.mark.django_db def test_create_workflow_job_template_node_no_template(run_module, admin_user, wfjt, job_template): - """This is a part of the API contract for creating approval nodes - and at some point in the future, tha feature will be supported by the collection - """ + """This is a part of the API contract for creating approval nodes""" this_identifier = '42🐉' result = run_module('tower_workflow_job_template_node', { 'identifier': this_identifier, 'workflow_job_template': wfjt.name, 'organization': wfjt.organization.name, + 'approval_node': {'name': 'foo-jt'} }, admin_user) assert not result.get('failed', False), result.get('msg', result) assert result.get('changed', False), result From 237727dd62df3ab06fdce433b55742ac4850a806 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Tue, 13 Oct 2020 20:56:50 -0500 Subject: [PATCH 15/25] update pytest --- awx_collection/test/awx/test_workflow_job_template_node.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/awx_collection/test/awx/test_workflow_job_template_node.py b/awx_collection/test/awx/test_workflow_job_template_node.py index 33fdc5fb58..7fdc606dc5 100644 --- a/awx_collection/test/awx/test_workflow_job_template_node.py +++ b/awx_collection/test/awx/test_workflow_job_template_node.py @@ -65,10 +65,7 @@ def test_create_workflow_job_template_node_no_template(run_module, admin_user, w assert not result.get('failed', False), result.get('msg', result) assert result.get('changed', False), result - node = WorkflowJobTemplateNode.objects.get(pk=result['id']) - # node = WorkflowJobTemplateNode.objects.first() - - assert result['id'] == node.id + node = WorkflowJobTemplateNode.objects.get(identifier=this_identifier) assert node.identifier == this_identifier assert node.workflow_job_template_id == wfjt.id From 0fee6d8b865b2d94e8bf399bd37e6f64f2464cf0 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Fri, 16 Oct 2020 10:52:56 -0500 Subject: [PATCH 16/25] update test --- awx_collection/test/awx/test_workflow_job_template_node.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/awx_collection/test/awx/test_workflow_job_template_node.py b/awx_collection/test/awx/test_workflow_job_template_node.py index 7fdc606dc5..b603ea2fd8 100644 --- a/awx_collection/test/awx/test_workflow_job_template_node.py +++ b/awx_collection/test/awx/test_workflow_job_template_node.py @@ -4,7 +4,7 @@ __metaclass__ = type import pytest -from awx.main.models import WorkflowJobTemplateNode, WorkflowJobTemplate, JobTemplate +from awx.main.models import WorkflowJobTemplateNode, WorkflowJobTemplate, JobTemplate, UnifiedJobTemplate @pytest.fixture @@ -60,16 +60,17 @@ def test_create_workflow_job_template_node_no_template(run_module, admin_user, w 'identifier': this_identifier, 'workflow_job_template': wfjt.name, 'organization': wfjt.organization.name, - 'approval_node': {'name': 'foo-jt'} + 'approval_node': {'name': 'foo-jt-approval'} }, admin_user) assert not result.get('failed', False), result.get('msg', result) assert result.get('changed', False), result node = WorkflowJobTemplateNode.objects.get(identifier=this_identifier) + approval_node = UnifiedJobTemplate.objects.get(name='foo-jt-approval') assert node.identifier == this_identifier assert node.workflow_job_template_id == wfjt.id - assert node.unified_job_template_id is None + assert node.unified_job_template_id is approval_node.id @pytest.mark.django_db From ad1937b3945b8168affe7247d0019a2a6500367f Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Fri, 16 Oct 2020 11:16:15 -0500 Subject: [PATCH 17/25] update test --- awx_collection/test/awx/test_workflow_job_template_node.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/awx_collection/test/awx/test_workflow_job_template_node.py b/awx_collection/test/awx/test_workflow_job_template_node.py index b603ea2fd8..6e3d7db23a 100644 --- a/awx_collection/test/awx/test_workflow_job_template_node.py +++ b/awx_collection/test/awx/test_workflow_job_template_node.py @@ -68,6 +68,8 @@ def test_create_workflow_job_template_node_no_template(run_module, admin_user, w node = WorkflowJobTemplateNode.objects.get(identifier=this_identifier) approval_node = UnifiedJobTemplate.objects.get(name='foo-jt-approval') + assert result['id'] == node.id + assert node.identifier == this_identifier assert node.workflow_job_template_id == wfjt.id assert node.unified_job_template_id is approval_node.id From 7ca2f33112b7655f10c723a45d148ddf7c409d22 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Fri, 16 Oct 2020 11:22:12 -0500 Subject: [PATCH 18/25] update test --- awx_collection/test/awx/test_workflow_job_template_node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx_collection/test/awx/test_workflow_job_template_node.py b/awx_collection/test/awx/test_workflow_job_template_node.py index 6e3d7db23a..1641aaf153 100644 --- a/awx_collection/test/awx/test_workflow_job_template_node.py +++ b/awx_collection/test/awx/test_workflow_job_template_node.py @@ -68,7 +68,7 @@ def test_create_workflow_job_template_node_no_template(run_module, admin_user, w node = WorkflowJobTemplateNode.objects.get(identifier=this_identifier) approval_node = UnifiedJobTemplate.objects.get(name='foo-jt-approval') - assert result['id'] == node.id + assert result['id'] == approval_node.id assert node.identifier == this_identifier assert node.workflow_job_template_id == wfjt.id From 862cd974ffc96c5ee7e01e1beed0b6ad1e9d2925 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Mon, 19 Oct 2020 12:12:08 -0500 Subject: [PATCH 19/25] linting --- awx_collection/plugins/module_utils/tower_api.py | 2 +- .../plugins/modules/tower_workflow_job_template_node.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index b609f76e67..643fbd05fc 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -366,7 +366,7 @@ class TowerAPIModule(TowerModule): if auto_exit: self.exit_json(**self.json_output) else: - return self.json_output ######### + return self.json_output else: 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])) diff --git a/awx_collection/plugins/modules/tower_workflow_job_template_node.py b/awx_collection/plugins/modules/tower_workflow_job_template_node.py index 71e312c625..1718af91e8 100644 --- a/awx_collection/plugins/modules/tower_workflow_job_template_node.py +++ b/awx_collection/plugins/modules/tower_workflow_job_template_node.py @@ -325,8 +325,8 @@ def main(): endpoint=approval_endpoint, item_type='workflow_job_template_approval_node', associations=association_fields ) - - module.exit_json(**module.json_output) + + if __name__ == '__main__': main() From 91ef686fe05daf436616390d7dba321529b8ec01 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Mon, 19 Oct 2020 15:11:19 -0500 Subject: [PATCH 20/25] update to incorporate requested changes, change approve to 201 response. --- awx/api/views/__init__.py | 2 +- awx_collection/plugins/module_utils/tower_api.py | 12 ++---------- .../modules/tower_workflow_job_template_node.py | 2 +- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 4f436c8f0e..7b97d82b66 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -3043,7 +3043,7 @@ class WorkflowJobTemplateNodeCreateApproval(RetrieveAPIView): approval_template, context=self.get_serializer_context() ).data - return Response(data, status=status.HTTP_200_OK) + return Response(data, status=status.HTTP_201_OK) def check_permissions(self, request): obj = self.get_object().workflow_job_template diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index 643fbd05fc..5c0da5a8cb 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -382,7 +382,7 @@ class TowerAPIModule(TowerModule): if auto_exit: self.exit_json(**self.json_output) else: - return None + return self.json_output def modify_associations(self, association_endpoint, new_association_list): # if we got None instead of [] we are not modifying the association_list @@ -438,16 +438,8 @@ class TowerAPIModule(TowerModule): response = self.post_endpoint(endpoint, **{'data': new_item}) - if response['status_code'] == 201: - self.json_output['name'] = 'unknown' - for key in ('name', 'username', 'identifier', 'hostname'): - if key in response['json']: - self.json_output['name'] = response['json'][key] - self.json_output['id'] = response['json']['id'] - self.json_output['changed'] = True - item_url = response['json']['url'] # 200 is response from approval node creation - elif response['status_code'] == 200 and item_type == 'workflow_job_template_approval_node': + if response['status_code'] == 201: self.json_output['name'] = 'unknown' for key in ('name', 'username', 'identifier', 'hostname'): if key in response['json']: diff --git a/awx_collection/plugins/modules/tower_workflow_job_template_node.py b/awx_collection/plugins/modules/tower_workflow_job_template_node.py index 1718af91e8..5fb6b03e53 100644 --- a/awx_collection/plugins/modules/tower_workflow_job_template_node.py +++ b/awx_collection/plugins/modules/tower_workflow_job_template_node.py @@ -319,7 +319,7 @@ def main(): # Due to not able to lookup workflow_approval_templates, find the existing item in another place if workflow_job_template_node['related'].get('unified_job_template') is not None: existing_item = module.get_endpoint(workflow_job_template_node['related']['unified_job_template'])['json'] - approval_endpoint = 'workflow_job_template_nodes/' + str(workflow_job_template_node_id) + '/create_approval_template/' + approval_endpoint = 'workflow_job_template_nodes/{0}/create_approval_template/'.format(workflow_job_template_node_id) module.create_or_update_if_needed( existing_item, new_fields, endpoint=approval_endpoint, item_type='workflow_job_template_approval_node', From 5959809feddaa355020ab93bbff5cccbb4daa166 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Mon, 19 Oct 2020 15:19:01 -0500 Subject: [PATCH 21/25] make workflow approval creation return an HTTP 201, not 200 OK --- awx/api/views/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 7b97d82b66..11df2b2089 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -3043,7 +3043,7 @@ class WorkflowJobTemplateNodeCreateApproval(RetrieveAPIView): approval_template, context=self.get_serializer_context() ).data - return Response(data, status=status.HTTP_201_OK) + return Response(data, status=status.HTTP_201_CREATED) def check_permissions(self, request): obj = self.get_object().workflow_job_template From a8159c0391d81a8047147ea41ed696bdd3b39173 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Mon, 19 Oct 2020 15:35:36 -0500 Subject: [PATCH 22/25] revert check --- awx/api/views/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 11df2b2089..7b97d82b66 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -3043,7 +3043,7 @@ class WorkflowJobTemplateNodeCreateApproval(RetrieveAPIView): approval_template, context=self.get_serializer_context() ).data - return Response(data, status=status.HTTP_201_CREATED) + return Response(data, status=status.HTTP_201_OK) def check_permissions(self, request): obj = self.get_object().workflow_job_template From fe55dca661ff5a6443ca359147b2e32132b6be21 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Mon, 19 Oct 2020 15:55:49 -0500 Subject: [PATCH 23/25] make workflow approval creation return an HTTP 201, not 200 OK --- awx/api/views/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 7b97d82b66..11df2b2089 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -3043,7 +3043,7 @@ class WorkflowJobTemplateNodeCreateApproval(RetrieveAPIView): approval_template, context=self.get_serializer_context() ).data - return Response(data, status=status.HTTP_201_OK) + return Response(data, status=status.HTTP_201_CREATED) def check_permissions(self, request): obj = self.get_object().workflow_job_template From dc2658046673a0dd5b05f602fd2cea803ac98022 Mon Sep 17 00:00:00 2001 From: Sean Sullivan Date: Mon, 19 Oct 2020 19:46:49 -0500 Subject: [PATCH 24/25] Update to response code set response code for current versions of tower/awx --- awx_collection/plugins/module_utils/tower_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index cfda5b20ab..7439cbfb8f 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -438,8 +438,8 @@ class TowerAPIModule(TowerModule): response = self.post_endpoint(endpoint, **{'data': new_item}) - # 200 is response from approval node creation - if response['status_code'] == 201: + # 200 is response from approval node creation on tower 3.7.3 or awx 15.0.0 or earlier. + if response['status_code'] in [200, 201]: self.json_output['name'] = 'unknown' for key in ('name', 'username', 'identifier', 'hostname'): if key in response['json']: From 03c7504d2bc0f01cab68ca85d7b661c74f904448 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Tue, 20 Oct 2020 09:40:45 -0500 Subject: [PATCH 25/25] fix existing item error --- .../plugins/modules/tower_workflow_job_template_node.py | 1 + awx_collection/test/awx/test_workflow_job_template_node.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/awx_collection/plugins/modules/tower_workflow_job_template_node.py b/awx_collection/plugins/modules/tower_workflow_job_template_node.py index 5fb6b03e53..14e02d6f70 100644 --- a/awx_collection/plugins/modules/tower_workflow_job_template_node.py +++ b/awx_collection/plugins/modules/tower_workflow_job_template_node.py @@ -316,6 +316,7 @@ def main(): workflow_job_template_node = module.get_one('workflow_job_template_nodes', **{'data': search_fields}) workflow_job_template_node_id = workflow_job_template_node['id'] module.json_output['workflow_node_id'] = workflow_job_template_node_id + existing_item = None # Due to not able to lookup workflow_approval_templates, find the existing item in another place if workflow_job_template_node['related'].get('unified_job_template') is not None: existing_item = module.get_endpoint(workflow_job_template_node['related']['unified_job_template'])['json'] diff --git a/awx_collection/test/awx/test_workflow_job_template_node.py b/awx_collection/test/awx/test_workflow_job_template_node.py index 1641aaf153..6127fde27e 100644 --- a/awx_collection/test/awx/test_workflow_job_template_node.py +++ b/awx_collection/test/awx/test_workflow_job_template_node.py @@ -53,7 +53,7 @@ def test_create_workflow_job_template_node(run_module, admin_user, wfjt, job_tem @pytest.mark.django_db -def test_create_workflow_job_template_node_no_template(run_module, admin_user, wfjt, job_template): +def test_create_workflow_job_template_node_approval_node(run_module, admin_user, wfjt, job_template): """This is a part of the API contract for creating approval nodes""" this_identifier = '42🐉' result = run_module('tower_workflow_job_template_node', {