Skip to content

Commit

Permalink
Merge pull request #49822 from mchugh19/xml_module
Browse files Browse the repository at this point in the history
initial xml module/state
  • Loading branch information
Mike Place authored Oct 19, 2018
2 parents fe3a582 + f6ba171 commit efd2365
Show file tree
Hide file tree
Showing 5 changed files with 425 additions and 0 deletions.
37 changes: 37 additions & 0 deletions doc/topics/releases/neon.rst
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,43 @@ New output:
Skipped:
0
XML Module
==========

A new state and execution module for editing XML files is now included. Currently it allows for
editing values from an xpath query, or editing XML IDs.

.. code-block:: bash
# salt-call xml.set_attribute /tmp/test.xml ".//actor[@id='3']" editedby "Jane Doe"
local:
True
# salt-call xml.get_attribute /tmp/test.xml ".//actor[@id='3']"
local:
----------
editedby:
Jane Doe
id:
3
# salt-call xml.get_value /tmp/test.xml ".//actor[@id='2']"
local:
Liam Neeson
# salt-call xml.set_value /tmp/test.xml ".//actor[@id='2']" "Patrick Stewart"
local:
True
# salt-call xml.get_value /tmp/test.xml ".//actor[@id='2']"
local:
Patrick Stewart
.. code-block:: yaml
ensure_value_true:
xml.value_present:
- name: /tmp/test.xml
- xpath: .//actor[@id='1']
- value: William Shatner
State Changes
=============
Expand Down
103 changes: 103 additions & 0 deletions salt/modules/xml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
'''
XML file mangler
.. versionadded:: Neon
'''
from __future__ import absolute_import, print_function, unicode_literals

import logging
import xml.etree.ElementTree as ET

log = logging.getLogger(__name__)


# Define the module's virtual name
__virtualname__ = 'xml'


def __virtual__():
'''
Only load the module if all modules are imported correctly.
'''
return __virtualname__


def get_value(file, element):
'''
Returns the value of the matched xpath element
CLI Example:
.. code-block:: bash
salt '*' xml.get_value /tmp/test.xml ".//element"
'''
try:
root = ET.parse(file)
element = root.find(element)
return element.text
except AttributeError:
log.error("Unable to find element matching %s", element)
return False


def set_value(file, element, value):
'''
Sets the value of the matched xpath element
CLI Example:
.. code-block:: bash
salt '*' xml.set_value /tmp/test.xml ".//element" "new value"
'''
try:
root = ET.parse(file)
relement = root.find(element)
except AttributeError:
log.error("Unable to find element matching %s", element)
return False
relement.text = str(value)
root.write(file)
return True


def get_attribute(file, element):
'''
Return the attributes of the matched xpath element.
CLI Example:
.. code-block:: bash
salt '*' xml.get_attribute /tmp/test.xml ".//element[@id='3']"
'''
try:
root = ET.parse(file)
element = root.find(element)
return element.attrib
except AttributeError:
log.error("Unable to find element matching %s", element)
return False


def set_attribute(file, element, key, value):
'''
Set the requested attribute key and value for matched xpath element.
CLI Example:
.. code-block:: bash
salt '*' xml.set_attribute /tmp/test.xml ".//element[@id='3']" editedby "gal"
'''
try:
root = ET.parse(file)
element = root.find(element)
except AttributeError:
log.error("Unable to find element matching %s", element)
return False
element.set(key, str(value))
root.write(file)
return True
76 changes: 76 additions & 0 deletions salt/states/xml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
'''
XML Mangler
===========
State managment of XML files
'''
from __future__ import absolute_import, print_function, unicode_literals

# Import Python libs
import logging

log = logging.getLogger(__name__)


def __virtual__():
'''
Only load if the XML execution module is available.
'''
if 'xml.get_value' in __salt__:
return 'xml'
else:
return False, "The xml execution module is not available"


def value_present(name, xpath, value, **kwargs):
'''
.. versionadded:: Neon
Manages a given XML file
name : string
The location of the XML file to manage, as an absolute path.
xpath : string
xpath location to manage
value : string
value to ensure present
.. code-block:: yaml
ensure_value_true:
xml.value_present:
- name: /tmp/test.xml
- xpath: .//playwright[@id='1']
- value: William Shakespeare
'''
ret = {'name': name,
'changes': {},
'result': True,
'comment': ''}

if 'test' not in kwargs:
kwargs['test'] = __opts__.get('test', False)

current_value = __salt__['xml.get_value'](name, xpath)
if not current_value:
ret['result'] = False
ret['comment'] = 'xpath query {0} not found in {1}'.format(xpath, name)
return ret

if current_value != value:
if kwargs['test']:
ret['result'] = None
ret['comment'] = '{0} will be updated'.format(name)
ret['changes'] = {name: {'old': current_value, 'new': value}}
else:
results = __salt__['xml.set_value'](name, xpath, value)
ret['result'] = results
ret['comment'] = '{0} updated'.format(name)
ret['changes'] = {name: {'old': current_value, 'new': value}}
else:
ret['comment'] = '{0} is already present'.format(value)

return ret
101 changes: 101 additions & 0 deletions tests/unit/modules/test_xml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# -*- coding: utf-8 -*-
'''
Tests for xml module
'''

from __future__ import absolute_import, print_function, unicode_literals

import os
import tempfile

from salt.modules import xml

from tests.support.mixins import LoaderModuleMockMixin
from tests.support.unit import TestCase, skipIf
from tests.support.mock import (
NO_MOCK,
NO_MOCK_REASON
)

XML_STRING = '''
<root xmlns:foo="http://www.foo.org/" xmlns:bar="http://www.bar.org">
<actors>
<actor id="1">Christian Bale</actor>
<actor id="2">Liam Neeson</actor>
<actor id="3">Michael Caine</actor>
</actors>
<foo:singers>
<foo:singer id="4">Tom Waits</foo:singer>
<foo:singer id="5">B.B. King</foo:singer>
<foo:singer id="6">Ray Charles</foo:singer>
</foo:singers>
</root>
'''


@skipIf(NO_MOCK, NO_MOCK_REASON)
class XmlTestCase(TestCase, LoaderModuleMockMixin):
'''
Test cases for salt.modules.xml
'''

def setup_loader_modules(self):
return {xml: {}}

def test_get_value(self):
'''
Verify xml.get_value
'''
with tempfile.NamedTemporaryFile('w+', delete=False) as xml_file:
xml_file.write(XML_STRING)
xml_file.flush()

xml_result = xml.get_value(xml_file.name, ".//actor[@id='2']")
self.assertEqual(xml_result, "Liam Neeson")

os.remove(xml_file.name)

def test_set_value(self):
'''
Verify xml.set_value
'''
with tempfile.NamedTemporaryFile('w+', delete=False) as xml_file:
xml_file.write(XML_STRING)
xml_file.flush()

xml_result = xml.set_value(xml_file.name, ".//actor[@id='2']", "Patrick Stewart")
assert xml_result is True

xml_result = xml.get_value(xml_file.name, ".//actor[@id='2']")
self.assertEqual(xml_result, "Patrick Stewart")

os.remove(xml_file.name)

def test_get_attribute(self):
'''
Verify xml.get_attribute
'''
with tempfile.NamedTemporaryFile('w+', delete=False) as xml_file:
xml_file.write(XML_STRING)
xml_file.flush()

xml_result = xml.get_attribute(xml_file.name, ".//actor[@id='3']")
self.assertEqual(xml_result, {"id": "3"})

os.remove(xml_file.name)

def test_set_attribute(self):
'''
Verify xml.set_value
'''
with tempfile.NamedTemporaryFile('w+', delete=False) as xml_file:
xml_file.write(XML_STRING)
xml_file.flush()

xml_result = xml.set_attribute(xml_file.name, ".//actor[@id='3']", "edited", "uh-huh")
assert xml_result is True

xml_result = xml.get_attribute(xml_file.name, ".//actor[@id='3']")
self.assertEqual(xml_result, {'edited': 'uh-huh', 'id': '3'})

os.remove(xml_file.name)
Loading

0 comments on commit efd2365

Please sign in to comment.