agents/docs/module-templates.md
This document provides comprehensive templates for writing Salt modules.
All execution modules follow this basic structure:
"""
Module docstring explaining purpose
"""
import logging
import salt.exceptions
import salt.utils.args
import salt.utils.data
import salt.utils.files
import salt.utils.path
import salt.utils.platform
# Set up logging
log = logging.getLogger(__name__)
# Module metadata
__virtualname__ = "mymodule" # Optional: override module name
__proxyenabled__ = ["*"] # Optional: enable for proxy minions
__func_alias__ = {
"list_": "list" # Optional: map function names to avoid Python keywords
}
def __virtual__():
"""
Only load this module if requirements are met
Returns True, False, or (False, reason)
"""
if not salt.utils.path.which("mycommand"):
return False, "mycommand binary not found"
return True
def my_function(name, param=None):
"""
Function description
name
Description of name parameter
param
Description of param parameter
CLI Example:
.. code-block:: bash
salt '*' mymodule.my_function foo param=bar
"""
log.debug("Running my_function with name=%s", name)
# Implementation
result = do_something(name, param)
return result
State modules provide idempotent, declarative configuration. They call execution modules to do the actual work.
Every state function MUST return this exact structure:
ret = {
"name": name, # The name parameter passed to the state
"result": result, # True (success), False (failure), or None (test mode)
"changes": {}, # Dictionary of changes made
"comment": comment # String or list describing what happened
}
"""
State module docstring
"""
import logging
import salt.exceptions
log = logging.getLogger(__name__)
def __virtual__():
"""
Only load if the corresponding execution module is available
"""
if "mymod.do_thing" in __salt__:
return True
return (False, "mymod execution module not available")
def managed(name, value=None, **kwargs):
"""
Ensure something is in the desired state
name
The resource identifier
value
The desired value
Example:
.. code-block:: yaml
/etc/myconfig:
mymod.managed:
- value: foo
"""
# Initialize return dict
ret = {
"name": name,
"result": False,
"changes": {},
"comment": ""
}
# 1. Validate input
if not name:
ret["comment"] = "Name is required"
return ret
# 2. Check current state
current = __salt__["mymod.get_current"](name)
# 3. Compare with desired state
if current == value:
ret["result"] = True
ret["comment"] = f"{name} is already in the desired state"
return ret
# 4. Handle test mode
if __opts__["test"]:
ret["result"] = None
ret["comment"] = f"{name} would be updated"
ret["changes"] = {"old": current, "new": value}
return ret
# 5. Make changes
try:
result = __salt__["mymod.set_value"](name, value)
except Exception as exc:
ret["comment"] = f"Failed to update {name}: {exc}"
return ret
# 6. Verify changes
new_value = __salt__["mymod.get_current"](name)
# 7. Set result and changes
if new_value == value:
ret["result"] = True
ret["changes"] = {"old": current, "new": new_value}
ret["comment"] = f"{name} was updated"
else:
ret["result"] = False
ret["comment"] = f"Failed to update {name}"
return ret
1. Validate Input → Invalid? Return with result=False
↓
2. Check Current State
↓
3. Already Correct? → Yes: Return with result=True, no changes
↓ No
4. Test Mode? → Yes: Return with result=None, proposed changes
↓ No
5. Make Changes
↓
6. Verify Changes
↓
7. Return Result (True/False) with changes dict
result=True: State succeeded, system is in desired stateresult=False: State failed, system is NOT in desired stateresult=None: Test mode, changes would be made but weren'tThe changes dictionary should show what changed:
# Simple change
ret["changes"] = {"old": "foo", "new": "bar"}
# Multiple changes
ret["changes"] = {
"user": {"old": "root", "new": "apache"},
"mode": {"old": "0644", "new": "0755"},
}
# New resource created
ret["changes"] = {"created": name}
# Resource removed
ret["changes"] = {"removed": name}
The __virtual__() function determines whether a module should load on a particular system. It runs before any other module code and should be fast.
True: Load module with filename as nameFalse: Don't load module(False, reason): Don't load, with explanation"string": Load module with this name (virtualname)("string", reason): Load with name, with explanationdef __virtual__():
"""
Only work on POSIX-like systems
"""
if salt.utils.platform.is_windows():
return (
False,
"The file execution module cannot be loaded: only available on "
"non-Windows systems - use win_file instead.",
)
return True
Used when multiple modules implement the same interface (e.g., pkg for different package managers).
__virtualname__ = "pkg"
def __virtual__():
"""
Confirm this module is on a Debian-based system
"""
if __grains__.get("os_family") == "Debian":
return __virtualname__
return False, "The pkg module could not be loaded: unsupported OS family"
def __virtual__():
"""
Only load if systemctl is available
"""
if not salt.utils.path.which("systemctl"):
return False, "The systemd module could not be loaded: systemctl not found"
return True
__virtualname__ = "service"
def __virtual__():
"""
Only work on systems that have been booted with systemd
"""
is_linux = __grains__.get("kernel") == "Linux"
is_booted = salt.utils.systemd.booted(__context__)
is_offline = salt.utils.systemd.offline(__context__)
if is_linux and (is_booted or is_offline):
return __virtualname__
return (
False,
"The systemd execution module failed to load: only available on Linux "
"systems which have been booted with systemd.",
)
Used in state modules to verify required execution modules exist.
def __virtual__():
"""
Only make these states available if a pkg provider has been detected
"""
if "pkg.install" in __salt__:
return True
return (False, "pkg module could not be loaded")
__virtual__() fast - it runs on every module scansalt.utils.platform for platform detection__context__ if neededSalt provides decorators for common tasks:
from salt.utils.decorators import depends, memoize
from salt.utils.decorators.path import which
# Ensure binary exists
@which("systemctl")
def restart_service(name):
"""Only available if systemctl is found"""
pass
# Ensure Python module exists
@depends("docker")
def list_containers():
"""Only available if docker module is installed"""
pass
# Memoize expensive operations
@memoize
def get_system_info():
"""Cache result for lifetime of process"""
pass
Map function names to avoid Python keywords:
__func_alias__ = {
"list_": "list", # my_module.list() → my_module.list_()
"exec_": "exec", # my_module.exec() → my_module.exec_()
}
def my_function():
"""Call other execution modules"""
# Get file contents
content = __salt__["file.read"]("/etc/hosts")
# Run a command
output = __salt__["cmd.run"]("ls -la")
# Install a package
result = __salt__["pkg.install"]("nginx")
return result
import salt.utils.args
def my_function(*args, **kwargs):
"""Handle various argument formats"""
# Clean extra kwargs from state data
kwargs = salt.utils.args.clean_kwargs(**kwargs)
# Parse key=value arguments
kwargs.update(salt.utils.args.parse_input(args))
return kwargs
import salt.utils.path
def my_function():
"""Handle paths safely"""
# Find binary in PATH
binary = salt.utils.path.which("nginx")
if not binary:
return False
# Join paths correctly for OS
config_path = salt.utils.path.join("/etc", "nginx", "nginx.conf")
return config_path
import salt.utils.platform
def my_function():
"""Handle platform differences"""
if salt.utils.platform.is_windows():
return "C:\\Windows"
elif salt.utils.platform.is_linux():
return "/etc"
elif salt.utils.platform.is_darwin():
return "/Library"
# Check specific OS family
if __grains__.get("os_family") == "Debian":
return "apt"
elif __grains__.get("os_family") == "RedHat":
return "yum"
def _get_cached_value():
"""Safe context usage for standalone calls"""
try:
context = __context__
except NameError:
context = {}
if "my_key" not in context:
context["my_key"] = expensive_operation()
return context.get("my_key")
Use Salt's exception classes for consistent error handling:
from salt.exceptions import (
CommandExecutionError, # Command/operation failed
SaltInvocationError, # Bad function arguments
MinionError, # General minion error
CommandNotFoundError, # Binary not found
)
def my_function(path, required_param):
"""
Example with proper error handling
"""
# Validate required parameters
if not required_param:
raise SaltInvocationError("required_param is mandatory")
# Validate parameter values
if not os.path.exists(path):
raise SaltInvocationError(f"Path does not exist: {path}")
# Check for required binary
binary = salt.utils.path.which("mytool")
if not binary:
raise CommandNotFoundError("mytool binary not found in PATH")
# Execute operation with error handling
try:
result = __salt__["cmd.run_all"](f"{binary} {path}")
except Exception as exc:
raise CommandExecutionError(
f"Failed to run mytool on {path}: {exc}"
)
# Check command result
if result["retcode"] != 0:
raise CommandExecutionError(
f"mytool failed: {result['stderr']}"
)
return result["stdout"]
SaltInvocationError: Invalid function arguments, missing required parametersCommandExecutionError: Operation failed (file not found, command failed, etc.)CommandNotFoundError: Required binary not in PATHMinionError: General minion-side errorslog = logging.getLogger(__name__)__virtual__() function if platform/dependency specific__context__ for caching expensive operations__opts__["test"])result correctly (True/False/None)changes dict with before/aftercomment explaining what happenedsalt/modules/test.pysalt/modules/file.pysalt/modules/aptpkg.py, salt/modules/systemd_service.pysalt/states/file.py, salt/states/pkg.pydoc/topics/development/modules/developing.rst, doc/ref/states/writing.rst