# 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](http://utility.lab.example.com/files)

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.

<details>

<summary>solution</summary>

`dnf -y install podman ansible-core ansible-navigator`

</details>

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

<details>

<summary>solution</summary>

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

</details>


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.

<details>

<summary>solution</summary>

```
# inventory
[dev]
node1

[test]
node2

[prod]
node3
node4

[balancers]
node5

[webservers:children]
prod
```

</details>

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

<details>

<summary>solution</summary>

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

</details>

*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/

<details>

<summary>solution</summary>

```
---
# 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 }}"

```

</details>

## 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/](http://utility.lab.example.com/files/)

ii) install the `balancer` and `phpinfo` roles from [http://utility.lab.example.com/files](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*

<details>

<summary>solution</summary>

```
# 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
```

</details>

## 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

<details>

<summary>solution</summary>

```
# 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']
```

</details>

## Task 5.

**create a role:**

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

<details>

<summary>solution</summary>

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

</details>

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

<details>

<summary>solution</summary>

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

</details>

ii) Allow the HTTP traffic through the firewall.  

<details>

<summary>solution</summary>

```
# 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
```

</details>

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

<details>

<summary>solution</summary>

```
# 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>
```

</details>

iv) Finally, run the role against the `dev` group

<details>

<summary>solution</summary>

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

</details>

## 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.

<details>

<summary>solution</summary>

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

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

</details>

## 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.

<details>

<summary>solution</summary>

```
---
- 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
```

</details>

## 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`.

<details>

<summary>solution</summary>

```
# 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'
```

</details>

## 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?*

<details>

<summary>solution</summary>

```
# 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"
```

</details>

## 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/webdev`  create `/webdev/index.html` to report hostname and ip address.

Allow traffic through the firewall for http.

It should be browseable by the dev group.

<details>

<summary>solution</summary>

```
# 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
```

</details>

## 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

<details>

<summary>solution</summary>

```
# 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
```

</details>

## 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.

<details>

<summary>solution</summary>

```
# 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' }}
```

</details>

## 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.

<details>

<summary>solution</summary>

```
# 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`

</details>

## 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

<details>

<summary>solution</summary>

```
# 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`

</details>

## Task 15.

**re-encrypt a vaulted file:**

Rekey variable file from [http://utility.lab.example.com/files/salaries.yml](http://utility.lab.example.com/files/salaries.yml)

i) Old password: changeme

ii) New password: redhat

<details>

<summary>solution</summary>

```
# 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
```

</details>

## 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".

<details>

<summary>solution</summary>

```
# 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
```

</details>

## 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

<details>

<summary>solution</summary>

```
# 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
```

</details>

## 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

<details>

<summary>solution</summary>

```
# 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'
```

</details>
