diff --git a/changelogs/fragments/12031-xml-predicate-xpath-nomatch.yml b/changelogs/fragments/12031-xml-predicate-xpath-nomatch.yml
new file mode 100644
index 0000000000..51eddd4da8
--- /dev/null
+++ b/changelogs/fragments/12031-xml-predicate-xpath-nomatch.yml
@@ -0,0 +1,5 @@
+minor_changes:
+ - xml - add ``create_if_missing`` option to control whether a node is created when
+ ``value`` is set and ``xpath`` finds no match
+ (https://github.com/ansible-collections/community.general/issues/8730,
+ https://github.com/ansible-collections/community.general/pull/12031).
diff --git a/plugins/modules/xml.py b/plugins/modules/xml.py
index 8624135aeb..545246a3cf 100644
--- a/plugins/modules/xml.py
+++ b/plugins/modules/xml.py
@@ -144,6 +144,13 @@ options:
- This parameter requires O(xpath) to be set.
type: bool
default: false
+ create_if_missing:
+ description:
+ - When using O(value) and the O(xpath) matches no nodes, create the node.
+ - When set to V(false), a no-match is silently ignored instead of creating a new node.
+ type: bool
+ default: true
+ version_added: "13.0.0"
requirements:
- lxml >= 2.3.0
notes:
@@ -673,11 +680,13 @@ def ensure_xpath_exists(module, tree, xpath, namespaces):
finish(module, tree, xpath, namespaces, changed)
-def set_target_inner(module, tree, xpath, namespaces, attribute, value):
+def set_target_inner(module, tree, xpath, namespaces, attribute, value, create_if_missing=True):
changed = False
try:
if not is_node(tree, xpath, namespaces):
+ if not create_if_missing:
+ return changed
changed = check_or_make_target(module, tree, xpath, namespaces)
except Exception as e:
missing_namespace = ""
@@ -722,8 +731,8 @@ def set_target_inner(module, tree, xpath, namespaces, attribute, value):
return changed
-def set_target(module, tree, xpath, namespaces, attribute, value):
- changed = set_target_inner(module, tree, xpath, namespaces, attribute, value)
+def set_target(module, tree, xpath, namespaces, attribute, value, create_if_missing):
+ changed = set_target_inner(module, tree, xpath, namespaces, attribute, value, create_if_missing)
finish(module, tree, xpath, namespaces, changed)
@@ -895,6 +904,7 @@ def main():
huge_tree=dict(type="bool", default=False),
insertbefore=dict(type="bool", default=False),
insertafter=dict(type="bool", default=False),
+ create_if_missing=dict(type="bool", default=True),
),
supports_check_mode=True,
required_by=dict(
@@ -939,6 +949,7 @@ def main():
huge_tree = module.params["huge_tree"]
insertbefore = module.params["insertbefore"]
insertafter = module.params["insertafter"]
+ create_if_missing = module.params["create_if_missing"]
# Check if we have lxml 2.3.0 or newer installed
if not HAS_LXML:
@@ -1014,7 +1025,7 @@ def main():
# Is the xpath target an attribute selector?
if value is not None:
- set_target(module, doc, xpath, namespaces, attribute, value)
+ set_target(module, doc, xpath, namespaces, attribute, value, create_if_missing)
# If an xpath was provided, we need to do something with the data
if xpath is not None:
diff --git a/tests/integration/targets/xml/fixtures/ansible-xml-items.xml b/tests/integration/targets/xml/fixtures/ansible-xml-items.xml
new file mode 100644
index 0000000000..f0d698d8f0
--- /dev/null
+++ b/tests/integration/targets/xml/fixtures/ansible-xml-items.xml
@@ -0,0 +1,8 @@
+
+
+
+ - value_a
+ - value_a
+ - value_b
+
+
diff --git a/tests/integration/targets/xml/fixtures/ansible-xml-items.xml.license b/tests/integration/targets/xml/fixtures/ansible-xml-items.xml.license
new file mode 100644
index 0000000000..edff8c7685
--- /dev/null
+++ b/tests/integration/targets/xml/fixtures/ansible-xml-items.xml.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/tests/integration/targets/xml/results/test-set-element-value-predicate.xml b/tests/integration/targets/xml/results/test-set-element-value-predicate.xml
new file mode 100644
index 0000000000..ae4ae99a75
--- /dev/null
+++ b/tests/integration/targets/xml/results/test-set-element-value-predicate.xml
@@ -0,0 +1,8 @@
+
+
+
+ - value_c
+ - value_c
+ - value_b
+
+
diff --git a/tests/integration/targets/xml/results/test-set-element-value-predicate.xml.license b/tests/integration/targets/xml/results/test-set-element-value-predicate.xml.license
new file mode 100644
index 0000000000..edff8c7685
--- /dev/null
+++ b/tests/integration/targets/xml/results/test-set-element-value-predicate.xml.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/tests/integration/targets/xml/tasks/main.yml b/tests/integration/targets/xml/tasks/main.yml
index 2dc633a25d..e69a28e578 100644
--- a/tests/integration/targets/xml/tasks/main.yml
+++ b/tests/integration/targets/xml/tasks/main.yml
@@ -66,6 +66,7 @@
- include_tasks: test-set-children-elements-value.yml
- include_tasks: test-set-element-value.yml
- include_tasks: test-set-element-value-empty.yml
+ - include_tasks: test-set-element-value-predicate.yml
- include_tasks: test-pretty-print.yml
- include_tasks: test-pretty-print-only.yml
- include_tasks: test-add-namespaced-children-elements.yml
diff --git a/tests/integration/targets/xml/tasks/test-set-element-value-predicate.yml b/tests/integration/targets/xml/tasks/test-set-element-value-predicate.yml
new file mode 100644
index 0000000000..90f2bdbc5c
--- /dev/null
+++ b/tests/integration/targets/xml/tasks/test-set-element-value-predicate.yml
@@ -0,0 +1,96 @@
+---
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Tests for https://github.com/ansible-collections/community.general/issues/8730
+
+# --- create_if_missing=false: predicate no-match must be a silent no-op ---
+
+- name: Setup test fixture
+ copy:
+ src: fixtures/ansible-xml-items.xml
+ dest: /tmp/ansible-xml-items.xml
+
+- name: Update items via predicate (create_if_missing=false, first run - matches)
+ xml:
+ path: /tmp/ansible-xml-items.xml
+ xpath: /business/items/item[text()='value_a']
+ value: value_c
+ create_if_missing: false
+ register: set_predicate_first_run
+
+- name: Update items via predicate (create_if_missing=false, second run - predicate no longer matches)
+ xml:
+ path: /tmp/ansible-xml-items.xml
+ xpath: /business/items/item[text()='value_a']
+ value: value_c
+ create_if_missing: false
+ register: set_predicate_second_run
+
+- name: Add trailing newline
+ shell: echo "" >> /tmp/ansible-xml-items.xml
+
+- name: Compare to expected result
+ copy:
+ src: results/test-set-element-value-predicate.xml
+ dest: /tmp/ansible-xml-items.xml
+ check_mode: true
+ diff: true
+ register: comparison
+
+- name: Assert create_if_missing=false with predicate no-match is a no-op
+ assert:
+ that:
+ - set_predicate_first_run is changed
+ - set_predicate_second_run is not changed
+ - comparison is not changed # no spurious elements created
+
+# --- create_if_missing=false: non-existent simple path must also be a no-op ---
+
+- name: Setup test fixture
+ copy:
+ src: fixtures/ansible-xml-items.xml
+ dest: /tmp/ansible-xml-items.xml
+
+- name: Set value on non-existent element (create_if_missing=false)
+ xml:
+ path: /tmp/ansible-xml-items.xml
+ xpath: /business/items/nonexistent
+ value: something
+ create_if_missing: false
+ register: set_nonexistent
+
+- name: Assert create_if_missing=false with missing simple path is a no-op
+ assert:
+ that:
+ - set_nonexistent is not changed
+
+# --- create_if_missing=true (default): no-match must create the node ---
+
+- name: Setup test fixture
+ copy:
+ src: fixtures/ansible-xml-items.xml
+ dest: /tmp/ansible-xml-items.xml
+
+- name: Set value on non-existent element (create_if_missing=true, should create)
+ xml:
+ path: /tmp/ansible-xml-items.xml
+ xpath: /business/items/item[text()='value_z']
+ value: value_z
+ create_if_missing: true
+ register: set_predicate_create
+
+- name: Set same value again (create_if_missing=true, should be idempotent)
+ xml:
+ path: /tmp/ansible-xml-items.xml
+ xpath: /business/items/item[text()='value_z']
+ value: value_z
+ create_if_missing: true
+ register: set_predicate_create_again
+
+- name: Assert create_if_missing=true creates the node and is then idempotent
+ assert:
+ that:
+ - set_predicate_create is changed
+ - set_predicate_create_again is not changed