Skip diff if before is empty

Hi,

When using the copy module, I would like to omit the diff output if the destination file does not exist. It seems like the pieces are all there, but they don’t quite fit together.

I wanted to do something like this:

diff: results.diff.before != ''

but I think that results are not available at this point? What’s the easiest way to accomplish this? Prepared to create a custom module if necessary for keeping the playbook simple.

diff: results.diff.before | default('') != ''

See Using filters to manipulate data — Ansible Community Documentation

The default doesn’t help a bit, since the value for diff for the current task is determined before the task is executed, and therefore in it you cannot use data from the result of the current task.

Basically the only way this is possible (with a single task) is through a custom callback plugin, which handles copy tasks specially.

Or you could add a ansible.builtin.stat task first that determines whether the file exists, and set diff accordingly (obviously this has a race condition, assuming someone else creates the file between the ansible.builtin.stat task and the ansible.builtin.copy task).

I see. Thank you.

A custom callback plugin would be preferable, in that case. Are there any examples of targeting specific modules?

Here's an example:
from __future__ import annotations

DOCUMENTATION = """
    name: copy_diff
    type: stdout
    short_description: Custom diff callback.
    description:
        - The default output callback for ansible-playbook with modified diff output for the copy module.
    extends_documentation_fragment:
      - default_callback
      - result_format_callback
    requirements:
      - set as stdout in configuration
"""

import os

from ansible.plugins.loader import callback_loader


CallbackBase = callback_loader.get("ansible.builtin.default", class_only=True)


class CallbackModule(CallbackBase):
    CALLBACK_VERSION = 2.0
    CALLBACK_TYPE = 'stdout'
    CALLBACK_NAME = 'copy_diff'

    def v2_on_file_diff(self, result: CallbackTaskResult) -> None:
        if result._task.resolved_action == "ansible.builtin.copy" and not os.path.exists(result.task.args["dest"]):
            return

        return super(CallbackModule, self).v2_on_file_diff(result)

Note that other actions, like assemble and template use the copy module, and wouldn’t be handled by this example.

Just for the records

---
- hosts: test
  become: false
  gather_facts: false

  pre_tasks:

    - name: Gathering File Facts
      ansible.builtin.stat:
        path: test.txt
      register: file_facts

  tasks:

    - name: Copy file to Remote Node
      ansible.builtin.copy:
        src: "test.txt"
        dest: "test.txt"
      diff: "{{ file_facts.stat.exists }}"

Thanks! I’ve used this for now, though I’m checking the before property instead, since dest can be a few different things, such as a destination directory that the file goes in.

One tangential thing I wanted to try doing with this is printing (or not) the diff for each file when copying a directory. Currently is only prints “changed” - without any details - when any file has changed. It doesn’t seem to be handled by v2_on_file_diff.

I’ve used this for now, though I’m checking the before property instead, since dest can be a few different things, such as a destination directory that the file goes in.

True, I overlooked that.

One tangential thing I wanted to try doing with this is printing (or not) the diff for each file when copying a directory. Currently is only prints “changed” - without any details - when any file has changed. It doesn’t seem to be handled by v2_on_file_diff.

There are multiple issues with the current copy diff and the callback is not the right place to fix it, so the multi-task approach would be a lot more reliable. The copy action plugin returns a single file diff (related: copy action plugin diff output for multi files 2 by simonLeary42 · Pull Request #84423 · ansible/ansible · GitHub) only containing the changed content or the diff returned by the file module (so it can be incomplete and formatted differently depending on the before state), and the copy module does not return a diff at all (related: [POC] copy - add diff mode support for remote_src=true by s-hertel · Pull Request #84324 · ansible/ansible · GitHub). I don’t think there’s a precedent in ansible-core of how to display the diff for multiple files, but I’ve been hesitant to add a 3rd format since it’s already inconsistent for a single file diff (and once something is added, it will be difficult to change). I think the task result diff object should contain all changes and be a consistent format, but it could be worse visually for people who like the existing diff behavior of copy, and without defining a better diff API for callback plugins, there isn’t a good way to customize what is displayed.