Chapter 8. Deploying Files

This chapter covers the following subjects:

Using Modules to Manipulate Files

Managing SELinux Properties

Using Jinja2 Templates

The following RHCE exam objectives are covered in this chapter:

• Use Ansible modules for system administration tasks that work with:

• File contents

• Use advanced Ansible features

• Create and use templates to create customized configuration files

“Do I Know This Already?” Quiz

The “Do I Know This Already?” quiz allows you to assess whether you should read this entire chapter thoroughly or jump to the “Exam Preparation Tasks” section. If you are in doubt about your answers to these questions or your own assessment of your knowledge of the topics, read the entire chapter. Table 8-1 lists the major headings in this chapter and their corresponding “Do I Know This Already?” quiz questions. You can find the answers in Appendix A, “Answers to the ’Do I Know This Already?’ Quizzes and Exam Questions.”

Table 8-1 “Do I Know This Already?” Section-to-Question Mapping

Image

1. Which module should you use to check the current permission mode on a file?

a. stat

b. file

c. permissions

d. acl

2. Which module should you use to replace a line of text in a configuration file with another line of text?

a. copy

b. regex

c. lineinfile

d. blockinfile

3. Which of the following shows correct syntax for a when statement that runs a task only if the permission mode as discovered by the stat module and registered to the st variable is not set to 0640?

a. st.mode != ’0640’

b. st.stat.mode != 0640

c. st.stat.mode != ’0640’

d. st.mode != 0640

4. Which of the following lines shows correct use of the lineinfile module to find a line that begins with PermitRootLogin based on a regular expression?

a. line: “PermitRootLogin”

b. line: “^PermitRootLogin”

c. regexp: “PermitRootLogin”

d. regexp: “^PermitRootLogin”

5. Which of the following is not a common task that the file module can do?

a. Remove files

b. Copy a line of text into a file

c. Create links

d. Set permissions

6. Which module can you use to copy a file from a managed node to the control node?

a. copy

b. file

c. sync

d. fetch

7. Different modules can be used when working with SELinux. Which of the following modules should you avoid?

a. file

b. sefcontext

c. command

d. selinux

8. After you set an SELinux context, the Linux restorecon command must be executed. How would you do this?

a. Use the command module to run the restorecon command.

b. Use the restorecon module.

c. Use the selinux module.

d. No further action is needed; this is done automatically when using the appropriate SELinux module.

9. What do you need to transform the contents of a variable to the JSON format?

a. The lineinfile module

b. A Jinja2 template

c. A filter

d. The copy module

10. What should you use to process host-specific facts from a template?

a. The hostvars macro

b. The hostvars magic variable

c. The hostvars module

d. The hostvars filter

Foundation Topics

Using Modules to Manipulate Files

Managing files is an important task for Linux administrators. Different types of manipulations are performed on files on a frequent basis. They include managing files, managing file contents, and moving files around. In this section you learn how to use Ansible modules to apply these different tasks.

File Module Manipulation Overview

Many modules are available to manage different aspects of files. Table 8-2 provides an overview of some of the most commonly used file modules.

Table 8-2 File Manipulation Module Overview

Image

Most of these modules are discussed in the following sections. When using file-related modules, you might need a module that is not discussed here. If that is the case, the best approach is to use the ansible-doc command. When you use this command on any module, you always see related modules mentioned in the SEE ALSO section of the documentation.

Managing File Attributes

If you need to work with file attributes, the stat module and the file module come in handy. The stat module enables you to retrieve file status information. Because this module gets status information and is not used to change anything, you mainly use it to check specific file properties and perform an action if the properties are not set as expected. In Listing 8-1 you can see a playbook that uses the stat and debug modules to explore what exactly the stat module is doing. Listing 8-2 shows the output shown while running ansible-playbook listing81.yaml.

Listing 8-1 Exploring the stat Module

---
- name: stat module tests
  hosts: ansible1
  tasks:
  - stat:
      path: /etc/hosts
    register: st
  - name: show current values
    debug:
      msg: current value of the st variable is {{ st }}

Listing 8-2 Running ansible-playbook listing81.yaml

[ansible@control ~]$ ansible-playbook listing81.yaml

PLAY [stat module tests] ***************************************************************

TASK [Gathering Facts] *****************************************************************
ok: [ansible1]

TASK [stat] ****************************************************************************
ok: [ansible1]

TASK [show current values] *************************************************************
ok: [ansible1] => {
    "msg": "current value of the st variable is {’changed’: False, ’stat’: {’exists’: True, ’path’: ’/etc/hosts’, ’mode’: ’0644’, ’isdir’: False, ’ischr’: False, ’isblk’: False, ’isreg’: True, ’isfifo’: False, ’islnk’: False, ’issock’: False, ’uid’: 0, ’gid’: 0, ’size’: 158, ’inode’: 16801440, ’dev’: 64768, ’nlink’: 1, ’atime’: 1586230060.147566, ’mtime’: 1536580263.0, ’ctime’: 1584958718.8117938, ’wusr’: True, ’rusr’: True, ’xusr’: False, ’wgrp’: False, ’rgrp’: True, ’xgrp’: False, ’woth’: False, ’roth’: True, ’xoth’: False, ’isuid’: False, ’isgid’: False, ’blocks’: 8, ’block_size’: 4096, ’device_type’: 0, ’readable’: True, ’writeable’: True, ’executable’: False, ’pw_name’: ’root’, ’gr_name’: ’root’, ’checksum’: ’7335999eb54c15c67566186bdfc46f64e0d5a1aa’, ’mimetype’: ’text/plain’, ’charset’: ’us-ascii’, ’version’: ’408552077’, ’attributes’: [], ’attr_flags’: ’’}, ’failed’: False}"
}

PLAY RECAP *****************************************************************************
ansible1                   : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

As you can see from Listing 8-2, the stat module returns many file properties. It tests which permission mode is set, whether it is a link, which checksum is set on the file, and much more. For a complete list of output data, you can consult the documentation as provided while running ansible-doc stat.

Based on the output that is provided, a conditional test can be performed. The sample playbook in Listing 8-3 shows how this can be done and how the playbook can write a message if the expected permission mode is not set.

Listing 8-3 Performing File State Tests with the stat Module

---
- name: stat module tests
  hosts: ansible1
  tasks:
  - command: touch /tmp/statfile
  - stat:
      path: /tmp/statfile
    register: st
  - name: show current values
    debug:
      msg: current value of the st variable is {{ st }}
  - fail:
      msg: "unexpected file mode, should be set to 0640"
    when: st.stat.mode != ’0640’

As you can see in the playbook output in Listing 8-4, the playbook fails with the unexpected file mode message. Also notice the warning in the Listing 8-4 output: it tells you that there is a better solution to do what you wanted to do here. This happens on multiple occasions when you might have selected a module that is not the best solution for the task you want to perform. Remember: Using the command module will work in almost all cases, but often a better solution is available.

Listing 8-4 Running ansible-playbook listing83.yaml Result

[ansible@control ~]$ ansible-playbook listing83.yaml

PLAY [stat module tests] ***************************************************************

TASK [Gathering Facts] *****************************************************************
ok: [ansible1]

TASK [command] *************************************************************************
[WARNING]: Consider using the file module with state=touch rather than running ’touch’.
If you need to use command because file is insufficient you can add ’warn: false’ to
this command task or set ’command_warnings=False’ in ansible.cfg to get rid of this
message.
changed: [ansible1]

TASK [stat] ****************************************************************************
ok: [ansible1]

TASK [show current values] *************************************************************
ok: [ansible1] => {
    "msg": "current value of the st variable is {’changed’: False, ’stat’: {’exists’: True, ’path’: ’/tmp/statfile’, ’mode’: ’0644’, ’isdir’: False, ’ischr’: False, ’isblk’: False, ’isreg’: True, ’isfifo’: False, ’islnk’: False, ’issock’: False, ’uid’: 0, ’gid’: 0, ’size’: 0, ’inode’: 51440456, ’dev’: 64768, ’nlink’: 1, ’atime’: 1586253087.057596, ’mtime’: 1586253087.057596, ’ctime’: 1586253087.057596, ’wusr’: True, ’rusr’: True, ’xusr’: False, ’wgrp’: False, ’rgrp’: True, ’xgrp’: False, ’woth’: False, ’roth’: True, ’xoth’: False, ’isuid’: False, ’isgid’: False, ’blocks’: 0, ’block_size’: 4096, ’device_type’: 0, ’readable’: True, ’writeable’: True, ’executable’: False, ’pw_name’: ’root’, ’gr_name’: ’root’, ’checksum’: ’da39a3ee5e6b4b0d3255bfef95601890afd80709’, ’mimetype’: ’inode/x-empty’, ’charset’: ’binary’, ’version’: ’158303785’, ’attributes’: [], ’attr_flags’: ’’}, ’failed’: False}"
}

TASK [fail] ****************************************************************************
fatal: [ansible1]: FAILED! => {"changed": false, "msg": "unexpected file mode, should be set to 0640"}

PLAY RECAP *****************************************************************************
ansible1                   : ok=4    changed=1    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0

In the earlier examples in this section, you saw how you can use the stat module to show different types of file properties. Based on the output of the stat module, you may use the file module to set specific file properties. In Listing 8-5 you can see how the playbook from Listing 8-3 is rewritten to automatically set the desired permissions state.

Listing 8-5 Using the file Module to Correct File Properties Discovered with stat

---
- name: stat module tests
  hosts: ansible1
  tasks:
  - command: touch /tmp/statfile
  - stat:
      path: /tmp/statfile
    register: st
  - name: show current values
    debug:
      msg: current value of the st variable is {{ st }}
  - name: changing file permissions if that’s needed
    file:
      path: /tmp/statfile
      mode: 0640
    when: st.stat.mode != ’0640’

Exam tip

In the examples in this chapter, some tasks don’t have a name assigned. Using a name for each task is not required; however, it does make troubleshooting a lot easier if each task does have a name. For that reason, on the exam it’s a good idea to use names anyway. Doing so makes it easier to identify which tasks lead to which specific result.


Managing File Contents

If you need to manage file contents, multiple modules can be useful. The find module enables you to find files, just like the Linux find command. The lineinfile module enables you to manipulate lines in files, and blockinfile enables you to manipulate complete blocks of text. Also, don’t forget the copy module. We look at it in the next section, but you can also use it to copy a specified text to a file. For managing text operations on files, however, it is recommended that you use lineinfile or blockinfile instead because these give more options to specify where exactly the text should be written to.

Listing 8-6 shows an example where lineinfile is used to change a string, based on a regular expression.

Listing 8-6 Changing File Contents Using lineinfile

---
- name: configuring SSH
  hosts: all
  tasks:
  - name: disable root SSH login
    lineinfile:
      dest: /etc/ssh/sshd_config
      regexp: "^PermitRootLogin"
      line: "PermitRootLogin no"
    notify: restart sshd

  handlers:
  - name: restart sshd
    service:
      name: sshd
      state: restarted

As you can see in Listing 8-6, lineinfile uses the dest key to specify the filename. Next, a regular expression is used to search for lines that have text starting with PermitRootLogin. If this regular expression is found, it is changed into the line PermitRootLogin no.

You can use the lineinfile module to manipulate a single line in a file. In some cases you have to manage multiple lines in a file. In that case, you can use the blockinfile module. Listing 8-7 provides an example.

Listing 8-7 Using blockinfile to Manipulate Multiple Lines of Text

---
- name: modifying file
  hosts: all
  tasks:
  - name: ensure /tmp/hosts exists
    file:
      path: /tmp/hosts
      state: touch
  - name: add some lines to /tmp/hosts
    blockinfile:
      path: /tmp/hosts
      block: |
        192.168.4.110 host1.example.com
        192.168.4.120 host2.example.com
      state: present

Based on what you’ve learned so far, the use of blockinfile should be easy to understand. Just remember the use of the | after block:. This character is used to specify that the next couple of lines should be treated as lines, adding the newline character to the end of the line. Alternatively, you could use block: >, but that would add one long line to the destination file.

Notice that when blockinfile is used, the text specified in the block is copied with a start and end indicator. See Listing 8-8 for an example:

Listing 8-8 Resulting File Modification by blockinfile

127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
192.168.4.200.   control.example.com.      control
192.168.4.201    ansible1.example.com      ansible1
192.168.4.202    ansible2.example.com      ansible2

# BEGIN ANSIBLE MANAGED BLOCK
192.168.4.110 host1.example.com
192.168.4.120 host2.example.com
# END ANSIBLE MANAGED BLOCK

Creating and Removing Files

In an earlier example in this chapter you saw how the command module was used to create a new file by using the Linux touch command. While running this playbook, you saw a warning that you shouldn’t do it this way, but you should use the file module instead, and that is totally right.

You can use the file module to perform some pretty common tasks:

Image

• Create new files or directories

• Create links

• Remove files

• Set permissions and ownership

Listing 8-9 shows a sample playbook where the file module is used to create a new directory and in that directory create an empty file, after which the same file module is used again to remove the directory recursively. This approach is not very useful, but at least it shows you some of the most common uses of the file module.

Listing 8-9 Creating and Removing Files with the file Module

---
- name: using the file module
  hosts: ansible1
  tasks:
  - name: create directory
    file:
      path: /newdir
      owner: ansible
      group: ansible
      mode: 770
      state: directory
  - name: create file in that directory
    file:
      path: /newdir/newfile
      state: touch
  - name: show the new file
    stat:
      path: /newdir/newfile
    register: result
  - debug:
      msg: |
           This shows that newfile was created
           "{{ result }}"
  - name: removing everything again
    file:
      path: /newdir
      state: absent

In Listing 8-9, you can see that the last task is configured to remove a directory. Just specifying the path to the directory and state: absent recursively removes the directory. You don’t need to specify any other options here, and the recurse key also is not required.

Moving Files Around

Three modules are particularly useful for moving files around. The copy module copies a file from the Ansible control host to a managed machine. The fetch module enables you to do the opposite, and the synchronize module performs Linux rsync-like tasks, ensuring that a file from the control host is synchronized to a file with that name on the managed host. The main difference between copy and synchronize is that the copy module always creates a new file, whereas the synchronize module updates a current existing file. In Listing 8-10 you can see how these modules are used.

Listing 8-10 Moving a File Around with Ansible

---
- name: file copy modules
  hosts: all
  tasks:
  - name: copy file demo
    copy:
      src: /etc/hosts
      dest: /tmp/
  - name: add some lines to /tmp/hosts
    blockinfile:
      path: /tmp/hosts
      block: |
        192.168.4.110 host1.example.com
        192.168.4.120 host2.example.com
      state: present
  - name: verify file checksum
    stat:
      path: /tmp/hosts
      checksum_algorithm: md5
    register: result
  - debug:
      msg: "The checksum of /tmp/hosts is {{ result.stat.checksum }}"
  - name: fetch a file
    fetch:
      src: /tmp/hosts
      dest: /tmp/

After running the playbook in Listing 8-10, you might expect to find the file /tmp/hosts on the Ansible control machine. This, however, is not the case, and the reason is easy to understand. Ansible playbooks typically are used on multiple hosts, so if a file is fetched from a managed host, it must be stored in a unique location. To guarantee the uniqueness, Ansible creates a subdirectory for each managed host in the dest directory and puts the file that fetch has copied from the remote host in that subdirectory. So the result of the playbook in Listing 8-10 is stored as /tmp/ansible1/hosts and /tmp/ansible2/hosts. You practice working with files in Exercise 8-1.

Exercise 8-1 Managing Files with Ansible

1. Create a file with the name exercise81.yaml and give it the following play header:

---
- name: testing file manipulation skills
  hosts: ansible1
  tasks:

2. Add a task that creates a new empty file:

---
- name: testing file manipulation skills
  hosts: ansible1
  tasks:
  - name: create a new file
    file:
      name: /tmp/newfile
      state: touch

3. Use the stat module to check on the status of the new file:

---
- name: testing file manipulation skills
  hosts: ansible1
  tasks:
  - name: create a new file
    file:
      name: /tmp/newfile
      state: touch
  - name: check status of the new file
    stat:
      path: /tmp/newfile
    register: newfile

4. To see what the status module is doing, add a line that uses the debug module:

- name: testing file manipulation skills
  hosts: ansible1
  tasks:
  - name: create a new file
    file:
      name: /tmp/newfile
      state: touch
  - name: check status of the new file
    stat:
      path: /tmp/newfile
    register: newfile
  - name: for debugging only
    debug:
      msg: the current values for newfile are {{ newfile }}

5. Now that you understand which values are stored in newfile, you can add a conditional playbook that changes the current owner if not set correctly:

---
- name: testing file manipulation skills
  hosts: ansible1
  tasks:
  - name: create a new file
    file:
      name: /tmp/newfile
      state: touch
  - name: check status of the new file
    stat:
      path: /tmp/newfile
    register: newfile
  - name: for debugging only
    debug:
      msg: the current values for newfile are {{ newfile }}
  - name: change file owner if needed
    file:
      path: /tmp/newfile
      owner: ansible
    when: newfile.stat.pw_name != ’ansible’

6. Add a second play to the playbook that fetches a remote file:

- name: fetching a remote file
  hosts: ansible1
  tasks:
  - name: fetch file from remote machine
    fetch:
      src: /etc/motd
      dest: /tmp

7. Now that you have fetched the file so that it is on the Ansible control machine, use blockinfile to edit it:

- name: adding text to the file that is now on localhost
  hosts: localhost
  tasks:
  - name: add a message
    blockinfile:
      path: /tmp/ansible1/etc/motd
      block: |
        welcome to this server
        for authorized users only
      state: present

8. In the final step, copy the modified file to ansible2 by including the following play:

- name: copy the modified file to ansible2
  hosts: ansible2
  tasks:
  - name: copy motd file
    copy:
      src: /tmp/ansible1/etc/motd
      dest: /tmp

9. At this point you’re ready to run the playbook. Type ansible-playbook exercise81.yaml to run it and observe the results.

10. Type ansible ansible2 -a “cat /tmp/motd” to verify that the modified motd file was successfully copied to ansible2.

Managing SELinux Properties

In the security of any Linux system, SELinux is an important component. SELinux can be used on files to manage file context; apart from that, context can be set on ports; and SELinux properties can be managed using Booleans. Ansible has a few modules that allow for making changes to the SELinux configuration, which are listed in Table 8-3.


Tip

To work with SELinux in Ansible, you need to have knowledge about SELinux. This is a part of the RHCSA level knowledge that is required for anyone who wants to take EX294. This section does not explain SELinux itself. For more information about SELinux, consult the Red Hat RHCSA 8 Cert Guide.


Table 8-3 Modules for Managing Changes on SELinux

Image

Managing SELinux File Context

The essential thing to understand when working with SELinux to secure files is that the context type that is set on the file defines which processes can work with the files. The file context type can be set on a file directly, or it can be set on the SELinux policy.

When you’re working with SELinux, all of its properties should be set in the SELinux policy. To do this, you use the Ansible sefcontext module. Setting a context type in the policy doesn’t automatically apply it to files though. You still need to run the Linux restorecon command to do this. Ansible does not offer a module to run this command; it needs to be invoked using the command module.

As an alternative, you can use the file module to set SELinux context. The disadvantage of this approach is that the context is set directly on the file, not in the SELinux policy. As a result, if at any time default context is applied from the policy to the file system, all context that has been set with the Ansible file module risks being overwritten. For that reason, the recommended way to manage SELinux context in Ansible is to use the sefcontext module.

To be able to work with the Ansible sefcontext module and the Linux restorecon command, you also need to make sure that the appropriate software is installed on Linux. This software comes from the policycoreutils-python-utils RPM package, which is not installed by default in all installation patterns.

Listing 8-11 shows a sample playbook that uses this module to manage SELinux context type.

Listing 8-11 Managing SELinux Context with sefcontext

---
- name: show selinux
  hosts: all
  tasks:
  - name: install required packages
    yum:
      name: policycoreutils-python-utils
      state: present
  - name: create testfile
    file:
      name: /tmp/selinux
      state: touch
  - name: set selinux context
    sefcontext:
      target: /tmp/selinux
      setype: httpd_sys_content_t
      state: present
    notify:
      - run restorecon
  handlers:
  - name: run restorecon
    command: restorecon -v /tmp/selinux

In the sample playbook in Listing 8-11, the required software package is installed first. Next, a test file is created using the file module; then in the next task the sefcontext command is used to write the new context to the policy. If executed successfully, this task will trigger a handler to run the Linux restorecon command by using the command module.

Don’t forget: A handler will run only if the task that triggers it generates a changed status. If the current state already matches the desired state, no changes are applied and the handler won’t run!


Exam tip

The exam assignment might not be as specific as to ask you to change a context using SELinux. You might just have to configure a service with a nondefault documentroot, which means that SELinux will deny access to the service. So also on the EX294 exam, with all tasks, you should ask yourself if this task requires any changes at an SELinux level.


Applying Generic SELinux Management Tasks

Some additional modules are available as well. The selinux module enables you to set the current state of SELinux to either permissive, enforcing, or disabled. The seboolean module enables you to easily enable or disable functionality in SELinux using Booleans. Listing 8-12 shows an example of a playbook that uses both of these modules.

Listing 8-12 Changing SELinux State and Booleans

---
- name: enabling SELinux and a boolean
  hosts: ansible1
  vars:
    myboolean: httpd_read_user_content
  tasks:
  - name: enabling SELinux
    selinux:
      policy: targeted
      state: enforcing
  - name: checking current {{ myboolean }} Boolean status
    shell: getsebool -a | grep {{ myboolean }}
    register: bool_stat
  - name: showing boolean status
    debug:
      msg: the current {{ myboolean }} status is {{ bool_stat.stdout }}
  - name: enabling boolean
    seboolean:
      name: "{{ myboolean }}"
      state: yes
      persistent: yes

In the sample playbook in Listing 8-12, to start with, the selinux module is used to ensure that SELinux is in the enforcing state. When using this module, you also have to specify the name of the policy, which in most cases is the targeted policy.

Next, the seboolean module is used to enable a Boolean. As you can see, this Boolean is defined as the variable myboolean. Before the Boolean is enabled, the shell and debug modules are used to show its current status. In Exercise 8-2 you practice working with SELinux.

Exercise 8-2 Changing SELinux Context

In this exercise you configure a more complicated playbook, running different tasks. To guide you through this process, which will prepare you for the exam in a somewhat better way, I show you a different approach this time. To start with, this is the assignment you’re going to work on.

Install, start, and configure a web server that has the DocumentRoot set to the /web directory. In this directory, create a file named index.html that shows the message “welcome to the Exercise 8-2 webserver.” Ensure that SELinux is enabled and allows access to the web server document root. Also ensure that SELinux allows users to publish web pages from their home directory.

1. Because this is a complex task, you should start this time by creating a playbook outline. A good approach for doing this is to create the playbook play header and list all tasks that need to be accomplished by providing a name as well as the name of the task that you want to run. Create this structure as follows:

---
- name: Managing web server SELinux properties
  hosts: ansible1
  tasks:
  - name: ensure SELinux is enabled and enforcing
  - name: install the webserver
  - name: start and enable the webserver
  - name: open the firewall service
  - name: create the /web directory
  - name: create the index.html file in /web
  - name: use lineinfile to change webserver configuration
  - name: use sefcontext to set context on new documentroot
  - name: run the restorecon command
  - name: allow the web server to run user content

2. Now that the base structure has been defined, you can define the rest of the task properties. To start with, enable SELinux and set to the enforcing state:

---
- name: Managing web server SELinux properties
  hosts: ansible1
  tasks:
  - name: ensure SELinux is enabled and enforcing
    selinux:
      policy: targeted
      state: enforcing

3. You can install the web server, start and enable it, create the /web directory, and create the index.html file in the /web directory. You should be familiar with these tasks, so you can do them all in one run:

  - name: install the webserver
    yum:
      name: httpd
      state: latest
  - name: start and enable the webserver
    service:
      name: httpd
      state: started
      enabled: yes
  - name: open the firewall service
    firewalld:
      service: http
      state: enabled
      immediate: yes

  - name: create the /web directory
    file:
      name: /web
      state: directory
  - name: create the index.html file in /web
    copy:
      content: ’welcome to the exercise82 web server’
      dest: /web/index.html
  - name: use lineinfile to change webserver configuration
  - name: use sefcontext to set context on new documentroot
  - name: run the restorecon command
  - name: allow the web server to run user content

4. You must use the lineinfile module to change the httpd.conf contents. Two different lines need to be changed, which you accomplish by making the following modifications:

  - name: use lineinfile to change webserver configuration
    lineinfile:
      path: /etc/httpd/conf/httpd.conf
      regexp: ’^DocumentRoot "/var/www/html"’
      line: DocumentRoot "/web"
  - name: use lineinfile to change webserver security
    lineinfile:
      path: /etc/httpd/conf/httpd.conf
      regexp: ’^<Directory "/var/www">’
      line: ’<Directory "/web">’
  - name: use sefcontext to set context on new documentroot
  - name: run the restorecon command
  - name: allow the web server to run user content

5. In the final steps, you take care of configuring the SELinux-specific settings:

  - name: use sefcontext to set context on new documentroot
    sefcontext:
      target: ’/web(/.*)?
      setype: httpd_sys_content_t
      state: present
  - name: run the restorecon command
    command: restorecon -Rv /web
  - name: allow the web server to run user content
    seboolean:
      name: httpd_read_user_content
      state: yes
      persistent: yes

6. At this point, the complete playbook should look as follows:

---
- name: Managing web server SELinux properties
  hosts: ansible1
  tasks:
  - name: ensure SELinux is enabled and enforcing
    selinux:
      policy: targeted
      state: enforcing
  - name: install the webserver
    yum:
      name: httpd
      state: latest
  - name: start and enable the webserver
    service:
      name: httpd
      state: started
      enabled: yes
  - name: open the firewall service
    firewalld:
      service: http
      state: enabled
      immediate: yes
  - name: create the /web directory
    file:
      name: /web
      state: directory
  - name: create the index.html file in /web
    copy:
      content: ’welcome to the exercise82 web server’
      dest: /web/index.html
  - name: use lineinfile to change webserver configuration
    lineinfile:
      path: /etc/httpd/conf/httpd.conf
      regexp: ’^DocumentRoot "/var/www/html"’
      line: DocumentRoot "/web"
  - name: use lineinfile to change webserver security
    lineinfile:
      path: /etc/httpd/conf/httpd.conf
      regexp: ’^<Directory "/var/www">’
      line: ’<Directory "/web">’
  - name: use sefcontext to set context on new documentroot
    sefcontext:
      target: ’/web(/.*)?’
      setype: httpd_sys_content_t
      state: present
  - name: run the restorecon command
    command: restorecon -Rv /web
  - name: allow the web server to run user content
    seboolean:
      name: httpd_read_user_content
      state: yes
      persistent: yes

7. Run the playbook by using ansible-playbook exercise82.yaml and verify its output.

8. Verify that the web service is accessible by using curl http://ansible1. In this case, it should not work. Try to analyze why. You can find the answer at the end of this chapter before the end-of-chapter lab.

Using Jinja2 Templates

A template is a configuration file that contains variables and, based on the variables, is generated on the managed hosts according to host-specific requirements. Using templates allows for a structural way to generate configuration files, which is much more powerful than changing specific lines from specific files. Ansible uses Jinja2 to generate templates.

Working with Simple Templates

Jinja2 is a generic templating language for Python developers. It is used in Ansible templates, but Jinja2-based approaches are also found in other parts of Ansible. For instance, the way variables are referred to is based on Jinja2.

In a Jinja2 template, three elements can be used. Table 8-4 provides an overview.

Image

Table 8-4 Jinja2 Template Elements

Image

To work with a template, you must create a template file, written in Jinja2. Next, this template file must be included in an Ansible playbook that uses the template module. Listing 8-13 shows what a template file might look like, and Listing 8-14 shows an example of a playbook that calls the template.

Listing 8-13 Sample Template

# {{ ansible_managed }}

<VirtualHost *:80>
        ServerAdmin webmaster@{{ ansible_facts[’fqdn’] }}
        ServerName {{ ansible_facts[’fqdn’] }}
        ErrorLog logs/{{ ansible_facts[’hostname’] }}-error.log
        CustomLog       logs/{{ ansible_facts[’hostname’] }}-common.log common
        DocumentRoot /var/www/vhosts/{{ ansible_facts[’hostname’] }}/

        <Directory /var/www/vhosts/{{ ansible_facts[’hostname’] }}>
                Options +Indexes +FollowSymlinks +Includes
                Order allow,deny
                Allow from all
        </Directory>
</VirtualHost>

The sample template in Listing 8-13 starts with # {{ ansible_managed }}. This string is commonly used to identify that a file is managed by Ansible so that administrators are not going to change file contents by accident. While processing the template, this string is replaced with the value of the ansible_managed variable. This variable can be set in ansible.cfg. For instance, you can use ansible_managed = This file is managed by Ansible to substitute the variable with its value while generating the template.

As for the remainder, the template file is just a text file that uses variables to substitute specific variables to their values. In this case that is just the ansible_fqdn and ansible_hostname variables that are set as Ansible facts. To generate the template, you need a playbook that uses the template module to call the template. Listing 8-14 shows an example.

Listing 8-14 Sample Playbook

---
- name: installing a template file
  hosts: ansible1
  tasks:
  - name: install http
    yum:
      name: httpd
      state: latest
  - name: start and enable httpd
    service:
      name: httpd
      state: started
      enabled: true
  - name: install vhost config file
    template:
      src: listing813.j2
      dest: /etc/httpd/conf.d/vhost.conf
      owner: root
      group: root
      mode: 0644
  - name: restart httpd
    service:
      name: httpd
      state: restarted

In the sample playbook in Listing 8-14, the template module is used to work on the source file specified as src, to generate the destination file, specified as dest. The result is that on the managed host the template is generated, with all the variables substituted to their values.

Applying Control Structures in Jinja2 Using for

In templates, control structures can be used to dynamically generate contents. A for statement can be used to iterate over all elements that exist as the value of a variable. Let’s look at some examples.

To start with, Listing 8-15 shows a template where a for statement is shown.

Listing 8-15 Exploring Jinja2 for Statements

{% for node in groups[’all’] %}
host_port={{ node }}:8080
{% endfor %}

In this Jinja2 file, a variable with the name host_ports is defined on the second line (which is the line that will be written to the target file). To produce its value, the host group all is processed in the for statement on the first line. While processing the host group, a temporary variable with the name node is defined. This value of the node variable is replaced with the name of the host while it is processed, and after the host name, the string :8080 is copied, which will result in a separate line for each host that was found. As the last element, {% endfor %} is used to close the for loop. In Listing 8-16 you can see an example of a playbook that runs this template.

Listing 8-16 Generating a Template with a Conditional Statement

---
- name: generate host list
  hosts: ansible2
  tasks:
  - name: template loop
    template:
      src: listing815.j2
      dest: /tmp/hostports.txt

As you can see, the sample playbook in Listing 8-16 uses the template as the source file and, based on the template, produces the file /tmp/hostports.txt on the managed host. To verify, you can use the ad hoc command ansible ansible2 -a “cat /tmp/hostports.txt”.

Using Conditional Statements with if

The for statement can be used in templates to iterate over a series of values. The if statement can be used to include text only if a variable contains a specific value or evaluates to a Boolean true. Listing 8-17 shows a sample template file that reacts on a variable that is set in the sample playbook in Listing 8-18.

Listing 8-17 Template Example with if

{% if apache_package == ’apache2’ %}
  Welcome to Apache2
{% else %}
  Welcome to httpd
{% endif %}

Listing 8-18 Using the Template with if

---
- name: work with template file
  vars:
    apache_package: ’httpd’
  hosts: ansible2
  tasks:
  - template:
      src: listing817.j2
      dest: /tmp/httpd.conf

Using Filters

In Jinja2 templates, you can use filters. Filters are a way to perform an operation on the value of a template expression, such as a variable. The filter is included in the variable definition itself, and the result of the variable and its filter is used in the file that is generated. Table 8-5 gives an overview of some common filters. In Exercise 8-3 you practice your skills and work with templates that use a conditional statement.

Table 8-5 Common Filters Overview

Image


Exam tip

The Ansible documentation at https://docs.ansible.com contains a section with the title “Frequently Asked Questions.” In this section you can find the question “How do I loop over a list of hosts in a group, inside a template.” Read it now, and study it. The response here provides a very nice example of using conditional statements in templates, and that information might be useful on the exam.


Exercise 8-3 Working with Conditional Statements in Templates

1. Use your editor to create the file exercise83.j2. Include the following line to open the Jinja2 conditional statement:

{% for host in groups[’all’] %}

2. This statement defines a variable with the name host. This variable iterates over the magic variable groups, which holds all Ansible host groups as defined in inventory. Of these groups, the all group (which holds all inventory host names) is processed.

3. Add the following line (write it as one line; it will wrap over two lines, but do not press Enter to insert a newline character):

{{ hostvars[host][’ansible_default_ipv4’][’address’] }} {{ hostvars[host][’ansible_fqdn’]  }} {{ hostvars[host][’ansible_hostname’] }}

This line writes a single line for each inventory host, containing three items. To do so, you use the magic variable hostvars, which can be used to identify Ansible facts that were discovered on the inventory host. The [host] part is replaced with the name of the current host, and after that, the specific facts are referred to. As a result, for each host a line is produced that holds the IP address, the FQDN, and next the host name.

4. Add the following line to close the for loop:

{% endfor %}

5. Verify that the complete file contents look like the following and write and quit the file:

{% for host in groups[’all’] %}
{{ hostvars[host][’ansible_default_ipv4’][’address’] }} {{ hostvars[host][’ansible_fqdn’]  }} {{
ostvars[host][’ansible_hostname’] }}
{% endfor %}

6. Use your editor to create the file exercise83.yaml. It should contain the following lines:

---
- name: generate /etc/hosts file
  hosts: all
  tasks:
  - name:
    template:
      src: exercise83.j2
      dest: /tmp/hosts

7. Run the playbook by using ansible-playbook exercise83.yaml.

8. Verify the /tmp/hosts file was generated by using ansible all -a “cat /tmp/hosts”.

Summary

In this chapter you learned how to manipulate text files with Ansible. In the first section you learned about the most important Ansible modules that can be used. Next, you learned how to manage SELinux with Ansible. In the last part of this chapter, you read about generating configuration files using Jinja2 templates.

Exam Preparation Tasks

As mentioned in the section “How to Use This Book” in the Introduction, you have a couple of choices for exam preparation: the exercises here, Chapter 16, “Final Preparation,” and the exam simulation questions on the companion website.

Review All Key Topics

Review the most important topics in this chapter, noted with the Key Topics icon in the outer margin of the page. Table 8-6 lists a reference of these key topics and the page numbers on which each is found.

Image

Table 8-6 Key Topics for Chapter 8

Image

Memory Tables

Print a copy of Appendix D, “Memory Tables” (found on the companion website), or at least the section for this chapter, and complete the tables and lists from memory. Appendix E, “Memory Tables Answer Key,” also on the companion website, includes completed tables and lists to check your work.

Define Key Terms

Define the following key terms from this chapter, and check your answers in the glossary:

Jinja2

template

Review Questions

1. Which module should you use to work with file system ACLs?

2. Which modules can you use to replace strings in text files based on regex? (List two.)

3. Which module should you use to retrieve file status?

4. List three tasks that are commonly executed using the file module.

5. Which module should you use to synchronize the contents of a file with the contents of a file on the control host?

6. What is wrong with using the file module to manipulate SELinux file context?

7. Which module can you use to change SELinux Booleans?

8. A playbook runs successfully, but the handler in that playbook is not triggered. What is the most common explanation?

9. How do you include a comment line in a Jinja2 template?

10. What is the if statement used for in Ansible templates?

Exercise Answers

After you perform all the steps in Exercise 8-2, the web server still doesn’t work. Further analysis shows that the changes in httpd.conf have been made successfully and also that the SELinux context is set correctly. However, after you apply the changes with lineinfile, the web server needs to be started. You can do this either by using a handler or by moving the service task to be performed after the lineinfile task.

End-of-Chapter Lab

Lab 8-1: Generate an /etc/hosts File

Write a playbook that generates an /etc/hosts file on all managed hosts. Apply the following requirements:

• All hosts that are defined in inventory should be added to the /etc/hosts file.

Lab 8-2: Manage a vsftpd Service

Write a playbook that uses at least two plays to install a vsftpd service, configure the vsftpd service using templates, and configure permissions as well as SELinux. Apply the following requirements:

• Install, start, and enable the vsftpd service. Also, open a port in the firewall to make it accessible.

• Use the /etc/vsftpd/vsftpd.conf file to generate a template. In this template, you should use the following variables to configure specific settings. Replace these settings with the variables and leave all else unmodified:

• Anonymous_enable: yes

• Local_enable: yes

• Write_enable: yes

• Anon_upload_enable: yes

• Set permissions on the /var/ftp/pub directory to mode 0777.

• Configure the ftpd_anon_write Boolean to allow anonymous user writes.

• Set the public_content_rw_t SELinux context type to the /var/ftp/pub directory.

• If any additional tasks are required to get this done, take care of them.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset