From 6fa8035599e010f73af1450117894e095ed5cd5a Mon Sep 17 00:00:00 2001 From: Simon Leary Date: Wed, 5 Nov 2025 15:44:17 -0500 Subject: [PATCH] add support for multiple creates, removes Signed-off-by: Simon Leary --- ...6136-command-multiple-creates-removes.yaml | 3 + lib/ansible/modules/command.py | 28 +- .../targets/command_shell/tasks/main.yml | 245 ++++++++++++++++-- 3 files changed, 238 insertions(+), 38 deletions(-) create mode 100644 changelogs/fragments/86136-command-multiple-creates-removes.yaml diff --git a/changelogs/fragments/86136-command-multiple-creates-removes.yaml b/changelogs/fragments/86136-command-multiple-creates-removes.yaml new file mode 100644 index 00000000000..82e0c455bee --- /dev/null +++ b/changelogs/fragments/86136-command-multiple-creates-removes.yaml @@ -0,0 +1,3 @@ +minor_changes: + - command - ``creates``, ``removes`` arguments now accept a list + - shell - ``creates``, ``removes`` arguments now accept a list diff --git a/lib/ansible/modules/command.py b/lib/ansible/modules/command.py index 82d35fda668..153c59783ae 100644 --- a/lib/ansible/modules/command.py +++ b/lib/ansible/modules/command.py @@ -63,14 +63,16 @@ options: - Only the string (free form) or the list (argv) form can be provided, not both. One or the other must be provided. version_added: "2.6" creates: - type: path + type: list + elements: str description: - - A filename or (since 2.0) glob pattern. If a matching file already exists, this step B(will not) be run. + - A list of filenames or (since 2.0) glob patterns. If all exist, this step B(will not) be run. - This is checked before O(removes) is checked. removes: - type: path + type: list + elements: str description: - - A filename or (since 2.0) glob pattern. If a matching file exists, this step B(will) be run. + - A list of filenames or (since 2.0) glob patterns. If none exist, this step B(will not) be run. - This is checked after O(creates) is checked. version_added: "0.8" chdir: @@ -104,6 +106,7 @@ notes: For instance, if you only want to run a command if a certain file does not exist, use this. - Check mode is supported when passing O(creates) or O(removes). If running in check mode and either of these are specified, the module will check for the existence of the file and report the correct changed status. If these are not supplied, the task will be skipped. + - C(changed_when) should not be specified when passing O(creates) or O(removes). - The O(ignore:executable) parameter is removed since version 2.4. If you have a need for this parameter, use the M(ansible.builtin.shell) module instead. - For Windows targets, use the M(ansible.windows.win_command) module instead. - For rebooting systems, use the M(ansible.builtin.reboot) or M(ansible.windows.win_reboot) module. @@ -254,8 +257,8 @@ def main(): chdir=dict(type='path'), executable=dict(), expand_argument_vars=dict(type='bool', default=True), - creates=dict(type='path'), - removes=dict(type='path'), + creates=dict(type='list', elements='str'), + removes=dict(type='list', elements='str'), # The default for this really comes from the action plugin stdin=dict(required=False), stdin_add_newline=dict(type='bool', default=True), @@ -311,17 +314,16 @@ def main(): # special skips for idempotence if file exists (assumes command creates) if creates: - if glob.glob(creates): - r['msg'] = "%s not run command since '%s' exists" % (shoulda, creates) - r['stdout'] = "skipped, since %s exists" % creates # TODO: deprecate - + if all(glob.glob(x) for x in creates): + r['msg'] = "%s not run command since '%s' all exist" % (shoulda, creates) + r['stdout'] = "skipped, since %s all exist" % creates # TODO: deprecate r['rc'] = 0 # special skips for idempotence if file does not exist (assumes command removes) if not r['msg'] and removes: - if not glob.glob(removes): - r['msg'] = "%s not run command since '%s' does not exist" % (shoulda, removes) - r['stdout'] = "skipped, since %s does not exist" % removes # TODO: deprecate + if (not any(glob.glob(x) for x in removes)): + r['msg'] = "%s not run command since none of '%s' exist" % (shoulda, removes) + r['stdout'] = "skipped, since none of %s exist" % removes # TODO: deprecate r['rc'] = 0 if r['msg']: diff --git a/test/integration/targets/command_shell/tasks/main.yml b/test/integration/targets/command_shell/tasks/main.yml index b1d7f78d8d9..6cb1cd4445e 100644 --- a/test/integration/targets/command_shell/tasks/main.yml +++ b/test/integration/targets/command_shell/tasks/main.yml @@ -112,7 +112,7 @@ # FIXME doesn't have the expected stdout. #- name: execute the test.sh script with executable via command -# command: "{{remote_tmp_dir_test }}/test.sh executable={{ bash.stdout }}" +# command: "{{ remote_tmp_dir_test }}/test.sh executable={{ bash.stdout }}" # register: command_result1 # #- name: assert that the script executed correctly with command @@ -148,13 +148,13 @@ # creates -- name: verify that afile.txt is absent +- name: ensure that afile.txt is absent file: path: "{{ remote_tmp_dir_test }}/afile.txt" state: absent - name: create afile.txt with create_afile.sh via command (check mode) - command: "{{ remote_tmp_dir_test }}/create_afile.sh {{remote_tmp_dir_test }}/afile.txt" + command: "{{ remote_tmp_dir_test }}/create_afile.sh {{ remote_tmp_dir_test }}/afile.txt" args: creates: "{{ remote_tmp_dir_test }}/afile.txt" register: check_mode_result @@ -167,19 +167,30 @@ - name: verify that afile.txt still does not exist stat: - path: "{{remote_tmp_dir_test}}/afile.txt" + path: "{{ remote_tmp_dir_test }}/afile.txt" register: stat_result failed_when: stat_result.stat.exists - name: create afile.txt with create_afile.sh via command - command: "{{ remote_tmp_dir_test }}/create_afile.sh {{remote_tmp_dir_test }}/afile.txt" + command: "{{ remote_tmp_dir_test }}/create_afile.sh {{ remote_tmp_dir_test }}/afile.txt" args: creates: "{{ remote_tmp_dir_test }}/afile.txt" - name: verify that afile.txt is present - file: + stat: path: "{{ remote_tmp_dir_test }}/afile.txt" - state: file + register: stat_result + failed_when: not stat_result.stat.exists + +- name: create afile.txt with create_afile.sh via command (again) + command: "{{ remote_tmp_dir_test }}/create_afile.sh {{ remote_tmp_dir_test }}/afile.txt" + args: + creates: "{{ remote_tmp_dir_test }}/afile.txt" + register: redundant_command_result + +- name: verify that changed=false + assert: + that: "redundant_command_result is not changed" - name: re-run previous command using creates with globbing (check mode) command: "{{ remote_tmp_dir_test }}/create_afile.sh {{ remote_tmp_dir_test }}/afile.txt" @@ -206,6 +217,11 @@ # removes +- name: ensure that afile.txt exists + file: + path: "{{ remote_tmp_dir_test }}/afile.txt" + state: touch + - name: remove afile.txt with remote_afile.sh via command (check mode) command: "{{ remote_tmp_dir_test }}/remove_afile.sh {{ remote_tmp_dir_test }}/afile.txt" args: @@ -220,7 +236,7 @@ - name: verify that afile.txt still exists stat: - path: "{{remote_tmp_dir_test}}/afile.txt" + path: "{{ remote_tmp_dir_test }}/afile.txt" register: stat_result failed_when: not stat_result.stat.exists @@ -230,7 +246,20 @@ removes: "{{ remote_tmp_dir_test }}/afile.txt" - name: verify that afile.txt is absent - file: path={{remote_tmp_dir_test}}/afile.txt state=absent + stat: + path: "{{ remote_tmp_dir_test }}/afile.txt" + register: stat_result + failed_when: stat_result.stat.exists + +- name: remove afile.txt with remove_afile.sh via command (again) + command: "{{ remote_tmp_dir_test }}/remove_afile.sh {{ remote_tmp_dir_test }}/afile.txt" + args: + removes: "{{ remote_tmp_dir_test }}/afile.txt" + register: redundant_command_result + +- name: verify that changed=false + assert: + that: "redundant_command_result is not changed" - name: re-run previous command using removes with globbing (check mode) command: "{{ remote_tmp_dir_test }}/remove_afile.sh {{ remote_tmp_dir_test }}/afile.txt" @@ -255,6 +284,170 @@ that: - command_result4.changed != True +# creates (multiple) + +- name: ensure that afile.txt, bfile.txt are absent + file: + path: "{{ item }}" + state: absent + loop: + - "{{ remote_tmp_dir_test }}/afile.txt" + - "{{ remote_tmp_dir_test }}/bfile.txt" + +- name: create afile.txt, bfile.txt via command (check mode) + command: touch {{ remote_tmp_dir_test }}/afile.txt {{ remote_tmp_dir_test }}/bfile.txt + args: + creates: + - "{{ remote_tmp_dir_test }}/afile.txt" + - "{{ remote_tmp_dir_test }}/bfile.txt" + register: check_mode_result + check_mode: yes + +- assert: + that: + - check_mode_result.changed + - "'skipped' not in check_mode_result" + +- name: verify that afile.txt, bfile.txt still do not exist + command: test ! -e {{ item }} + loop: + - "{{ remote_tmp_dir_test }}/afile.txt" + - "{{ remote_tmp_dir_test }}/bfile.txt" + +- name: create afile.txt, bfile.txt via command + command: touch {{ remote_tmp_dir_test }}/afile.txt {{ remote_tmp_dir_test }}/bfile.txt + args: + creates: + - "{{ remote_tmp_dir_test }}/afile.txt" + - "{{ remote_tmp_dir_test }}/bfile.txt" + +- name: verify that afile.txt, bfile.txt are present + command: test -e {{ item }} + loop: + - "{{ remote_tmp_dir_test }}/afile.txt" + - "{{ remote_tmp_dir_test }}/bfile.txt" + +- name: create afile.txt, bfile.txt via command (again) + command: touch {{ remote_tmp_dir_test }}/afile.txt {{ remote_tmp_dir_test }}/bfile.txt + args: + creates: + - "{{ remote_tmp_dir_test }}/afile.txt" + - "{{ remote_tmp_dir_test }}/bfile.txt" + register: redundant_command_result + +- name: verify that changed=false + assert: + that: "redundant_command_result is not changed" + +- name: re-run previous command using creates with globbing (check mode) + command: touch {{ remote_tmp_dir_test }}/afile.txt {{ remote_tmp_dir_test }}/bfile.txt + args: + creates: "{{ remote_tmp_dir_test }}/afile.*" + register: check_mode_result + check_mode: yes + +- assert: + that: + - not check_mode_result.changed + - "'skipped' not in check_mode_result" + +- name: re-run previous command using creates with globbing + command: touch {{ remote_tmp_dir_test }}/afile.txt {{ remote_tmp_dir_test }}/bfile.txt + args: + creates: + - "{{ remote_tmp_dir_test }}/afile.*" + - "{{ remote_tmp_dir_test }}/bfile.*" + register: command_result3 + +- name: assert that creates with globbing is working + assert: + that: + - command_result3 is not changed + +# removes (multiple) + +- name: ensure that afile.txt, bfile.txt exist + file: + path: "{{ item }}" + state: touch + loop: + - "{{ remote_tmp_dir_test }}/afile.txt" + - "{{ remote_tmp_dir_test }}/bfile.txt" + +- name: remove afile.txt, bfile.txt via command (check mode) + command: rm {{ remote_tmp_dir_test }}/afile.txt {{ remote_tmp_dir_test }}/bfile.txt + args: + removes: + - "{{ remote_tmp_dir_test }}/afile.txt" + - "{{ remote_tmp_dir_test }}/bfile.txt" + register: check_mode_result + check_mode: yes + +- assert: + that: + - check_mode_result.changed + - "'skipped' not in check_mode_result" + +- name: verify that afile.txt, bfile.txt still exist + command: test -e {{ item }} + loop: + - "{{ remote_tmp_dir_test }}/afile.txt" + - "{{ remote_tmp_dir_test }}/bfile.txt" + +- name: remove afile.txt, bfile.txt via command + command: rm {{ remote_tmp_dir_test }}/afile.txt {{ remote_tmp_dir_test }}/bfile.txt + args: + removes: + - "{{ remote_tmp_dir_test }}/afile.txt" + - "{{ remote_tmp_dir_test }}/bfile.txt" + +- name: verify that afile.txt, bfile.txt are absent + command: test ! -e {{ item }} + loop: + - "{{ remote_tmp_dir_test }}/afile.txt" + - "{{ remote_tmp_dir_test }}/bfile.txt" + +- name: remove afile.txt, bfile.txt via command (again) + command: rm {{ remote_tmp_dir_test }}/afile.txt {{ remote_tmp_dir_test }}/bfile.txt + args: + removes: + - "{{ remote_tmp_dir_test }}/afile.txt" + - "{{ remote_tmp_dir_test }}/bfile.txt" + register: redundant_command_result + +- name: verify that changed=false + assert: + that: "redundant_command_result is not changed" + +- name: re-run previous command using removes with globbing (check mode) + command: rm {{ remote_tmp_dir_test }}/afile.txt {{ remote_tmp_dir_test }}/bfile.txt + args: + removes: + - "{{ remote_tmp_dir_test }}/afile.*" + - "{{ remote_tmp_dir_test }}/bfile.*" + register: check_mode_result + check_mode: yes + +- assert: + that: + - not check_mode_result.changed + - "'skipped' not in check_mode_result" + +- name: re-run previous command using removes with globbing + command: rm {{ remote_tmp_dir_test }}/afile.txt {{ remote_tmp_dir_test }}/bfile.txt + args: + removes: + - "{{ remote_tmp_dir_test }}/afile.*" + - "{{ remote_tmp_dir_test }}/bfile.*" + register: command_result4 + +- name: assert that removes with globbing is working + assert: + that: + - command_result4.changed != True + +# end of removes + - name: pass stdin to cat via command command: cat args: @@ -328,7 +521,7 @@ # FIXME doesn't pass the expected stdout #- name: execute the test.sh script -# shell: "{{remote_tmp_dir_test }}/test.sh executable={{ bash.stdout }}" +# shell: "{{ remote_tmp_dir_test }}/test.sh executable={{ bash.stdout }}" # register: shell_result1 # #- name: assert that the shell executed correctly @@ -358,9 +551,10 @@ # creates - name: Verify that afile.txt is absent - file: + stat: path: "{{ remote_tmp_dir_test }}/afile.txt" - state: absent + register: stat_result + failed_when: stat_result.stat.exists - name: Execute the test.sh script with chdir shell: "{{ remote_tmp_dir_test }}/test.sh > {{ remote_tmp_dir_test }}/afile.txt" @@ -369,9 +563,10 @@ creates: "{{ remote_tmp_dir_test }}/afile.txt" - name: Verify that afile.txt is present - file: + stat: path: "{{ remote_tmp_dir_test }}/afile.txt" - state: file + register: stat_result + failed_when: not stat_result.stat.exists # multiline @@ -433,14 +628,14 @@ - shell_result7.stdout == 'One\n Two\n Three' - name: execute a shell command with no trailing newline to stdin - shell: cat > {{remote_tmp_dir_test }}/afile.txt + shell: cat > {{ remote_tmp_dir_test }}/afile.txt args: stdin: test stdin_add_newline: no - name: make sure content matches expected copy: - dest: "{{remote_tmp_dir_test }}/afile.txt" + dest: "{{ remote_tmp_dir_test }}/afile.txt" content: test register: shell_result7 failed_when: @@ -448,14 +643,14 @@ shell_result7 is changed - name: execute a shell command with trailing newline to stdin - shell: cat > {{remote_tmp_dir_test }}/afile.txt + shell: cat > {{ remote_tmp_dir_test }}/afile.txt args: stdin: test stdin_add_newline: yes - name: make sure content matches expected copy: - dest: "{{remote_tmp_dir_test }}/afile.txt" + dest: "{{ remote_tmp_dir_test }}/afile.txt" content: | test register: shell_result8 @@ -464,13 +659,13 @@ shell_result8 is changed - name: execute a shell command with trailing newline to stdin, default - shell: cat > {{remote_tmp_dir_test }}/afile.txt + shell: cat > {{ remote_tmp_dir_test }}/afile.txt args: stdin: test - name: make sure content matches expected copy: - dest: "{{remote_tmp_dir_test }}/afile.txt" + dest: "{{ remote_tmp_dir_test }}/afile.txt" content: | test register: shell_result9 @@ -514,23 +709,23 @@ block: - name: Create target folders file: - path: '{{remote_tmp_dir}}/www_root/site' + path: '{{ remote_tmp_dir }}/www_root/site' state: directory - name: Create symlink file: - path: '{{remote_tmp_dir}}/www' + path: '{{ remote_tmp_dir }}/www' state: link - src: '{{remote_tmp_dir}}/www_root' + src: '{{ remote_tmp_dir }}/www_root' - name: check parent using chdir shell: dirname "$PWD" args: - chdir: '{{remote_tmp_dir}}/www/site' + chdir: '{{ remote_tmp_dir }}/www/site' register: parent_dir_chdir - name: check parent using cd - shell: cd "{{remote_tmp_dir}}/www/site" && dirname "$PWD" + shell: cd "{{ remote_tmp_dir }}/www/site" && dirname "$PWD" register: parent_dir_cd - name: check expected outputs