Files
RHCE/README.md
2026-01-25 08:31:56 -07:00

26 KiB
Raw Permalink Blame History

RHCE Practice Lab

This repo contains all the files needed to deploy an RHCE practice lab. The target infrastructure is Openshift Virtualuzation, and network services (eg; DNS) are handled by OPNsense. Once deployed, the lab consists of 7 VMs:

  • controller
  • utility
  • node1
  • node2
  • node3
  • node4
  • node5

The lab uses the domain name lab.example.com.

All of the files needed to complete the tasks on the exam are hosted on the utility server, eg; http://utility.lab.example.com

You will perform all tasks as the ansible user on the controller node from the directory /home/ansible/ansible.

The ansible user's password is ansible (really original, I know).

Unless otherwise specified, the password for any vaulted files is redhat.

The lab is easily deployed with the following command:

ansible-playbook create-lab.yml -e @vault.yml --vault-password-file vault-password

The lab can be torn down by running the command:

ansible-playbook destroy-lab.yml

Helpful hints:

ansible localhost -m setup to print system facts. You may want to pipe that out to a text file to avoid having to run the command repeatedly and save yourself some time.

ansible-config init --disabled > ansible.cfg to generate a config file with all options commented.

You can use ansible.builtin.debug to print out things like facts to make sure your syntax is correct, eg;

# printfacts.yml
- name: Print facts
  hosts: jump01.lab.cudanet.org
  gather_facts: true
  remote_user: root

  tasks:
  - name: print facts
    ansible.builtin.debug:
      msg: "The default IPv4 address for {{ inventory_hostname }} is {{ ansible_default_ipv4.address }}"

Task 1.

install and configure ansible:

i) Install podman, ansible-core and ansible-navigator. /etc/yum.redos.d/rhce.repo should already be configured to pull packages from utility.lab.example.com.

solution

dnf -y install podman ansible-core ansible-navigator

ii) configure ansible.cfg to install collections by default to ~/ansible/mycollections and roles to ~/ansible/roles

solution
# ansible.cfg
[defaults]
inventory = /home/ansible/ansible/inventory
remote_user = ansible
roles_path = /home/ansible/ansible/roles
collections_path = /home/ansible/ansible/mycollections

iii) configure inventory as follows:

node1 is in the dev group. node2 is in the test group. nodes 3 and 4 are in the prod group. node5 is in the balancers group. the prod group is in the webservers group.

solution
# inventory
[dev]
node1

[test]
node2

[prod]
node3
node4

[balancers]
node5

[webservers:children]
prod

iv) ansible-navigator.yml is configured to pull the EE image from the utility server if missing. The registry is located at utility.lab.example.com:5000

solution
# ansible-navigator.yml
---
ansible-navigator:
  execution-environment:
    image: utility.lab.example.com:5000/ee-supported-rhel9:latest
    pull:
      policy: missing
  playbook-artifact:
    enable: false

NOTE: You're basically going to have to memorize the contents of this file, because unlike ansible.cfg there is no way to generate an ansible-navigator.yml file with dummy values.

Task 2.

manage repositories:

Write a playbook called repos.yml to add the BaseOS and AppStream repos to all managed hosts with GPG check enabled. Mirror is located at http://utility.lab.example.com/rhel9/

solution
---
# repos.yml
- name: Add BaseOS and AppStream repos to all hosts
  hosts: all
  become: true

  vars:
    repos:
      - BaseOS
      - AppStream

    baseurl: http://utility.lab.example.com/rhel9
    gpgkey_url: http://utility.lab.example.com/rhel9/RPM-GPG-KEY-redhat-release
    repo_file: /etc/yum.repos.d/rhce

  tasks:
    - name: Add {{ item }} repository
      ansible.builtin.yum_repository:
        name: "EX294_{{ item }}"
        description: "EX294 {{ item }} Repository"
        baseurl: "{{ baseurl }}/{{ item }}"
        enabled: true
        gpgcheck: true
        gpgkey: "{{ gpgkey_url }}"
        file: "{{ repo_file }}"
      loop: "{{ repos }}"

Task 3.

install roles and collections:

i) Install collections for ansible.posix, community.general and redhat.rhel_system_roles to '~/ansible/mycollections/'. Collections are hosted at http://utility.lab.example.com/files/

ii) install the balancer and phpinfo roles from http://utility.lab.example.com/files using a requirements.yml file.

NOTE: although, not a requirement, you can specify both roles and collections in your requirements file

solution
# requirements.yml
---
roles:
  - name: phpinfo
    src: http://utility.lab.example.com/files/phpinfo.tar.gz
    path: /home/ansible/ansible/roles

  - name: balancer
    src: http://utility.lab.example.com/files/haproxy.tar.gz
    path: /home/ansible/ansible/roles

collections:
  - name: ansible.posix
    source: http://utility.lab.example.com/files/ansible-posix-2.1.0.tar.gz
    type: url

  - name: redhat.rhel_system_roles
    source: http://utility.lab.example.com/files/redhat-rhel_system_roles-1.108.6.tar.gz
    type: url

  - name: community.general
    source: http://utility.lab.example.com/files/community-general-12.1.0.tar.gz
    type: url
# bash
mkdir -p /home/ansible/ansible/{roles,mycollections}
ansible-galaxy role install -r requirements.yml
ansible-galaxy collection install -r requirements.yml -p /home/ansible/ansible/mycollections

Task 4:

install packages and groups:

Write a playbook called install.yml to install php and httpd on the test group, and RPM Development Tools group in dev group only

solution
# install.yml
---
- name: Install Packages and Groups
  hosts: all

  become: true

  tasks:
  - name: Install packages on test group
    ansible.builtin.dnf:
      name:
        - httpd
        - php
      state: latest
    when: inventory_hostname in groups['test']

  - name: Install RPM Development Tools group on test group
    ansible.builtin.dnf:
      name: "@RPM Development Tools"
      state: latest
    when: inventory_hostname in groups['dev']

Task 5.

create a role:

i) Create a role called apache to install, start and persistently enable httpd and firewalld.

solution
# defaults/main.yml
---
apache_packages:
  - httpd
  - firewalld
# handlers/main.yml
---
- name: restart httpd
  ansible.builtin.service:
    name: httpd
    state: restarted

NOTE: You can create the basic filestructure of the role with ansible-galaxy role init apache

solution
apache/
├── defaults/
│   └── main.yml
├── handlers/
│   └── main.yml
├── tasks/
│   └── main.yml
├── templates/
│   └── index.html.j2
└── meta/
    └── main.yml

ii) Allow the HTTP traffic through the firewall.

solution
# tasks/main.yml
---
- name: Install httpd and firewalld
  ansible.builtin.package:
    name: "{{ apache_packages }}"
    state: present

- name: Enable and start firewalld
  ansible.builtin.service:
    name: firewalld
    state: started
    enabled: true

- name: Enable and start httpd
  ansible.builtin.service:
    name: httpd
    state: started
    enabled: true

- name: Allow HTTP service through firewalld
  ansible.posix.firewalld:
    service: http
    permanent: true
    state: enabled
    immediate: true

- name: Deploy index.html with FQDN and IPv4
  ansible.builtin.template:
    src: index.html.j2
    dest: /var/www/html/index.html
    owner: root
    group: root
    mode: '0644'
  notify: restart httpd
# handlers/main.yml
---
- name: restart httpd
  ansible.builtin.service:
    name: httpd
    state: restarted

iii) Populate out index.html with FQDN and IPv4 address using a jinja2 template, pulling those variables from ansible facts.

solution
# templates/index.html.j2
<!DOCTYPE html>
<html>
<head>
  <title>Apache Test Page</title>
</head>
<body>
  <h1>Apache is working</h1>
  <p><strong>FQDN:</strong> {{ ansible_facts.fqdn }}</p>
  <p><strong>IPv4 Address:</strong> {{ ansible_facts.default_ipv4.address }}</p>
</body>
</html>

iv) Finally, run the role against the dev group

solution
# apache.yml
---
- name: Configure Apache web servers
  hosts: dev
  become: true
  roles:
    - apache

Task 6.

use a role:

i) Use roles to apply the balancer role to the balancers group and phpinfo role to webservers group. Servers with the phpinfo role applied should report the FQDN and IP address of the web server, and refreshing the web browser should round robin between nodes 3 and 4. You should have already installed these roles in task 3.

solution
# roles.yml
---
- name: Configure load balancer
  hosts: balancers
  become: yes
  roles:
    - balancer

- name: Configure web servers
  hosts: webservers
  become: yes
  roles:
    - phpinfo

Task 7.

manage SELinux:

i) Use the ansible.posix.selinux role to configure SELinux to be enabled and enforcing on all managed hosts. Don't forget - changes to SELinux require a reboot to take effect.

solution
---
- name: Ensure SELinux is enabled and enforcing
  hosts: all
  become: true

  tasks:
    - name: Set SELinux to enforcing
      ansible.posix.selinux:
        policy: targeted
        state: enforcing
      notify: Reboot if SELinux state changed

  handlers:
    - name: Reboot if SELinux state changed
      ansible.builtin.reboot:
        msg: "Rebooting to apply SELinux changes"
        reboot_timeout: 600

Task 8.

manage file content:

i) Populate /etc/issue with the name of the lifecycle environment, eg; "Development" for dev, "Testing" for test and "Production" for prod.

solution
# issue.yml
---
- name: Automatically populate /etc/issue with environment name
  hosts:
    - dev
    - test
    - prod
  become: true

  tasks:
    - name: Determine environment name from inventory groups
      ansible.builtin.set_fact:
        env_name: >-
          {% if 'prod' in group_names %}
          Production
          {% elif 'test' in group_names %}
          Testing
          {% elif 'dev' in group_names %}
          Development
          {% endif %}

    - name: Populate /etc/issue
      ansible.builtin.copy:
        dest: /etc/issue
        content: |
          {{ env_name }}
        owner: root
        group: root
        mode: '0644'

Task 9.

manage storage:

i) Write a playbook called partition.yml. It should create a 1500MiB partition on vdb as ext4 mounted at /devmount, a 1500MiB partition on vdc as ext4 mounted at /devmount1, unless there isn't enough space on vdc, in which case make it 800MiB and print a message stating such. Check for vde. If there is no vde present, print message stating there's no such drive.

NOTE: My exam said to create partitions, but all examples I've seen point to logical volumes. Maybe practice both?

solution
# partition.yml
---
- name: Configure disk partitions and mounts
  hosts: all
  become: true
  gather_facts: true

  tasks:
    ####################################################################
    # /dev/vdb — always create 1500MB partition mounted at /devmount
    ####################################################################
    - name: Create 1500MB partition on /dev/vdb
      community.general.parted:
        device: /dev/vdb
        number: 1
        state: present
        part_end: 1500MiB

    - name: Create XFS filesystem on /dev/vdb1
      ansible.builtin.filesystem:
        fstype: xfs
        dev: /dev/vdb1

    - name: Mount /dev/vdb1 at /devmount
      ansible.builtin.mount:
        path: /devmount
        src: /dev/vdb1
        fstype: xfs
        state: mounted

    ####################################################################
    # /dev/vdc — size-based logic (1500MB or 800MB)
    ####################################################################
    - name: Determine size of /dev/vdc partition
      ansible.builtin.set_fact:
        vdc_part_size: >-
          {{ '1500MiB'
             if (ansible_facts.devices.vdc.sectors | int *
                 ansible_facts.devices.vdc.sectorsize | int) >= (1500 * 1024 * 1024)
             else '800MiB' }}
      when: "'vdc' in ansible_facts.devices"

    - name: Create partition on /dev/vdc
      community.general.parted:
        device: /dev/vdc
        number: 1
        state: present
        part_end: "{{ vdc_part_size }}"
      when: "'vdc' in ansible_facts.devices"

    - name: Create XFS filesystem on /dev/vdc1
      ansible.builtin.filesystem:
        fstype: xfs
        dev: /dev/vdc1
      when: "'vdc' in ansible_facts.devices"

    - name: Mount /dev/vdc1
      ansible.builtin.mount:
        path: >-
          {{ '/devmount1'
             if vdc_part_size == '1500MiB'
             else '/dev/mount' }}
        src: /dev/vdc1
        fstype: xfs
        state: mounted
      when: "'vdc' in ansible_facts.devices"

    ####################################################################
    # /dev/vde presence check
    ####################################################################
    - name: Warn if /dev/vde is not present
      ansible.builtin.debug:
        msg: "Disk /dev/vde is not present"
      when: "'vde' not in ansible_facts.devices"

Task 10.

manage directories and symlinks:

i) create the directory /webdev with U=RWX,G=RWX,O=RX permissions. It should be owned by webdev group. It should have special permissions set group id (I think that means 2775 in octal). Symlink from /webdev > /var/www/html/webdevcreate /webdev/index.html to report hostname and ip address.

Allow traffic through the firewall for http.

It should be browseable by the dev group.

solution
# webcontent.yml
---
- name: Configure restricted web content for dev hosts
  hosts: dev
  become: true
  gather_facts: true

  tasks:
    # ---------------- SELinux ----------------
    - name: Ensure SELinux is enforcing
      ansible.posix.selinux:
        policy: targeted
        state: enforcing

    - name: Install SELinux utilities
      ansible.builtin.package:
        name: policycoreutils-python-utils
        state: present

    # ---------------- Groups & Users ----------------
    - name: Ensure webdev group exists
      ansible.builtin.group:
        name: webdev
        state: present

    - name: Add ansible user to webdev group
      ansible.builtin.user:
        name: ansible
        groups: webdev
        append: true

    # ---------------- Web Content ----------------
    - name: Create /webdev directory with setgid permissions
      ansible.builtin.file:
        path: /webdev
        state: directory
        owner: root
        group: webdev
        mode: "2775"

    - name: Create index.html using Ansible facts
      ansible.builtin.copy:
        dest: /webdev/index.html
        owner: root
        group: webdev
        mode: "0644"
        content: |
          <!DOCTYPE html>
          <html>
          <head><title>WebDev Host Info</title></head>
          <body>
            <h1>WebDev Page</h1>
            <p><strong>Hostname:</strong> {{ ansible_facts['hostname'] }}</p>
            <p><strong>IP Address:</strong> {{ ansible_facts['default_ipv4']['address'] }}</p>
          </body>
          </html>

    # ---------------- Apache + Symlink ----------------
    - name: Create symlink from /webdev to /var/www/html/webdev
      ansible.builtin.file:
        src: /webdev
        dest: /var/www/html/webdev
        state: link
        force: true

    # ---------------- SELinux Context ----------------
    - name: Allow Apache to read /webdev via SELinux
      ansible.builtin.command:
        cmd: semanage fcontext -a -t httpd_sys_content_t "/webdev(/.*)?"
      register: semanage_result
      failed_when: semanage_result.rc not in [0,1]

    - name: Apply SELinux context
      ansible.builtin.command: restorecon -Rv /webdev
      changed_when: false

    # ---------------- Firewall ----------------
    - name: Ensure firewalld is started and enabled
      ansible.builtin.service:
        name: firewalld
        state: started
        enabled: true

    - name: Allow HTTP through firewall
      ansible.posix.firewalld:
        service: http
        permanent: true
        immediate: true
        state: enabled

    # ---------------- Apache Access Control ----------------
    - name: Restrict access to webdev content to node1 only
      ansible.builtin.copy:
        dest: /etc/httpd/conf.d/webdev.conf
        owner: root
        group: root
        mode: "0644"
        content: |
          <Directory "/var/www/html/webdev">
              Options FollowSymLinks
              Require all granted
              Require ip 127.0.0.1
              Require ip {{ ansible_facts['default_ipv4']['address'] }}
          </Directory>

    # ---------------- Services ----------------
    - name: Ensure httpd is started and enabled
      ansible.builtin.service:
        name: httpd
        state: started
        enabled: true

    - name: Restart httpd to apply configuration
      ansible.builtin.service:
        name: httpd
        state: restarted

Task 11.

manage file content with templates:

populate /etc/myhosts using hosts.j2 template and hosts.yml. Do not modify hosts.yml at all, it should handle all of the looping through the hosts in the template file use a for loop on the j2 template to loop through each host

solution
# hosts.j2
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1       localhost localhost.localdomain localhost6 localhost6.localdomain6
{% for node in groups['all'] %}
{{ hostvars[node]['ansible_facts']['default_ipv4']['address'] }} {{ hostvars[node]['ansible_facts']['fqdn'] }} {{ hostvars[node]['ansible_facts']['hostname'] }}
{% endfor%}
# hosts.yml
- name: Hosts config deploy
  hosts: all
  become: True
  tasks:

    - name: Template a file to /etc/myhosts
      when: inventory_hostname in groups['dev']
      ansible.builtin.template:
        src: ./hosts.j2
        dest: /etc/myhosts

Task 12.

modify file contents:

Download hwreport.empty from utility.lab.example.com to /root/hwreport.txt on all hosts.

Replace key value pairs for hostname, bios version, memoryMiB, size of vda, vdb and vdc. If device does not exist, put NONE.

solution
# hwreport.yml
---
- name: Generate hardware report
  hosts: all
  become: yes

  tasks:
    - name: Download empty hwreport file
      get_url:
        url: http://utility.lab.example.com/files/hwreport.empty
        dest: /root/hwreport.txt
        mode: '0644'

    - name: Set hostname
      lineinfile:
        path: /root/hwreport.txt
        regexp: '^HOST='
        line: "HOST={{ ansible_hostname }}"

    - name: Set BIOS version
      lineinfile:
        path: /root/hwreport.txt
        regexp: '^BIOS='
        line: "BIOS={{ ansible_bios_version | default('NONE') }}"

    - name: Set memory size
      lineinfile:
        path: /root/hwreport.txt
        regexp: '^MEMORY='
        line: "MEMORY={{ ansible_memtotal_mb }} MB"

    - name: Set vdb disk size
      lineinfile:
        path: /root/hwreport.txt
        regexp: '^VDB='
        line: "VDB={{ ansible_devices.vdb.size | default('NONE') }}"

    - name: Set vdc disk size
      lineinfile:
        path: /root/hwreport.txt
        regexp: '^VDC='
        line: "VDC={{ ansible_devices.vdc.size | default('NONE') }}"

    - name: Set vdd disk size (NONE if missing)
      lineinfile:
        path: /root/hwreport.txt
        regexp: '^VDD='
        line: >-
          VDD={{ ansible_devices.vdd.size if 'vdd' in ansible_devices else 'NONE' }}

Task 13.

use ansible vault to encrypt a file:

Create an encrypted variable file called locker.yml which should contain two variables and their values.

pw_developer is value imadev pw_manager is value imamgr

locker.yml file should be encrypted using the password whenyouwishuponastar

store the password in a file named secret.txt, which is used to encrypt the variable file.

solution
# secret.txt
echo "whenyouwishuponastar" > secret.txt
chmod 600 secret.txt
# locker.yml
pw_developer: imadev
pw_manager: imamgr

ansible-vault encrypt locker.yml --vault-password-file secret.txt

Task 14.

manage users:

Download the variable file "http://utility.lab.example.com/files/user_list.yml" and write a playbook named "users.yml" and then run the playbook on all the nodes using two variable files user_list.yml and locker.yml.

i)

* Create a group opsdev

* Create user from users variable who job is equal to developer and need to be in opsdev group

* Assign a password using SHA512 format and run playbook on dev and test group.

* User password is {{ pw_developer }}

ii)

* Create a group opsmgr

* Create user from users varaible who job is equal to manager and need to be in opsmgr group

* Assign a password using SHA512 format and run playbook on prod group.

* User password is {{ pw_manager }}

iii) Use when condition for each play

solution
# user_list.yml
users:
  - name: Fred
    role: manager

  - name: Wilma
    role: manager

  - name: Barney
    role: developer

  - name: Betty
    role: developer
# users.yml
---
- name: Download user_list.yml variable file
  hosts: all
  gather_facts: false
  tasks:
    - name: Download user_list.yml
      ansible.builtin.get_url:
        url: http://utility.lab.example.com/files/user_list.yml
        dest: ./user_list.yml
      run_once: true
      delegate_to: localhost

- name: Create developer users on dev and test
  hosts: dev:test
  become: true
  vars_files:
    - user_list.yml
    - locker.yml
  tasks:
    - name: Ensure opsdev group exists
      ansible.builtin.group:
        name: opsdev
        state: present

    - name: Create developer users
      ansible.builtin.user:
        name: "{{ item.name }}"
        groups: opsdev
        append: yes
        password: "{{ pw_developer | password_hash('sha512') }}"
        state: present
      loop: "{{ users }}"
      when: item.role == "developer"


- name: Create manager users on prod
  hosts: prod
  become: true
  vars_files:
    - user_list.yml
    - locker.yml
  tasks:
    - name: Ensure opsmgr group exists
      ansible.builtin.group:
        name: opsmgr
        state: present

    - name: Create manager users
      ansible.builtin.user:
        name: "{{ item.name }}"
        groups: opsmgr
        append: yes
        password: "{{ pw_manager | password_hash('sha512') }}"
        state: present
      loop: "{{ users }}"
      when: item.role == "manager"

ansible-navigator run -m stdout users.yml --vault-password-file secret.txt

Task 15.

re-encrypt a vaulted file:

Rekey variable file from http://utility.lab.example.com/files/salaries.yml

i) Old password: changeme

ii) New password: redhat

solution
# salaries.yml
fred: $100000
wilma:$100000
barney: $100000
betty: $100000

wget http://utility.lab.example.com/files/salaries.yml

ansible-vault rekey salaries.yml

Vault password:            changeme
New Vault password:        redhat
Confirm New Vault password:redhat

Task 16.

manage cron:

Create a cronjob for the user ansible on all nodes, playbook name is crontab.yml and the job details are below:

i) Every 2 minutes the job will execute logger "EX294 in progress".

solution
# cron.yml
---
- name: Create cron job for user ansible
  hosts: all
  become: true
  tasks:
    - name: Ensure cron job runs every 2 minutes
      ansible.builtin.cron:
        name: "EX294 progress log"
        user: ansible
        minute: "*/2"
        job: 'logger "EX294 in progress"'
        state: present

Task 17.

Use the RHEL timesync system role:

i) Create a playbook called "timesync.yml" that:

  • Runs on all managed nodes
  • Uses the timesync role
  • Configures the role to use the currently active NTP provider
  • Configure the role to use the time server utility.lab.example.com
  • Configure the role to enable the iburst parameter
solution
# timesync.yml
- name: Configure time synchronization using RHEL timesync role
  hosts: all
  become: true
  roles:
    - role: redhat.rhel_system_roles.timesync
      vars:
        timesync_ntp_provider: auto
        timesync_ntp_servers:
          - hostname: utility.lab.example.com
            iburst: true

Task 18.

configure MOTD:

Create a playbook called motd.yml.

i) Run the playbook. ii) Whenever you ssh into any node (node1 here), the message will be as follows: Welcome to node1 OS: RedHat 9.4 Architecture: x86_64

solution
# motd.yml
---
- name: Configure MOTD for all nodes
  hosts: all
  become: true
  gather_facts: true
  tasks:
    - name: Set MOTD file
      ansible.builtin.copy:
        dest: /etc/motd
        content: |
          Welcome to {{ inventory_hostname }}
          OS: {{ ansible_distribution }} {{ ansible_distribution_version }}
          Architecture: {{ ansible_architecture }}
        owner: root
        group: root
        mode: '0644'