Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions salt/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
from salt.exceptions import (
SaltInvocationError,
SaltClientError,
EauthAuthenticationError
EauthAuthenticationError,
SaltSystemExit
)


Expand Down Expand Up @@ -130,6 +131,7 @@ def run(self):
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:
Expand All @@ -143,21 +145,33 @@ def run(self):
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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might not want to raise an error like this, but just return with the error return code, what do you think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you mean just passing on the return code that is generated from the minion. The problem is what to do when you get multiple different return codes. For instance one minion has no errors, another has a single state return retcode 1, another minion returns retcode 2. How to represent this? I've been contemplating options on how to provide the information and feel that given the possible permutations we really have little option as far as the process return/exit code but to choose a distinct error code for this 'minion failures' use case.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, no, I like your approach with a static exit code! I just think we should use sys.exit instead of raise SaltSystemExit


except (SaltInvocationError, EauthAuthenticationError) as exc:
ret = str(exc)
out = ''
Expand All @@ -182,11 +196,14 @@ def _format_ret(self, full_ret):
'''
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):
'''
Expand Down
14 changes: 14 additions & 0 deletions salt/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1100,6 +1100,7 @@ def get_cli_static_event_returns(
'''
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)
Expand Down Expand Up @@ -1174,6 +1175,7 @@ def get_cli_event_returns(
'''
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, basestring):
minions = set([minions])
Expand Down Expand Up @@ -1205,6 +1207,11 @@ def get_cli_event_returns(
# 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'])
Expand All @@ -1214,10 +1221,16 @@ def get_cli_event_returns(
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
Expand Down Expand Up @@ -1279,6 +1292,7 @@ def get_event_iter_returns(self, jid, minions, timeout=None):
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,
Expand Down
1 change: 1 addition & 0 deletions salt/minion.py
Original file line number Diff line number Diff line change
Expand Up @@ -1026,6 +1026,7 @@ def _return_pub(self, ret, ret_cmd='_return'):
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):
Expand Down
1 change: 1 addition & 0 deletions salt/output/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
235 changes: 235 additions & 0 deletions salt/states/test.py
Original file line number Diff line number Diff line change
@@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why would

 elif changes is True:

be less readable?

# 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
1 change: 1 addition & 0 deletions salt/templates/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# -*- coding: utf-8 -*-
Loading