4.2 Ansible Playbooks - Templates

In this lab we start to use templates!

Task 1

  • Rewrite your playbook motd.yml without using the ansible.builtin.copy module, but rather using the ansbile.builtin.template module.
  • Use a Jinja2 template file called motd.j2 which uses the variable motd_content.
Solution Task 1

Create the file motd.j2 with the following one-liner:

1
2
$ cat motd.j2
{{ motd_content }}

Edit your motd.yml playbook to something like this:

1
2
3
4
5
6
7
8
9
---
- hosts: all
  become: true
  tasks:
    - name: set content of /etc/motd
      ansible.builtin.template:
        src: motd.j2
        dest: /etc/motd
        mode: "0644"

Run the playbook again.

1
 ansible-playbook motd.yml -l node1,node2

Task 2

  • Improve the template motd.j2 by adding the default IP address of the server to the template.
  • Add information about the installed operating system to the motd file as well.
Solution Task 2

Add IP and OS to motd.j2:

1
2
3
4
$ cat motd.j2
{{ motd_content }}
IP ADDRESS: {{ ansible_default_ipv4.address }}
OS:         {{ ansible_os_family }}

Rerun the playbook and check if the text has been changed accordingly:

1
2
ansible-playbook motd.yml -l node1,node2
ansible all -a "cat /etc/motd"

Task 3 (Advanced)

Create a variable users like the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
users:
  - name: jim
    food: pizza
  - name: sabrina
    food: burger
  - name: hans
    food: vegan
  - name: eveline
    food: burger
  - name: santos

Put the variable in an appropriate place of your choice.

Create a playbook userplay.yml doing the following and running on node1 and node2:

  • On node1: Create a file /etc/dinner.txt with the content below by using the ansible.builtin.template module:
    <name_of_user> <food_for_user>
    
  • On node1: There should be an entry in the file /etc/dinner.txt for each user in the variable users. Use a for loop in the template.
  • On node1: If a user has no food specified, use “kebab”. Look for filters in the online docs. You should be familiar with searching the online docs by now.
  • On node2: The same playbook userplay.yml should create a (Linux) group for every different food specified in the variable users. If a user has no food defined, create the group “kebab” instead.
  • On node2: Create a user for every entry in the users variable. Ensure that this user is also in the group with the same name as his food. Again, if no food is defined for this user, add group “kebab”.

Bonus 1

  • On node2: Set the login shell to /bin/zsh for all users.

Bonus 2

  • On node2: If (and only if) the user is “santos”, disable login. Do this by setting santos’ login shell to /usr/sbin/nologin. Use an if/else statement in the template for that purpose.

Bonus 3

  • All on node2:
    • Set the default password for all the newly created users to “N0t_5o_s3cur3
    • Once the password has been set, your playbook should not set it again. Not even when it got changed.
    • Hash the password using the sha512 algorithm.
    • Don’t define a salt for the password.
    • Verify that you are able to log in as one of the users via SSH providing the password.
Solution Task 3

Documentation about filters

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
$ pwd
/home/ansible/techlab

$ cat uservars.yml
users:
  - name: jim
    food: pizza
  - name: sabrina
    food: burger
  - name: hans
    food: vegan
  - name: eveline
    food: burger
  - name: santos

$ cat userplay.yml
---
- hosts: node1
  become: true
  vars_files:
    - uservars.yml
  tasks:
    - name: put template
      ansible.builtin.template:
        src: user_template.j2
        dest: /etc/dinner.txt
        mode: "0644"

- hosts: node2
  become: true
  vars_files:
    - uservars.yml
  tasks:
    - name: create groups
      ansible.builtin.group:
        name: "{{ item.food | default('kebab') }}"
      loop: "{{ users }}"
    - name: ensure zsh is installed
      ansible.builtin.dnf:
        name: zsh
        state: installed
    - name: create users
      ansible.builtin.user:
        name: "{{ item.name }}"
        group: "{{ item.food | default('kebab') }}"
        append: true
        shell: "{% if item.name == 'santos' %}/usr/sbin/nologin{% else %}/usr/bin/zsh{% endif %}"
        password: "{{ 'N0t_5o_s3cur3' | password_hash('sha512') }}"
        update_password: on_create
      loop: "{{ users }}"

$ cat user_template.j2
{% for person in users %}
{{ person.name }}               {{ person.food | default('kebab') }}
{% endfor %}

Run the playbook, then check on node1 (as user root) if everything is as expected:

1
2
3
4
5
6
# cat /etc/dinner.txt
jim         pizza
sabrina     burger
hans        vegan
eveline     burger
santos      kebab

Check as well on node2 (as user root):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# grep  'jim\|sabrina\|hans\|eveline\|santos' /etc/passwd
jim:x:1002:1002::/home/jim:/usr/bin/zsh
sabrina:x:1003:1003::/home/sabrina:/usr/bin/zsh
hans:x:1004:1004::/home/hans:/usr/bin/zsh
eveline:x:1005:1003::/home/eveline:/usr/bin/zsh
santos:x:1006:1005::/home/santos:/usr/sbin/nologin

# grep  'pizza\|burger\|vegan\|kebab' /etc/group
pizza:x:1002:
burger:x:1003:
vegan:x:1004:
kebab:x:1005:

Login to node2 as user jim, providing the password via SSH prompt:

1
2
ssh -l jim <IP address of node2>
jim@192.168.122.31's password:

Task 4 (Maester)

Create a playbook serverinfo.yml that does the following:

  • On all nodes: Place a file /root/serverinfo.txt with a line like the following repeated for each and every server in the inventory:
<hostname>: OS: <operating system> IP: <IP address> Virtualization Role: <hardware type>
  • Replace hostname, operating system, IP address and hardware type with a reasonable fact.

  • Run your playbook and check on all servers by using an ansible ad hoc command if the content of the file /root/serverinfo.txt is as expected.

  • Are you an Ansible Maester already? Solve the solution once by using a template and once without using a template!

Solution Task 4

Possible solution 1:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ cat serverinfo.txt.j2
{% for host in groups['all'] %}
{{ hostvars[host].ansible_hostname }}: OS: {{ hostvars[host].ansible_os_family }} IP: {{ hostvars[host].ansible_default_ipv4.address }} Virtualization Role: {{ hostvars[host].ansible_virtualization_role }}
{% endfor %}

$ cat serverinfo.yml
---
- hosts: all
  become: true
  tasks:
    - name: put serverinfo.txt
      ansible.builtin.template:
        src: serverinfo.txt.j2
        dest: /root/serverinfo.txt
        mode: "0640"

Possible solution 2:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
$ cat serverinfo.yml
---
- hosts: localhost
  tasks:
    - name: create the serverinfo file to be distributed later
      ansible.builtin.file:
        path: /home/ansible/techlab/serverinfo.txt
        state: touch
        mode: "0644"

- hosts: all
  tasks:
    - name: fill in stuff to local serverinfo.txt
      ansible.builtin.lineinfile:
        path: /home/ansible/techlab/serverinfo.txt
        regexp: "^{{ ansible_hostname }}"
        line: "{{ ansible_hostname }}: OS: {{ ansible_os_family }} IP: {{ ansible_default_ipv4.address }} Virtualization Role: {{ ansible_virtualization_role }}"
      delegate_to: localhost

- hosts: all
  become: true
  tasks:
    - name: place the file serverinfo.txt
      ansible.builtin.copy:
        src: /home/ansible/techlab/serverinfo.txt
        dest: /root/serverinfo.txt
        mode: "0640"

$ ansible-playbook serverinfo.yml
$ ansible all -b -a "cat /root/serverinfo.txt"

All done?

Last modified February 14, 2025: Nitpicking 3 nitpick bugaloo (#224) (2284a0f)