SaltStack Development: Behavior of Exceptions in Modules

The SaltStack developer docs are missing information about exceptions that can be thrown and how the state system and the CLI behaves when they are thrown.

Thankfully this is easy to test and is actually a pretty good development exercise. So, let’s write an execution module, a state module, and an sls file, then run them to determine the behavior.

A simple example execution module

modules/modules/example.py:

from salt.exceptions import CommandExecutionError

def example(name):
    if name == 'succeed':
        return True
    elif name == 'fail':
        return False
    else:
        raise CommandExecutionError('Example function failed due to unexpected input.')

A simple example state module

modules/states/example.py:

def present(name):
    ret = {'name': name, 'result': True, 'comment': '', 'changes': {}}
    if not __salt__['example.example'](name):
        ret['result'] = False
    return ret

In the above we’re calling the execution module via the salt dunder dictionary, which is a special convenience method for calling execution modules without needing to know their inclusion path.

Also in the above we’re using a special return format that Salt expects to receive when calling state modules.

A simple example sls file

example.sls:

Test fail behavior:
  example.present:
    - name: fail

Test error behavior:
  example.present:
    - name: error

Test succeed behavior:
  example.present:
    - name: succeed

Testing the behavior

State run behavior

# salt-call --retcode-passthrough --file-root . -m modules state.sls example
local:
----------
          ID: Test fail behavior
    Function: example.present
        Name: fail
      Result: False
     Comment:
     Started: 05:22:16.220802
     Duration: 0 ms
     Changes:
----------
          ID: Test error behavior
    Function: example.present
        Name: error
      Result: False
     Comment: An exception occurred in this state: Traceback (most recent call last):
                File "/srv/salt/venv/src/salt/salt/state.py", line 1518, in call
                  **cdata['kwargs'])
                File "/root/modules/states/example.py", line 3, in present
                  if not __salt__['example.example'](name):
                File "/root/modules/modules/example.py", line 9, in example
                  raise CommandExecutionError('Example function failed due to unexpected input.')
              CommandExecutionError: Example function failed due to unexpected input.
     Started: 05:22:16.221730
     Duration: 1 ms
     Changes:
----------
          ID: Test succeed behavior
    Function: example.present
        Name: succeed
      Result: True
     Comment:
     Started: 05:22:16.223336
     Duration: 0 ms
     Changes:

Summary
------------
Succeeded: 1
Failed:    2
------------
Total states run:     3

# echo $?
2

In the above I’m executing salt-call with a couple options. I’m including the modules and sls files explicitly from my relative path (‘-m modules’ and ‘–file-root .’). I do this for convenience and to be completely positive that my code is being loaded from exactly where I expect.

The state run behavior isn’t surprising. The exception is passed through to the output when there’s a legitimate error, otherwise the False value indicates a normal state failure. The return code is non-zero when using –retcode-passthrough as well.

CLI behavior

# salt-call -m modules example.example 'succeed'
local:
    True

# echo $?
0

# salt-call -m modules testme.testme 'fail'
local:
    False

# echo $?
0

# salt-call -m modules example.example 'error'
Error running 'example.example': Example function failed due to unexpected input.

# echo $?
1

When the execution module’s function successfully returns (with either True or False), Salt prints the result through stdout and returns a zero return code. When the function throws an exception, Salt prints an error to stderr and returns a non-zero return code.