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 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 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): if isinstance(ret, list):
__context__['retcode'] = 1 __context__['retcode'] = 1
return return
if not salt.utils.check_state_result(ret): if not salt.utils.check_state_result(ret, highstate=highstate):
__context__['retcode'] = 2 __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) st_ = salt.state.State(opts, pillar, pillar_enc=pillar_enc)
ret = st_.call_high(data) ret = st_.call_high(data)
_set_retcode(ret) _set_retcode(ret, highstate=data)
return ret return ret
@ -393,7 +394,7 @@ def template(tem, queue=False, **kwargs):
__context__['retcode'] = 1 __context__['retcode'] = 1
return errors return errors
ret = st_.state.call_high(high_state) ret = st_.state.call_high(high_state)
_set_retcode(ret) _set_retcode(ret, highstate=high_state)
return ret return ret
@ -859,7 +860,7 @@ def highstate(test=None,
serial = salt.payload.Serial(__opts__) serial = salt.payload.Serial(__opts__)
cache_file = os.path.join(__opts__['cachedir'], 'highstate.p') 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 # Work around Windows multiprocessing bug, set __opts__['test'] back to
# value from before this function was run. # value from before this function was run.
_snapper_post(opts, kwargs.get('__pub_jid', 'called localy'), snapper_pre) _snapper_post(opts, kwargs.get('__pub_jid', 'called localy'), snapper_pre)
@ -1091,7 +1092,7 @@ def sls(mods,
except (IOError, OSError): except (IOError, OSError):
msg = 'Unable to write to SLS cache file {0}. Check permission.' msg = 'Unable to write to SLS cache file {0}. Check permission.'
log.error(msg.format(cache_file)) log.error(msg.format(cache_file))
_set_retcode(ret) _set_retcode(ret, high_)
# Work around Windows multiprocessing bug, set __opts__['test'] back to # Work around Windows multiprocessing bug, set __opts__['test'] back to
# value from before this function was run. # value from before this function was run.
__opts__['test'] = orig_test __opts__['test'] = orig_test
@ -1195,7 +1196,7 @@ def top(topfn,
finally: finally:
st_.pop_active() st_.pop_active()
_set_retcode(ret) _set_retcode(ret, highstate=st_.building_highstate)
# Work around Windows multiprocessing bug, set __opts__['test'] back to # Work around Windows multiprocessing bug, set __opts__['test'] back to
# value from before this function was run. # value from before this function was run.
_snapper_post(opts, kwargs.get('__pub_jid', 'called localy'), snapper_pre) _snapper_post(opts, kwargs.get('__pub_jid', 'called localy'), snapper_pre)
@ -1396,7 +1397,7 @@ def sls_id(
if chunk.get('__id__', '') == id_: if chunk.get('__id__', '') == id_:
ret.update(st_.state.call_chunk(chunk, {}, chunks)) 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 # Work around Windows multiprocessing bug, set __opts__['test'] back to
# value from before this function was run. # value from before this function was run.
__opts__['test'] = orig_test __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) 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 Check the total return value of the run and determine if the running
dict has any issues dict has any issues
@ -1880,7 +1968,7 @@ def check_state_result(running, recurse=False):
return False return False
ret = True 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): if not recurse and not isinstance(state_result, dict):
ret = False ret = False
if ret and isinstance(state_result, dict): if ret and isinstance(state_result, dict):
@ -1889,7 +1977,13 @@ def check_state_result(running, recurse=False):
ret = False ret = False
# only override return value if we are not already failed # only override return value if we are not already failed
elif result is _empty and isinstance(state_result, dict) and ret: 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 # return as soon as we got a failure
if not ret: if not ret:
break break

View File

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

View File

@ -452,6 +452,243 @@ class UtilsTestCase(TestCase):
self.assertTrue( self.assertTrue(
utils.check_state_result(data), utils.check_state_result(data),
msg='{0} failed'.format(test)) 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}}} test_valid_false_state = {'host1': {'test_state': {'result': False}}}
self.assertFalse(utils.check_state_result(test_valid_false_state)) self.assertFalse(utils.check_state_result(test_valid_false_state))