Skip to content

Commit 187b58f

Browse files
Utils fix issues with cli_parse and facts (#428)
* fix utils * Update fix for tpp * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update changes for tests * fix uts again * sanittyyy * test for utils ipaddress_utils * Check --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent c5cf775 commit 187b58f

File tree

12 files changed

+205
-71
lines changed

12 files changed

+205
-71
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
bugfixes:
3+
- cli_parse - Honor ttp_results.results flat_list in TTP parser so output is a single-level list instead of double-wrapped (https://github.com/ansible-collections/ansible.utils/issues/402).
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
bugfixes:
3+
- update_fact - Use task_vars at top-level instead of the deprecated ``vars`` key for compatibility with ansible-core 2.24 (ansible/ansible issue #426).
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
bugfixes:
3+
- cidr_merge - Fix filter failing when used inside a Jinja2 macro called with ``with context`` by unwrapping Ansible lazy template lists before validation.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
bugfixes:
3+
- ipaddress_utils - Support Python 3.14+ by using the public ``version`` attribute instead of the removed private ``_version`` on ``ipaddress`` network objects (bpo-118710).

plugins/action/update_fact.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -163,23 +163,30 @@ def run(self, tmp=None, task_vars=None):
163163
self._result["changed"] = False
164164
self._check_argspec()
165165
results = set()
166+
full_replaces = set() # keys that were fully replaced (no path)
166167
self._ensure_valid_jinja()
168+
# Use task_vars (top-level) instead of task_vars["vars"] to avoid the
169+
# deprecated internal "vars" dictionary (ansible-core 2.24, issue #426).
167170
for entry in self._task.args["updates"]:
168171
parts = self._field_split(entry["path"])
169172
obj, path = parts[0], parts[1:]
170173
results.add(obj)
171-
if obj not in task_vars["vars"]:
174+
if obj not in task_vars:
172175
msg = "'{obj}' was not found in the current facts.".format(obj=obj)
173176
raise AnsibleActionFail(msg)
174-
retrieved = task_vars["vars"].get(obj)
177+
retrieved = task_vars.get(obj)
175178
if path:
176179
self.set_value(retrieved, path, entry["value"])
177180
else:
178-
if task_vars["vars"][obj] != entry["value"]:
179-
task_vars["vars"][obj] = entry["value"]
181+
if retrieved != entry["value"]:
182+
self._result.setdefault("ansible_facts", {})[obj] = entry["value"]
183+
full_replaces.add(obj)
180184
self._result["changed"] = True
181185

182186
for key in results:
183-
value = task_vars["vars"].get(key)
187+
if key in full_replaces:
188+
value = self._result.get("ansible_facts", {}).get(key)
189+
else:
190+
value = task_vars.get(key)
184191
self._result[key] = value
185192
return self._result

plugins/filter/cidr_merge.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,26 @@ class mac_linux(netaddr.mac_unix):
129129
"""
130130

131131

132+
def _unwrap_lazy_list(value):
133+
"""
134+
If value is an Ansible lazy template list, return a list of raw elements without
135+
triggering templar.template() resolution. This avoids Template.__new__() errors
136+
when the filter is used inside a Jinja2 macro called with ``with context``.
137+
"""
138+
if value is not None and hasattr(value, "_yield_non_lazy_list_items"):
139+
return list(value._yield_non_lazy_list_items())
140+
return value
141+
142+
132143
@pass_environment
133144
def _cidr_merge(*args, **kwargs):
134-
"""Convert the given data from json to xml."""
145+
"""Merge CIDRs; compatible with pipe syntax (value | cidr_merge)."""
135146
keys = ["value", "action"]
136147
data = dict(zip(keys, args[1:]))
137148
data.update(kwargs)
149+
# Unwrap lazy list when present to avoid resolution bug in macro context (no template.j2 change).
150+
if "value" in data:
151+
data["value"] = _unwrap_lazy_list(data["value"])
138152
aav = AnsibleArgSpecValidator(data=data, schema=DOCUMENTATION, name="cidr_merge")
139153
valid, errors, updated_data = aav.validate()
140154
if not valid:

plugins/plugin_utils/base/ipaddress_utils.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,19 @@ def _get_network_version(network):
7070

7171

7272
def _is_subnet_of(network_a, network_b):
73+
"""
74+
Return True if network_a is a subnet of network_b (same logic as ipaddress).
75+
Uses the public .version attribute for compatibility with Python 3.14+ where
76+
the private _version was removed (see bpo-118710 / cpython@c530ce1).
77+
"""
7378
try:
7479
if _get_network_version(network_a) != _get_network_version(network_b):
7580
return False
7681
return (
7782
network_b.network_address <= network_a.network_address
7883
and network_b.broadcast_address >= network_a.broadcast_address
7984
)
80-
except Exception:
85+
except (AttributeError, TypeError):
8186
return False
8287

8388

plugins/sub_plugins/cli_parser/ttp_parser.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,23 @@ def parse(self, *_args, **_kwargs):
119119
if parser_param.get("vars")
120120
else {}
121121
)
122-
results = parser.result(**ttp_results)
122+
# TTP's result() uses keyword "structure", but we accept "results" from users
123+
result_kwargs = dict(ttp_results)
124+
if "results" in result_kwargs:
125+
result_kwargs["structure"] = result_kwargs.pop("results")
126+
results = parser.result(**result_kwargs)
127+
# When flat_list is requested, flatten one level if we got [[...]]
128+
requested_flat = (
129+
ttp_results.get("results") == "flat_list"
130+
or ttp_results.get("structure") == "flat_list"
131+
)
132+
if (
133+
requested_flat
134+
and isinstance(results, list)
135+
and len(results) == 1
136+
and isinstance(results[0], list)
137+
):
138+
results = results[0]
123139
except Exception as exc:
124140
msg = "Template Text Parser returned an error while parsing. Error: {err}"
125141
return {"errors": [msg.format(err=to_native(exc))]}

tests/unit/plugins/action/test_update_fact.py

Lines changed: 47 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -137,24 +137,23 @@ def test_missing_var(self):
137137
"""Check for a missing fact"""
138138
self._plugin._task.args = {"updates": [{"path": "a.b.c", "value": 5}]}
139139
with self.assertRaises(Exception) as error:
140-
self._plugin.run(task_vars={"vars": {}})
140+
self._plugin.run(task_vars={})
141141
self.assertIn("'a' was not found in the current facts.", str(error.exception))
142142

143143
def test_run_simple(self):
144144
"""Confirm a valid argspec passes"""
145-
task_vars = {"vars": {"a": {"b": [1, 2, 3]}}}
146-
expected = copy.deepcopy(task_vars["vars"])
145+
task_vars = {"a": {"b": [1, 2, 3]}}
146+
expected = copy.deepcopy(task_vars)
147147
expected["a"]["b"] = 5
148-
expected.update({"changed": True})
148+
expected["changed"] = True
149149
self._plugin._task.args = {"updates": [{"path": "a.b", "value": 5}]}
150150
result = self._plugin.run(task_vars=task_vars)
151151
self.assertEqual(result, expected)
152152

153153
def test_run_multiple(self):
154154
"""Confirm multiple paths passes"""
155-
task_vars = {"vars": {"a": {"b1": [1, 2, 3], "b2": {"c": "123", "d": False}}}}
156-
expected = {"a": {"b1": [1, 2, 3, 4], "b2": {"c": 456, "d": True}}}
157-
expected.update({"changed": True})
155+
task_vars = {"a": {"b1": [1, 2, 3], "b2": {"c": "123", "d": False}}}
156+
expected = {"a": {"b1": [1, 2, 3, 4], "b2": {"c": 456, "d": True}}, "changed": True}
158157
self._plugin._task.args = {
159158
"updates": [
160159
{"path": "a.b1.3", "value": 4},
@@ -167,60 +166,60 @@ def test_run_multiple(self):
167166

168167
def test_run_replace_in_list(self):
169168
"""Replace in list"""
170-
task_vars = {"vars": {"a": {"b": [1, 2, 3]}}}
171-
expected = copy.deepcopy(task_vars["vars"])
169+
task_vars = {"a": {"b": [1, 2, 3]}}
170+
expected = copy.deepcopy(task_vars)
172171
expected["a"]["b"][1] = 5
173-
expected.update({"changed": True})
172+
expected["changed"] = True
174173
self._plugin._task.args = {"updates": [{"path": "a.b.1", "value": 5}]}
175174
result = self._plugin.run(task_vars=task_vars)
176175
self.assertEqual(result, expected)
177176

178177
def test_run_append_to_list(self):
179178
"""Append to list"""
180-
task_vars = {"vars": {"a": {"b": [1, 2, 3]}}}
181-
expected = copy.deepcopy(task_vars["vars"])
179+
task_vars = {"a": {"b": [1, 2, 3]}}
180+
expected = copy.deepcopy(task_vars)
182181
expected["a"]["b"].append(4)
183-
expected.update({"changed": True})
182+
expected["changed"] = True
184183
self._plugin._task.args = {"updates": [{"path": "a.b.3", "value": 4}]}
185184
result = self._plugin.run(task_vars=task_vars)
186185
self.assertEqual(result, expected)
187186

188187
def test_run_bracket_single_quote(self):
189188
"""Bracket notation sigle quote"""
190-
task_vars = {"vars": {"a": {"b": [1, 2, 3]}}}
191-
expected = copy.deepcopy(task_vars["vars"])
189+
task_vars = {"a": {"b": [1, 2, 3]}}
190+
expected = copy.deepcopy(task_vars)
192191
expected["a"]["b"].append(4)
193-
expected.update({"changed": True})
192+
expected["changed"] = True
194193
self._plugin._task.args = {"updates": [{"path": "a['b'][3]", "value": 4}]}
195194
result = self._plugin.run(task_vars=task_vars)
196195
self.assertEqual(result, expected)
197196

198197
def test_run_bracket_double_quote(self):
199198
"""Bracket notation double quote"""
200-
task_vars = {"vars": {"a": {"b": [1, 2, 3]}}}
201-
expected = copy.deepcopy(task_vars["vars"])
199+
task_vars = {"a": {"b": [1, 2, 3]}}
200+
expected = copy.deepcopy(task_vars)
202201
expected["a"]["b"].append(4)
203-
expected.update({"changed": True})
202+
expected["changed"] = True
204203
self._plugin._task.args = {"updates": [{"path": 'a["b"][3]', "value": 4}]}
205204
result = self._plugin.run(task_vars=task_vars)
206205
self.assertEqual(result, expected)
207206

208207
def test_run_int_dict_keys(self):
209208
"""Integer dict keys"""
210-
task_vars = {"vars": {"a": {0: [1, 2, 3]}}}
211-
expected = copy.deepcopy(task_vars["vars"])
209+
task_vars = {"a": {0: [1, 2, 3]}}
210+
expected = copy.deepcopy(task_vars)
212211
expected["a"][0][0] = 0
213-
expected.update({"changed": True})
212+
expected["changed"] = True
214213
self._plugin._task.args = {"updates": [{"path": "a.0.0", "value": 0}]}
215214
result = self._plugin.run(task_vars=task_vars)
216215
self.assertEqual(result, expected)
217216

218217
def test_run_int_as_string(self):
219218
"""Integer dict keys as string"""
220-
task_vars = {"vars": {"a": {"0": [1, 2, 3]}}}
221-
expected = copy.deepcopy(task_vars["vars"])
219+
task_vars = {"a": {"0": [1, 2, 3]}}
220+
expected = copy.deepcopy(task_vars)
222221
expected["a"]["0"][0] = 0
223-
expected.update({"changed": True})
222+
expected["changed"] = True
224223
self._plugin._task.args = {"updates": [{"path": 'a["0"].0', "value": 0}]}
225224
result = self._plugin.run(task_vars=task_vars)
226225
self.assertEqual(result, expected)
@@ -229,62 +228,60 @@ def test_run_invalid_path_quote_after_dot(self):
229228
"""Invalid path format"""
230229
self._plugin._task.args = {"updates": [{"path": "a.'b'", "value": 0}]}
231230
with self.assertRaises(Exception) as error:
232-
self._plugin.run(task_vars={"vars": {}})
231+
self._plugin.run(task_vars={})
233232
self.assertIn("malformed", str(error.exception))
234233

235234
def test_run_invalid_path_bracket_after_dot(self):
236235
"""Invalid path format"""
237236
self._plugin._task.args = {"updates": [{"path": "a.['b']", "value": 0}]}
238237
with self.assertRaises(Exception) as error:
239-
self._plugin.run(task_vars={"vars": {}})
238+
self._plugin.run(task_vars={})
240239
self.assertIn("malformed", str(error.exception))
241240

242241
def test_run_invalid_key_start_with_dot(self):
243242
"""Invalid key format"""
244243
self._plugin._task.args = {"updates": [{"path": ".abc", "value": 0}]}
245244
with self.assertRaises(Exception) as error:
246-
self._plugin.run(task_vars={"vars": {}})
245+
self._plugin.run(task_vars={})
247246
self.assertIn("malformed", str(error.exception))
248247

249248
def test_run_no_update_list(self):
250249
"""Confirm no change when same"""
251-
task_vars = {"vars": {"a": {"b": [1, 2, 3]}}}
252-
expected = copy.deepcopy(task_vars["vars"])
253-
expected["a"]["b"] = [1, 2, 3]
254-
expected.update({"changed": False})
250+
task_vars = {"a": {"b": [1, 2, 3]}}
251+
expected = copy.deepcopy(task_vars)
252+
expected["changed"] = False
255253
self._plugin._task.args = {"updates": [{"path": "a.b.0", "value": 1}]}
256254
result = self._plugin.run(task_vars=task_vars)
257255
self.assertEqual(result, expected)
258256

259257
def test_run_no_update_dict(self):
260258
"""Confirm no change when same"""
261-
task_vars = {"vars": {"a": {"b": [1, 2, 3]}}}
262-
expected = copy.deepcopy(task_vars["vars"])
263-
expected["a"]["b"] = [1, 2, 3]
264-
expected.update({"changed": False})
259+
task_vars = {"a": {"b": [1, 2, 3]}}
260+
expected = copy.deepcopy(task_vars)
261+
expected["changed"] = False
265262
self._plugin._task.args = {"updates": [{"path": "a.b", "value": [1, 2, 3]}]}
266263
result = self._plugin.run(task_vars=task_vars)
267264
self.assertEqual(result, expected)
268265

269266
def test_run_missing_key(self):
270267
"""Confirm error when key not found"""
271-
task_vars = {"vars": {"a": {"b": 1}}}
268+
task_vars = {"a": {"b": 1}}
272269
self._plugin._task.args = {"updates": [{"path": "a.c.d", "value": 1}]}
273270
with self.assertRaises(Exception) as error:
274271
self._plugin.run(task_vars=task_vars)
275272
self.assertIn("the key 'c' was not found", str(error.exception))
276273

277274
def test_run_list_not_int(self):
278275
"""Confirm error when key not found"""
279-
task_vars = {"vars": {"a": {"b": [1]}}}
276+
task_vars = {"a": {"b": [1]}}
280277
self._plugin._task.args = {"updates": [{"path": "a.b['0']", "value": 2}]}
281278
with self.assertRaises(Exception) as error:
282279
self._plugin.run(task_vars=task_vars)
283280
self.assertIn("index provided was not an integer", str(error.exception))
284281

285282
def test_run_list_not_long(self):
286283
"""List not long enough"""
287-
task_vars = {"vars": {"a": {"b": [0]}}}
284+
task_vars = {"a": {"b": [0]}}
288285
self._plugin._task.args = {"updates": [{"path": "a.b.2", "value": 2}]}
289286
with self.assertRaises(Exception) as error:
290287
self._plugin.run(task_vars=task_vars)
@@ -303,47 +300,43 @@ def test_not_mutable_sequence_or_mapping(self):
303300

304301
def test_run_not_dotted_success_one(self):
305302
"""Test with a not dotted key"""
306-
task_vars = {"vars": {"a": 0}}
307-
expected = copy.deepcopy(task_vars["vars"])
308-
expected["a"] = 1
309-
expected.update({"changed": True})
303+
task_vars = {"a": 0}
304+
expected = {"a": 1, "ansible_facts": {"a": 1}, "changed": True}
310305
self._plugin._task.args = {"updates": [{"path": "a", "value": 1}]}
311306
result = self._plugin.run(task_vars=task_vars)
312307
self.assertEqual(result, expected)
313308

314309
def test_run_not_dotted_success_three(self):
315310
"""Test with a not dotted key longer"""
316-
task_vars = {"vars": {"abc": 0}}
317-
expected = copy.deepcopy(task_vars["vars"])
318-
expected["abc"] = 1
319-
expected.update({"changed": True})
311+
task_vars = {"abc": 0}
312+
expected = {"abc": 1, "ansible_facts": {"abc": 1}, "changed": True}
320313
self._plugin._task.args = {"updates": [{"path": "abc", "value": 1}]}
321314
result = self._plugin.run(task_vars=task_vars)
322315
self.assertEqual(result, expected)
323316

324317
def test_run_not_dotted_fail_missing(self):
325318
"""Test with a not dotted key, missing"""
326-
task_vars = {"vars": {"abc": 0}}
319+
task_vars = {"abc": 0}
327320
self._plugin._task.args = {"updates": [{"path": "123", "value": 1}]}
328321
with self.assertRaises(Exception) as error:
329322
self._plugin.run(task_vars=task_vars)
330323
self.assertIn("'123' was not found in the current facts", str(error.exception))
331324

332325
def test_run_not_dotted_success_same(self):
333326
"""Test with a not dotted key, no change"""
334-
task_vars = {"vars": {"a": 0}}
335-
expected = copy.deepcopy(task_vars["vars"])
336-
expected.update({"changed": False})
327+
task_vars = {"a": 0}
328+
expected = copy.deepcopy(task_vars)
329+
expected["changed"] = False
337330
self._plugin._task.args = {"updates": [{"path": "a", "value": 0}]}
338331
result = self._plugin.run(task_vars=task_vars)
339332
self.assertEqual(result, expected)
340333

341334
def test_run_looks_like_a_bool(self):
342335
"""Test with a key that looks like a bool"""
343-
task_vars = {"vars": {"a": {"True": 0}}}
344-
expected = copy.deepcopy(task_vars["vars"])
336+
task_vars = {"a": {"True": 0}}
337+
expected = copy.deepcopy(task_vars)
345338
expected["a"]["True"] = 1
346-
expected.update({"changed": True})
339+
expected["changed"] = True
347340
self._plugin._task.args = {"updates": [{"path": "a['True']", "value": 1}]}
348341
result = self._plugin.run(task_vars=task_vars)
349342
self.assertEqual(result, expected)

0 commit comments

Comments
 (0)