diff options
Diffstat (limited to '')
14 files changed, 551 insertions, 0 deletions
diff --git a/test/integration/targets/async/aliases b/test/integration/targets/async/aliases new file mode 100644 index 0000000..c989cd7 --- /dev/null +++ b/test/integration/targets/async/aliases @@ -0,0 +1,3 @@ +async_status +async_wrapper +shippable/posix/group2 diff --git a/test/integration/targets/async/callback_test.yml b/test/integration/targets/async/callback_test.yml new file mode 100644 index 0000000..01afd59 --- /dev/null +++ b/test/integration/targets/async/callback_test.yml @@ -0,0 +1,7 @@ +- hosts: localhost + gather_facts: false + tasks: + - name: Async poll callback test + command: sleep 5 + async: 6 + poll: 1 diff --git a/test/integration/targets/async/library/async_test.py b/test/integration/targets/async/library/async_test.py new file mode 100644 index 0000000..f89bd10 --- /dev/null +++ b/test/integration/targets/async/library/async_test.py @@ -0,0 +1,49 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import json +import sys + +from ansible.module_utils.basic import AnsibleModule + + +def main(): + if "--interactive" in sys.argv: + import ansible.module_utils.basic + ansible.module_utils.basic._ANSIBLE_ARGS = json.dumps(dict( + ANSIBLE_MODULE_ARGS=dict( + fail_mode="graceful" + ) + )) + + module = AnsibleModule( + argument_spec=dict( + fail_mode=dict(type='list', default=['success']) + ) + ) + + result = dict(changed=True) + + fail_mode = module.params['fail_mode'] + + try: + if 'leading_junk' in fail_mode: + print("leading junk before module output") + + if 'graceful' in fail_mode: + module.fail_json(msg="failed gracefully") + + if 'exception' in fail_mode: + raise Exception('failing via exception') + + if 'stderr' in fail_mode: + print('printed to stderr', file=sys.stderr) + + module.exit_json(**result) + + finally: + if 'trailing_junk' in fail_mode: + print("trailing junk after module output") + + +main() diff --git a/test/integration/targets/async/meta/main.yml b/test/integration/targets/async/meta/main.yml new file mode 100644 index 0000000..07faa21 --- /dev/null +++ b/test/integration/targets/async/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - prepare_tests diff --git a/test/integration/targets/async/tasks/main.yml b/test/integration/targets/async/tasks/main.yml new file mode 100644 index 0000000..05c789e --- /dev/null +++ b/test/integration/targets/async/tasks/main.yml @@ -0,0 +1,300 @@ +# test code for the async keyword +# (c) 2014, James Tanner <tanner.jc@gmail.com> + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +- name: run a 2 second loop + shell: for i in $(seq 1 2); do echo $i ; sleep 1; done; + async: 10 + poll: 1 + register: async_result + + +- debug: var=async_result + +- name: validate async returns + assert: + that: + - "'ansible_job_id' in async_result" + - "'changed' in async_result" + - "'cmd' in async_result" + - "'delta' in async_result" + - "'end' in async_result" + - "'rc' in async_result" + - "'start' in async_result" + - "'stderr' in async_result" + - "'stdout' in async_result" + - "'stdout_lines' in async_result" + - async_result.rc == 0 + - async_result.finished == 1 + - async_result is finished + +- name: assert temp async directory exists + stat: + path: "~/.ansible_async" + register: dir_st + +- assert: + that: + - dir_st.stat.isdir is defined and dir_st.stat.isdir + +- name: stat temp async status file + stat: + path: "~/.ansible_async/{{ async_result.ansible_job_id }}" + register: tmp_async_file_st + +- name: validate automatic cleanup of temp async status file on completed run + assert: + that: + - not tmp_async_file_st.stat.exists + +- name: test async without polling + command: sleep 5 + async: 30 + poll: 0 + register: async_result + +- debug: var=async_result + +- name: validate async without polling returns + assert: + that: + - "'ansible_job_id' in async_result" + - "'started' in async_result" + - async_result.finished == 0 + - async_result is not finished + +- name: test skipped task handling + command: /bin/true + async: 15 + poll: 0 + when: False + +# test async "fire and forget, but check later" + +- name: 'start a task with "fire-and-forget"' + command: sleep 3 + async: 30 + poll: 0 + register: fnf_task + +- name: assert task was successfully started + assert: + that: + - fnf_task.started == 1 + - fnf_task is started + - "'ansible_job_id' in fnf_task" + +- name: 'check on task started as a "fire-and-forget"' + async_status: jid={{ fnf_task.ansible_job_id }} + register: fnf_result + until: fnf_result is finished + retries: 10 + delay: 1 + +- name: assert task was successfully checked + assert: + that: + - fnf_result.finished + - fnf_result is finished + +- name: test graceful module failure + async_test: + fail_mode: graceful + async: 30 + poll: 1 + register: async_result + ignore_errors: true + +- name: assert task failed correctly + assert: + that: + - async_result.ansible_job_id is match('\d+\.\d+') + - async_result.finished == 1 + - async_result is finished + - async_result is not changed + - async_result is failed + - async_result.msg == 'failed gracefully' + +- name: test exception module failure + async_test: + fail_mode: exception + async: 5 + poll: 1 + register: async_result + ignore_errors: true + +- name: validate response + assert: + that: + - async_result.ansible_job_id is match('\d+\.\d+') + - async_result.finished == 1 + - async_result is finished + - async_result.changed == false + - async_result is not changed + - async_result.failed == true + - async_result is failed + - async_result.stderr is search('failing via exception', multiline=True) + +- name: test leading junk before JSON + async_test: + fail_mode: leading_junk + async: 5 + poll: 1 + register: async_result + +- name: validate response + assert: + that: + - async_result.ansible_job_id is match('\d+\.\d+') + - async_result.finished == 1 + - async_result is finished + - async_result.changed == true + - async_result is changed + - async_result is successful + +- name: test trailing junk after JSON + async_test: + fail_mode: trailing_junk + async: 5 + poll: 1 + register: async_result + +- name: validate response + assert: + that: + - async_result.ansible_job_id is match('\d+\.\d+') + - async_result.finished == 1 + - async_result is finished + - async_result.changed == true + - async_result is changed + - async_result is successful + - async_result.warnings[0] is search('trailing junk after module output') + +- name: test stderr handling + async_test: + fail_mode: stderr + async: 30 + poll: 1 + register: async_result + ignore_errors: true + +- assert: + that: + - async_result.stderr == "printed to stderr\n" + +# NOTE: This should report a warning that cannot be tested +- name: test async properties on non-async task + command: sleep 1 + register: non_async_result + +- name: validate response + assert: + that: + - non_async_result is successful + - non_async_result is changed + - non_async_result is finished + - "'ansible_job_id' not in non_async_result" + +- name: set fact of custom tmp dir + set_fact: + custom_async_tmp: ~/.ansible_async_test + +- name: ensure custom async tmp dir is absent + file: + path: '{{ custom_async_tmp }}' + state: absent + +- block: + - name: run async task with custom dir + command: sleep 1 + register: async_custom_dir + async: 5 + poll: 1 + vars: + ansible_async_dir: '{{ custom_async_tmp }}' + + - name: check if the async temp dir is created + stat: + path: '{{ custom_async_tmp }}' + register: async_custom_dir_result + + - name: assert run async task with custom dir + assert: + that: + - async_custom_dir is successful + - async_custom_dir is finished + - async_custom_dir_result.stat.exists + + - name: remove custom async dir again + file: + path: '{{ custom_async_tmp }}' + state: absent + + - name: remove custom async dir after deprecation test + file: + path: '{{ custom_async_tmp }}' + state: absent + + - name: run fire and forget async task with custom dir + command: echo moo + register: async_fandf_custom_dir + async: 5 + poll: 0 + vars: + ansible_async_dir: '{{ custom_async_tmp }}' + + - name: fail to get async status with custom dir with defaults + async_status: + jid: '{{ async_fandf_custom_dir.ansible_job_id }}' + register: async_fandf_custom_dir_fail + ignore_errors: yes + + - name: get async status with custom dir using newer format + async_status: + jid: '{{ async_fandf_custom_dir.ansible_job_id }}' + register: async_fandf_custom_dir_result + vars: + ansible_async_dir: '{{ custom_async_tmp }}' + + - name: assert run fire and forget async task with custom dir + assert: + that: + - async_fandf_custom_dir is successful + - async_fandf_custom_dir_fail is failed + - async_fandf_custom_dir_fail.msg == "could not find job" + - async_fandf_custom_dir_result is successful + + always: + - name: remove custom tmp dir after test + file: + path: '{{ custom_async_tmp }}' + state: absent + +- name: Test that async has stdin + command: > + {{ ansible_python_interpreter|default('/usr/bin/python') }} -c 'import os; os.fdopen(os.dup(0), "r")' + async: 1 + poll: 1 + +- name: run async poll callback test playbook + command: ansible-playbook {{ role_path }}/callback_test.yml + delegate_to: localhost + register: callback_output + +- assert: + that: + - '"ASYNC POLL on localhost" in callback_output.stdout' diff --git a/test/integration/targets/async_extra_data/aliases b/test/integration/targets/async_extra_data/aliases new file mode 100644 index 0000000..6452e6d --- /dev/null +++ b/test/integration/targets/async_extra_data/aliases @@ -0,0 +1,2 @@ +shippable/posix/group2 +context/target diff --git a/test/integration/targets/async_extra_data/library/junkping.py b/test/integration/targets/async_extra_data/library/junkping.py new file mode 100644 index 0000000..b61d965 --- /dev/null +++ b/test/integration/targets/async_extra_data/library/junkping.py @@ -0,0 +1,15 @@ +#!/usr/bin/python +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import json + + +def main(): + print("junk_before_module_output") + print(json.dumps(dict(changed=False, source='user'))) + print("junk_after_module_output") + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/async_extra_data/runme.sh b/test/integration/targets/async_extra_data/runme.sh new file mode 100755 index 0000000..4613273 --- /dev/null +++ b/test/integration/targets/async_extra_data/runme.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -eux + +# Verify that extra data before module JSON output during async call is ignored, and that the warning exists. +ANSIBLE_DEBUG=0 ansible-playbook -i ../../inventory test_async.yml -v "$@" \ + | grep 'junk after the JSON data: junk_after_module_output' diff --git a/test/integration/targets/async_extra_data/test_async.yml b/test/integration/targets/async_extra_data/test_async.yml new file mode 100644 index 0000000..480a2a6 --- /dev/null +++ b/test/integration/targets/async_extra_data/test_async.yml @@ -0,0 +1,10 @@ +- hosts: testhost + gather_facts: false + tasks: + # make sure non-JSON data before module output is ignored + - name: async ping wrapped in extra junk + junkping: + async: 10 + poll: 1 + register: result + - debug: var=result diff --git a/test/integration/targets/async_fail/action_plugins/normal.py b/test/integration/targets/async_fail/action_plugins/normal.py new file mode 100644 index 0000000..297cbd9 --- /dev/null +++ b/test/integration/targets/async_fail/action_plugins/normal.py @@ -0,0 +1,62 @@ +# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com> +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.errors import AnsibleError +from ansible.plugins.action import ActionBase +from ansible.utils.vars import merge_hash + + +class ActionModule(ActionBase): + + def run(self, tmp=None, task_vars=None): + + # individual modules might disagree but as the generic the action plugin, pass at this point. + self._supports_check_mode = True + self._supports_async = True + + result = super(ActionModule, self).run(tmp, task_vars) + del tmp # tmp no longer has any effect + + if not result.get('skipped'): + + if result.get('invocation', {}).get('module_args'): + # avoid passing to modules in case of no_log + # should not be set anymore but here for backwards compatibility + del result['invocation']['module_args'] + + # FUTURE: better to let _execute_module calculate this internally? + wrap_async = self._task.async_val and not self._connection.has_native_async + + # do work! + result = merge_hash(result, self._execute_module(task_vars=task_vars, wrap_async=wrap_async)) + + # hack to keep --verbose from showing all the setup module result + # moved from setup module as now we filter out all _ansible_ from result + if self._task.action == 'setup': + result['_ansible_verbose_override'] = True + + # Simulate a transient network failure + if self._task.action == 'async_status' and 'finished' in result and result['finished'] != 1: + raise AnsibleError('Pretend to fail somewher ein executing async_status') + + if not wrap_async: + # remove a temporary path we created + self._remove_tmp_path(self._connection._shell.tmpdir) + + return result diff --git a/test/integration/targets/async_fail/aliases b/test/integration/targets/async_fail/aliases new file mode 100644 index 0000000..c989cd7 --- /dev/null +++ b/test/integration/targets/async_fail/aliases @@ -0,0 +1,3 @@ +async_status +async_wrapper +shippable/posix/group2 diff --git a/test/integration/targets/async_fail/library/async_test.py b/test/integration/targets/async_fail/library/async_test.py new file mode 100644 index 0000000..e0cbd6f --- /dev/null +++ b/test/integration/targets/async_fail/library/async_test.py @@ -0,0 +1,53 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import json +import sys +import time + +from ansible.module_utils.basic import AnsibleModule + + +def main(): + if "--interactive" in sys.argv: + import ansible.module_utils.basic + ansible.module_utils.basic._ANSIBLE_ARGS = json.dumps(dict( + ANSIBLE_MODULE_ARGS=dict( + fail_mode="graceful" + ) + )) + + module = AnsibleModule( + argument_spec=dict( + fail_mode=dict(type='list', default=['success']) + ) + ) + + result = dict(changed=True) + + fail_mode = module.params['fail_mode'] + + try: + if 'leading_junk' in fail_mode: + print("leading junk before module output") + + if 'graceful' in fail_mode: + module.fail_json(msg="failed gracefully") + + if 'exception' in fail_mode: + raise Exception('failing via exception') + + if 'recovered_fail' in fail_mode: + result = {"msg": "succeeded", "failed": False, "changed": True} + # Wait in the middle to setup a race where the controller reads incomplete data from our + # special async_status the first poll + time.sleep(5) + + module.exit_json(**result) + + finally: + if 'trailing_junk' in fail_mode: + print("trailing junk after module output") + + +main() diff --git a/test/integration/targets/async_fail/meta/main.yml b/test/integration/targets/async_fail/meta/main.yml new file mode 100644 index 0000000..07faa21 --- /dev/null +++ b/test/integration/targets/async_fail/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - prepare_tests diff --git a/test/integration/targets/async_fail/tasks/main.yml b/test/integration/targets/async_fail/tasks/main.yml new file mode 100644 index 0000000..40f72e1 --- /dev/null +++ b/test/integration/targets/async_fail/tasks/main.yml @@ -0,0 +1,36 @@ +# test code for the async keyword failing in the middle of output +# (c) 2018, Ansible Project + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +# This uses a special copy of the normal action plugin which simulates +# a transient failure in the module +- name: test that we can recover from initial failures to read + async_test: + fail_mode: recovered_fail + async: 10 + poll: 1 + register: async_result + +- name: validate that by the end of the retry interval, we succeeded + assert: + that: + - async_result.ansible_job_id is match('\d+\.\d+') + - async_result.finished == 1 + - async_result is finished + - async_result is changed + - async_result is successful + - async_result.msg is search('succeeded') |