Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

initial xml module/state #49822

Merged
merged 10 commits into from
Oct 19, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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