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

master-port 49822 and 52217 #54977

Merged
merged 12 commits into from
Jan 6, 2020
1 change: 1 addition & 0 deletions doc/ref/modules/all/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,7 @@ execution modules
xapi_virt
xbpspkg
xfs
xml
xmpp
yumpkg
zabbix
Expand Down
6 changes: 6 additions & 0 deletions doc/ref/modules/all/salt.modules.xml.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
================
salt.modules.xml
================

.. automodule:: salt.modules.xml
:members:
1 change: 1 addition & 0 deletions doc/ref/states/all/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ state modules
win_wusa
winrepo
x509
xml
xmpp
zabbix_action
zabbix_host
Expand Down
6 changes: 6 additions & 0 deletions doc/ref/states/all/salt.states.xml.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
===============
salt.states.xml
===============

.. automodule:: salt.states.xml
:members:
39 changes: 36 additions & 3 deletions doc/topics/releases/neon.rst
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,41 @@ as well as managing keystore files.
Hn+GmxZA
-----END CERTIFICATE-----

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

Jinja enhancements
==================
Expand Down Expand Up @@ -111,7 +146,6 @@ The module can be also used to test ``json`` and ``yaml`` maps:

salt myminion jinja.import_json myformula/defaults.json


json_query filter
-----------------

Expand Down Expand Up @@ -170,7 +204,6 @@ Also, slot parsing is now supported inside of nested state data structures (dict
- "DO NOT OVERRIDE"
ignore_if_missing: True


State Changes
=============

Expand Down Expand Up @@ -452,4 +485,4 @@ salt.auth.Authorize Class Removal
---------------------------------
- The salt.auth.Authorize Class inside of the `salt/auth/__init__.py` file has been removed and
the `any_auth` method inside of the file `salt/utils/minions.py`. These method and classes were
not being used inside of the salt code base.
not being used inside of the salt code base.
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 manager

.. 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 Manager
===========

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)
mchugh19 marked this conversation as resolved.
Show resolved Hide resolved
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