salt/salt/modules/puppet.py
2022-09-22 12:02:10 -07:00

401 lines
11 KiB
Python

"""
Execute puppet routines
"""
import datetime
import logging
import os
import salt.utils.args
import salt.utils.files
import salt.utils.path
import salt.utils.platform
import salt.utils.stringutils
import salt.utils.yaml
from salt.exceptions import CommandExecutionError
log = logging.getLogger(__name__)
def __virtual__():
"""
Only load if puppet is installed
"""
unavailable_exes = ", ".join(
exe for exe in ("facter", "puppet") if salt.utils.path.which(exe) is None
)
if unavailable_exes:
return (
False,
"The puppet execution module cannot be loaded: {} unavailable.".format(
unavailable_exes
),
)
else:
return "puppet"
def _format_fact(output):
try:
fact, value = output.split(" => ", 1)
value = value.strip()
except ValueError:
fact = None
value = None
return (fact, value)
class _Puppet:
"""
Puppet helper class. Used to format command for execution.
"""
def __init__(self):
"""
Setup a puppet instance, based on the premis that default usage is to
run 'puppet agent --test'. Configuration and run states are stored in
the default locations.
"""
self.subcmd = "agent"
self.subcmd_args = [] # e.g. /a/b/manifest.pp
self.kwargs = {"color": "false"} # e.g. --tags=apache::server
self.args = [] # e.g. --noop
puppet_config = __salt__["cmd.run"](
"puppet config print --render-as yaml vardir rundir confdir"
)
conf = salt.utils.yaml.safe_load(puppet_config)
self.vardir = conf["vardir"]
self.rundir = conf["rundir"]
self.confdir = conf["confdir"]
self.disabled_lockfile = self.vardir + "/state/agent_disabled.lock"
self.run_lockfile = self.vardir + "/state/agent_catalog_run.lock"
self.agent_pidfile = self.rundir + "/agent.pid"
self.lastrunfile = self.vardir + "/state/last_run_summary.yaml"
def __repr__(self):
"""
Format the command string to executed using cmd.run_all.
"""
cmd = "puppet {subcmd} --vardir {vardir} --confdir {confdir}".format(
**self.__dict__
)
args = " ".join(self.subcmd_args)
args += "".join([" --{}".format(k) for k in self.args]) # single spaces
args += "".join([" --{} {}".format(k, v) for k, v in self.kwargs.items()])
# Ensure that the puppet call will return 0 in case of exit code 2
if salt.utils.platform.is_windows():
return "cmd /V:ON /c {} {} ^& if !ERRORLEVEL! EQU 2 (EXIT 0) ELSE (EXIT /B)".format(
cmd, args
)
return "({} {}) || test $? -eq 2".format(cmd, args)
def arguments(self, args=None):
"""
Read in arguments for the current subcommand. These are added to the
cmd line without '--' appended. Any others are redirected as standard
options with the double hyphen prefixed.
"""
# permits deleting elements rather than using slices
args = args and list(args) or []
# match against all known/supported subcmds
if self.subcmd == "apply":
# apply subcommand requires a manifest file to execute
self.subcmd_args = [args[0]]
del args[0]
if self.subcmd == "agent":
# no arguments are required
args.extend(["test"])
# finally do this after subcmd has been matched for all remaining args
self.args = args
def run(*args, **kwargs):
"""
Execute a puppet run and return a dict with the stderr, stdout,
return code, etc. The first positional argument given is checked as a
subcommand. Following positional arguments should be ordered with arguments
required by the subcommand first, followed by non-keyword arguments.
Tags are specified by a tag keyword and comma separated list of values. --
http://docs.puppetlabs.com/puppet/latest/reference/lang_tags.html
CLI Examples:
.. code-block:: bash
salt '*' puppet.run
salt '*' puppet.run tags=basefiles::edit,apache::server
salt '*' puppet.run agent onetime no-daemonize no-usecacheonfailure no-splay ignorecache
salt '*' puppet.run debug
salt '*' puppet.run apply /a/b/manifest.pp modulepath=/a/b/modules tags=basefiles::edit,apache::server
"""
puppet = _Puppet()
# new args tuple to filter out agent/apply for _Puppet.arguments()
buildargs = ()
for arg in args:
# based on puppet documentation action must come first. making the same
# assertion. need to ensure the list of supported cmds here matches
# those defined in _Puppet.arguments()
if arg in ["agent", "apply"]:
puppet.subcmd = arg
else:
buildargs += (arg,)
# args will exist as an empty list even if none have been provided
puppet.arguments(buildargs)
puppet.kwargs.update(salt.utils.args.clean_kwargs(**kwargs))
ret = __salt__["cmd.run_all"](repr(puppet), python_shell=True)
return ret
def noop(*args, **kwargs):
"""
Execute a puppet noop run and return a dict with the stderr, stdout,
return code, etc. Usage is the same as for puppet.run.
CLI Example:
.. code-block:: bash
salt '*' puppet.noop
salt '*' puppet.noop tags=basefiles::edit,apache::server
salt '*' puppet.noop debug
salt '*' puppet.noop apply /a/b/manifest.pp modulepath=/a/b/modules tags=basefiles::edit,apache::server
"""
args += ("noop",)
return run(*args, **kwargs)
def enable():
"""
.. versionadded:: 2014.7.0
Enable the puppet agent
CLI Example:
.. code-block:: bash
salt '*' puppet.enable
"""
puppet = _Puppet()
if os.path.isfile(puppet.disabled_lockfile):
try:
os.remove(puppet.disabled_lockfile)
except OSError as exc:
msg = "Failed to enable: {}".format(exc)
log.error(msg)
raise CommandExecutionError(msg)
else:
return True
return False
def disable(message=None):
"""
.. versionadded:: 2014.7.0
Disable the puppet agent
message
.. versionadded:: 2015.5.2
Disable message to send to puppet
CLI Example:
.. code-block:: bash
salt '*' puppet.disable
salt '*' puppet.disable 'Disabled, contact XYZ before enabling'
"""
puppet = _Puppet()
if os.path.isfile(puppet.disabled_lockfile):
return False
else:
with salt.utils.files.fopen(puppet.disabled_lockfile, "w") as lockfile:
try:
# Puppet chokes when no valid json is found
msg = (
'{{"disabled_message":"{0}"}}'.format(message)
if message is not None
else "{}"
)
lockfile.write(salt.utils.stringutils.to_str(msg))
lockfile.close()
return True
except OSError as exc:
msg = "Failed to disable: {}".format(exc)
log.error(msg)
raise CommandExecutionError(msg)
def status():
"""
.. versionadded:: 2014.7.0
Display puppet agent status
CLI Example:
.. code-block:: bash
salt '*' puppet.status
"""
puppet = _Puppet()
if os.path.isfile(puppet.disabled_lockfile):
return "Administratively disabled"
if os.path.isfile(puppet.run_lockfile):
try:
with salt.utils.files.fopen(puppet.run_lockfile, "r") as fp_:
pid = int(salt.utils.stringutils.to_unicode(fp_.read()))
os.kill(pid, 0) # raise an OSError if process doesn't exist
except (OSError, ValueError):
return "Stale lockfile"
else:
return "Applying a catalog"
if os.path.isfile(puppet.agent_pidfile):
try:
with salt.utils.files.fopen(puppet.agent_pidfile, "r") as fp_:
pid = int(salt.utils.stringutils.to_unicode(fp_.read()))
os.kill(pid, 0) # raise an OSError if process doesn't exist
except (OSError, ValueError):
return "Stale pidfile"
else:
return "Idle daemon"
return "Stopped"
def summary():
"""
.. versionadded:: 2014.7.0
Show a summary of the last puppet agent run
CLI Example:
.. code-block:: bash
salt '*' puppet.summary
"""
puppet = _Puppet()
try:
with salt.utils.files.fopen(puppet.lastrunfile, "r") as fp_:
report = salt.utils.yaml.safe_load(fp_)
result = {}
if "time" in report:
try:
result["last_run"] = datetime.datetime.fromtimestamp(
int(report["time"]["last_run"])
).isoformat()
except (TypeError, ValueError, KeyError):
result["last_run"] = "invalid or missing timestamp"
result["time"] = {}
for key in ("total", "config_retrieval"):
if key in report["time"]:
result["time"][key] = report["time"][key]
if "resources" in report:
result["resources"] = report["resources"]
except salt.utils.yaml.YAMLError as exc:
raise CommandExecutionError(
"YAML error parsing puppet run summary: {}".format(exc)
)
except OSError as exc:
raise CommandExecutionError("Unable to read puppet run summary: {}".format(exc))
return result
def plugin_sync():
"""
Runs a plugin sync between the puppet master and agent
CLI Example:
.. code-block:: bash
salt '*' puppet.plugin_sync
"""
ret = __salt__["cmd.run"]("puppet plugin download")
if not ret:
return ""
return ret
def facts(puppet=False):
"""
Run facter and return the results
CLI Example:
.. code-block:: bash
salt '*' puppet.facts
"""
ret = {}
opt_puppet = "--puppet" if puppet else ""
cmd_ret = __salt__["cmd.run_all"]("facter {}".format(opt_puppet))
if cmd_ret["retcode"] != 0:
raise CommandExecutionError(cmd_ret["stderr"])
output = cmd_ret["stdout"]
# Loop over the facter output and properly
# parse it into a nice dictionary for using
# elsewhere
for line in output.splitlines():
if not line:
continue
fact, value = _format_fact(line)
if not fact:
continue
ret[fact] = value
return ret
def fact(name, puppet=False):
"""
Run facter for a specific fact
CLI Example:
.. code-block:: bash
salt '*' puppet.fact kernel
"""
opt_puppet = "--puppet" if puppet else ""
ret = __salt__["cmd.run_all"](
"facter {} {}".format(opt_puppet, name), python_shell=False
)
if ret["retcode"] != 0:
raise CommandExecutionError(ret["stderr"])
if not ret["stdout"]:
return ""
return ret["stdout"]