diff --git a/doc/ref/modules/all/index.rst b/doc/ref/modules/all/index.rst index 339b3ebe4d76..c1bb6791f0d4 100644 --- a/doc/ref/modules/all/index.rst +++ b/doc/ref/modules/all/index.rst @@ -511,6 +511,7 @@ execution modules xapi_virt xbpspkg xfs + xml xmpp yumpkg zabbix diff --git a/doc/ref/modules/all/salt.modules.xml.rst b/doc/ref/modules/all/salt.modules.xml.rst new file mode 100644 index 000000000000..52b4ffa06898 --- /dev/null +++ b/doc/ref/modules/all/salt.modules.xml.rst @@ -0,0 +1,6 @@ +================ +salt.modules.xml +================ + +.. automodule:: salt.modules.xml + :members: diff --git a/doc/ref/states/all/index.rst b/doc/ref/states/all/index.rst index 699895d8a226..6670befcae2c 100644 --- a/doc/ref/states/all/index.rst +++ b/doc/ref/states/all/index.rst @@ -315,6 +315,7 @@ state modules win_wusa winrepo x509 + xml xmpp zabbix_action zabbix_host diff --git a/doc/ref/states/all/salt.states.xml.rst b/doc/ref/states/all/salt.states.xml.rst new file mode 100644 index 000000000000..e6756c6639ab --- /dev/null +++ b/doc/ref/states/all/salt.states.xml.rst @@ -0,0 +1,6 @@ +=============== +salt.states.xml +=============== + +.. automodule:: salt.states.xml + :members: diff --git a/doc/topics/releases/neon.rst b/doc/topics/releases/neon.rst index 0c7caef44e45..456de0f130c6 100644 --- a/doc/topics/releases/neon.rst +++ b/doc/topics/releases/neon.rst @@ -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 ================== @@ -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 ----------------- @@ -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 ============= @@ -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. \ No newline at end of file diff --git a/salt/modules/xml.py b/salt/modules/xml.py new file mode 100644 index 000000000000..a439c6d7e9c7 --- /dev/null +++ b/salt/modules/xml.py @@ -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 diff --git a/salt/states/xml.py b/salt/states/xml.py new file mode 100644 index 000000000000..78c4bc0c336b --- /dev/null +++ b/salt/states/xml.py @@ -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 diff --git a/tests/unit/modules/test_xml.py b/tests/unit/modules/test_xml.py new file mode 100644 index 000000000000..07662added01 --- /dev/null +++ b/tests/unit/modules/test_xml.py @@ -0,0 +1,96 @@ +# -*- 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 + +XML_STRING = ''' + + + Christian Bale + Liam Neeson + Michael Caine + + + Tom Waits + B.B. King + Ray Charles + + + ''' + + +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) diff --git a/tests/unit/states/test_xml.py b/tests/unit/states/test_xml.py new file mode 100644 index 000000000000..29a6337eace1 --- /dev/null +++ b/tests/unit/states/test_xml.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +''' +Test cases for xml state +''' +# Import Python libs +from __future__ import absolute_import, print_function, unicode_literals + +# Import Salt Testing Libs +from tests.support.mixins import LoaderModuleMockMixin +from tests.support.unit import TestCase +from tests.support.mock import ( + MagicMock, + patch) + +# Import Salt Libs +import salt.states.xml as xml + + +class XMLTestCase(TestCase, LoaderModuleMockMixin): + ''' + Test cases for salt.states.xml + ''' + def setup_loader_modules(self): + return {xml: {}} + + def test_value_already_present(self): + ''' + Test for existing value_present + ''' + + name = "testfile.xml" + xpath = ".//list[@id='1']" + value = "test value" + + state_return = { + 'name': name, + 'changes': {}, + 'result': True, + 'comment': '{0} is already present'.format(value) + } + + with patch.dict(xml.__salt__, {'xml.get_value': MagicMock(return_value=value)}): + self.assertDictEqual(xml.value_present(name, xpath, value), state_return) + + def test_value_update(self): + ''' + Test for updating value_present + ''' + + name = "testfile.xml" + xpath = ".//list[@id='1']" + value = "test value" + + old_value = "not test value" + + state_return = { + 'name': name, + 'changes': {name: {'new': value, 'old': old_value}}, + 'result': True, + 'comment': '{0} updated'.format(name) + } + + with patch.dict(xml.__salt__, {'xml.get_value': MagicMock(return_value=old_value)}): + with patch.dict(xml.__salt__, {'xml.set_value': MagicMock(return_value=True)}): + self.assertDictEqual(xml.value_present(name, xpath, value), state_return) + + def test_value_update_test(self): + ''' + Test for value_present test=True + ''' + + name = "testfile.xml" + xpath = ".//list[@id='1']" + value = "test value" + + old_value = "not test value" + + state_return = { + 'name': name, + 'changes': {name: {'old': old_value, 'new': value}}, + 'result': None, + 'comment': '{0} will be updated'.format(name) + } + + with patch.dict(xml.__salt__, {'xml.get_value': MagicMock(return_value=old_value)}): + self.assertDictEqual(xml.value_present(name, xpath, value, test=True), state_return) + + def test_value_update_invalid_xpath(self): + ''' + Test for value_present invalid xpath + ''' + + name = "testfile.xml" + xpath = ".//list[@id='1']" + value = "test value" + + state_return = { + 'name': name, + 'changes': {}, + 'result': False, + 'comment': 'xpath query {0} not found in {1}'.format(xpath, name) + } + + with patch.dict(xml.__salt__, {'xml.get_value': MagicMock(return_value=False)}): + self.assertDictEqual(xml.value_present(name, xpath, value, test=True), state_return)