TT Lab
Get started
Learn Learning paths Courses

Ansible Fundamentals

The day mode 640 shipped as 1204: managing files as state

Continue in TT Lab

Goal

You declare files and directories as state rather than as commands. You measure for yourself the permission that an unquoted octal leaves, add a basis for reverting and blocking broken configurations with backup and validation, and go on to build a section with markers, links, a state report, and a path inspector.

Why it matters

If you carry the four shell lines mkdir, cp, chmod, and sed -i over as they are, it looks like automation, but three things collapse at once. The second run differs from the first, nothing remains anywhere of what was overwritten, and a broken configuration goes up as it is. Ansible's file modules answer these three with state, backup, and validate respectively. Add to that the marker of blockinfile, which manages a multi-line section safely, the two kinds of links used in deployments, and stat, which changes nothing and only reads facts, and the skeleton of configuration deployment is complete. This lab sets up that skeleton one piece at a time by hand, and at the end you build a tool that judges "what state is this path in right now".

Steps

  1. In /root/ans/files/hosts.ini, for the web group, write the host web1 (ansible_host=127.0.0.1, ansible_port=2222, ansible_user=root), and with /root/ans/files/p01.yml create four directories — /root/ans/files/tree/conf, /root/ans/files/tree/logs, and /root/ans/files/out with 0755, and /root/ans/files/tree/secrets with 0700.
  2. With /root/ans/files/p02.yml, create three files — /root/ans/files/tree/secrets/api.key with "0600", /root/ans/files/tree/conf/motd.txt with "0644", and /root/ans/files/scratch/decimal.txt written without quotes as mode: 644. Then write the value that stat -c %a gives for decimal.txt as one line in /root/ans/files/out/mode-trap.txt.
  3. With /root/ans/files/p03.yml, place /root/ans/files/tree/conf/app.conf. Its content is the two lines listen_port={{ app_port }} and env=prod, the default of app_port is 8080, the permission is "0644", and you turn on backup. Make the same playbook also create /root/ans/files/tree/conf/deprecated.conf and /root/ans/files/tree/logs/archive/old.log. First run it once with the defaults, then run it again with -e app_port=9090 so that a backup is created, and leave the path of that backup in /root/ans/files/out/backup-path.txt.
  4. With /root/ans/files/p04.yml, place /root/ans/files/tree/conf/app.json. Its content comes from the app_json variable, and the default is JSON in which service is web and port is 8080, and on copy you add validate so that it checks the JSON syntax. Then run the same playbook once more with -e "app_json=not json at all" and leave the failed output in /root/ans/files/out/validate-fail.txt.
  5. With /root/ans/files/p05.yml, handle /root/ans/files/tree/conf/hosts.block. Create it with the single line 127.0.0.1 localhost only when the file does not exist (if it already exists, do not overwrite it), and with blockinfile, in a section that uses the marker # {mark} ANSIBLE MANAGED BLOCK: web pool, put the two lines 10.10.0.11 web1 and 10.10.0.12 web2. Even if you run the playbook twice, there must be exactly one block.
  6. With /root/ans/files/p06.yml, do four things — create /root/ans/files/tree/logs/app.log only when it does not exist, make /root/ans/files/tree/logs/latest.log a symbolic link that points to that file, make /root/ans/files/tree/conf/motd.hard a hard link to /root/ans/files/tree/conf/motd.txt, and finally, on the path latest.log, apply "0640" while following the link so that the permission of app.log changes.
  7. With /root/ans/files/p07.yml, check the state of /root/ans/files/tree/conf/app.conf with stat, taking the checksum as sha256, and leave in /root/ans/files/out/stat-report.json JSON holding the five keys path, exists, mode, size, and checksum. Then bring /root/ans/files/tree/conf/app.json with fetch so that it lands as the single file /root/ans/files/fetched/app.json.
  8. With /root/ans/files/p08.yml, put /root/ans/files/tree/conf/deprecated.conf and /root/ans/files/tree/logs/archive into the absent state. Then write a script that takes a path list file as an argument and prints, for each path, one line of the form <경로> LINK <가리키는 곳>, <경로> DIR <권한>, <경로> FILE <권한>, or <경로> ABSENT (path, then kind, then where it points or its permission), as /root/ans/files/audit.sh. In /root/ans/files/paths.txt, write the two paths above plus /root/ans/files/tree/conf/app.conf, /root/ans/files/tree/logs/latest.log, and /root/ans/files/tree/secrets, and save the result to /root/ans/files/out/state-report.txt.

Notes

Declare a directory tree as state

In /root/ans/files/hosts.ini, for the web group, write the host web1 (ansible_host=127.0.0.1, ansible_port=2222, ansible_user=root), and with /root/ans/files/p01.yml create four directories — /root/ans/files/tree/conf, /root/ans/files/tree/logs, and /root/ans/files/out with 0755, and /root/ans/files/tree/secrets with 0700.

In ansible.builtin.file, the single state value that means directory also creates the intermediate paths. Always write mode as a quoted string — you will measure for yourself why in the next step. The grader also runs the playbook once more in check mode and looks at whether the changes are 0.

Measure for yourself the permission an unquoted octal leaves

With /root/ans/files/p02.yml, create three files — /root/ans/files/tree/secrets/api.key with "0600", /root/ans/files/tree/conf/motd.txt with "0644", and /root/ans/files/scratch/decimal.txt written without quotes as mode: 644. Then write the value that stat -c %a gives for decimal.txt as one line in /root/ans/files/out/mode-trap.txt.

YAML reads a number without a leading 0 as decimal. What value results when that integer is used as it is for the permission bits, don't work out in your head — actually create it and measure it with stat. The scratch directory is also created in the same playbook.

Keep the content from before the overwrite

With /root/ans/files/p03.yml, place /root/ans/files/tree/conf/app.conf. Its content is the two lines listen_port={{ app_port }} and env=prod, the default of app_port is 8080, the permission is "0644", and you turn on backup. Make the same playbook also create /root/ans/files/tree/conf/deprecated.conf and /root/ans/files/tree/logs/archive/old.log. First run it once with the defaults, then run it again with -e app_port=9090 so that a backup is created, and leave the path of that backup in /root/ans/files/out/backup-path.txt.

The backup feature of copy leaves the content just before the overwrite in the same directory. The path is held in the return value, so if you receive it with register, you can use it straight away in the next task. On the first run there is nothing to overwrite, so no backup is created; put an is defined condition on the task that records the path.

Put in place only a configuration that passes the check

With /root/ans/files/p04.yml, place /root/ans/files/tree/conf/app.json. Its content comes from the app_json variable, and the default is JSON in which service is web and port is 8080, and on copy you add validate so that it checks the JSON syntax. Then run the same playbook once more with -e "app_json=not json at all" and leave the failed output in /root/ans/files/out/validate-fail.txt.

Into a validate string, at the %s spot, the temporary file path goes. The check command only has to read the JSON and end with a non-zero value if there is a problem. When you keep the output of a failed run, you must capture standard error too, and make sure the script does not stop because of the failed command. Check whether the original file is still intact after the failure.

Replace only the section with markers

With /root/ans/files/p05.yml, handle /root/ans/files/tree/conf/hosts.block. Create it with the single line 127.0.0.1 localhost only when the file does not exist (if it already exists, do not overwrite it), and with blockinfile, in a section that uses the marker # {mark} ANSIBLE MANAGED BLOCK: web pool, put the two lines 10.10.0.11 web1 and 10.10.0.12 web2. Even if you run the playbook twice, there must be exactly one block.

copy has a switch that keeps it from overwriting a file that already exists. If you leave it out, on the second run the block disappears entirely and is attached again. BEGIN and END go into the {mark} spot of the marker string respectively — if you change this wording later, the block becomes two sets. The grader also runs the playbook again in check mode and looks at whether the changes are 0.

Symbolic links, hard links, and follow

With /root/ans/files/p06.yml, do four things — create /root/ans/files/tree/logs/app.log only when it does not exist, make /root/ans/files/tree/logs/latest.log a symbolic link that points to that file, make /root/ans/files/tree/conf/motd.hard a hard link to /root/ans/files/tree/conf/motd.txt, and finally, on the path latest.log, apply "0640" while following the link so that the permission of app.log changes.

Symbolic links and hard links have different state values. Both take src and dest. For the last task, use the state that only adjusts the attributes of an existing file, and explicitly turn on the argument that decides whether to follow the link. On Linux the permission of a symbolic link itself is meaningless, so the permission applies not to the link but to the file at its end.

Read the state, leave it as a report, and bring it to the controller

With /root/ans/files/p07.yml, check the state of /root/ans/files/tree/conf/app.conf with stat, taking the checksum as sha256, and leave in /root/ans/files/out/stat-report.json JSON holding the five keys path, exists, mode, size, and checksum. Then bring /root/ans/files/tree/conf/app.json with fetch so that it lands as the single file /root/ans/files/fetched/app.json.

stat changes nothing and only returns facts. The checksum is held in the return value only when you specify the algorithm. fetch by default creates a host-name directory and reproduces the original path as it is, so to land it as a single file you have to turn on the argument that switches off that behavior. For the report, using a filter that builds a dictionary and turns it into JSON finishes it in one task.

Cleaning up is a state too — and building a path inspector

With /root/ans/files/p08.yml, put /root/ans/files/tree/conf/deprecated.conf and /root/ans/files/tree/logs/archive into the absent state. Then write a script that takes a path list file as an argument and prints, for each path, one line of the form <경로> LINK <가리키는 곳>, <경로> DIR <권한>, <경로> FILE <권한>, or <경로> ABSENT (path, then kind, then where it points or its permission), as /root/ans/files/audit.sh. In /root/ans/files/paths.txt, write the two paths above plus /root/ans/files/tree/conf/app.conf, /root/ans/files/tree/logs/latest.log, and /root/ans/files/tree/secrets, and save the result to /root/ans/files/out/state-report.txt.

Write the permission exactly as stat -c %a gives it. The order of judgment is the whole of this step — -d and -f follow symbolic links, so if you look at links later, a link masquerades as a directory or a file. The grader also runs this script against a separate path list it made itself, so a script that has the answer written into it will not pass.