diff options
author | Matt Martz <matt@sivel.net> | 2020-09-29 08:39:44 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-09-29 08:39:44 -0500 |
commit | f2512f793135c9e7bfba71d95241935afad92f4c (patch) | |
tree | d976e1485b58a0c76a80a1500117ad2fde75423a /test/support | |
parent | cbd3df369f0efa4f67b74ad76c431a3b2c357f26 (diff) | |
download | ansible-f2512f793135c9e7bfba71d95241935afad92f4c.tar.gz |
Remove incidental_xml integration tests (#71917)
* Add explicit integration tests to cover incidental_xml coverage
* normalize diff
* More normalization
* diff without changed
* ci_complete ci_coverage
* nuke incidental_xml
* ci_complete ci_coverage
Diffstat (limited to 'test/support')
-rw-r--r-- | test/support/integration/plugins/modules/xml.py | 966 |
1 files changed, 0 insertions, 966 deletions
diff --git a/test/support/integration/plugins/modules/xml.py b/test/support/integration/plugins/modules/xml.py deleted file mode 100644 index b5b35a384e..0000000000 --- a/test/support/integration/plugins/modules/xml.py +++ /dev/null @@ -1,966 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2014, Red Hat, Inc. -# Copyright: (c) 2014, Tim Bielawa <tbielawa@redhat.com> -# Copyright: (c) 2014, Magnus Hedemark <mhedemar@redhat.com> -# Copyright: (c) 2017, Dag Wieers <dag@wieers.com> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = r''' ---- -module: xml -short_description: Manage bits and pieces of XML files or strings -description: -- A CRUD-like interface to managing bits of XML files. -version_added: '2.4' -options: - path: - description: - - Path to the file to operate on. - - This file must exist ahead of time. - - This parameter is required, unless C(xmlstring) is given. - type: path - required: yes - aliases: [ dest, file ] - xmlstring: - description: - - A string containing XML on which to operate. - - This parameter is required, unless C(path) is given. - type: str - required: yes - xpath: - description: - - A valid XPath expression describing the item(s) you want to manipulate. - - Operates on the document root, C(/), by default. - type: str - namespaces: - description: - - The namespace C(prefix:uri) mapping for the XPath expression. - - Needs to be a C(dict), not a C(list) of items. - type: dict - state: - description: - - Set or remove an xpath selection (node(s), attribute(s)). - type: str - choices: [ absent, present ] - default: present - aliases: [ ensure ] - attribute: - description: - - The attribute to select when using parameter C(value). - - This is a string, not prepended with C(@). - type: raw - value: - description: - - Desired state of the selected attribute. - - Either a string, or to unset a value, the Python C(None) keyword (YAML Equivalent, C(null)). - - Elements default to no value (but present). - - Attributes default to an empty string. - type: raw - add_children: - description: - - Add additional child-element(s) to a 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(<ansible/>) 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: list - set_children: - description: - - Set the child-element(s) of a selected element for a given C(xpath). - - Removes any existing children. - - Child elements must be specified as in C(add_children). - - This parameter requires C(xpath) to be set. - type: list - count: - description: - - Search for a given C(xpath) and provide the count of any matches. - - This parameter requires C(xpath) to be set. - type: bool - default: no - print_match: - description: - - Search for a given C(xpath) and print out any matches. - - This parameter requires C(xpath) to be set. - type: bool - default: no - pretty_print: - description: - - Pretty print XML output. - type: bool - default: no - content: - description: - - Search for a given C(xpath) and get content. - - This parameter requires C(xpath) to be set. - type: str - choices: [ attribute, text ] - input_type: - description: - - Type of input for C(add_children) and C(set_children). - type: str - choices: [ xml, yaml ] - default: yaml - backup: - description: - - Create a backup file including the timestamp information so you can get - the original file back if you somehow clobbered it incorrectly. - type: bool - default: no - strip_cdata_tags: - description: - - Remove CDATA tags surrounding text values. - - Note that this might break your XML file if text values contain characters that could be interpreted as XML. - 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(<ansible/>) 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(<ansible/>) 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: -- Use the C(--check) and C(--diff) options when testing your expressions. -- The diff output is automatically pretty-printed, so may not reflect the actual file content, only the file structure. -- This module does not handle complicated xpath expressions, so limit xpath selectors to simple expressions. -- Beware that in case your XML elements are namespaced, you need to use the C(namespaces) parameter, see the examples. -- Namespaces prefix should be used for all children of an element where namespace is defined, unless another namespace is defined for them. -seealso: -- name: Xml module development community wiki - description: More information related to the development of this xml module. - link: https://github.com/ansible/community/wiki/Module:-xml -- name: Introduction to XPath - description: A brief tutorial on XPath (w3schools.com). - link: https://www.w3schools.com/xml/xpath_intro.asp -- name: XPath Reference document - description: The reference documentation on XSLT/XPath (developer.mozilla.org). - link: https://developer.mozilla.org/en-US/docs/Web/XPath -author: -- Tim Bielawa (@tbielawa) -- Magnus Hedemark (@magnus919) -- Dag Wieers (@dagwieers) -''' - -EXAMPLES = r''' -# Consider the following XML file: -# -# <business type="bar"> -# <name>Tasty Beverage Co.</name> -# <beers> -# <beer>Rochefort 10</beer> -# <beer>St. Bernardus Abbot 12</beer> -# <beer>Schlitz</beer> -# </beers> -# <rating subjective="true">10</rating> -# <website> -# <mobilefriendly/> -# <address>http://tastybeverageco.com</address> -# </website> -# </business> - -- name: Remove the 'subjective' attribute of the 'rating' element - xml: - path: /foo/bar.xml - xpath: /business/rating/@subjective - state: absent - -- name: Set the rating to '11' - xml: - path: /foo/bar.xml - xpath: /business/rating - value: 11 - -# Retrieve and display the number of nodes -- name: Get count of 'beers' nodes - xml: - path: /foo/bar.xml - xpath: /business/beers/beer - count: yes - register: hits - -- debug: - var: hits.count - -# Example where parent XML nodes are created automatically -- name: Add a 'phonenumber' element to the 'business' element - xml: - path: /foo/bar.xml - xpath: /business/phonenumber - value: 555-555-1234 - -- name: Add several more beers to the 'beers' element - xml: - path: /foo/bar.xml - xpath: /business/beers - add_children: - - beer: Old Rasputin - - 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: - path: /foo/bar.xml - xpath: /business/website/validxhtml - -- name: Add an empty 'validatedon' attribute to the 'validxhtml' element - xml: - path: /foo/bar.xml - xpath: /business/website/validxhtml/@validatedon - -- name: Add or modify an attribute, add element if needed - xml: - path: /foo/bar.xml - xpath: /business/website/validxhtml - attribute: validatedon - value: 1976-08-05 - -# How to read an attribute value and access it in Ansible -- name: Read an element's attribute values - xml: - path: /foo/bar.xml - xpath: /business/website/validxhtml - content: attribute - register: xmlresp - -- name: Show an attribute value - debug: - var: xmlresp.matches[0].validxhtml.validatedon - -- name: Remove all children from the 'website' element (option 1) - xml: - path: /foo/bar.xml - xpath: /business/website/* - state: absent - -- name: Remove all children from the 'website' element (option 2) - xml: - path: /foo/bar.xml - xpath: /business/website - children: [] - -# In case of namespaces, like in below XML, they have to be explicitly stated. -# -# <foo xmlns="http://x.test" xmlns:attr="http://z.test"> -# <bar> -# <baz xmlns="http://y.test" attr:my_namespaced_attribute="true" /> -# </bar> -# </foo> - -# NOTE: There is the prefix 'x' in front of the 'bar' element, too. -- name: Set namespaced '/x:foo/x:bar/y:baz/@z:my_namespaced_attribute' to 'false' - xml: - path: foo.xml - xpath: /x:foo/x:bar/y:baz - namespaces: - x: http://x.test - y: http://y.test - z: http://z.test - attribute: z:my_namespaced_attribute - value: 'false' -''' - -RETURN = r''' -actions: - description: A dictionary with the original xpath, namespaces and state. - type: dict - returned: success - sample: {xpath: xpath, namespaces: [namespace1, namespace2], state=present} -backup_file: - description: The name of the backup file that was created - type: str - returned: when backup=yes - sample: /path/to/file.xml.1942.2017-08-24@14:16:01~ -count: - description: The count of xpath matches. - type: int - returned: when parameter 'count' is set - sample: 2 -matches: - description: The xpath matches found. - type: list - returned: when parameter 'print_match' is set -msg: - description: A message related to the performed action(s). - type: str - returned: always -xmlstring: - description: An XML string of the resulting output. - type: str - returned: when parameter 'xmlstring' is set -''' - -import copy -import json -import os -import re -import traceback - -from distutils.version import LooseVersion -from io import BytesIO - -LXML_IMP_ERR = None -try: - from lxml import etree, objectify - HAS_LXML = True -except ImportError: - LXML_IMP_ERR = traceback.format_exc() - HAS_LXML = False - -from ansible.module_utils.basic import AnsibleModule, json_dict_bytes_to_unicode, missing_required_lib -from ansible.module_utils.six import iteritems, string_types -from ansible.module_utils._text import to_bytes, to_native -from ansible.module_utils.common._collections_compat import MutableMapping - -_IDENT = r"[a-zA-Z-][a-zA-Z0-9_\-\.]*" -_NSIDENT = _IDENT + "|" + _IDENT + ":" + _IDENT -# Note: we can't reasonably support the 'if you need to put both ' and " in a string, concatenate -# strings wrapped by the other delimiter' XPath trick, especially as simple XPath. -_XPSTR = "('(?:.*)'|\"(?:.*)\")" - -_RE_SPLITSIMPLELAST = re.compile("^(.*)/(" + _NSIDENT + ")$") -_RE_SPLITSIMPLELASTEQVALUE = re.compile("^(.*)/(" + _NSIDENT + ")/text\\(\\)=" + _XPSTR + "$") -_RE_SPLITSIMPLEATTRLAST = re.compile("^(.*)/(@(?:" + _NSIDENT + "))$") -_RE_SPLITSIMPLEATTRLASTEQVALUE = re.compile("^(.*)/(@(?:" + _NSIDENT + "))=" + _XPSTR + "$") -_RE_SPLITSUBLAST = re.compile("^(.*)/(" + _NSIDENT + ")\\[(.*)\\]$") -_RE_SPLITONLYEQVALUE = re.compile("^(.*)/text\\(\\)=" + _XPSTR + "$") - - -def has_changed(doc): - orig_obj = etree.tostring(objectify.fromstring(etree.tostring(orig_doc))) - obj = etree.tostring(objectify.fromstring(etree.tostring(doc))) - return (orig_obj != obj) - - -def do_print_match(module, tree, xpath, namespaces): - match = tree.xpath(xpath, namespaces=namespaces) - match_xpaths = [] - for m in match: - match_xpaths.append(tree.getpath(m)) - match_str = json.dumps(match_xpaths) - msg = "selector '%s' match: %s" % (xpath, match_str) - finish(module, tree, xpath, namespaces, changed=False, msg=msg) - - -def count_nodes(module, tree, xpath, namespaces): - """ Return the count of nodes matching the xpath """ - hits = tree.xpath("count(/%s)" % xpath, namespaces=namespaces) - msg = "found %d nodes" % hits - finish(module, tree, xpath, namespaces, changed=False, msg=msg, hitcount=int(hits)) - - -def is_node(tree, xpath, namespaces): - """ Test if a given xpath matches anything and if that match is a node. - - For now we just assume you're only searching for one specific thing.""" - if xpath_matches(tree, xpath, namespaces): - # OK, it found something - match = tree.xpath(xpath, namespaces=namespaces) - if isinstance(match[0], etree._Element): - return True - - return False - - -def is_attribute(tree, xpath, namespaces): - """ Test if a given xpath matches and that match is an attribute - - An xpath attribute search will only match one item""" - if xpath_matches(tree, xpath, namespaces): - match = tree.xpath(xpath, namespaces=namespaces) - if isinstance(match[0], etree._ElementStringResult): - return True - elif isinstance(match[0], etree._ElementUnicodeResult): - return True - return False - - -def xpath_matches(tree, xpath, namespaces): - """ Test if a node exists """ - if tree.xpath(xpath, namespaces=namespaces): - return True - return False - - -def delete_xpath_target(module, tree, xpath, namespaces): - """ Delete an attribute or element from a tree """ - try: - for result in tree.xpath(xpath, namespaces=namespaces): - # Get the xpath for this result - if is_attribute(tree, xpath, namespaces): - # Delete an attribute - parent = result.getparent() - # Pop this attribute match out of the parent - # node's 'attrib' dict by using this match's - # 'attrname' attribute for the key - parent.attrib.pop(result.attrname) - elif is_node(tree, xpath, namespaces): - # Delete an element - result.getparent().remove(result) - else: - raise Exception("Impossible error") - except Exception as e: - module.fail_json(msg="Couldn't delete xpath target: %s (%s)" % (xpath, e)) - else: - finish(module, tree, xpath, namespaces, changed=True) - - -def replace_children_of(children, match): - for element in list(match): - match.remove(element) - match.extend(children) - - -def set_target_children_inner(module, tree, xpath, namespaces, children, in_type): - matches = tree.xpath(xpath, namespaces=namespaces) - - # Create a list of our new children - children = children_to_nodes(module, children, in_type) - children_as_string = [etree.tostring(c) for c in children] - - changed = False - - # xpaths always return matches as a list, so.... - for match in matches: - # Check if elements differ - if len(list(match)) == len(children): - for idx, element in enumerate(list(match)): - if etree.tostring(element) != children_as_string[idx]: - replace_children_of(children, match) - changed = True - break - else: - replace_children_of(children, match) - changed = True - - return changed - - -def set_target_children(module, tree, xpath, namespaces, children, in_type): - changed = set_target_children_inner(module, tree, xpath, namespaces, children, in_type) - # Write it out - finish(module, tree, xpath, namespaces, changed=changed) - - -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) - 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] - - -def split_xpath_last(xpath): - """split an XPath of the form /foo/bar/baz into /foo/bar and baz""" - xpath = xpath.strip() - m = _RE_SPLITSIMPLELAST.match(xpath) - if m: - # requesting an element to exist - return (m.group(1), [(m.group(2), None)]) - m = _RE_SPLITSIMPLELASTEQVALUE.match(xpath) - if m: - # requesting an element to exist with an inner text - return (m.group(1), [(m.group(2), _extract_xpstr(m.group(3)))]) - - m = _RE_SPLITSIMPLEATTRLAST.match(xpath) - if m: - # requesting an attribute to exist - return (m.group(1), [(m.group(2), None)]) - m = _RE_SPLITSIMPLEATTRLASTEQVALUE.match(xpath) - if m: - # requesting an attribute to exist with a value - return (m.group(1), [(m.group(2), _extract_xpstr(m.group(3)))]) - - m = _RE_SPLITSUBLAST.match(xpath) - if m: - content = [x.strip() for x in m.group(3).split(" and ")] - return (m.group(1), [('/' + m.group(2), content)]) - - m = _RE_SPLITONLYEQVALUE.match(xpath) - if m: - # requesting a change of inner text - return (m.group(1), [("", _extract_xpstr(m.group(2)))]) - return (xpath, []) - - -def nsnameToClark(name, namespaces): - if ":" in name: - (nsname, rawname) = name.split(":") - # return "{{%s}}%s" % (namespaces[nsname], rawname) - return "{{{0}}}{1}".format(namespaces[nsname], rawname) - - # no namespace name here - return name - - -def check_or_make_target(module, tree, xpath, namespaces): - (inner_xpath, changes) = split_xpath_last(xpath) - if (inner_xpath == xpath) or (changes is None): - module.fail_json(msg="Can't process Xpath %s in order to spawn nodes! tree is %s" % - (xpath, etree.tostring(tree, pretty_print=True))) - return False - - changed = False - - if not is_node(tree, inner_xpath, namespaces): - changed = check_or_make_target(module, tree, inner_xpath, namespaces) - - # we test again after calling check_or_make_target - if is_node(tree, inner_xpath, namespaces) and changes: - for (eoa, eoa_value) in changes: - if eoa and eoa[0] != '@' and eoa[0] != '/': - # implicitly creating an element - new_kids = children_to_nodes(module, [nsnameToClark(eoa, namespaces)], "yaml") - if eoa_value: - for nk in new_kids: - nk.text = eoa_value - - for node in tree.xpath(inner_xpath, namespaces=namespaces): - node.extend(new_kids) - changed = True - # module.fail_json(msg="now tree=%s" % etree.tostring(tree, pretty_print=True)) - elif eoa and eoa[0] == '/': - element = eoa[1:] - new_kids = children_to_nodes(module, [nsnameToClark(element, namespaces)], "yaml") - for node in tree.xpath(inner_xpath, namespaces=namespaces): - node.extend(new_kids) - for nk in new_kids: - for subexpr in eoa_value: - # module.fail_json(msg="element=%s subexpr=%s node=%s now tree=%s" % - # (element, subexpr, etree.tostring(node, pretty_print=True), etree.tostring(tree, pretty_print=True)) - check_or_make_target(module, nk, "./" + subexpr, namespaces) - changed = True - - # module.fail_json(msg="now tree=%s" % etree.tostring(tree, pretty_print=True)) - elif eoa == "": - for node in tree.xpath(inner_xpath, namespaces=namespaces): - if (node.text != eoa_value): - node.text = eoa_value - changed = True - - elif eoa and eoa[0] == '@': - attribute = nsnameToClark(eoa[1:], namespaces) - - for element in tree.xpath(inner_xpath, namespaces=namespaces): - changing = (attribute not in element.attrib or element.attrib[attribute] != eoa_value) - - if changing: - changed = changed or changing - if eoa_value is None: - value = "" - else: - value = eoa_value - element.attrib[attribute] = value - - # module.fail_json(msg="arf %s changing=%s as curval=%s changed tree=%s" % - # (xpath, changing, etree.tostring(tree, changing, element[attribute], pretty_print=True))) - - else: - module.fail_json(msg="unknown tree transformation=%s" % etree.tostring(tree, pretty_print=True)) - - return changed - - -def ensure_xpath_exists(module, tree, xpath, namespaces): - changed = False - - if not is_node(tree, xpath, namespaces): - changed = check_or_make_target(module, tree, xpath, namespaces) - - finish(module, tree, xpath, namespaces, changed) - - -def set_target_inner(module, tree, xpath, namespaces, attribute, value): - changed = False - - try: - if not is_node(tree, xpath, namespaces): - changed = check_or_make_target(module, tree, xpath, namespaces) - except Exception as e: - missing_namespace = "" - # NOTE: This checks only the namespaces defined in root element! - # TODO: Implement a more robust check to check for child namespaces' existence - if tree.getroot().nsmap and ":" not in xpath: - missing_namespace = "XML document has namespace(s) defined, but no namespace prefix(es) used in xpath!\n" - module.fail_json(msg="%sXpath %s causes a failure: %s\n -- tree is %s" % - (missing_namespace, xpath, e, etree.tostring(tree, pretty_print=True)), exception=traceback.format_exc()) - - if not is_node(tree, xpath, namespaces): - module.fail_json(msg="Xpath %s does not reference a node! tree is %s" % - (xpath, etree.tostring(tree, pretty_print=True))) - - for element in tree.xpath(xpath, namespaces=namespaces): - if not attribute: - changed = changed or (element.text != value) - if element.text != value: - element.text = value - else: - changed = changed or (element.get(attribute) != value) - if ":" in attribute: - attr_ns, attr_name = attribute.split(":") - # attribute = "{{%s}}%s" % (namespaces[attr_ns], attr_name) - attribute = "{{{0}}}{1}".format(namespaces[attr_ns], attr_name) - if element.get(attribute) != value: - element.set(attribute, value) - - return changed - - -def set_target(module, tree, xpath, namespaces, attribute, value): - changed = set_target_inner(module, tree, xpath, namespaces, attribute, value) - finish(module, tree, xpath, namespaces, changed) - - -def get_element_text(module, tree, xpath, namespaces): - if not is_node(tree, xpath, namespaces): - module.fail_json(msg="Xpath %s does not reference a node!" % xpath) - - elements = [] - for element in tree.xpath(xpath, namespaces=namespaces): - elements.append({element.tag: element.text}) - - finish(module, tree, xpath, namespaces, changed=False, msg=len(elements), hitcount=len(elements), matches=elements) - - -def get_element_attr(module, tree, xpath, namespaces): - if not is_node(tree, xpath, namespaces): - module.fail_json(msg="Xpath %s does not reference a node!" % xpath) - - elements = [] - for element in tree.xpath(xpath, namespaces=namespaces): - child = {} - for key in element.keys(): - value = element.get(key) - child.update({key: value}) - elements.append({element.tag: child}) - - finish(module, tree, xpath, namespaces, changed=False, msg=len(elements), hitcount=len(elements), matches=elements) - - -def child_to_element(module, child, in_type): - if in_type == 'xml': - infile = BytesIO(to_bytes(child, errors='surrogate_or_strict')) - - try: - parser = etree.XMLParser() - node = etree.parse(infile, parser) - return node.getroot() - except etree.XMLSyntaxError as e: - module.fail_json(msg="Error while parsing child element: %s" % e) - elif in_type == 'yaml': - if isinstance(child, string_types): - return etree.Element(child) - elif isinstance(child, MutableMapping): - if len(child) > 1: - module.fail_json(msg="Can only create children from hashes with one key") - - (key, value) = next(iteritems(child)) - if isinstance(value, MutableMapping): - children = value.pop('_', None) - - node = etree.Element(key, value) - - if children is not None: - if not isinstance(children, list): - module.fail_json(msg="Invalid children type: %s, must be list." % type(children)) - - subnodes = children_to_nodes(module, children) - node.extend(subnodes) - else: - node = etree.Element(key) - node.text = value - return node - else: - module.fail_json(msg="Invalid child type: %s. Children must be either strings or hashes." % type(child)) - else: - module.fail_json(msg="Invalid child input type: %s. Type must be either xml or yaml." % in_type) - - -def children_to_nodes(module=None, children=None, type='yaml'): - """turn a str/hash/list of str&hash into a list of elements""" - children = [] if children is None else children - - return [child_to_element(module, child, type) for child in children] - - -def make_pretty(module, tree): - xml_string = etree.tostring(tree, xml_declaration=True, encoding='UTF-8', pretty_print=module.params['pretty_print']) - - result = dict( - changed=False, - ) - - if module.params['path']: - xml_file = module.params['path'] - with open(xml_file, 'rb') as xml_content: - if xml_string != xml_content.read(): - result['changed'] = True - if not module.check_mode: - if module.params['backup']: - result['backup_file'] = module.backup_local(module.params['path']) - tree.write(xml_file, xml_declaration=True, encoding='UTF-8', pretty_print=module.params['pretty_print']) - - elif module.params['xmlstring']: - result['xmlstring'] = xml_string - # NOTE: Modifying a string is not considered a change ! - if xml_string != module.params['xmlstring']: - result['changed'] = True - - module.exit_json(**result) - - -def finish(module, tree, xpath, namespaces, changed=False, msg='', hitcount=0, matches=tuple()): - - result = dict( - actions=dict( - xpath=xpath, - namespaces=namespaces, - state=module.params['state'] - ), - changed=has_changed(tree), - ) - - if module.params['count'] or hitcount: - result['count'] = hitcount - - if module.params['print_match'] or matches: - result['matches'] = matches - - if msg: - result['msg'] = msg - - if result['changed']: - if module._diff: - result['diff'] = dict( - before=etree.tostring(orig_doc, xml_declaration=True, encoding='UTF-8', pretty_print=True), - after=etree.tostring(tree, xml_declaration=True, encoding='UTF-8', pretty_print=True), - ) - - if module.params['path'] and not module.check_mode: - if module.params['backup']: - result['backup_file'] = module.backup_local(module.params['path']) - tree.write(module.params['path'], xml_declaration=True, encoding='UTF-8', pretty_print=module.params['pretty_print']) - - if module.params['xmlstring']: - result['xmlstring'] = etree.tostring(tree, xml_declaration=True, encoding='UTF-8', pretty_print=module.params['pretty_print']) - - module.exit_json(**result) - - -def main(): - module = AnsibleModule( - argument_spec=dict( - path=dict(type='path', aliases=['dest', 'file']), - xmlstring=dict(type='str'), - xpath=dict(type='str'), - namespaces=dict(type='dict', default={}), - state=dict(type='str', default='present', choices=['absent', 'present'], aliases=['ensure']), - value=dict(type='raw'), - attribute=dict(type='raw'), - add_children=dict(type='list'), - set_children=dict(type='list'), - count=dict(type='bool', default=False), - print_match=dict(type='bool', default=False), - pretty_print=dict(type='bool', default=False), - content=dict(type='str', choices=['attribute', 'text']), - 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, - required_by=dict( - add_children=['xpath'], - # TODO: Reinstate this in Ansible v2.12 when we have deprecated the incorrect use below - # attribute=['value'], - content=['xpath'], - set_children=['xpath'], - value=['xpath'], - ), - required_if=[ - ['count', True, ['xpath']], - ['print_match', True, ['xpath']], - ['insertbefore', True, ['xpath']], - ['insertafter', True, ['xpath']], - ], - required_one_of=[ - ['path', 'xmlstring'], - ['add_children', 'content', 'count', 'pretty_print', 'print_match', 'set_children', 'value'], - ], - mutually_exclusive=[ - ['add_children', 'content', 'count', 'print_match', 'set_children', 'value'], - ['path', 'xmlstring'], - ['insertbefore', 'insertafter'], - ], - ) - - xml_file = module.params['path'] - xml_string = module.params['xmlstring'] - xpath = module.params['xpath'] - namespaces = module.params['namespaces'] - state = module.params['state'] - value = json_dict_bytes_to_unicode(module.params['value']) - attribute = module.params['attribute'] - set_children = json_dict_bytes_to_unicode(module.params['set_children']) - add_children = json_dict_bytes_to_unicode(module.params['add_children']) - pretty_print = module.params['pretty_print'] - content = module.params['content'] - input_type = module.params['input_type'] - print_match = module.params['print_match'] - 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: - module.fail_json(msg=missing_required_lib("lxml"), exception=LXML_IMP_ERR) - elif LooseVersion('.'.join(to_native(f) for f in etree.LXML_VERSION)) < LooseVersion('2.3.0'): - module.fail_json(msg='The xml ansible module requires lxml 2.3.0 or newer installed on the managed machine') - elif LooseVersion('.'.join(to_native(f) for f in etree.LXML_VERSION)) < LooseVersion('3.0.0'): - module.warn('Using lxml version lower than 3.0.0 does not guarantee predictable element attribute order.') - - # Report wrongly used attribute parameter when using content=attribute - # TODO: Remove this in Ansible v2.12 (and reinstate strict parameter test above) and remove the integration test example - if content == 'attribute' and attribute is not None: - module.deprecate("Parameter 'attribute=%s' is ignored when using 'content=attribute' only 'xpath' is used. Please remove entry." % attribute, - '2.12', collection_name='ansible.builtin') - - # Check if the file exists - if xml_string: - infile = BytesIO(to_bytes(xml_string, errors='surrogate_or_strict')) - elif os.path.isfile(xml_file): - infile = open(xml_file, 'rb') - else: - module.fail_json(msg="The target XML source '%s' does not exist." % xml_file) - - # Parse and evaluate xpath expression - if xpath is not None: - try: - etree.XPath(xpath) - except etree.XPathSyntaxError as e: - module.fail_json(msg="Syntax error in xpath expression: %s (%s)" % (xpath, e)) - except etree.XPathEvalError as e: - module.fail_json(msg="Evaluation error in xpath expression: %s (%s)" % (xpath, e)) - - # Try to parse in the target XML file - try: - parser = etree.XMLParser(remove_blank_text=pretty_print, strip_cdata=strip_cdata_tags) - doc = etree.parse(infile, parser) - except etree.XMLSyntaxError as e: - module.fail_json(msg="Error while parsing document: %s (%s)" % (xml_file or 'xml_string', e)) - - # Ensure we have the original copy to compare - global orig_doc - orig_doc = copy.deepcopy(doc) - - if print_match: - do_print_match(module, doc, xpath, namespaces) - - if count: - count_nodes(module, doc, xpath, namespaces) - - if content == 'attribute': - get_element_attr(module, doc, xpath, namespaces) - elif content == 'text': - get_element_text(module, doc, xpath, namespaces) - - # File exists: - if state == 'absent': - # - absent: delete xpath target - delete_xpath_target(module, doc, xpath, namespaces) - - # - present: carry on - - # children && value both set?: should have already aborted by now - # add_children && set_children both set?: should have already aborted by now - - # set_children set? - if set_children: - set_target_children(module, doc, xpath, namespaces, set_children, input_type) - - # add_children set? - if add_children: - add_target_children(module, doc, xpath, namespaces, add_children, input_type, insertbefore, insertafter) - - # No?: Carry on - - # Is the xpath target an attribute selector? - if value is not None: - set_target(module, doc, xpath, namespaces, attribute, value) - - # If an xpath was provided, we need to do something with the data - if xpath is not None: - ensure_xpath_exists(module, doc, xpath, namespaces) - - # Otherwise only reformat the xml data? - if pretty_print: - make_pretty(module, doc) - - module.fail_json(msg="Don't know what to do") - - -if __name__ == '__main__': - main() |