mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00
546 lines
14 KiB
Python
546 lines
14 KiB
Python
"""
|
|
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.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
import re
|
|
|
|
import salt.utils.args
|
|
import salt.utils.data
|
|
import salt.utils.stringutils
|
|
from salt.exceptions import SaltInvocationError
|
|
|
|
# Make sure augeas python interface is installed
|
|
HAS_AUGEAS = False
|
|
try:
|
|
from augeas import Augeas as _Augeas # pylint: disable=no-name-in-module
|
|
|
|
HAS_AUGEAS = True
|
|
except ImportError:
|
|
pass
|
|
|
|
|
|
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
|
|
yield from _recurmatch(i, aug)
|
|
|
|
|
|
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, str):
|
|
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 not _paths:
|
|
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 {} is not supported (yet)".format(cmd)
|
|
return ret
|
|
|
|
method = METHOD_MAP[cmd]
|
|
nargs = arg_map[method]
|
|
|
|
parts = salt.utils.args.shlex_split(arg)
|
|
|
|
if len(parts) not in nargs:
|
|
err = "{} takes {} args: {}".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 {}'.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(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: {}".format(arg)
|
|
return ret
|
|
|
|
args = salt.utils.data.decode(args, to_str=True)
|
|
log.debug("%s: %s", method, args)
|
|
|
|
func = getattr(aug, method)
|
|
func(**args)
|
|
|
|
try:
|
|
aug.save()
|
|
ret["retval"] = True
|
|
except OSError 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 "{}.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 += "/{}".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: {}".format(err)
|
|
|
|
try:
|
|
aug.save()
|
|
ret["retval"] = True
|
|
except OSError 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, OSError) 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(salt.utils.stringutils.to_str(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 matches.items():
|
|
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)])
|