Exam Scenario 2 Answers
...

Below are the answers for Exam Scenario 2. You can also find all the files that would need to be created for this scenario on this folder.

Task 1
...

Install Ansible
...

Bash
1
useradd ansible
Bash
1
su - ansible
Bash
1
mkdir exam-files
$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/ansible/.ssh/id_rsa): id_rsa
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in id_rsa.
Your public key has been saved in id_rsa.pub.
The key fingerprint is:
SHA256:rI3irvdNYBw1mUz/heLTAVloIiPfoR1KzJMwU/IC2N4 ansible@rhel8.localdomain
The key's randomart image is:
+---[RSA 3072]----+
| o. ==.++o.+.    |
|. ...=O.Bo+. .   |
| . ..+oO =o o .  |
|  . Eo+oo. + o   |
|      + S o o    |
|     . =   .     |
|    . o o        |
|   ... o         |
|  .+o.. .        |
+----[SHA256]-----+
Bash
1
mkdir {roles,vars,playbooks,scripts,files}

inventory

Ini
1
node1
2
node2
3
node3
4
node4

ansible.cfg

Ini
1
[defaults]
2

3
inventory = ./inventory
4
roles_path = ./roles
5
remote_user = ansible
6
private_key_file = ./id_rsa
7
host_key_checking = false
8
nocows = 1
9
retry_files_enabled = false
Bash
1
useradd ansible
2
passwd ansible
Bash
1
visudo -f /etc/sudoers.d/ansible
Sudoers
1
ansible ALL=(ALL) NOPASSWD: ALL
# su - ansible

$ sudo -ln
Matching Defaults entries for ansible on node2:
    !visiblepw, always_set_home, match_group_by_gid, always_query_group_plugin, env_reset,
    env_keep="COLORS DISPLAY HOSTNAME HISTSIZE KDEDIR LS_COLORS", env_keep+="MAIL PS1 PS2 QTDIR USERNAME
    LANG LC_ADDRESS LC_CTYPE", env_keep+="LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES",
    env_keep+="LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE", env_keep+="LC_TIME LC_ALL LANGUAGE
    LINGUAS _XKB_CHARSET XAUTHORITY", secure_path=/sbin\:/bin\:/usr/sbin\:/usr/bin

User ansible may run the following commands on node2:
    (ALL) NOPASSWD: ALL
Bash
1
for i in 1 2 3 4 ; do
2
sshpass -p ansible ssh-copy-id -i ./id_rsa.pub ansible@node${i}
3
done
Bash
1
ssh ansible@node1 -i id_rsa

Add to /etc/ssh/sshd_config

Config
1
Match User ansible
2
PasswordAuthentication no

scripts/check-connection.sh

Bash
1
#!/bin/bash
2

3
ansible all -m ping

Task 2
...

Script: get-server-info.sh
...

scripts/get-server-info.sh

1
#!/bin/bash
2

3
tuned_profile="$(tuned-adm active | grep 'Current active profile')"
4

5
if [ ! "$tuned_profile" ] ; then
6
tuned_status="inactive"
7
tuned_profile=" disabled"
8
else
9
tuned_status="active"
10
tuned_profile="$(echo "$tuned_profile" | awk -F':' '{print $2}')"
11
fi
12

13
echo "Hostname: $(hostname)
14
Name: $(grep -E '^NAME=' /etc/os-release | awk -F"=" '{print $2}')
15
Version: $(grep -E '^VERSION=' /etc/os-release | awk -F"=" '{print $2}')
16
Tuned status: $tuned_status
17
Current active profile:${tuned_profile}"

Script: task2.sh
...

scripts/task2.sh

Bash
1
#!/bin/bash
2

3
ansible all -m copy -a 'src=/home/ansible/exam-files/scripts/get-server-info.sh dest=/usr/local/bin/get-server-info.sh mode=0755 owner=root group=root' -b
4

5
ansible all -a '/usr/local/bin/get-server-info.sh' -b

Task 3
...

Ini
1
[webservers]
2
node1
3
node2
4

5
[databases]
6
node3
7
node4
8

9
[mysql]
10
node3
11

12
[postgresql]
13
node4
14

15
[version1]
16
node1
17

18
[version2]
19
node2

Task 4
...

playbooks/task4.yml

YAML
1
---
2
- hosts: all
3
become: true
4

5
tasks:
6

7
- name: Creates /data/backup
8
file:
9
path: /data/backup
10
state: directory
11
mode: g+x,o+x
12
when: '"webservers" in group_names'
13

14
- name: Create /etc/server_role
15
copy:
16
dest: /etc/server_role
17
content: "{{ group_names | string | regex_search('webservers|databases')}}"
18

19
- name: Cheks if httpd is installed
20
command: rpm -qa | grep -qE '^httpd-[0-9]'
21
args:
22
warn: false
23
register: httpd_install_status
24
changed_when: false
25
when: '"webservers" in group_names or "databases" in group_names'
26

27
- name: Shows httpd package as installed
28
debug:
29
msg: "HTTPD is installed"
30
when:
31
- 'httpd_install_status.rc == 0 and
32
("webservers" in group_names or "databases" in group_names)'
33

34
- name: Shows httpd package as not installed
35
debug:
36
msg: "HTTPD is not installed"
37
when:
38
- 'httpd_install_status.rc != 0 and
39
("webservers" in group_names or "databases" in group_names)'
40

41
- name: Makes sure default target is multi-user.target
42
shell: |
43
if ! systemctl get-default | grep -q multi-user.target ; then
44
systemctl set-default multi-user.target
45
/bin/false
46
else
47
exit 0
48
fi
49
register: targetlevel_output
50
changed_when: targetlevel_output.rc == 1
51
ignore_errors: true
52

Task 5
...

playbooks/task5.yml

YAML
1
---
2
- hosts: all
3
become: true
4

5
handlers:
6
- name: Restat HTTPD
7
systemd:
8
name: httpd
9
state: restarted
10
listen: "Restart HTTPD"
11
when: '"webservers" in group_names'
12

13
- name: Backup httpd.conf
14
archive:
15
path: /etc/httpd/conf/httpd.conf
16
dest: "/data/backup/http.conf-{{ ansible_date_time.date | replace('-', '') }}_{{ ansible_date_time.time | replace(':', '') }}.zip"
17
format: zip
18
listen: "Backup httpd.conf"
19
when: '"webservers" in group_names'
20

21
tasks:
22

23
- name: Uploads root_space_check.sh
24
copy:
25
src: /home/ansible/exam-files/files/root_space_check.sh
26
dest: /usr/local/bin/root_space_check.sh
27
mode: ugo+x
28

29
- name: Adds root_space_check.sh to con
30
cron:
31
name: Runs root_space_check.sh every hour
32
special_time: hourly
33
job: /usr/local/bin/root_space_check.sh
34

35
# Block starts
36
- name: Block for webservers
37
block:
38

39
- name: Installs httpd
40
dnf:
41
name: httpd
42

43
- name: Enables the httpd service
44
systemd:
45
name: httpd
46
enabled: yes
47
state: started
48

49
- name: Opens ports for httpd
50
firewalld:
51
service: "{{ item }}"
52
permanent: yes
53
state: enabled
54
loop:
55
- http
56
- https
57

58
- name: Changes the Listen option in /etc/httpd/conf/httpd.conf
59
lineinfile:
60
path: /etc/httpd/conf/httpd.conf
61
line: "Listen {{ ansible_eth1.ipv4.address }}:80"
62
regexp: "^Listen .*"
63
notify:
64
- "Restart HTTPD"
65
- "Backup httpd.conf"
66

67
when: '"webservers" in group_names'
68
# Block end
69

70
# Block starts
71
- name: Start block for databases
72
block:
73

74
- name: Creates PV and VG
75
lvg:
76
vg: databases_vg
77
pvs: /dev/sdb
78

79
- name: Create LV
80
lvol:
81
vg: databases_vg
82
lv: databases_lv
83
size: 100%FREE
84
shrink: false
85

86
- name: Formats to ext4
87
filesystem:
88
fstype: ext4
89
dev: /dev/mapper/databases_vg-databases_lv
90
opts: '-L DATABASES'
91

92
- name: Create the mountpoint for DATABASES
93
mount:
94
path: /data/databases
95
src: LABEL=DATABASES
96
fstype: ext4
97
state: present
98

99
when: '"mysql" in group_names'
100
# Block end
101

102
- name: Enables SELinux for databases
103
selinux:
104
state: enforcing
105
policy: targeted
106
when: '"databases" in group_names'
107

Task 6
...

Role: start-page
...

Bash
1
cd roles
2
ansible-galaxy init start-page

roles/start-page/templates/index.html.j2

Django/Jinja2
1
<!DOCTYPE html>
2
<html lang="en">
3
<head>
4
<meta charset="UTF-8">
5
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6
<title>Server Information</title>
7
<style>
8
body {
9
font-family: Arial, sans-serif;
10
margin: 20px;
11
}
12
div {
13
margin-bottom: 10px;
14
}
15
</style>
16
</head>
17
<body>
18
<h1>Server Information</h1>
19

20
<div>
21
<strong>Hostname:</strong> <span id="hostname">{{ ansible_fqdn }}</span>
22
</div>
23

24
<div>
25
<strong>Node Group:</strong> <span id="group">{{ group_names | string | regex_search('version.') }}</span>
26
</div>
27

28
<div>
29
<strong>IP Address:</strong> <span id="ip">{{ ansible_eth1.ipv4.address }}</span>
30
</div>
31

32
<div>
33
<strong>Timezone:</strong> <span id="timezone">{{ ansible_date_time.tz }}</span>
34
</div>
35

36
</body>
37
</html>

roles/start-page/tasks/main.yml

YAML
1
---
2
# tasks file for start-page
3

4
- name: Pushes index.html
5
template:
6
src: index.html.j2
7
dest: /var/www/html/index.html

Role: journald-persistent
...

Bash
1
cd roles
2
ansible-galaxy init journald-persistent

roles/journald-persistent/tasks/main.yml

YAML
1
---
2
# tasks file for journald-persistent
3

4
- name: Create /var/log/journal
5
file:
6
path: /var/log/journal
7
state: directory
8
owner: root
9
group: root
10

11
- name: Configures /etc/systemd/journald.conf
12
lineinfile:
13
path: /etc/systemd/journald.conf
14
line: "{{ item.line }}"
15
regexp: "{{ item.regexp }}"
16
loop:
17
- { line: 'SystemMaxUse=100M', regexp: '^SystemMaxUse=.*' }
18
- { line: 'Storage=persistent', regexp: '^Storage=.*' }
19
notify: "Restart Journald"

roles/journald-persistent/handlers/main.yml

YAML
1
---
2
# handlers file for journald-persistent
3

4
- name: Restarts
5
systemd:
6
name: systemd-journald.service
7
state: restarted
8
listen: "Restart Journald"

Playbook
...

playbooks/task6.yml

Django/Jinja2
1
---
2
- hosts: all
3
become: true
4

5
roles:
6
- name: start-page
7
when: '"webservers" in group_names'
8

9
- name: journald-persistent

Task 7
...

node1:

sudo mkdir -p /etc/ansible/facts.d

/etc/ansible/facts.d/exam.fact

Ini
1
[server_info]
2
group=webservers
3
app_version=1

node2:

sudo mkdir -p /etc/ansible/facts.d

/etc/ansible/facts.d/exam.fact

Ini
1
[server_info]
2
group=webservers
3
app_version=2

ansible-console

ansible@webservers (2)[f:5]$ ls
node1 | CHANGED | rc=0 >>

node2 | CHANGED | rc=0 >>

ansible@webservers (2)[f:5]$ setup

ansible@webservers (2)[f:5]$ debug var=ansible_local
node1 | SUCCESS => {
    "ansible_local": {
        "exam": {
            "server_info": {
                "app_version": "1",
                "group": "webservers"
            }
        }
    }
}
node2 | SUCCESS => {
    "ansible_local": {
        "exam": {
            "server_info": {
                "app_version": "2",
                "group": "webservers"
            }
        }
    }
}

Task 8
...

Role: postgresql
...

roles/postgresql/tasks/main.yml

YAML
1
---
2
# tasks file for postgresql
3

4
- name: Installs the VDO package
5
dnf:
6
name:
7
- vdo
8
- kmod-kvdo
9

10
- name: Starts the VDO service
11
systemd:
12
name: vdo.service
13
state: started
14
enabled: true
15

16
# The force option in the vdo module is not present on Ansible 2.9
17
- name: Checks if VDO volume already exists
18
command: vdostats databases_vdo
19
register: vgostats_output
20
changed_when: false
21
ignore_errors: true
22

23
- name: Creates the VDO partition
24
vdo:
25
name: databases_vdo
26
device: /dev/sdb
27
logicalsize: 20G
28
writepolicy: auto
29
deduplication: disabled
30
#force: false
31
when: vgostats_output == 1
32

33
- name: Create the volume group
34
lvg:
35
pvs: /dev/mapper/databases_vdo
36
vg: databases_vg
37

38
- name: Create the logical volume
39
lvol:
40
lv: databases_lv
41
vg: databases_vg
42
size: 100%FREE
43
force: false
44
shrink: false
45

46
- name: Creates the ext4 filesystem
47
filesystem:
48
dev: /dev/mapper/databases_vg-databases_lv
49
fstype: ext4
50
opts: -E nodiscard
51

52
- name: Mounts the filesystem
53
mount:
54
fstype: ext4
55
opts: defaults,_netdev,discard,x-systemd.requires=vdo.service,x-systemd.device-timeout=0
56
src: /dev/mapper/databases_vg-databases_lv
57
path: /data/databases
58
state: mounted
59

60
- name: Installs the postgresql module
61
dnf:
62
name: '@postgresql'
63
register: postgresql_install
64

65
- name: Configures the data folder for postgresql service
66
lineinfile:
67
path: /usr/lib/systemd/system/postgresql.service
68
line: 'Environment=PGDATA=/data/databases/postgresql_data'
69
regexp: '^Environment=PGDATA=.*'
70
notify: "Reload daemon"
71

72
- name: Creates /data/databases/postgresql_data
73
file:
74
path: /data/databases/postgresql_data
75
state: directory
76
owner: postgres
77
group: postgres
78
mode: '0700'
79
register: create_postgresql_data
80

81
- name: Initializes the DB
82
command: postgresql-setup --initdb
83
args:
84
creates: /data/databases/postgresql_data/PG_VERSION
85
when:
86
- postgresql_install.changed == true
87
- postgresql_data.changed == true
88

89
- name: Installs semanage
90
dnf:
91
name: setroubleshoot-server
92

93
- name: Enables the selinuxuser_postgresql_connect_enabled boolean
94
seboolean:
95
name: selinuxuser_postgresql_connect_enabled
96
state: yes
97
persistent: yes
98

99
- name: Fixes the SELinux context for the postgresql data files
100
sefcontext:
101
target: '/data/databases(/.*)?'
102
setype: postgresql_db_t
103
state: present
104
notify: "Restore SELinux context"

roles/postgresql/handlers/main.yml

YAML
1
---
2
# handlers file for postgresql
3

4
- name: Reload daemon
5
systemd:
6
name: postgresql.service
7
daemon_reload: true
8
enabled: true
9
listen: "Reload daemon"
10

11
- name: Restore SELinux context
12
command: restorecon -irv /data/databases
13
when: change_selinux_context.changed == true
14
listen: "Restore SELinux context"
15
notify: "Restart postgresql service"
16

17
- name: Restart postgresql.service
18
systemd:
19
name: postgresql.service
20
state: restarted
21
listen: "Restart postgresql service"

Playbook
...

playbooks/deploy-postgresql.yml

YAML
1
---
2
- hosts: postgresql
3
become: true
4

5
roles:
6
- postgresql
7

8
tasks:
9
- name: Checks if /data/db_troubleshoot exists
10
stat:
11
path: /data/db_troubleshoot
12
register: stat_db_troubleshoot
13

14
- name: Creates /data/db_troubleshoot
15
file:
16
path: /data/db_troubleshoot
17
state: directory
18
owner: postgres
19
group: postgres
20
mode: '0700'
21
force: false
22
when: stat_db_troubleshoot.stat.exists == false
23

24
- name: Creates the group pgsqladmin
25
group:
26
name: pgsqladmin
27

28
- name: Creates the user dbadmin
29
user:
30
name: dbadmin
31
group: pgsqladmin
32

33
- name: Sets default ACL for /data/db_troubleshoot
34
acl:
35
path: /data/db_troubleshoot
36
default: true
37
etype: group
38
entity: pgsqladmin
39
permissions: rwx
40
state: present
41

42
- name: Sets ACL for /data/db_troubleshoot
43
acl:
44
path: /data/db_troubleshoot
45
etype: group
46
entity: pgsqladmin
47
permissions: rwx
48
state: present

Task 9
...

Download Role
...

$ ansible-galaxy role install geerlingguy.mysql
- downloading role 'mysql', owned by geerlingguy
- downloading role from https://github.com/geerlingguy/ansible-role-mysql/archive/4.3.3.tar.gz
- extracting geerlingguy.mysql to /home/ansible/exam-files/roles/geerlingguy.mysql
- geerlingguy.mysql (4.3.3) was installed successfully

...

Bash
1
$ tr -dc A-Za-z0-9*_$^! < /dev/urandom | head -c 24 > .vault_passwd
Ini
1
[defaults]
2

3
inventory = ./inventory
4
roles_path = ./roles
5
remote_user = ansible
6
private_key_file = ./id_rsa
7
host_key_checking = false
8
nocows = 1
9
retry_files_enabled = false
10
vault_password_file = ./.vault_passwd

vars/mysql.yml

YAML
1
---
2
mysql_root_username: root
3
mysql_root_password: sqlrootpassword
$ ansible-vault encrypt vars/mysql.yml
Encryption successful

Change the line below in roles/geerlingguy.mysql/defaults/main.yml

YAML
1
mysql_root_password_update: true

playbooks/deploy-mysql.yml

YAML
1
---
2
- hosts: mysql
3
become: true
4

5
vars_files:
6
- /home/ansible/exam-files/vars/mysql.yml
7

8
roles:
9
- geerlingguy.mysql

Task 10
...

...

ansible-config list > ansible.cfg.template

ansible-config dump > ansible.cfg.dump

ansible-doc -l > ansible-modules.txt

Install jinja2 documentation
...

dnf install -y python3-jinja2.noarch

Documentation files can be found with:

rpm -ql python3-jinja2.noarch | grep index.html
/usr/share/doc/python3-jinja2/examples/rwbench/django/index.html
/usr/share/doc/python3-jinja2/examples/rwbench/genshi/index.html
/usr/share/doc/python3-jinja2/examples/rwbench/jinja/index.html
/usr/share/doc/python3-jinja2/examples/rwbench/mako/index.html
/usr/share/doc/python3-jinja2/ext/django2jinja/templates/index.html
/usr/share/doc/python3-jinja2/html/genindex.html
/usr/share/doc/python3-jinja2/html/index.html
/usr/share/doc/python3-jinja2/html/latexindex.html
/usr/share/doc/python3-jinja2/html/py-modindex.html