This problem is loaded with traps. It would be easy to construct some expressions that seem to work but don’t. For example,
when:
- output2.stdout is not search(item.network)
- output2.stdout is not search(item.prefix)
(Note: the list form above is equivalent to “when:
expr1 and
expr2”.)
This seems to work, but only because Test-nets
shows up in all your lines, and that’s the only value this code looks for. So the first expression fails on the first line every time, and the second expression never executes. (Perhaps this is what you meant by “However, it seems that only the first string is hit.” in your initial post?)
Below is a simple (is that ever true?) playbook that demonstrates four ways to do this, all of which are rather obvious, and half of them don’t work! I’ll break it up and intersperse the output of each task into the body to reduce scrolling back and forth during discussion. I’ve modified your original stdout
text stream to make the problem areas more obvious. Here we go:
- name: Testing multi-search
hosts: localhost
gather_facts: false
vars:
output2:
stdout: |
ip prefix-list Test-nets-0 seq 20 permit 10.0.0.0/25
ip prefix-list Test-nets-2 seq 25 permit 10.0.2.0/25
ip prefix-list Test-nets-5 seq 25 permit 10.0.5.0/25
stdout_lines:
- ip prefix-list Test-nets-0 seq 20 permit 10.0.0.0/25
- ip prefix-list Test-nets-2 seq 25 permit 10.0.2.0/25
- ip prefix-list Test-nets-5 seq 25 permit 10.0.5.0/25
networks:
- {"network": "Test-nets-0"}
- {"network": "Test-nets-2"}
- {"network": "Test-nets-5"}
prefixes:
- {"prefix": "10.0.0.0/25"}
- {"prefix": "10.0.2.0/25"}
- {"prefix": "10.0.5.0/25"}
loop_items: '{{ networks | product(prefixes) | map("combine") }}'
tasks:
- name: Show Cartesian product of networks and prefixes
ansible.builtin.debug:
msg: '{{ loop_items }}'
The variables above create a modified version of your registered output2.stdout
and output2.stdout_lines
(the list version of .stdout
, stripped of new-lines). The loop_items
variable contains a list of all combinations of networks
and prefixes
, a few of which actually show up in output2.*
. The output of the debug
task looks like this (almost; I added a couple of line breaks to make it more obvious what’s going on):
TASK [Show Cartesian product of networks and prefixes] *******************************
ok: [localhost] =>
msg:
- network: Test-nets-0
prefix: 10.0.0.0/25
- network: Test-nets-0
prefix: 10.0.2.0/25
- network: Test-nets-0
prefix: 10.0.5.0/25
- network: Test-nets-2
prefix: 10.0.0.0/25
- network: Test-nets-2
prefix: 10.0.2.0/25
- network: Test-nets-2
prefix: 10.0.5.0/25
- network: Test-nets-5
prefix: 10.0.0.0/25
- network: Test-nets-5
prefix: 10.0.2.0/25
- network: Test-nets-5
prefix: 10.0.5.0/25
We’ll use this list in our loop:
in each of the four following tasks.
In these tasks, instead of skipping with a when:
, we’ll print each combination of network
and prefix
we’re testing, and indicate whether the test method “sees” the data in output2
.
- name: Check configured network::prefix combinations on stdout with "search() and search()" -- FAILS
debug:
msg: "{{inventory_hostname}}: {{ item.network }} permit {{ item.prefix }} configured: {{ is_configured }}"
vars:
is_configured: '{{ output2.stdout is search(item.network | regex_escape()) and
output2.stdout is search(item.prefix | regex_escape()) }}'
loop: '{{ loop_items }}'
This task FAILS, because it finds each item.network
and each item.prefix
in our output2
. It’s totally oblivious to the values in question being on the same line in only one third of our tests.
One thing it does do right is to regex_escape()
the strings it’s searching for. It isn’t an issue for item.network
, but item.prefix
has these nasty “.
” characters that happily match any character. That’s actually okay-ish for this data, but data changes; better to get in the habit of being careful with searches.
Anyway, here’s the output:
TASK [Check configured network::prefix combinations on stdout with "search() and search()" -- FAILS] ***
ok: [localhost] => (item={'network': 'Test-nets-0', 'prefix': '10.0.0.0/25'}) =>
msg: 'localhost: Test-nets-0 permit 10.0.0.0/25 configured: True'
ok: [localhost] => (item={'network': 'Test-nets-0', 'prefix': '10.0.2.0/25'}) =>
msg: 'localhost: Test-nets-0 permit 10.0.2.0/25 configured: True'
ok: [localhost] => (item={'network': 'Test-nets-0', 'prefix': '10.0.5.0/25'}) =>
msg: 'localhost: Test-nets-0 permit 10.0.5.0/25 configured: True'
ok: [localhost] => (item={'network': 'Test-nets-2', 'prefix': '10.0.0.0/25'}) =>
msg: 'localhost: Test-nets-2 permit 10.0.0.0/25 configured: True'
ok: [localhost] => (item={'network': 'Test-nets-2', 'prefix': '10.0.2.0/25'}) =>
msg: 'localhost: Test-nets-2 permit 10.0.2.0/25 configured: True'
ok: [localhost] => (item={'network': 'Test-nets-2', 'prefix': '10.0.5.0/25'}) =>
msg: 'localhost: Test-nets-2 permit 10.0.5.0/25 configured: True'
ok: [localhost] => (item={'network': 'Test-nets-5', 'prefix': '10.0.0.0/25'}) =>
msg: 'localhost: Test-nets-5 permit 10.0.0.0/25 configured: True'
ok: [localhost] => (item={'network': 'Test-nets-5', 'prefix': '10.0.2.0/25'}) =>
msg: 'localhost: Test-nets-5 permit 10.0.2.0/25 configured: True'
ok: [localhost] => (item={'network': 'Test-nets-5', 'prefix': '10.0.5.0/25'}) =>
msg: 'localhost: Test-nets-5 permit 10.0.5.0/25 configured: True'
You see it claimed them all “configured: True
”? But only the 1st, 5th, and 9th combinations actually exist in our input.
Let’s try again with a single “constructed” regex:
- name: Check configured network:prefix combinations on stdout with one constructed search() -- SUCCEEDS
debug:
msg: "{{inventory_hostname}}: {{ item.network }} permit {{ item.prefix }} configured: {{ is_configured }}"
vars:
is_configured: '{{ output2.stdout is search((item.network | regex_escape()) ~ ".*" ~ (item.prefix | regex_escape())
loop: '{{ loop_items }}'
This works, but only because multi-line search is off by default. That is, the constructed regular expression won’t span across new-lines in the input. So the fact that it succeeds is a sort of “happy accidental side effect” of searches. It would be better to be explicit about that. Anyway, here’s its output:
TASK [Check configured network:prefix combinations on stdout with one constructed search() -- SUCCEEDS] ***
ok: [localhost] => (item={'network': 'Test-nets-0', 'prefix': '10.0.0.0/25'}) =>
msg: 'localhost: Test-nets-0 permit 10.0.0.0/25 configured: True'
ok: [localhost] => (item={'network': 'Test-nets-0', 'prefix': '10.0.2.0/25'}) =>
msg: 'localhost: Test-nets-0 permit 10.0.2.0/25 configured: False'
ok: [localhost] => (item={'network': 'Test-nets-0', 'prefix': '10.0.5.0/25'}) =>
msg: 'localhost: Test-nets-0 permit 10.0.5.0/25 configured: False'
ok: [localhost] => (item={'network': 'Test-nets-2', 'prefix': '10.0.0.0/25'}) =>
msg: 'localhost: Test-nets-2 permit 10.0.0.0/25 configured: False'
ok: [localhost] => (item={'network': 'Test-nets-2', 'prefix': '10.0.2.0/25'}) =>
msg: 'localhost: Test-nets-2 permit 10.0.2.0/25 configured: True'
ok: [localhost] => (item={'network': 'Test-nets-2', 'prefix': '10.0.5.0/25'}) =>
msg: 'localhost: Test-nets-2 permit 10.0.5.0/25 configured: False'
ok: [localhost] => (item={'network': 'Test-nets-5', 'prefix': '10.0.0.0/25'}) =>
msg: 'localhost: Test-nets-5 permit 10.0.0.0/25 configured: False'
ok: [localhost] => (item={'network': 'Test-nets-5', 'prefix': '10.0.2.0/25'}) =>
msg: 'localhost: Test-nets-5 permit 10.0.2.0/25 configured: False'
ok: [localhost] => (item={'network': 'Test-nets-5', 'prefix': '10.0.5.0/25'}) =>
msg: 'localhost: Test-nets-5 permit 10.0.5.0/25 configured: True'
Here you can see that only tests 1, 5, and 9 set “configured: True
”.
Here’s another take on the same data. This uses the Jinja2 “in()
” test. It looks at items in a list, or (as in this case) substrings within strings. Again, easy to implement, and it gets it wrong two thirds of the time, in basically the same way the first task did:
- name: Check configured network::prefix combinations on stdout with Jinja2 "in" test -- FAILS
debug:
msg: "{{inventory_hostname}}: {{ item.network }} permit {{ item.prefix }} configured: {{ is_configured }}"
vars:
is_configured: '{{ item.network is in output2.stdout and
item.prefix is in output2.stdout }}'
loop: '{{ loop_items }}'
and its erroneous output:
TASK [Check configured network::prefix combinations on stdout with Jinja2 "in" test -- FAILS] ***
ok: [localhost] => (item={'network': 'Test-nets-0', 'prefix': '10.0.0.0/25'}) =>
msg: 'localhost: Test-nets-0 permit 10.0.0.0/25 configured: True'
ok: [localhost] => (item={'network': 'Test-nets-0', 'prefix': '10.0.2.0/25'}) =>
msg: 'localhost: Test-nets-0 permit 10.0.2.0/25 configured: True'
ok: [localhost] => (item={'network': 'Test-nets-0', 'prefix': '10.0.5.0/25'}) =>
msg: 'localhost: Test-nets-0 permit 10.0.5.0/25 configured: True'
ok: [localhost] => (item={'network': 'Test-nets-2', 'prefix': '10.0.0.0/25'}) =>
msg: 'localhost: Test-nets-2 permit 10.0.0.0/25 configured: True'
ok: [localhost] => (item={'network': 'Test-nets-2', 'prefix': '10.0.2.0/25'}) =>
msg: 'localhost: Test-nets-2 permit 10.0.2.0/25 configured: True'
ok: [localhost] => (item={'network': 'Test-nets-2', 'prefix': '10.0.5.0/25'}) =>
msg: 'localhost: Test-nets-2 permit 10.0.5.0/25 configured: True'
ok: [localhost] => (item={'network': 'Test-nets-5', 'prefix': '10.0.0.0/25'}) =>
msg: 'localhost: Test-nets-5 permit 10.0.0.0/25 configured: True'
ok: [localhost] => (item={'network': 'Test-nets-5', 'prefix': '10.0.2.0/25'}) =>
msg: 'localhost: Test-nets-5 permit 10.0.2.0/25 configured: True'
ok: [localhost] => (item={'network': 'Test-nets-5', 'prefix': '10.0.5.0/25'}) =>
msg: 'localhost: Test-nets-5 permit 10.0.5.0/25 configured: True'
Finally, here’s an example that looks at the input stream in list form: output2.stdout_lines
. It uses filters to select only the lines that “contain
” the network
, then selects from that subset only the lines that match the corresponding prefix
, and declares success if the result contains at least one item. Personally, I like this better because it is more applicable to structured data and avoids some pitfalls with text streams. But really it comes down to what you’re more comfortable with.
- name: Check configured network::prefix combinations on stdout_lines with filtering -- SUCCEEDS
debug:
msg: "{{inventory_hostname}}: {{ item.network }} permit {{ item.prefix }} configured: {{ is_configured }}"
vars:
is_configured: '{{ output2.stdout_lines
| select("contains", item.network)
| select("contains", item.prefix)
| length > 0 | bool }}'
loop: '{{ loop_items }}'
And here’s its corresponding - and correct - output:
TASK [Check configured network::prefix combinations on stdout_lines with filtering -- SUCCEEDS] ***
ok: [localhost] => (item={'network': 'Test-nets-0', 'prefix': '10.0.0.0/25'}) =>
msg: 'localhost: Test-nets-0 permit 10.0.0.0/25 configured: True'
ok: [localhost] => (item={'network': 'Test-nets-0', 'prefix': '10.0.2.0/25'}) =>
msg: 'localhost: Test-nets-0 permit 10.0.2.0/25 configured: False'
ok: [localhost] => (item={'network': 'Test-nets-0', 'prefix': '10.0.5.0/25'}) =>
msg: 'localhost: Test-nets-0 permit 10.0.5.0/25 configured: False'
ok: [localhost] => (item={'network': 'Test-nets-2', 'prefix': '10.0.0.0/25'}) =>
msg: 'localhost: Test-nets-2 permit 10.0.0.0/25 configured: False'
ok: [localhost] => (item={'network': 'Test-nets-2', 'prefix': '10.0.2.0/25'}) =>
msg: 'localhost: Test-nets-2 permit 10.0.2.0/25 configured: True'
ok: [localhost] => (item={'network': 'Test-nets-2', 'prefix': '10.0.5.0/25'}) =>
msg: 'localhost: Test-nets-2 permit 10.0.5.0/25 configured: False'
ok: [localhost] => (item={'network': 'Test-nets-5', 'prefix': '10.0.0.0/25'}) =>
msg: 'localhost: Test-nets-5 permit 10.0.0.0/25 configured: False'
ok: [localhost] => (item={'network': 'Test-nets-5', 'prefix': '10.0.2.0/25'}) =>
msg: 'localhost: Test-nets-5 permit 10.0.2.0/25 configured: False'
ok: [localhost] => (item={'network': 'Test-nets-5', 'prefix': '10.0.5.0/25'}) =>
msg: 'localhost: Test-nets-5 permit 10.0.5.0/25 configured: True'
It should be reasonably straightforward to adapt any of these examples (preferably one of the working ones!) to your needs.
But as always, if anything is not clear, just ask. We’re all here to help each other.
Cheers!