text: # -*- coding: utf-8 -*-
  '''
  Manages configuration files via augeas
  
  This module requires the ``augeas`` Python module.
  
  .. _Augeas: http://augeas.net/
  
  .. warning::
  
      Minimal installations of Debian and Ubuntu have been seen to have packaging
      bugs with python-augeas, causing the augeas module to fail to import. If
      the minion has the augeas module installed, but the functions in this
      execution module fail to run due to being unavailable, first restart the
      salt-minion service. If the problem persists past that, the following
      command can be run from the master to determine what is causing the import
      to fail:
  
      .. code-block:: bash
  
          salt minion-id cmd.run 'python -c "from augeas import Augeas"'
  
      For affected Debian/Ubuntu hosts, installing ``libpython2.7`` has been
      known to resolve the issue.
  '''
  from __future__ import absolute_import
  
  # Import python libs
  import os
  import re
  import logging
  from salt.ext.six.moves import zip
  import salt.ext.six as six
  
  # Make sure augeas python interface is installed
  HAS_AUGEAS = False
  try:
      from augeas import Augeas as _Augeas
      HAS_AUGEAS = True
  except ImportError:
      pass
  
  # Import salt libs
  import salt.utils
  from salt.exceptions import SaltInvocationError
  
  log = logging.getLogger(__name__)
  
  # Define the module's virtual name
  __virtualname__ = 'augeas'
  
  METHOD_MAP = {
      'set':    'set',
      'setm':    'setm',
      'mv':     'move',
      'move':   'move',
      'ins':    'insert',
      'insert': 'insert',
      'rm':     'remove',
      'remove': 'remove',
  }
  
  
  def __virtual__():
      '''
      Only run this module if the augeas python module is installed
      '''
      if HAS_AUGEAS:
          return __virtualname__
      return (False, 'Cannot load augeas_cfg module: augeas python module not installed')
  
  
  def _recurmatch(path, aug):
      '''
      Recursive generator providing the infrastructure for
      augtools print behavior.
  
      This function is based on test_augeas.py from
      Harald Hoyer <harald@redhat.com>  in the python-augeas
      repository
      '''
      if path:
          clean_path = path.rstrip('/*')
          yield (clean_path, aug.get(path))
  
          for i in aug.match(clean_path + '/*'):
              i = i.replace('!', '\\!')  # escape some dirs
              for _match in _recurmatch(i, aug):
                  yield _match
  
  
  def _lstrip_word(word, prefix):
      '''
      Return a copy of the string after the specified prefix was removed
      from the beginning of the string
      '''
  
      if str(word).startswith(prefix):
          return str(word)[len(prefix):]
      return word
  
  
  def _check_load_paths(load_path):
      '''
      Checks the validity of the load_path, returns a sanitized version
      with invalid paths removed.
      '''
      if load_path is None or not isinstance(load_path, six.string_types):
          return None
  
      _paths = []
  
      for _path in load_path.split(':'):
          if os.path.isabs(_path) and os.path.isdir(_path):
              _paths.append(_path)
          else:
              log.info('Invalid augeas_cfg load_path entry: %s removed', _path)
  
      if len(_paths) == 0:
          return None
  
      return ':'.join(_paths)
  
  
  def execute(context=None, lens=None, commands=(), load_path=None):
      '''
      Execute Augeas commands
  
      .. versionadded:: 2014.7.0
  
      CLI Example:
  
      .. code-block:: bash
  
          salt '*' augeas.execute /files/etc/redis/redis.conf \\
          commands='["set bind 0.0.0.0", "set maxmemory 1G"]'
  
      context
          The Augeas context
  
      lens
          The Augeas lens to use
  
      commands
          The Augeas commands to execute
  
      .. versionadded:: 2016.3.0
  
      load_path
          A colon-spearated list of directories that modules should be searched
          in. This is in addition to the standard load path and the directories
          in AUGEAS_LENS_LIB.
      '''
      ret = {'retval': False}
  
      arg_map = {
          'set':    (1, 2),
          'setm':   (2, 3),
          'move':   (2,),
          'insert': (3,),
          'remove': (1,),
      }
  
      def make_path(path):
          '''
          Return correct path
          '''
          if not context:
              return path
  
          if path.lstrip('/'):
              if path.startswith(context):
                  return path
  
              path = path.lstrip('/')
              return os.path.join(context, path)
          else:
              return context
  
      load_path = _check_load_paths(load_path)
  
      flags = _Augeas.NO_MODL_AUTOLOAD if lens and context else _Augeas.NONE
      aug = _Augeas(flags=flags, loadpath=load_path)
  
      if lens and context:
          aug.add_transform(lens, re.sub('^/files', '', context))
          aug.load()
  
      for command in commands:
          try:
              # first part up to space is always the
              # command name (i.e.: set, move)
              cmd, arg = command.split(' ', 1)
  
              if cmd not in METHOD_MAP:
                  ret['error'] = 'Command {0} is not supported (yet)'.format(cmd)
                  return ret
  
              method = METHOD_MAP[cmd]
              nargs = arg_map[method]
  
              parts = salt.utils.shlex_split(arg, posix=False)
  
              if len(parts) not in nargs:
                  err = '{0} takes {1} args: {2}'.format(method, nargs, parts)
                  raise ValueError(err)
              if method == 'set':
                  path = make_path(parts[0])
                  value = parts[1] if len(parts) == 2 else None
                  args = {'path': path, 'value': value}
              elif method == 'setm':
                  base = make_path(parts[0])
                  sub = parts[1]
                  value = parts[2] if len(parts) == 3 else None
                  args = {'base': base, 'sub': sub, 'value': value}
              elif method == 'move':
                  path = make_path(parts[0])
                  dst = parts[1]
                  args = {'src': path, 'dst': dst}
              elif method == 'insert':
                  label, where, path = parts
                  if where not in ('before', 'after'):
                      raise ValueError(
                          'Expected "before" or "after", not {0}'.format(where))
                  path = make_path(path)
                  args = {
                      'path': path,
                      'label': label,
                      'before': where == 'before'}
              elif method == 'remove':
                  path = make_path(parts[0])
                  args = {'path': path}
          except ValueError as err:
              log.error(str(err))
              # if command.split fails arg will not be set
              if 'arg' not in locals():
                  arg = command
              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='', load_path=None):
      '''
      Get a value for a specific augeas path
  
      CLI Example:
  
      .. code-block:: bash
  
          salt '*' augeas.get /files/etc/hosts/1/ ipaddr
  
      path
          The path to get the value of
  
      value
          The optional value to get
  
      .. versionadded:: 2016.3.0
  
      load_path
          A colon-spearated list of directories that modules should be searched
          in. This is in addition to the standard load path and the directories
          in AUGEAS_LENS_LIB.
      '''
      load_path = _check_load_paths(load_path)
  
      aug = _Augeas(loadpath=load_path)
      ret = {}
  
      path = path.rstrip('/')
      if value:
          path += '/{0}'.format(value.strip('/'))
  
      try:
          _match = aug.match(path)
      except RuntimeError as err:
          return {'error': str(err)}
  
      if _match:
          ret[path] = aug.get(path)
      else:
          ret[path] = ''  # node does not exist
  
      return ret
  
  
  def setvalue(*args):
      '''
      Set a value for a specific augeas path
  
      CLI Example:
  
      .. code-block:: bash
  
          salt '*' augeas.setvalue /files/etc/hosts/1/canonical localhost
  
      This will set the first entry in /etc/hosts to localhost
  
      CLI Example:
  
      .. code-block:: bash
  
          salt '*' augeas.setvalue /files/etc/hosts/01/ipaddr 192.168.1.1 \\
                                   /files/etc/hosts/01/canonical test
  
      Adds a new host to /etc/hosts the ip address 192.168.1.1 and hostname test
  
      CLI Example:
  
      .. code-block:: bash
  
          salt '*' augeas.setvalue prefix=/files/etc/sudoers/ \\
                   "spec[user = '%wheel']/user" "%wheel" \\
                   "spec[user = '%wheel']/host_group/host" 'ALL' \\
                   "spec[user = '%wheel']/host_group/command[1]" 'ALL' \\
                   "spec[user = '%wheel']/host_group/command[1]/tag" 'PASSWD' \\
                   "spec[user = '%wheel']/host_group/command[2]" '/usr/bin/apt-get' \\
                   "spec[user = '%wheel']/host_group/command[2]/tag" NOPASSWD
  
      Ensures that the following line is present in /etc/sudoers::
  
          %wheel ALL = PASSWD : ALL , NOPASSWD : /usr/bin/apt-get , /usr/bin/aptitude
      '''
      load_path = None
      load_paths = [x for x in args if str(x).startswith('load_path=')]
      if load_paths:
          if len(load_paths) > 1:
              raise SaltInvocationError(
                  'Only one \'load_path=\' value is permitted'
              )
          else:
              load_path = load_paths[0].split('=', 1)[1]
      load_path = _check_load_paths(load_path)
  
      aug = _Augeas(loadpath=load_path)
      ret = {'retval': False}
  
      tuples = [
          x for x in args
          if not str(x).startswith('prefix=') and
          not str(x).startswith('load_path=')]
      prefix = [x for x in args if str(x).startswith('prefix=')]
      if prefix:
          if len(prefix) > 1:
              raise SaltInvocationError(
                  'Only one \'prefix=\' value is permitted'
              )
          else:
              prefix = prefix[0].split('=', 1)[1]
  
      if len(tuples) % 2 != 0:
          raise SaltInvocationError('Uneven number of path/value arguments')
  
      tuple_iter = iter(tuples)
      for path, value in zip(tuple_iter, tuple_iter):
          target_path = path
          if prefix:
              target_path = os.path.join(prefix.rstrip('/'), path.lstrip('/'))
          try:
              aug.set(target_path, str(value))
          except ValueError as err:
              ret['error'] = 'Multiple values: {0}'.format(err)
  
      try:
          aug.save()
          ret['retval'] = True
      except IOError as err:
          ret['error'] = str(err)
      return ret
  
  
  def match(path, value='', load_path=None):
      '''
      Get matches for path expression
  
      CLI Example:
  
      .. code-block:: bash
  
          salt '*' augeas.match /files/etc/services/service-name ssh
  
      path
          The path to match
  
      value
          The value to match on
  
      .. versionadded:: 2016.3.0
  
      load_path
          A colon-spearated list of directories that modules should be searched
          in. This is in addition to the standard load path and the directories
          in AUGEAS_LENS_LIB.
      '''
      load_path = _check_load_paths(load_path)
  
      aug = _Augeas(loadpath=load_path)
      ret = {}
  
      try:
          matches = aug.match(path)
      except RuntimeError:
          return ret
  
      for _match in matches:
          if value and aug.get(_match) == value:
              ret[_match] = value
          elif not value:
              ret[_match] = aug.get(_match)
      return ret
  
  
  def remove(path, load_path=None):
      '''
      Get matches for path expression
  
      CLI Example:
  
      .. code-block:: bash
  
          salt '*' augeas.remove \\
          /files/etc/sysctl.conf/net.ipv4.conf.all.log_martians
  
      path
          The path to remove
  
      .. versionadded:: 2016.3.0
  
      load_path
          A colon-spearated list of directories that modules should be searched
          in. This is in addition to the standard load path and the directories
          in AUGEAS_LENS_LIB.
      '''
      load_path = _check_load_paths(load_path)
  
      aug = _Augeas(loadpath=load_path)
      ret = {'retval': False}
      try:
          count = aug.remove(path)
          aug.save()
          if count == -1:
              ret['error'] = 'Invalid node'
          else:
              ret['retval'] = True
      except (RuntimeError, IOError) as err:
          ret['error'] = str(err)
  
      ret['count'] = count
  
      return ret
  
  
  def ls(path, load_path=None):  # pylint: disable=C0103
      '''
      List the direct children of a node
  
      CLI Example:
  
      .. code-block:: bash
  
          salt '*' augeas.ls /files/etc/passwd
  
      path
          The path to list
  
      .. versionadded:: 2016.3.0
  
      load_path
          A colon-spearated list of directories that modules should be searched
          in. This is in addition to the standard load path and the directories
          in AUGEAS_LENS_LIB.
      '''
      def _match(path):
          ''' Internal match function '''
          try:
              matches = aug.match(path)
          except RuntimeError:
              return {}
  
          ret = {}
          for _ma in matches:
              ret[_ma] = aug.get(_ma)
          return ret
  
      load_path = _check_load_paths(load_path)
  
      aug = _Augeas(loadpath=load_path)
  
      path = path.rstrip('/') + '/'
      match_path = path + '*'
  
      matches = _match(match_path)
      ret = {}
  
      for key, value in six.iteritems(matches):
          name = _lstrip_word(key, path)
          if _match(key + '/*'):
              ret[name + '/'] = value  # has sub nodes, e.g. directory
          else:
              ret[name] = value
      return ret
  
  
  def tree(path, load_path=None):
      '''
      Returns recursively the complete tree of a node
  
      CLI Example:
  
      .. code-block:: bash
  
          salt '*' augeas.tree /files/etc/
  
      path
          The base of the recursive listing
  
      .. versionadded:: 2016.3.0
  
      load_path
          A colon-spearated list of directories that modules should be searched
          in. This is in addition to the standard load path and the directories
          in AUGEAS_LENS_LIB.
      '''
      load_path = _check_load_paths(load_path)
  
      aug = _Augeas(loadpath=load_path)
  
      path = path.rstrip('/') + '/'
      match_path = path
      return dict([i for i in _recurmatch(match_path, aug)])
  