Make returncode friendly to onfail requisites

Ensure that correctly handled states execution (via onfail)
will return a 0 code if onfail requisites suceed.
This commit is contained in:
Mathieu Le Marec - Pasquet 2017-03-07 19:21:52 +01:00
parent 3c108bd95b
commit fa2f9cf7bd
4 changed files with 344 additions and 11 deletions

View File

@ -83,7 +83,7 @@ def _filter_running(runnings):
return ret
def _set_retcode(ret):
def _set_retcode(ret, highstate=None):
'''
Set the return code based on the data back from the state system
'''
@ -94,7 +94,8 @@ def _set_retcode(ret):
if isinstance(ret, list):
__context__['retcode'] = 1
return
if not salt.utils.check_state_result(ret):
if not salt.utils.check_state_result(ret, highstate=highstate):
__context__['retcode'] = 2
@ -345,7 +346,7 @@ def high(data, test=None, queue=False, **kwargs):
st_ = salt.state.State(opts, pillar, pillar_enc=pillar_enc)
ret = st_.call_high(data)
_set_retcode(ret)
_set_retcode(ret, highstate=data)
return ret
@ -393,7 +394,7 @@ def template(tem, queue=False, **kwargs):
__context__['retcode'] = 1
return errors
ret = st_.state.call_high(high_state)
_set_retcode(ret)
_set_retcode(ret, highstate=high_state)
return ret
@ -859,7 +860,7 @@ def highstate(test=None,
serial = salt.payload.Serial(__opts__)
cache_file = os.path.join(__opts__['cachedir'], 'highstate.p')
_set_retcode(ret)
_set_retcode(ret, highstate=st_.building_highstate)
# Work around Windows multiprocessing bug, set __opts__['test'] back to
# value from before this function was run.
_snapper_post(opts, kwargs.get('__pub_jid', 'called localy'), snapper_pre)
@ -1091,7 +1092,7 @@ def sls(mods,
except (IOError, OSError):
msg = 'Unable to write to SLS cache file {0}. Check permission.'
log.error(msg.format(cache_file))
_set_retcode(ret)
_set_retcode(ret, high_)
# Work around Windows multiprocessing bug, set __opts__['test'] back to
# value from before this function was run.
__opts__['test'] = orig_test
@ -1195,7 +1196,7 @@ def top(topfn,
finally:
st_.pop_active()
_set_retcode(ret)
_set_retcode(ret, highstate=st_.building_highstate)
# Work around Windows multiprocessing bug, set __opts__['test'] back to
# value from before this function was run.
_snapper_post(opts, kwargs.get('__pub_jid', 'called localy'), snapper_pre)
@ -1396,7 +1397,7 @@ def sls_id(
if chunk.get('__id__', '') == id_:
ret.update(st_.state.call_chunk(chunk, {}, chunks))
_set_retcode(ret)
_set_retcode(ret, highstate=highstate)
# Work around Windows multiprocessing bug, set __opts__['test'] back to
# value from before this function was run.
__opts__['test'] = orig_test

View File

@ -1868,7 +1868,95 @@ def gen_state_tag(low):
return '{0[state]}_|-{0[__id__]}_|-{0[name]}_|-{0[fun]}'.format(low)
def check_state_result(running, recurse=False):
def search_onfail_requisites(sid, highstate):
"""
For a particular low chunk, search relevant onfail related
states
"""
onfails = []
if '_|-' in sid:
st = salt.state.split_low_tag(sid)
else:
st = {'__id__': sid}
for fstate, fchunks in six.iteritems(highstate):
if fstate == st['__id__']:
continue
else:
for mod_, fchunk in six.iteritems(fchunks):
if (
not isinstance(mod_, six.string_types) or
mod_.startswith('__')
):
continue
else:
if not isinstance(fchunk, list):
continue
else:
for fdata in fchunk:
if not isinstance(fdata, dict):
continue
for knob, fvalue in six.iteritems(fdata):
if knob != 'onfail':
continue
for freqs in fvalue:
for fmod, fid in six.iteritems(freqs):
if not (
fid == st['__id__'] and
fmod == st.get('state', fmod)
):
continue
onfails.append((fstate, mod_, fchunk))
return onfails
def check_onfail_requisites(state_id, state_result, running, highstate):
'''
When a state fail and is part of a highstate, check
if there is onfail requisites.
When we find onfail requisites, we will consider the state failed
only if at least one of those onfail requisites also failed
Returns:
True: if onfail handlers suceeded
False: if one on those handler failed
None: if the state does not have onfail requisites
'''
nret = None
if (
state_id and state_result and
highstate and isinstance(highstate, dict)
):
onfails = search_onfail_requisites(state_id, highstate)
if onfails:
for handler in onfails:
fstate, mod_, fchunk = handler
ofresult = True
for rstateid, rstate in six.iteritems(running):
if '_|-' in rstateid:
st = salt.state.split_low_tag(rstateid)
# in case of simple state, try to guess
else:
id_ = rstate.get('__id__', rstateid)
if not id_:
raise ValueError('no state id')
st = {'__id__': id_, 'state': mod_}
if mod_ == st['state'] and fstate == st['__id__']:
ofresult = rstate.get('result', _empty)
if ofresult in [False, True]:
nret = ofresult
if ofresult is False:
# as soon as we find an errored onfail, we stop
break
# consider that if we parsed onfailes without changing
# the ret, that we have failed
if nret is None:
nret = False
return nret
def check_state_result(running, recurse=False, highstate=None):
'''
Check the total return value of the run and determine if the running
dict has any issues
@ -1880,7 +1968,7 @@ def check_state_result(running, recurse=False):
return False
ret = True
for state_result in six.itervalues(running):
for state_id, state_result in six.iteritems(running):
if not recurse and not isinstance(state_result, dict):
ret = False
if ret and isinstance(state_result, dict):
@ -1889,7 +1977,13 @@ def check_state_result(running, recurse=False):
ret = False
# only override return value if we are not already failed
elif result is _empty and isinstance(state_result, dict) and ret:
ret = check_state_result(state_result, recurse=True)
ret = check_state_result(
state_result, recurse=True, highstate=highstate)
# if we detect a fail, check for onfail requisites
if not ret:
# ret can be None in case of no onfail reqs, recast it to bool
ret = bool(check_onfail_requisites(state_id, state_result,
running, highstate))
# return as soon as we got a failure
if not ret:
break

View File

@ -136,6 +136,7 @@ class MockState(object):
'pillar': {}}
def __init__(self, opts, pillar=None, *args, **kwargs):
self.building_highstate = {}
self.state = MockState.State(opts,
pillar=pillar)

View File

@ -452,6 +452,243 @@ class UtilsTestCase(TestCase):
self.assertTrue(
utils.check_state_result(data),
msg='{0} failed'.format(test))
test_invalid_true_ht_states = {
'test_onfail_integ2': (
OrderedDict([
('host1',
OrderedDict([
('t_|-test_ivstate0_|-echo_|-run', {
'result': False}),
('cmd_|-test_ivstate0_|-echo_|-run', {
'result': False}),
('cmd_|-test_ivstate1_|-echo_|-run', {
'result': False}),
])),
]),
{
'test_ivstate0': {
'__env__': 'base',
'__sls__': u'a',
'cmd': [OrderedDict([('name', '/bin/true')]),
'run',
{'order': 10002}],
't': [OrderedDict([('name', '/bin/true')]),
'run',
{'order': 10002}]},
'test_ivstate1': {
'__env__': 'base',
'__sls__': u'a',
'cmd': [OrderedDict([('name', '/bin/true')]),
OrderedDict([
('onfail',
[OrderedDict([('cmd', 'test_ivstate0')])])
]),
'run',
{'order': 10004}]},
}
),
'test_onfail_integ3': (
OrderedDict([
('host1',
OrderedDict([
('t_|-test_ivstate0_|-echo_|-run', {
'result': True}),
('cmd_|-test_ivstate0_|-echo_|-run', {
'result': False}),
('cmd_|-test_ivstate1_|-echo_|-run', {
'result': False}),
])),
]),
{
'test_ivstate0': {
'__env__': 'base',
'__sls__': u'a',
'cmd': [OrderedDict([('name', '/bin/true')]),
'run',
{'order': 10002}],
't': [OrderedDict([('name', '/bin/true')]),
'run',
{'order': 10002}]},
'test_ivstate1': {
'__env__': 'base',
'__sls__': u'a',
'cmd': [OrderedDict([('name', '/bin/true')]),
OrderedDict([
('onfail',
[OrderedDict([('cmd', 'test_ivstate0')])])
]),
'run',
{'order': 10004}]},
}
),
'test_onfail_integ4': (
OrderedDict([
('host1',
OrderedDict([
('t_|-test_ivstate0_|-echo_|-run', {
'result': False}),
('cmd_|-test_ivstate0_|-echo_|-run', {
'result': False}),
('cmd_|-test_ivstate1_|-echo_|-run', {
'result': True}),
])),
]),
{
'test_ivstate0': {
'__env__': 'base',
'__sls__': u'a',
'cmd': [OrderedDict([('name', '/bin/true')]),
'run',
{'order': 10002}],
't': [OrderedDict([('name', '/bin/true')]),
'run',
{'order': 10002}]},
'test_ivstate1': {
'__env__': 'base',
'__sls__': u'a',
'cmd': [OrderedDict([('name', '/bin/true')]),
OrderedDict([
('onfail',
[OrderedDict([('cmd', 'test_ivstate0')])])
]),
'run',
{'order': 10004}]},
}
),
'test_onfail': (
OrderedDict([
('host1',
OrderedDict([
('test_state0', {'result': False}),
('test_state', {'result': True}),
])),
]),
None
),
'test_onfail_d': (
OrderedDict([
('host1',
OrderedDict([
('test_state0', {'result': False}),
('test_state', {'result': True}),
])),
]),
{}
)
}
for test, testdata in six.iteritems(test_invalid_true_ht_states):
data, ht = testdata
for t_ in [a for a in data['host1']]:
tdata = data['host1'][t_]
if '_|-' in t_:
t_ = t_.split('_|-')[1]
tdata['__id__'] = t_
self.assertFalse(
utils.check_state_result(data, highstate=ht),
msg='{0} failed'.format(test))
test_valid_true_ht_states = {
'test_onfail_integ': (
OrderedDict([
('host1',
OrderedDict([
('cmd_|-test_ivstate0_|-echo_|-run', {
'result': False}),
('cmd_|-test_ivstate1_|-echo_|-run', {
'result': True}),
])),
]),
{
'test_ivstate0': {
'__env__': 'base',
'__sls__': u'a',
'cmd': [OrderedDict([('name', '/bin/true')]),
'run',
{'order': 10002}]},
'test_ivstate1': {
'__env__': 'base',
'__sls__': u'a',
'cmd': [OrderedDict([('name', '/bin/true')]),
OrderedDict([
('onfail',
[OrderedDict([('cmd', 'test_ivstate0')])])
]),
'run',
{'order': 10004}]},
}
),
'test_onfail_intega3': (
OrderedDict([
('host1',
OrderedDict([
('t_|-test_ivstate0_|-echo_|-run', {
'result': True}),
('cmd_|-test_ivstate0_|-echo_|-run', {
'result': False}),
('cmd_|-test_ivstate1_|-echo_|-run', {
'result': True}),
])),
]),
{
'test_ivstate0': {
'__env__': 'base',
'__sls__': u'a',
'cmd': [OrderedDict([('name', '/bin/true')]),
'run',
{'order': 10002}],
't': [OrderedDict([('name', '/bin/true')]),
'run',
{'order': 10002}]},
'test_ivstate1': {
'__env__': 'base',
'__sls__': u'a',
'cmd': [OrderedDict([('name', '/bin/true')]),
OrderedDict([
('onfail',
[OrderedDict([('cmd', 'test_ivstate0')])])
]),
'run',
{'order': 10004}]},
}
),
'test_onfail_simple': (
OrderedDict([
('host1',
OrderedDict([
('test_vstate0', {'result': False}),
('test_vstate1', {'result': True}),
])),
]),
{
'test_vstate0': {
'__env__': 'base',
'__sls__': u'a',
'cmd': [OrderedDict([('name', '/bin/true')]),
'run',
{'order': 10002}]},
'test_vstate1': {
'__env__': 'base',
'__sls__': u'a',
'cmd': [OrderedDict([('name', '/bin/true')]),
OrderedDict([
('onfail',
[OrderedDict([('cmd', 'test_vstate0')])])
]),
'run',
{'order': 10004}]},
}
)
}
for test, testdata in six.iteritems(test_valid_true_ht_states):
data, ht = testdata
for t_ in [a for a in data['host1']]:
tdata = data['host1'][t_]
if '_|-' in t_:
t_ = t_.split('_|-')[1]
tdata['__id__'] = t_
self.assertTrue(
utils.check_state_result(data, highstate=ht),
msg='{0} failed'.format(test))
test_valid_false_state = {'host1': {'test_state': {'result': False}}}
self.assertFalse(utils.check_state_result(test_valid_false_state))