LearnSalt Configuration ManagementState Files

File States

Use file.managed to deploy configuration files from the Salt master to minions.

A file you have never opened, on a machine you never log into

There is a file on minion1 called /etc/motd. It holds seven lines of boilerplate that came with the Debian image. Nobody has ever edited it. You are about to replace every one of those lines from where you are sitting, and you are not going to open a session on that machine to do it.

Copying a file is not the interesting part. Any tool can copy a file. The interesting part is what Salt hands back afterwards. It does not say done. It shows you, line by line, what that file used to say and what it says now, in the minion's own words.

By the end of this page a directory will also exist on that machine that does not exist right now.

The black boxes below are a practice terminal: a safe sandbox that checks the one command each step teaches. Both screens in this lesson were captured from a real Salt master with one real minion attached, which is exactly the machine your lab boots. The lab user is saltops, the master is named saltmaster, and there is exactly one minion, named minion1. Timestamps and durations came from that capture, so your own numbers will differ.

What a file state promises

In the last lesson you made a package appear on a minion by describing the end result instead of typing the steps. A state is that description, written in a file. file.managed is the state that describes a file.

It makes one promise: this path on the minion holds this content. Salt keeps the promise whichever way it has to.

That is why the naming is past tense. file.managed is not an order to copy something. It is a statement about how the world should look, and Salt closes whatever gap it finds.

Every file state has two halves in two different places. The source is content sitting on the master, where you can read it, edit it and keep it in version control. The destination is a path on the minion. Your job is to write down both, once.

The master is also a file server

Thomas S. Hatch released Salt in 2011. Pushing commands to a fleet was the headline, but he ran into the same wall everyone does about a week in: commands are not enough. Real servers need content. Configuration files, certificates, scripts. The tools of the day answered that with a second machine, an HTTP server or an rsync host, plus another set of credentials to look after.

So he built a small file server into the master itself. The directories listed under file_roots are published to the minions, and on this lab box that is /srv/salt, the same directory your state files live in.

Content in the file root gets its own address scheme, written like a web URL: salt://. It always means the master's file root and nothing else. salt://motd is the file named motd in /srv/salt. The minion pulls that content down over the connection it already holds to the master, so there is no second service to run and no second password to manage.

Put the content on the master first

Order matters here. Salt cannot ship content it does not have, so the source file comes first. Write it into the file root:

cat > /srv/salt/motd << 'EOF'
Managed by Salt
EOF

Read that command in two pieces. cat > /srv/salt/motd opens the file for writing. << 'EOF' says take everything up to the next line that reads EOF and treat it as the text to write. The quotes around EOF matter: they tell the shell to copy your text literally, without touching anything that looks like a variable.

One line of content, Managed by Salt. That is the whole file. Nothing about it is special yet; it is an ordinary file in an ordinary directory. What makes it Salt content is the directory it sits in.

answer: /srv/salt/motd hint: The salt:// prefix means the master's file root, which on this box is /srv/salt. Add the file name to it.

Write the state that moves it

Now the description. State files are YAML, they end in .sls, and they live in the same file root:

cat > /srv/salt/motd.sls << 'EOF'
/etc/motd:
  file.managed:
    - source: salt://motd
    - user: root
    - group: root
    - mode: "0644"
EOF

Four things are going on in those six lines.

Always put quotes around the mode. Write mode: "0644", never mode: 0644. YAML reads a bare 0644 as a number rather than as the four characters you typed, and the leading zero does not survive the trip. Quote it and Salt receives exactly what you wrote. This one is the most common beginner bug in Salt state files, and the error it causes shows up much later, as a file with permissions nobody asked for.

answer: mode: "0644"|||mode: '0644'|||- mode: "0644"|||- mode: '0644' hint: The key, a colon, then the four digits wrapped in quotes.

Commit: what will the receipt show?

Both files are on the master. Nothing has reached the minion yet. Before you send anything, take a position on what comes back.

Apply it

Every Salt command keeps the same three-part shape you already know: sudo salt, then the target in quotes, then the work. The work here is state.apply followed by the name of the state file without the .sls on the end. Salt finds /srv/salt/motd.sls, reads it, and hands the job to the minion.

sudo salt 'minion1' state.apply motd

prompt: saltops@saltmaster:~$ answer: sudo salt 'minion1' state.apply motd|||sudo salt "minion1" state.apply motd|||sudo salt minion1 state.apply motd|||sudo salt 'minion1' state.sls motd|||sudo salt '*' state.apply motd output: minion1: ---------- ID: /etc/motd Function: file.managed Result: True Comment: File /etc/motd updated Started: 09:10:03.473828 Duration: 7.782 ms Changes: ---------- diff: --- +++ @@ -1,7 +1 @@ - -The programs included with the Debian GNU/Linux system are free software; -the exact distribution terms for each program are described in the -individual files in /usr/share/doc/*/copyright. - -Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent -permitted by applicable law. +Managed by Salt

Summary for minion1 ------------ Succeeded: 1 (changed=1) Failed: 0 ------------ Total states run: 1 Total run time: 7.782 ms hint: Target first, then the function, then the state file name with no .sls: sudo salt 'minion1' state.apply motd

Read it in three bands.

The header. minion1: names who is talking, then a row of dashes opens one block per state that ran. ID: /etc/motd is the label you wrote. Function: file.managed is what ran. Result: True is the verdict. Comment: File /etc/motd updated is Salt's own sentence about it. Notice what is missing: there is no Name: line. Salt prints one only when the destination differs from the ID, and you put the path in the ID, so there was nothing extra to print.

The changes. This is the band that teaches. @@ -1,7 +1 @@ says the old file was 7 lines starting at line 1, and the new one is 1 line. Every line below it starting with a minus left the file. The single line starting with a plus arrived: +Managed by Salt. You are not reading Salt's opinion of what it did. You are reading the minion's before and after. It is still Salt reporting on Salt, though, so the next step is to make the machine answer for itself.

The summary. Succeeded: 1 (changed=1) counts states that worked and, in the brackets, states that actually changed something. Those two numbers are different questions and the gap between them is the useful signal. Failed: 0 is the one you scan for first on a bad day. Started and Duration are per state, so you can see which part of a long run cost you the time.

Make the minion answer for itself

Salt just told you the file changed. Salt is also the thing that changed it. Before you trust any tool's report of its own work, ask the machine directly.

cmd.run runs a plain shell command on the minion and hands you back what it printed. The command that reads a file is cat, so cat /etc/motd is the read-back you want. Same three parts as always: sudo salt, the target in quotes, then the work.

answer: sudo salt 'minion1' cmd.run 'cat /etc/motd'|||sudo salt "minion1" cmd.run 'cat /etc/motd'|||sudo salt 'minion1' cmd.run "cat /etc/motd"|||sudo salt "minion1" cmd.run "cat /etc/motd"|||sudo salt minion1 cmd.run 'cat /etc/motd' hint: The module is cmd and the function is run, with the shell command in quotes: sudo salt 'minion1' cmd.run 'cat /etc/motd'

What comes back is the single line you wrote on the master, Managed by Salt, read off the minion's own disk. That is the difference between a report and a proof, and the lab scores it as one of its five tasks.

Milestone: you push content now, not just commands

Count what changed. Everything you have sent minion1 until now was an instruction to run: ping, report a grain, install a package. This was different. You moved content.

A file that lives on your master, that you can read, edit, review and keep in version control, is now the file that lives on that machine. That is configuration management in one sentence, and the rest of Salt is variations on it.

Commit: run the same command again

Nothing on the minion has changed since the run finished. You press the up arrow and send the identical command a second time.

Directories are a state too

Not every file state is about content. Sometimes a service just needs somewhere to put things, and the directory has to exist before anything else will work.

file.directory makes that promise: this path on the minion is a directory. Write a second state file for it:

cat > /srv/salt/webdir.sls << 'EOF'
/srv/www:
  file.directory:
    - user: root
    - group: root
    - mode: "0755"
    - makedirs: True
EOF

Same shape as before, and one thing missing. The ID is the path again, file.directory replaces file.managed, and ownership and mode work the same way. There is no - source: line, because there is no content to fetch. A directory is not made of anything.

- makedirs: True is the new setting. It is the state file version of mkdir -p: create any parent directories that are missing on the way down.

Commit: what if the parent is missing?

That last setting looks like a detail until the day it is not. Take a position on it before you move on.

Challenge: point at a file you have not deployed yet

Scaffolding off. Nothing is shown from here on.

Your next job on this master is an nginx configuration. You have already put the source file in the file root, at /srv/salt/nginx.conf, and you are writing the file.managed block that will carry it to the minion. Every other line is done. The one line left is the one that tells Salt where to fetch the content from.

answer: - source: salt://nginx.conf|||source: salt://nginx.conf hint: Same address scheme as the last state file, pointed at the new file name in the master's file root.

Challenge: make a directory exist on minion1

Still no command shown.

minion1 needs /srv/www to exist. You wrote that state a few minutes ago and saved it as /srv/salt/webdir.sls. So far it has done nothing at all. A state file sitting on the master is just a file until you put it to work, so put it to work on the one minion this master trusts.

prompt: saltops@saltmaster:~$ answer: sudo salt 'minion1' state.apply webdir|||sudo salt "minion1" state.apply webdir|||sudo salt minion1 state.apply webdir|||sudo salt 'minion1' state.sls webdir|||sudo salt '*' state.apply webdir output: minion1: ---------- ID: /srv/www Function: file.directory Result: True Comment: Started: 09:10:04.542262 Duration: 3.261 ms Changes: ---------- /srv/www: ---------- directory: new

Summary for minion1 ------------ Succeeded: 1 (changed=1) Failed: 0 ------------ hint: Same three parts as the last apply, with the other state file's name and no .sls.

directory: then new. That is the whole change, and it is a different shape from the last one on purpose. A file state reports a diff because a file has contents that can differ. A directory state reports what it made, because a directory either is there or is not.

Look at the Comment: field: it is empty. Salt writes a sentence there when it has something to add beyond the change itself, and here it did not. An empty comment next to Result: True is not a problem, it is a state that had nothing to explain.

This capture stops at the second dashed rule, so the totals rows that closed the first screen are simply not shown here.

The kit you just earned

You moved content onto a machine you never logged into, and then made a directory appear on it. Here is every form this lesson put in your hands:

FormWhat it does
cat > /srv/salt/motd << 'EOF'Write source content into the master's file root
cat > /srv/salt/motd.sls << 'EOF'Write the state file that carries it
file.managedThis path on the minion holds this content
- source: salt://motdFetch that content from /srv/salt/motd on the master
- user: / - group:Set ownership of the file on the minion
- mode: "0644"Set permissions, always quoted
sudo salt 'minion1' state.apply motdApply one state file by name, with no .sls
sudo salt 'minion1' cmd.run 'cat /etc/motd'Make the minion read the deployed file back to you
file.directoryThis path on the minion is a directory
- makedirs: TrueCreate the missing parents on the way down
sudo salt 'minion1' state.apply webdirApply the directory state

And the four facts underneath them:

Result and Changes answer two different questions, and reading them as one is the mistake to avoid. Result says whether the state worked. Changes says whether anything on the machine actually moved. True with an empty Changes block is the quietest good news in Salt: the machine was already right.

Ready to practice

The practice terminal walked you through the whole loop. You read the two writes off the page, the source file and the state file. Then you typed the parts that matter yourself: the path salt://motd resolves to, the quoted mode line, and the apply that put the state to work. You also typed the read-back that made the minion prove it, and the apply that made a directory appear.

The lab is that same loop on a real Salt master with one real minion attached. It scores five things, and this lesson has shown you every one. A source file in the master's file root, a file.managed state written, the state applied, the deployed file read back o

Practice File States in a real Linux terminal at The Linux Camp. Progress is verified automatically as you type commands on the machine.

More lessons in State Files