Summary
Running an Ansible playbook that manages BGP route-maps on my VyOS 1.4 edge routers, the vyos.vyos.vyos_route_maps task (state=gathered or state=merged) takes ~50 seconds per host, sometimes 100s+, against devices with only a handful of route-maps configured. The persistent connection times out before the module finishes and the failure surfaces as a misleading "socket path does not exist":
text
fatal: [router]: FAILED! => {"changed": false,
"msg": "vyos failed: socket path does not exist or cannot be found. ..."}The connection helper is fine; the module subprocess is stuck inside a single parse() call. Bumping [persistent_connection] connect_timeout to 600 s in ansible.cfg hides the symptom but not the underlying slowness.
The cause is a regex in plugins/module_utils/network/vyos/rm_templates/route_maps_14.py, parser name="route_map", lines 33-39:
re.compile( r""" ^set\spolicy\sroute-map\s(?P<route_map>\S+) *$""", re.VERBOSE, )
re.VERBOSE strips unescaped whitespace, newlines included, so the pattern that actually executes is ^set\spolicy\sroute-map\s(?P<route_map>\S+)*$. The trailing * ends up quantifying the named group. When that runs against any line that shares the set policy route-map prefix but doesn't match overall (any rule-line under any route-map), the engine has to enumerate every way of splitting the first contiguous non-whitespace run into pieces of (\S+)+, which is O(2^n) in that run's length.
For a 20-character name like ADVERTISE-ANYCAST-v6 the cost is about 2.5 s per non-matching line on Python 3.12. A 12-line route-map config costs around 50 s, and parse() is invoked twice in facts/route_maps/route_maps.py:81-82.
The same pattern shape ((?P<X>\S+)\n *$ inside re.VERBOSE) exists in 197 sibling parsers across nine rm_templates/*.py files (survey below). I searched vyos.dev before filing and didn't find prior reports for the older templates; presumably the inputs they parse use short identifiers where 2^(name length) per non-matching line still finishes in microseconds. The cliff only becomes visible once names are 20+ characters, which is normal for any operator using descriptive naming.
Affected versions
- Verified on vyos.vyos collection version 6.0.0 at vyos/vyos.vyos@d3f9811 (current main HEAD).
- git blame plugins/module_utils/network/vyos/rm_templates/route_maps_14.py on the offending lines points at commit cd2f41d3 from T6883 ("vyos_route_map doesn't support some additive route-map options"), upstream PR #402, 2025-05-16. Every parser added or rewritten in that PR carries the same pattern.
- The same pattern shape exists in older bgp_*, snmp_server, and ospf_interfaces* templates that pre-date T6883.
Affected parsers (full survey)
Walking every PARSERS list under plugins/module_utils/network/vyos/rm_templates/, applying the same whitespace strip that re.VERBOSE does, and counting patterns whose stripped form ends in (?P<...>\S+)*$:
| File | Bad parsers |
| -------------------------- | ----------: |
| route_maps.py | 43 |
| route_maps_14.py | 41/49 |
| bgp_global.py | 31 |
| bgp_global_14.py | 31 |
| bgp_address_family.py | 15 |
| bgp_address_family_14.py | 15 |
| snmp_server.py | 15 |
| ospf_interfaces_14.py | 4 |
| ospf_interfaces.py | 3 |
| Total | 198 |
Files in rm_templates/ with no affected parsers: hostname.py, logging_global.py, ntp_global.py, prefix_lists.py, vrf.py, __init__.py.
Reproduction
Three ways to reproduce, in order of decreasing dependency footprint.
A. Standalone, no ansible needed
repro_standalone.py imports only re and time, copies the offending pattern verbatim, and times it against 12 and 25 realistic non-matching lines plus a \s*$-fixed variant for comparison.
python3 repro_standalone.pySample output, Python 3.12 / x86_64:
text Negative case (rule lines that share the prefix but don't match): bad pattern matched=0 elapsed= 51.7349s (12 lines) fixed pattern matched=0 elapsed= 0.0000s (12 lines) Negative case scaled to 25 lines (more realistic device output): bad pattern matched=0 elapsed=103.6123s (25 lines) fixed pattern matched=0 elapsed= 0.0000s (25 lines)
The cost is linear in lines and exponential in the length of the first contiguous \S+ run after the prefix. Short names like MY-MAP (6 chars) finish in microseconds and won't reproduce; use 20+ char names.
B. Collection-level
repro_with_collection.py drives the actual Route_mapsTemplate14.parse() call:
ANSIBLE_COLLECTIONS_PATHS=~/.ansible/collections \ python3 repro_with_collection.py
text input: 12 lines, 940 chars PARSERS in Route_mapsTemplate14: 49 parse() elapsed: 53.37s (returned 5 route_maps)
C. End-to-end Ansible
This is what I hit originally. Any playbook that drives vyos.vyos.vyos_route_maps (state=gathered or state=merged) against a device whose show configuration commands | grep route-map output contains route-maps with realistic-length names will hit the failure shown in the Summary above.
Fix
Replacing the trailing literal newline + indent + *$ with \s*$ behaves the same on positive inputs and avoids the backtracking on non-matching ones:
# BAD is what the source compiles to after re.VERBOSE strips whitespace. BAD = re.compile(r"^set\spolicy\sroute-map\s(?P<route_map>\S+)*$", re.VERBOSE) FIXED = re.compile(r"^set\spolicy\sroute-map\s(?P<route_map>\S+)\s*$", re.VERBOSE) assert BAD.match("set policy route-map FOO").group("route_map") == "FOO" assert FIXED.match("set policy route-map FOO").group("route_map") == "FOO"
Happy to follow up with a PR covering all nine files plus a regression test.
Test coverage gap
Two reasons this regression went unnoticed:
- The largest unit fixture for route_maps_14, tests/unit/modules/network/vyos/fixtures/vyos_route_maps_config_v14.cfg, is 18 lines, one route-map named test3, one rule. At that size the bad pattern still finishes in microseconds because 2^5 backtracks per non-matching line is fast. The same is true for the bgp_* and snmp_server fixtures.
- There are no perf or timeout assertions in the test suite. A unit that genuinely takes 60 s to match would just make pytest a minute slower; nothing fails.
A simple regression check, suitable for tests/unit/modules/network/vyos/test_vyos_route_maps14.py (or a new file):
import time from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.route_maps_14 import ( Route_mapsTemplate14, ) def test_parse_does_not_backtrack_pathologically(): # 12 rule-lines with realistic-length names; the kind of input a # real device returns from `show configuration commands | grep # route-map`. lines = [ "set policy route-map ADVERTISE-ANYCAST-v6 rule 10 action 'permit'", "set policy route-map ADVERTISE-ANYCAST-v6 rule 10 match ipv6 address prefix-list 'ANYCAST-AGGREGATE-v6'", "set policy route-map DEFAULT-ORIGINATE-SENTINEL-v6 rule 10 action 'permit'", # ...etc; see repro_standalone.py for a full set ] parser = Route_mapsTemplate14(lines=lines) t0 = time.perf_counter() parser.parse() dt = time.perf_counter() - t0 # Without the fix this is ~50 s; with it, well under 100 ms. assert dt < 1.0, f"parse() took {dt:.1f}s, possible regex backtracking regression"
The same shape works for bgp_global*, bgp_address_family*, snmp_server, and ospf_interfaces* templates.
Environment used to verify
- Python 3.12.4
- vyos.vyos 6.0.0 at upstream commit d3f9811 (2026-05-01 HEAD)
- ansible-core 2.18.x
- Linux x86_64
The bug is independent of the Ansible/network-cli stack; both reproducers demonstrate it without any ansible runtime involved.