From 2dbade4adc8c9e3bf288e2e9e1a3b219dfd41f0c Mon Sep 17 00:00:00 2001 From: rwinand Date: Tue, 8 Jan 2019 15:38:51 +0100 Subject: [PATCH] Introduce 'insertbefore' and 'insertafter' to specify the position (#44811) * Introduce 'insertbefore' and 'insertafter' to specify the position children have to be inserted. * Added version_added to new options * Xpath in example needs single quotes * Added integration tests for insertafter and insertbefore * Changed version_added to 2.8 --- lib/ansible/modules/files/xml.py | 64 +++++++++++++++++-- .../results/test-add-children-insertafter.xml | 17 +++++ .../test-add-children-insertbefore.xml | 17 +++++ test/integration/targets/xml/tasks/main.yml | 2 + .../tasks/test-add-children-insertafter.yml | 32 ++++++++++ .../tasks/test-add-children-insertbefore.yml | 32 ++++++++++ 6 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 test/integration/targets/xml/results/test-add-children-insertafter.xml create mode 100644 test/integration/targets/xml/results/test-add-children-insertbefore.xml create mode 100644 test/integration/targets/xml/tasks/test-add-children-insertafter.yml create mode 100644 test/integration/targets/xml/tasks/test-add-children-insertbefore.yml diff --git a/lib/ansible/modules/files/xml.py b/lib/ansible/modules/files/xml.py index e62a3c37b5..66a52e2917 100644 --- a/lib/ansible/modules/files/xml.py +++ b/lib/ansible/modules/files/xml.py @@ -122,6 +122,26 @@ options: type: bool default: no version_added: '2.7' + insertbefore: + description: + - Add additional child-element(s) before the first selected element for a given C(xpath). + - Child elements must be given in a list and each item may be either a string + (eg. C(children=ansible) to add an empty C() child element), + or a hash where the key is an element name and the value is the element value. + - This parameter requires C(xpath) to be set. + type: bool + default: no + version_added: '2.8' + insertafter: + description: + - Add additional child-element(s) after the last selected element for a given C(xpath). + - Child elements must be given in a list and each item may be either a string + (eg. C(children=ansible) to add an empty C() child element), + or a hash where the key is an element name and the value is the element value. + - This parameter requires C(xpath) to be set. + type: bool + default: no + version_added: '2.8' requirements: - lxml >= 2.3.0 notes: @@ -202,6 +222,16 @@ EXAMPLES = r''' - beer: Old Motor Oil - beer: Old Curmudgeon +- name: Add several more beers to the 'beers' element and add them before the 'Rochefort 10' element + xml: + path: /foo/bar.xml + xpath: '/business/beers/beer[text()=\"Rochefort 10\"]' + insertbefore: yes + add_children: + - beer: Old Rasputin + - beer: Old Motor Oil + - beer: Old Curmudgeon + # NOTE: The 'state' defaults to 'present' and 'value' defaults to 'null' for elements - name: Add a 'validxhtml' element to the 'website' element xml: @@ -446,16 +476,35 @@ def set_target_children(module, tree, xpath, namespaces, children, in_type): finish(module, tree, xpath, namespaces, changed=changed) -def add_target_children(module, tree, xpath, namespaces, children, in_type): +def add_target_children(module, tree, xpath, namespaces, children, in_type, insertbefore, insertafter): if is_node(tree, xpath, namespaces): new_kids = children_to_nodes(module, children, in_type) - for node in tree.xpath(xpath, namespaces=namespaces): - node.extend(new_kids) + if insertbefore or insertafter: + insert_target_children(tree, xpath, namespaces, new_kids, insertbefore, insertafter) + else: + for node in tree.xpath(xpath, namespaces=namespaces): + node.extend(new_kids) finish(module, tree, xpath, namespaces, changed=True) else: finish(module, tree, xpath, namespaces) +def insert_target_children(tree, xpath, namespaces, children, insertbefore, insertafter): + """ + Insert the given children before or after the given xpath. If insertbefore is True, it is inserted before the + first xpath hit, with insertafter, it is inserted after the last xpath hit. + """ + insert_target = tree.xpath(xpath, namespaces=namespaces) + loc_index = 0 if insertbefore else -1 + index_in_parent = insert_target[loc_index].getparent().index(insert_target[loc_index]) + parent = insert_target[0].getparent() + if insertafter: + index_in_parent += 1 + for child in children: + parent.insert(index_in_parent, child) + index_in_parent += 1 + + def _extract_xpstr(g): return g[1:-1] @@ -776,6 +825,8 @@ def main(): input_type=dict(type='str', default='yaml', choices=['xml', 'yaml']), backup=dict(type='bool', default=False), strip_cdata_tags=dict(type='bool', default=False), + insertbefore=dict(type='bool', default=False), + insertafter=dict(type='bool', default=False), ), supports_check_mode=True, # TODO: Implement this as soon as #28662 (required_by functionality) is merged @@ -790,6 +841,8 @@ def main(): ['content', 'text', ['xpath']], ['count', True, ['xpath']], ['print_match', True, ['xpath']], + ['insertbefore', True, ['xpath']], + ['insertafter', True, ['xpath']], ], required_one_of=[ ['path', 'xmlstring'], @@ -798,6 +851,7 @@ def main(): mutually_exclusive=[ ['add_children', 'content', 'count', 'print_match', 'set_children', 'value'], ['path', 'xmlstring'], + ['insertbefore', 'insertafter'], ], ) @@ -817,6 +871,8 @@ def main(): count = module.params['count'] backup = module.params['backup'] strip_cdata_tags = module.params['strip_cdata_tags'] + insertbefore = module.params['insertbefore'] + insertafter = module.params['insertafter'] # Check if we have lxml 2.3.0 or newer installed if not HAS_LXML: @@ -881,7 +937,7 @@ def main(): # add_children set? if add_children: - add_target_children(module, doc, xpath, namespaces, add_children, input_type) + add_target_children(module, doc, xpath, namespaces, add_children, input_type, insertbefore, insertafter) # No?: Carry on diff --git a/test/integration/targets/xml/results/test-add-children-insertafter.xml b/test/integration/targets/xml/results/test-add-children-insertafter.xml new file mode 100644 index 0000000000..8da9633636 --- /dev/null +++ b/test/integration/targets/xml/results/test-add-children-insertafter.xml @@ -0,0 +1,17 @@ + + + Tasty Beverage Co. + + Rochefort 10 + St. Bernardus Abbot 12 + Old Rasputin + Old Motor Oil + Old Curmudgeon + Schlitz + + 10 + + +
http://tastybeverageco.com
+
+
diff --git a/test/integration/targets/xml/results/test-add-children-insertbefore.xml b/test/integration/targets/xml/results/test-add-children-insertbefore.xml new file mode 100644 index 0000000000..c409e54bfa --- /dev/null +++ b/test/integration/targets/xml/results/test-add-children-insertbefore.xml @@ -0,0 +1,17 @@ + + + Tasty Beverage Co. + + Rochefort 10 + Old Rasputin + Old Motor Oil + Old Curmudgeon + St. Bernardus Abbot 12 + Schlitz + + 10 + + +
http://tastybeverageco.com
+
+
diff --git a/test/integration/targets/xml/tasks/main.yml b/test/integration/targets/xml/tasks/main.yml index e992990a28..ad2967351b 100644 --- a/test/integration/targets/xml/tasks/main.yml +++ b/test/integration/targets/xml/tasks/main.yml @@ -37,6 +37,8 @@ - include_tasks: test-add-children-elements.yml - include_tasks: test-add-children-from-groupvars.yml + - include_tasks: test-add-children-insertafter.yml + - include_tasks: test-add-children-insertbefore.yml - include_tasks: test-add-children-with-attributes.yml - include_tasks: test-add-element-implicitly.yml - include_tasks: test-count.yml diff --git a/test/integration/targets/xml/tasks/test-add-children-insertafter.yml b/test/integration/targets/xml/tasks/test-add-children-insertafter.yml new file mode 100644 index 0000000000..2d42e2d54e --- /dev/null +++ b/test/integration/targets/xml/tasks/test-add-children-insertafter.yml @@ -0,0 +1,32 @@ +--- + - name: Setup test fixture + copy: + src: fixtures/ansible-xml-beers.xml + dest: /tmp/ansible-xml-beers.xml + + + - name: Add child element + xml: + path: /tmp/ansible-xml-beers.xml + xpath: '/business/beers/beer[text()="St. Bernardus Abbot 12"]' + insertafter: yes + add_children: + - beer: Old Rasputin + - beer: Old Motor Oil + - beer: Old Curmudgeon + pretty_print: yes + register: add_children_insertafter + + - name: Compare to expected result + copy: + src: results/test-add-children-insertafter.xml + dest: /tmp/ansible-xml-beers.xml + check_mode: yes + diff: yes + register: comparison + + - name: Test expected result + assert: + that: + - add_children_insertafter.changed == true + - comparison.changed == false # identical diff --git a/test/integration/targets/xml/tasks/test-add-children-insertbefore.yml b/test/integration/targets/xml/tasks/test-add-children-insertbefore.yml new file mode 100644 index 0000000000..8550f12cf7 --- /dev/null +++ b/test/integration/targets/xml/tasks/test-add-children-insertbefore.yml @@ -0,0 +1,32 @@ +--- + - name: Setup test fixture + copy: + src: fixtures/ansible-xml-beers.xml + dest: /tmp/ansible-xml-beers.xml + + + - name: Add child element + xml: + path: /tmp/ansible-xml-beers.xml + xpath: '/business/beers/beer[text()="St. Bernardus Abbot 12"]' + insertbefore: yes + add_children: + - beer: Old Rasputin + - beer: Old Motor Oil + - beer: Old Curmudgeon + pretty_print: yes + register: add_children_insertbefore + + - name: Compare to expected result + copy: + src: results/test-add-children-insertbefore.xml + dest: /tmp/ansible-xml-beers.xml + check_mode: yes + diff: yes + register: comparison + + - name: Test expected result + assert: + that: + - add_children_insertbefore.changed == true + - comparison.changed == false # identical