Merge pull request #11337 from techdragon/fix-hightstate-failure-retcode

Fix for broken salt cmd return codes - issue #7013
This commit is contained in:
Thomas S Hatch 2014-03-22 00:31:19 -06:00
commit 4af873dcaf
9 changed files with 283 additions and 30 deletions

View File

@ -24,7 +24,8 @@ from salt.utils.verify import check_user, verify_env, verify_files
from salt.exceptions import (
SaltInvocationError,
SaltClientError,
EauthAuthenticationError
EauthAuthenticationError,
SaltSystemExit
)
@ -130,6 +131,7 @@ class SaltCMD(parsers.SaltCMDOptionParser):
jid = local.cmd_async(**kwargs)
print('Executed command with job ID: {0}'.format(jid))
return
retcodes = []
try:
# local will be None when there was an error
if local:
@ -143,21 +145,33 @@ class SaltCMD(parsers.SaltCMDOptionParser):
if self.options.verbose:
kwargs['verbose'] = True
full_ret = local.cmd_full_return(**kwargs)
ret, out = self._format_ret(full_ret)
ret, out, retcode = self._format_ret(full_ret)
self._output_ret(ret, out)
elif self.config['fun'] == 'sys.doc':
ret = {}
out = ''
for full_ret in local.cmd_cli(**kwargs):
ret_, out = self._format_ret(full_ret)
ret_, out, retcode = self._format_ret(full_ret)
ret.update(ret_)
self._output_ret(ret, out)
else:
if self.options.verbose:
kwargs['verbose'] = True
for full_ret in cmd_func(**kwargs):
ret, out = self._format_ret(full_ret)
ret, out, retcode = self._format_ret(full_ret)
retcodes.append(retcode)
self._output_ret(ret, out)
# NOTE: Return code is set here based on if all minions
# returned 'ok' with a retcode of 0.
# This is the final point before the 'salt' cmd returns,
# which is why we set the retcode here.
if retcodes.count(0) < len(retcodes):
err = 'All Minions did not return a retcode of 0. One or more minions had a problem'
# NOTE: This could probably be made more informative.
# I chose 11 since its not in use.
raise SaltSystemExit(code=11, msg=err)
except (SaltInvocationError, EauthAuthenticationError) as exc:
ret = str(exc)
out = ''
@ -182,11 +196,14 @@ class SaltCMD(parsers.SaltCMDOptionParser):
'''
ret = {}
out = ''
retcode = 0
for key, data in full_ret.items():
ret[key] = data['ret']
if 'out' in data:
out = data['out']
return ret, out
if 'retcode' in data:
retcode = data['retcode']
return ret, out, retcode
def _print_docs(self, ret):
'''

View File

@ -1089,6 +1089,7 @@ class LocalClient(object):
'''
Get the returns for the command line interface via the event system
'''
log.debug("entered - function get_cli_static_event_returns()")
minions = set(minions)
if verbose:
msg = 'Executing job with jid {0}'.format(jid)
@ -1163,6 +1164,7 @@ class LocalClient(object):
'''
Get the returns for the command line interface via the event system
'''
log.debug("func get_cli_event_returns()")
if not isinstance(minions, set):
if isinstance(minions, string_types):
minions = set([minions])
@ -1194,6 +1196,11 @@ class LocalClient(object):
# Wait 0 == forever, use a minimum of 1s
wait = max(1, time_left)
raw = self.event.get_event(wait, jid)
log.debug(
"get_cli_event_returns()" +
" called self.event.get_event()" +
" and recieved : raw = " + str(raw)
)
if raw is not None:
if 'minions' in raw.get('data', {}):
minions.update(raw['data']['minions'])
@ -1203,10 +1210,16 @@ class LocalClient(object):
continue
if 'return' not in raw:
continue
found.add(raw.get('id'))
ret = {raw['id']: {'ret': raw['return']}}
if 'out' in raw:
ret[raw['id']]['out'] = raw['out']
if 'retcode' in raw:
ret[raw['id']]['retcode'] = raw['retcode']
log.trace("raw = " + str(raw))
log.trace("ret = " + str(ret))
log.trace("yeilding 'ret'")
yield ret
if len(found.intersection(minions)) >= len(minions):
# All minions have returned, break out of the loop
@ -1268,6 +1281,7 @@ class LocalClient(object):
Gather the return data from the event system, break hard when timeout
is reached.
'''
log.debug("entered - function get_event_iter_returns()")
if timeout is None:
timeout = self.opts['timeout']
jid_dir = salt.utils.jid_dir(jid,

View File

@ -1017,6 +1017,7 @@ class Minion(MinionBase):
if not os.path.isdir(jdir):
os.makedirs(jdir)
salt.utils.fopen(fn_, 'w+b').write(self.serial.dumps(ret))
log.debug('ret_val = ' + str(ret_val))
return ret_val
def _state_run(self):

View File

@ -39,6 +39,7 @@ def display_output(data, out, opts=None):
display_data = get_printout('nested', opts)(data).rstrip()
output_filename = opts.get('output_file', None)
log.debug("data = " + str(data))
try:
if output_filename is not None:
with salt.utils.fopen(output_filename, 'a') as ofh:

235
salt/states/test.py Normal file
View File

@ -0,0 +1,235 @@
# -*- coding: utf-8 -*-
'''
Test States
==================
Provide test case states that enable easy testing of things to do with
state calls, e.g. running, calling, logging, output filtering etc.
.. code-block:: yaml
always-passes:
test.succeed_without_changes:
- name: foo
always-fails:
test.fail_without_changes:
- name: foo
always-changes-and-succeeds:
test.succeed_with_changes:
- name: foo
always-changes-and-fails:
test.fail_with_changes:
- name: foo
my-custom-combo:
test.configurable_test_state:
- name: foo
- changes: True
- result: False
- comment: bar.baz
'''
# Import Python libs
import logging
import random
from salt.exceptions import SaltInvocationError
log = logging.getLogger(__name__)
def succeed_without_changes(name):
'''
Returns successful.
name
A unique string.
'''
ret = {
'name': name,
'changes': {},
'result': True,
'comment': 'This is just a test, nothing actually happened'
}
if __opts__['test']:
ret['result'] = None
ret['comment'] = (
'Yo dawg I heard you like tests,'
' so I put tests in your tests,'
' so you can test while you test.'
)
return ret
def fail_without_changes(name):
'''
Returns failure.
name:
A unique string.
'''
ret = {
'name': name,
'changes': {},
'result': False,
'comment': 'This is just a test, nothing actually happened'
}
if __opts__['test']:
ret['result'] = None
ret['comment'] = (
'Yo dawg I heard you like tests,'
' so I put tests in your tests,'
' so you can test while you test.'
)
return ret
def succeed_with_changes(name):
'''
Returns successful and changes is not empty
name:
A unique string.
'''
ret = {
'name': name,
'changes': {},
'result': True,
'comment': 'This is just a test, nothing actually happened'
}
# Following the docs as written here
# http://docs.saltstack.com/ref/states/writing.html#return-data
ret['changes'] = {
'testing': {
'old': 'Nothing has changed yet',
'new': 'Were pretending really hard that we changed something'
}
}
if __opts__['test']:
ret['result'] = None
ret['comment'] = (
'Yo dawg I heard you like tests,'
' so I put tests in your tests,'
' so you can test while you test.'
)
return ret
def fail_with_changes(name):
'''
Returns failure and changes is not empty.
name:
A unique string.
'''
ret = {
'name': name,
'changes': {},
'result': False,
'comment': 'This is just a test, nothing actually happened'
}
# Following the docs as written here
# http://docs.saltstack.com/ref/states/writing.html#return-data
ret['changes'] = {
'testing': {
'old': 'Nothing has changed yet',
'new': 'Were pretending really hard that we changed something'
}
}
if __opts__['test']:
ret['result'] = None
ret['comment'] = (
'Yo dawg I heard you like tests,'
' so I put tests in your tests,'
' so you can test while you test.'
)
return ret
def configurable_test_state(name, changes=True, result=True, comment=''):
'''
A configurable test state which determines its output based on the inputs.
name:
A unique string.
changes:
Do we return anything in the changes field?
Accepts True, False, and 'Random'
Default is True
result:
Do we return sucessfuly or not?
Accepts True, False, and 'Random'
Default is True
comment:
String to fill the comment field with.
Default is ''
'''
ret = {
'name': name,
'changes': {},
'result': False,
'comment': comment
}
# E8712 is disabled because this code is a LOT cleaner if we allow it.
if changes == "Random":
if random.choice([True, False]):
# Following the docs as written here
# http://docs.saltstack.com/ref/states/writing.html#return-data
ret['changes'] = {
'testing': {
'old': 'Nothing has changed yet',
'new': 'Were pretending really hard that we changed something'
}
}
elif changes == True: # pylint: disable=E8712
# If changes is True we place our dummy change dictionary into it.
# Following the docs as written here
# http://docs.saltstack.com/ref/states/writing.html#return-data
ret['changes'] = {
'testing': {
'old': 'Nothing has changed yet',
'new': 'Were pretending really hard that we changed something'
}
}
elif changes == False: # pylint: disable=E8712
ret['changes'] = {}
else:
err = ('You have specified the state option \'Changes\' with'
' invalid arguments. It must be either '
' \'True\', \'False\', or \'Random\'')
raise SaltInvocationError(err)
if result == 'Random':
# since result is a boolean, if its random we just set it here,
ret['result'] = random.choice([True, False])
elif result == True: # pylint: disable=E8712
ret['result'] = True
elif result == False: # pylint: disable=E8712
ret['result'] = False
else:
err = ('You have specified the state option \'Result\' with'
' invalid arguments. It must be either '
' \'True\', \'False\', or \'Random\'')
raise SaltInvocationError(err)
if __opts__['test']:
ret['result'] = None
ret['comment'] = (
'Yo dawg I heard you like tests,'
' so I put tests in your tests,'
' so you can test while you test.'
)
return ret

View File

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View File

@ -1360,28 +1360,14 @@ def check_state_result(running):
return False
if not running:
return False
for host in running:
if not isinstance(running[host], dict):
return False
if host.find('_|-') >= 3:
# This is a single ret, no host associated
rets = running[host]
for state in running.keys():
if type(running[str(state)]) is not list:
if not running[str(state)]['result']:
return False
else:
rets = running[host].values()
if isinstance(rets, dict) and 'result' in rets:
if rets['result'] is False:
return False
return True
for ret in rets:
if not isinstance(ret, dict):
return False
if 'result' not in ret:
return False
if ret['result'] is False:
return False
# return false when hosts return a list instead of a dict
return False
return True

View File

@ -282,6 +282,7 @@ class SaltEvent(object):
wait = timeout_at - time.time()
continue
log.debug("get_event() recieved = " + str(ret))
if full:
return ret
return ret['data']
@ -331,6 +332,7 @@ class SaltEvent(object):
else: # new style longer than 20 chars
tagend = TAGEND
log.debug("Sending event - data = " + str(data))
event = '{0}{1}{2}'.format(tag, tagend, self.serial.dumps(data))
try:
self.push.send(event)

View File

@ -224,15 +224,11 @@ class UtilsTestCase(TestCase):
self.assertDictEqual(utils.clean_kwargs(__foo_bar='gwar'), {'__foo_bar': 'gwar'})
def test_check_state_result(self):
self.assertFalse(utils.check_state_result([]), "Failed to handle an invalid data type.")
self.assertFalse(utils.check_state_result(None), "Failed to handle None as an invalid data type.")
self.assertFalse(utils.check_state_result({'host1': []}),
"Failed to handle an invalid data structure for a host")
self.assertFalse(utils.check_state_result([]), "Failed to handle an invalid data type.")
self.assertFalse(utils.check_state_result({}), "Failed to handle an empty dictionary.")
self.assertFalse(utils.check_state_result({'host1': []}), "Failed to handle an invalid host data structure.")
self.assertTrue(utils.check_state_result({' _|-': {}}))
test_valid_state = {'host1': {'test_state': {'result': 'We have liftoff!'}}}
self.assertTrue(utils.check_state_result(test_valid_state))