-
set_fact polluting global fact namespace - While true, it’s irrelevant, especially if you aren’t caching facts. If you are caching facts, then pick a naming convention that makes this not a problem. (Pro’ly should do that anyway.)
-
custom modules seem like overkill - Also true, but custom filters and tests (which run on the controller, btw) and custom modules (which run on the target) allow you to bring the power of Python right into Ansible. Oddly, this can often be more readable/maintainable that a pipeline of Jinja filters, at least to people more comfortable with Python. And that’s okay.
-
register pollutes global variable namespace - Also also true, but again, naming conventions are your friend. It may seem “not right”, but for now it’s The Way™ to carry info from one task to another.
-
What am I missing? - Lazy evaluation. You can define a variable to be the expression you’ll want to evaluate, including other variables which don’t exist yet, and it won’t be evaluated until you use it. See the example below.
Here’s a solution to your problem, not as a role but as a playbook. You could make it a role if you want to. And it’s a little mind-bending at first because it sort of runs your pseudocode steps through a blender, but all the parts are still there. Strap in:
- name: Stat stuff
hosts: localhost
gather_facts: false
vars:
files_to_stat:
- ./date-utc.yml # This file exists.
- ./date-utc2.yml # This file exists also.
- ./date-utc99.yml # This file doesn't exist.
min_mtime: >-
{{ [{"mtime":0.0}]
| product(stats.results
| map(attribute="stat"))
| map("combine")
| map(attribute="mtime")
| sort
| first }}
tasks:
- name: Get stat of files_to_stat
ansible.builtin.stat:
path: '{{ item }}'
register: stats
loop: '{{ files_to_stat }}'
- name: Show the minimum mtime
ansible.builtin.debug:
msg: '{{ min_mtime }}'
The only thing not immediately obvious is what’s happening in the min_mtime
expression. Let’s start with the parameter to the product()
filter. It’s taking the results
list from our not-yet-existing stats
registered variable, then the map()
filter is pulling from each of those their "stat"
dict. So: product(
“just the 'stat” dicts")
. Notice that stat
dicts for existing files will have an mtime
, while non-existing files’ stat
dicts will contain only exists:false
.
Just before “| product(…
” there’s a single-element list containing a Json dict that has only the "mtime":0.0
key-value pair. That’s the default mtime
we want for stat
dicts of files which don’t exist as per your original requirements – except it’s a float to be consistent with the value of mtime
s of existing files.
The product
filter takes that single element list, and produces the Carteasian product of it and the stat
dicts from all the files. That is, it produces a list of lists, where each “inner list” contains two elements: that {"mtime":0.0}
default dict as the first element, and one of the files of interest’s stat
dict as the second element.
Next, the map()
filter takes those inner list pairs in turn and runs the combine
filter on each pair. That is, it takes that “default” mtime
dict, then overlays onto it all the values from the corresponding stat
dict. Existing files’ mtime
s will replace the default, but non-existing files - which have no mtime
- will retain the mtime
default of 0.0
. The result then is a list of stat
dicts just like we had before, except now they all have an mtime
key-value, including those for non-existent files.
Then another map pulls out just the mtime
values. A sort
puts them in low→high order, and first
takes the first one, which is the lowest value, which I believe is what you were after to start with.
Please ask about anything that isn’t totally clear. And enjoy using Ansible!