mirror of
https://github.com/saltstack/salt.git
synced 2025-04-16 09:40:20 +00:00

Fixes #43762. Successful puppet return codes are 0 and 2. When return code is 2 salt will fail. puppet.py intercepted that for the json return, however, the salt job will still fail, because it only parses the return code of the actual process. This commit changes the actual process to return 0 for 0 and 2.
405 lines
12 KiB
Python
405 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
|
'''
|
|
Execute puppet routines
|
|
'''
|
|
|
|
# Import python libs
|
|
from __future__ import absolute_import
|
|
from distutils import version # pylint: disable=no-name-in-module
|
|
import logging
|
|
import os
|
|
import datetime
|
|
|
|
# Import salt libs
|
|
import salt.utils.args
|
|
import salt.utils.files
|
|
import salt.utils.path
|
|
import salt.utils.platform
|
|
from salt.exceptions import CommandExecutionError
|
|
|
|
# Import 3rd-party libs
|
|
import yaml
|
|
from salt.ext import six
|
|
from salt.ext.six.moves import range
|
|
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: '
|
|
'{0} 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(object):
|
|
'''
|
|
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
|
|
|
|
if salt.utils.platform.is_windows():
|
|
self.vardir = 'C:\\ProgramData\\PuppetLabs\\puppet\\var'
|
|
self.rundir = 'C:\\ProgramData\\PuppetLabs\\puppet\\run'
|
|
self.confdir = 'C:\\ProgramData\\PuppetLabs\\puppet\\etc'
|
|
else:
|
|
self.puppet_version = __salt__['cmd.run']('puppet --version')
|
|
if 'Enterprise' in self.puppet_version:
|
|
self.vardir = '/var/opt/lib/pe-puppet'
|
|
self.rundir = '/var/opt/run/pe-puppet'
|
|
self.confdir = '/etc/puppetlabs/puppet'
|
|
elif self.puppet_version != [] and version.StrictVersion(self.puppet_version) >= version.StrictVersion('4.0.0'):
|
|
self.vardir = '/opt/puppetlabs/puppet/cache'
|
|
self.rundir = '/var/run/puppetlabs'
|
|
self.confdir = '/etc/puppetlabs/puppet'
|
|
else:
|
|
self.vardir = '/var/lib/puppet'
|
|
self.rundir = '/var/run/puppet'
|
|
self.confdir = '/etc/puppet'
|
|
|
|
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(
|
|
[' --{0}'.format(k) for k in self.args] # single spaces
|
|
)
|
|
args += ''.join([
|
|
' --{0} {1}'.format(k, v) for k, v in six.iteritems(self.kwargs)]
|
|
)
|
|
|
|
# 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 {0} {1} ^& if !ERRORLEVEL! EQU 2 (EXIT 0) ELSE (EXIT /B)'.format(cmd, args)
|
|
return '({0} {1}) || 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 range(len(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 args[arg] in ['agent', 'apply']:
|
|
puppet.subcmd = args[arg]
|
|
else:
|
|
buildargs += (args[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 (IOError, OSError) as exc:
|
|
msg = 'Failed to enable: {0}'.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
|
|
str = '{{"disabled_message":"{0}"}}'.format(message) if message is not None else '{}'
|
|
lockfile.write(str)
|
|
lockfile.close()
|
|
return True
|
|
except (IOError, OSError) as exc:
|
|
msg = 'Failed to disable: {0}'.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(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(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 = yaml.safe_load(fp_.read())
|
|
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 yaml.YAMLError as exc:
|
|
raise CommandExecutionError(
|
|
'YAML error parsing puppet run summary: {0}'.format(exc)
|
|
)
|
|
except IOError as exc:
|
|
raise CommandExecutionError(
|
|
'Unable to read puppet run summary: {0}'.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 ''
|
|
output = __salt__['cmd.run']('facter {0}'.format(opt_puppet))
|
|
|
|
# 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'](
|
|
'facter {0} {1}'.format(opt_puppet, name),
|
|
python_shell=False)
|
|
if not ret:
|
|
return ''
|
|
return ret
|