Merge pull request #13773 from pkruithof/issue-7610-augeas-lenses

Added improvements to Augeas module/state
This commit is contained in:
Thomas S Hatch 2014-06-26 19:38:29 -06:00
commit 0ab251c214
2 changed files with 255 additions and 43 deletions

View File

@ -26,6 +26,8 @@ This module requires the ``augeas`` Python module.
# Import python libs # Import python libs
import os import os
import re
import logging
# Make sure augeas python interface is installed # Make sure augeas python interface is installed
HAS_AUGEAS = False HAS_AUGEAS = False
@ -38,6 +40,8 @@ except ImportError:
# Import salt libs # Import salt libs
from salt.exceptions import SaltInvocationError from salt.exceptions import SaltInvocationError
log = logging.getLogger(__name__)
# Define the module's virtual name # Define the module's virtual name
__virtualname__ = 'augeas' __virtualname__ = 'augeas'
@ -81,6 +85,85 @@ def _lstrip_word(word, prefix):
return word return word
def execute(context=None, lens=None, commands=()):
'''
Execute Augeas commands
'''
ret = {'retval': False}
method_map = {
'set': 'set',
'mv': 'move',
'move': 'move',
'ins': 'insert',
'insert': 'insert',
'rm': 'remove',
'remove': 'remove',
}
flags = Augeas.NO_MODL_AUTOLOAD if lens else Augeas.NONE
aug = Augeas(flags=flags)
if lens:
aug.add_transform(lens, re.sub('^/files', '', context))
aug.load()
for command in commands:
# first part up to space is always the command name (ie: set, move)
cmd, arg = command.split(' ', 1)
if not cmd in method_map:
ret['error'] = 'Command {0} is not supported (yet)'.format(cmd)
return ret
method = method_map[cmd]
try:
if method == 'set':
path, value, remainder = re.split('([^\'" ]+|"[^"]+"|\'[^\']+\')$', arg, 1)
if context:
path = os.path.join(context.rstrip('/'), path.lstrip('/'))
value = value.strip('"').strip("'")
args = {'path': path, 'value': value}
elif method == 'move':
path, dst = arg.split(' ', 1)
if context:
path = os.path.join(context.rstrip('/'), path.lstrip('/'))
args = {'src': path, 'dst': dst}
elif method == 'insert':
path, where, label = re.split(' (before|after) ', arg)
if context:
path = os.path.join(context.rstrip('/'), path.lstrip('/'))
args = {'path': path, 'label': label, 'before': where == 'before'}
elif method == 'remove':
path = arg
if context:
path = os.path.join(context.rstrip('/'), path.lstrip('/'))
args = {'path': path}
except ValueError as err:
log.error(str(err))
ret['error'] = 'Invalid formatted command, ' \
'see debug log for details: {0}'.format(arg)
return ret
log.debug('{0}: {1}'.format(method, args))
func = getattr(aug, method)
func(**args)
try:
aug.save()
ret['retval'] = True
except IOError as err:
ret['error'] = str(err)
if lens and not lens.endswith('.lns'):
ret['error'] += '\nLenses are normally configured as "name.lns". ' \
'Did you mean "{0}.lns"?'.format(lens)
aug.close()
return ret
def get(path, value=''): def get(path, value=''):
''' '''
Get a value for a specific augeas path Get a value for a specific augeas path

View File

@ -9,9 +9,7 @@ This state requires the ``augeas`` Python module.
.. _Augeas: http://augeas.net/ .. _Augeas: http://augeas.net/
Augeas_ can be used to manage configuration files. Currently only the ``set`` Augeas_ can be used to manage configuration files.
command is supported via this state. The :mod:`augeas
<salt.modules.augeas_cfg>` module also has support for get, match, remove, etc.
.. warning:: .. warning::
@ -29,54 +27,185 @@ command is supported via this state. The :mod:`augeas
For affected Debian/Ubuntu hosts, installing ``libpython2.7`` has been For affected Debian/Ubuntu hosts, installing ``libpython2.7`` has been
known to resolve the issue. known to resolve the issue.
Usage examples:
1. Set the first entry in ``/etc/hosts`` to ``localhost``:
.. code-block:: yaml
hosts:
augeas.setvalue:
- changes:
- /files/etc/hosts/1/canonical: localhost
2. Add a new host to ``/etc/hosts`` with the IP address ``192.168.1.1`` and
hostname ``test``:
.. code-block:: yaml
hosts:
augeas.setvalue:
- changes:
- /files/etc/hosts/2/ipaddr: 192.168.1.1
- /files/etc/hosts/2/canonical: foo.bar.com
- /files/etc/hosts/2/alias[1]: foosite
- /files/etc/hosts/2/alias[2]: foo
A prefix can also be set, to avoid redundancy:
.. code-block:: yaml
nginx-conf:
augeas.setvalue:
- prefix: /files/etc/nginx/nginx.conf
- changes:
- user: www-data
- worker_processes: 2
- http/server_tokens: off
- http/keepalive_timeout: 65
''' '''
import re
import os.path
import difflib
def __virtual__(): def __virtual__():
return 'augeas' if 'augeas.setvalue' in __salt__ else False return 'augeas' if 'augeas.execute' in __salt__ else False
def change(name, context=None, changes=None, lens=None, **kwargs):
'''
.. versionadded:: 2014.1.6
This state replaces :py:func:`~salt.states.augeas.setvalue`.
Issue changes to Augeas, optionally for a specific context, with a
specific lens.
name
State name
context
The context to use. Set this to a file path, prefixed by ``/files``, to
avoid redundancy, eg:
.. code-block:: yaml
redis-conf:
augeas.change:
- context: /files/etc/redis/redis.conf
- changes:
- set bind 0.0.0.0
- set maxmemory 1G
changes
List of changes that are issued to Augeas. Available commands are
``set``, ``mv``/``move``, ``ins``/``insert``, and ``rm``/``remove``.
lens
The lens to use, needs to be suffixed with `.lns`, eg: `Nginx.lns`. See
the `list of stock lenses <http://augeas.net/stock_lenses.html>`_
shipped with Augeas.
Usage examples:
Set the ``bind`` parameter in ``/etc/redis/redis.conf``:
.. code-block:: yaml
redis-conf:
augeas.change:
- changes:
- set /files/etc/redis/redis.conf/bind 0.0.0.0
.. note::
Use the ``context`` parameter to specify the file you want to
manipulate. This way you don't have to include this in the changes
every time:
.. code-block:: yaml
redis-conf:
augeas.change:
- context: /files/etc/redis/redis.conf
- changes:
- set bind 0.0.0.0
- set databases 4
- set maxmemory 1G
Augeas is aware of a lot of common configuration files and their syntax.
It knows the difference between for example ini and yaml files, but also
files with very specific syntax, like the hosts file. This is done with
*lenses*, which provide mappings between the Augeas tree and the file.
There are many `preconfigured lenses`_ that come with Augeas by default,
and they specify the common locations for configuration files. So most
of the time Augeas will know how to manipulate a file. In the event that
you need to manipulate a file that Augeas doesn't know about, you can
specify the lens to use like this:
.. code-block:: yaml
redis-conf:
augeas.change:
- lens: redis
- context: /files/etc/redis/redis.conf
- changes:
- set bind 0.0.0.0
.. note::
Even though Augeas knows that ``/etc/redis/redis.conf`` is a Redis
configuration file and knows how to parse it, it is recommended to
specify the lens anyway. This is because by default, Augeas loads all
known lenses and their associated file paths. All these files are
parsed when Augeas is loaded, which can take some time. When specifying
a lens, Augeas is loaded with only that lens, which speeds things up
quite a bit.
.. _preconfigured lenses: http://augeas.net/stock_lenses.html
A more complex example, this adds an entry to the services file for Zabbix,
and removes an obsolete service:
.. code-block:: yaml
zabbix-service:
augeas.change:
- lens: services
- context: /files/etc/services
- changes:
- ins service-name after service-name[last()]
- set service-name[last()] zabbix-agent
- set service-name[. = 'zabbix-agent']/#comment "Zabbix Agent service"
- set service-name[. = 'zabbix-agent']/port 10050
- set service-name[. = 'zabbix-agent']/protocol tcp
- rm service-name[. = 'im-obsolete']
- unless: grep "zabbix-agent" /etc/services
.. warning::
Don't forget the ``unless`` here, otherwise a new entry will be added
everytime this state is run.
'''
ret = {'name': name, 'result': False, 'comment': '', 'changes': {}}
if not changes or not isinstance(changes, list):
ret['comment'] = '\'changes\' must be specified as a list'
return ret
if __opts__['test']:
ret['result'] = None
ret['comment'] = 'Executing commands'
if context:
ret['comment'] += ' in file "{1}"'.format(context)
ret['comment'] += "\n".join(changes)
return ret
old_file = []
if context:
filename = re.sub('^/files|/$', '', context)
if os.path.isfile(filename):
file = open(filename, 'r')
old_file = file.readlines()
file.close()
result = __salt__['augeas.execute'](context=context, lens=lens, commands=changes)
ret['result'] = result['retval']
if ret['result'] is False:
ret['comment'] = 'Error: {0}'.format(result['error'])
return ret
if old_file:
file = open(filename, 'r')
diff = ''.join(difflib.unified_diff(old_file, file.readlines(), n=0))
file.close()
if diff:
ret['comment'] = 'Changes have been saved'
ret['changes'] = diff
else:
ret['comment'] = 'No changes made'
else:
ret['comment'] = 'Changes have been saved'
ret['changes'] = changes
return ret
def setvalue(name, prefix=None, changes=None, **kwargs): def setvalue(name, prefix=None, changes=None, **kwargs):
''' '''
.. deprecated:: 2014.1.6
Use :py:func:`~salt.states.augeas.change` instead.
Set a value for a specific augeas path Set a value for a specific augeas path
''' '''
ret = {'name': name, 'result': False, 'comment': '', 'changes': {}} ret = {'name': name, 'result': False, 'comment': '', 'changes': {}}