Learn › Salt Configuration Management › Salt Fundamentals
Discover Salt execution modules with sys.list_modules, explore functions, and install packages remotely.
A ticket lands on you. Install nginx on minion1, then confirm whether a service is running on it. You are sitting on the master, saltmaster. You do not have that machine's password. You are not going to open a session on it. And there is a smaller problem underneath the big one: you do not yet know the name of the command that installs software through Salt.
Here is what makes this lesson different from looking it up. You will not have to know it. The minion is carrying a catalogue of everything it can be asked to do, and that catalogue is itself something you can ask it for. By the end of this page you will have made a machine list its own abilities, explain one of them in its own words, and then do the job.
The black boxes below are a practice terminal, a safe sandbox that checks the one command each step teaches. Every output shown was captured from the real topology this lab boots: a Debian 12 master named saltmaster and one salt-minion container named minion1. Uptimes and package versions come from that capture, so your own numbers will differ. Three of the listings run to hundreds of lines on a real minion, so what is printed here stops partway, and the step says so when it does.
An execution module is a bundle of related work that Salt knows how to do on a minion. A function is one specific job inside that bundle. You always name the two together, separated by a dot: module.function.
Picture the master as a switchboard and the minion as a building full of departments. pkg is the department that handles software packages. service handles running services. file handles files. user handles accounts. cmd runs raw shell commands. test handles simple checks. When you call pkg.install, you are ringing the packages department and asking one worker there to do one thing.
The command shape never changes. Type salt, then the target, then module.function, then any arguments:
sudo salt 'minion1' module.function arguments
Target first, work second. That order is the whole Salt command line.
Every command in this lesson starts with sudo. The salt program talks to the master's socket, which is owned by root, and the saltops account on this lab is permitted to reach it only through sudo. Drop the sudo and the command fails on a permissions error before it ever reaches the minion. Type the sudo.
Thomas Hatch wrote the first version of Salt in 2011 because the remote execution tools of the day were too slow for the number of machines he had to touch. His fix was to keep a small agent running permanently on every machine and push work to all of them at once over a fast message bus, instead of opening a fresh login session per server per command. Everything you are about to type rides on that one decision.
That design has a consequence you will use in a moment. Because the agent is a running program with the module code already loaded, it can be asked about itself.
There is a module called sys whose entire job is to describe the other modules. It does not touch packages, files or services. It answers questions about what Salt on that minion is carrying. Three of its functions do almost all the work: sys.list_modules, sys.list_functions and sys.doc. You will run all three on this page.
Start with the bluntest function in the library so the mechanism is impossible to miss. cmd.run takes a raw shell command and returns whatever that command printed.
Before you run it, decide one thing and hold on to your answer: when the result comes back, will the uptime belong to saltmaster, the machine you typed on, or to minion1, the machine you targeted?
sudo salt 'minion1' cmd.run 'uptime'
prompt: [saltops@saltmaster ~]$ answer: sudo salt 'minion1' cmd.run 'uptime'|||sudo salt "minion1" cmd.run "uptime"|||sudo salt '*' cmd.run 'uptime' output: minion1: 05:02:04 up 49 days, 23:52, 0 user, load average: 0.55, 0.39, 0.26 hint: Target first, then module.function, then the shell command in quotes: sudo salt 'minion1' cmd.run 'uptime'
The uptime belongs to minion1. The master never ran uptime itself. It sent the instruction across to the agent on the minion, that agent ran the command with its own shell, and only the result travelled back.
Read the shape before the numbers. Line one is minion1:, the name of the machine that answered. Everything indented under it is that machine's reply. Salt labels most returns with the minion they came from, and that habit is what keeps a hundred-machine answer readable on one screen.
cmd.run is the escape hatch, though, and it is worth naming that now. It has no idea what it just did, so it cannot report whether anything changed, and you are the one writing the shell, so the same line breaks on a machine that is not Debian. Real modules fix both problems. Reach for cmd.run when nothing else fits, not because it is the quickest thing to type.
cmd.run proved the mechanism, but it is the department you should use least. The ticket needs a real function, and you still do not know its name. Commit to where you would look before you read on.
man salt on the mastersudo salt 'minion1' cmd.run 'salt --help'sudo salt 'minion1' sys.list_modules>>> sys.list_modules. It asks the agent to report every execution module it has loaded, so the answer describes that exact machine. man salt documents the salt command line itself, its targeting flags and its output options, and it does not list the hundreds of module functions, so it answers a different question. The cmd.run 'salt --help' line is worse than useless here: --help prints command-line usage, and it would be printing the usage of whatever is installed on the minion, not a catalogue of modules. Asking Salt about Salt is a job for the sys module.
sys.list_modules takes no arguments. Point it at the minion and it prints, alphabetically, the name of every execution module that minion has loaded and can run.
sudo salt 'minion1' sys.list_modules
prompt: [saltops@saltmaster ~]$ answer: sudo salt 'minion1' sys.list_modules|||sudo salt "minion1" sys.list_modules|||sudo salt '*' sys.list_modules output: minion1:
hint: The module that describes the other modules is sys, and the function lists them: sudo salt 'minion1' sys.list_modules
That is an alphabetical list, and it is cut off partway through the letter d. On the real minion it keeps going for hundreds of entries, well past pkg, service, user and the rest. Page it with less when you want to read the whole thing.
Two entries in that short window are already familiar. cmd is the department you rang a moment ago with cmd.run. archive, chroot, cloud and cryptdev are departments you have not met, and that is the point of running this: the catalogue is the answer to what can Salt do here, and it comes from the machine rather than from a web page about some other version.
One nuance worth holding. This list describes minion1 specifically. A minion running a different operating system loads a different set, because Salt only loads the modules that make sense on the machine it is sitting on.
Hundreds of modules is too wide. Once you know which department you want, sys.list_functions prints just the jobs inside it. It takes one argument, the module name.
Your ticket is about installing software, so the department is pkg. Before you run this, commit to a guess: how many of the function names that come back will start with something other than pkg.?
sudo salt 'minion1' sys.list_functions pkg
prompt: [saltops@saltmaster ~]$ answer: sudo salt 'minion1' sys.list_functions pkg|||sudo salt "minion1" sys.list_functions pkg|||sudo salt '*' sys.list_functions pkg output: minion1:
hint: Same sys module, the function that lists functions, then the module you care about: sudo salt 'minion1' sys.list_functions pkg
None of them. Every name is fully qualified as pkg.something, because a function only exists inside its module and Salt prints it the way you would type it. Copy any line out of that list, put a target in front of it, and you have a working command.
There it is, thirteen lines down: pkg.install. That is the function the ticket needs, and you did not look it up anywhere. The list is cut off again after pkg.latest_version, and the real one keeps going past it.
Read a few of the others while they are in front of you, because they teach the naming style. pkg.available_version asks what version could be installed. pkg.latest_version asks what the newest one is. pkg.info_installed reports details about what is already there. pkg.hold pins a package so it stops being upgraded. Salt names functions after the question you are asking, not after the tool that answers it.
The catalogue gave you a name and nothing more. It did not say what arguments a function takes or what it hands back. That is the third sys function's job.
sys.doc is Salt's built-in manual. Hand it a module.function and the minion answers with what it knows about that function. Try it on test.ping, the check function from the test module, because its entry is short enough to read whole.
Before you run it, decide what you expect back. A link to a website, or the documentation text itself?
sudo salt 'minion1' sys.doc test.ping
prompt: [saltops@saltmaster ~]$ answer: sudo salt 'minion1' sys.doc test.ping|||sudo salt "minion1" sys.doc test.ping|||sudo salt '*' sys.doc test.ping output: test.ping:
Used to make sure the minion is up and responding. Not an ICMP ping.
Returns `True`.
CLI Example:
salt '*' test.ping hint: The docs function lives in the sys module, and it takes the module.function you want to read: sudo salt 'minion1' sys.doc test.ping
The documentation text itself, not a link. Four parts, and every sys.doc entry carries the same four: the function name, a sentence saying what it does, a line saying what it returns, and a worked example.
Look at the top line too. It reads test.ping:, the function you asked about, rather than minion1:. This is one of the returns that heads its block with the subject instead of the machine, so a missing minion name here is not a missing answer.
Read that worked example carefully before you copy it. It is printed exactly as it ships inside the module, salt '*' test.ping, with no sudo in front. Typed that way on this lab it fails on a permissions error, because saltops can reach the salt program only through sudo. The example is showing you the shape, not the whole line.
One line in there deserves a full stop. Not an ICMP ping means test.ping has nothing to do with the network ping command. It does not send a packet at an address. It asks the Salt agent on the minion to answer, and True means that agent heard you and replied. A machine can pass test.ping while a firewall drops ordinary pings, and it can fail test.ping while pinging perfectly.
Notice where that text came from. The minion read it out of the module code it has loaded, so it matches the version of Salt actually running over there. A web search returns documentation for some version, not necessarily this one, and that gap is exactly where a renamed argument quietly bites you. Hand sys.doc a bare module name instead, as in sys.doc test, and Salt answers with one entry like this per function in that module.
That is the whole discovery loop, and it works on every unfamiliar corner of Salt. sys.list_modules to find the department, sys.list_functions MODULE to find the job, sys.doc MODULE.FUNCTION to read how it works. Three commands from no idea to a working call, without leaving the terminal.
You have the function name and you know how to read up on any function. One decision is left, and it is the decision that separates people who use Salt from people who use Salt as a fancy way to run shell commands. Commit to an answer.
sudo salt 'minion1' apt.install nginxsudo salt 'minion1' cmd.run 'apt-get install -y nginx'sudo salt 'minion1' pkg.install nginx>>> pkg.install. pkg is a virtual module: the name you type stays the same on every machine, and each minion loads the implementation that matches what it is running. On this Debian minion, pkg is backed by the apt code. On a Red Hat machine the identical pkg.install call is backed by that system's own package tooling instead. The apt.install answer hard-codes the package tool into your command, which is the portability you are paying Salt to remove, and it is not even a module name Salt hands you: the listing you just printed said pkg.install. The cmd.run answer does technically install nginx, but it throws away everything a real module gives you. It cannot tell you which version landed, it cannot tell you whether anything changed, and it breaks on the first machine in the fleet that is not Debian.
You found the function in the catalogue, and sys.doc pkg.install is there whenever you want the detail. Now fire it. pkg.install takes the package name as its argument.
Before you press Enter, commit to what a successful install should print. A quiet True, or something that names what changed?
sudo salt 'minion1' pkg.install nginx
prompt: [saltops@saltmaster ~]$ answer: sudo salt 'minion1' pkg.install nginx|||sudo salt "minion1" pkg.install nginx|||sudo salt '*' pkg.install nginx output: minion1: ---------- nginx: ---------- new: 1.22.1-9+deb12u9 old: nginx-common: ---------- new: 1.22.1-9+deb12u9 old: hint: The packages department, the install job, then the package name: sudo salt 'minion1' pkg.install nginx
Not a True. Something that names what changed, and the change is the whole point, so read it as a before and after.
Look at the pairs. Under nginx, the field old: is empty and the field new: reads 1.22.1-9+deb12u9. Empty old means nginx was not on that machine at all a second ago. Populated new means it is there now, at that exact version. Two lines, and they tell you both the starting state and the ending state.
Now look at the second block. You asked for one package and got two. nginx-common appears with the same empty old: and the same populated new:, because it is a dependency that came along for the ride. Salt reports every package that changed, not just the one you named, so nothing gets installed behind your back without appearing in the return.
The rows of dashes are Salt's way of drawing a nested block. ---------- under minion1: opens the set of changes, and another one under nginx: opens that package's own old and new pair. Wherever you see that line, an indented group is starting.
This is the difference between a real module and cmd.run. cmd.run 'apt-get install -y nginx' would have installed the same software and handed you back a wall of apt chatter for you to read. pkg.install handed you a structured answer that says precisely what moved.
The minion container in this lab has no route to the internet, on purpose, since you are able to run commands on it. It installs from a small package repository baked into its own image, which is why nginx resolves here without a Debian mirror. On a production minion the same pkg.install line reaches whatever repositories that machine is configured to use.
Stop and take stock, because you just did the whole thing end to end.
You started not knowing the name of the command. You made the machine list its departments. You narrowed to the one you wanted and read its jobs. You made a function print its own documentation out of the code the minion is running. Then you installed software on a machine you never logged into, and the machine told you exactly what changed.
That loop does not get more complicated later. Every one of the hundreds of Salt modules answers to the same target, then module.function, then arguments shape, and the three sys functions walk you to any of them. There is no second syntax waiting for you. There are only more departments to meet.
systemctl status printsTrue or False>>> A single True or False. service.status answers one question, is it running, and it answers it as a plain boolean you can act on. That is deliberate: a module is meant to give you an answer, not a screenful of text to read by eye. The systemctl status block is what you would get from cmd.run 'systemctl status cron', and it would also tie your command to systemd, while service.status works the same whether the minion uses systemd, SysV init or something else. The process id answer belongs to a different question, and a different function: service.status is not trying to identify the process, only to report whether the service is up.
service.status takes the name of a service and reports whether it is currently running. Ask about cron, the scheduler daemon that ships on almost every Debian machine.
sudo salt 'minion1' service.status cron
prompt: [saltops@saltmaster ~]$ answer: sudo salt 'minion1' service.status cron|||sudo salt "minion1" service.status cron|||sudo salt '*' service.status cron output: minion1: False hint: The services department, the status job, then the service name: sudo salt 'minion1' service.status cron
False. Not an error, not a crash, not a complaint. A plain no: that service is not running on minion1 right now. True would have meant it is.
This is a container rather than a full machine, and it is not running the usual pile of background daemons, so a False here is the honest state of the box rather than a sign that something broke.
Now the trap, and it catches people for years. A service name you spelled wrong also comes back False. service.status reports not running, and a service that does not exist is certainly not running, so both cases look identical from here. False is therefore never proof that a
Practice Modules and Functions in a real Linux terminal at The Linux Camp. Progress is verified automatically as you type commands on the machine.